ID Agents Dashboard (Mac App)

Full reference for the macOS desktop app, covering every view, the status indicator, context menus, the command palette, keyboard shortcuts, and settings

ID Agents Dashboard (Mac App)#

The ID Agents Dashboard is the proprietary macOS desktop application for operating and observing an ID Agents fleet. It gives you a graphical view of your projects, agents, news, tasks, schedules, and library, plus a command palette and a file browser.

Download it from /download. See Quick Start for install steps.

Where this fits. The Dashboard does not run agents itself. It is a graphical client layered on the open-source id-agents core: the manager daemon spawns and tracks agents, and the Dashboard reads that state and drives it. Everything here operates the same teams you would manage from the CLI. The app owns the manager process it starts, so you do not need to launch the daemon separately.

Window Layout#

The window has four persistent regions.

RegionContents
Title stripThe title ID Agents, the Files toggle, and the manager status indicator
SidebarThe Views and Library section lists, with a pinned Connect button at the bottom
ToolbarTeam scope selector, agent filter, and the ⌘K command palette button
Status bardaemon: followed by the manager URL, then reconciled just now and ⌘K commands

Most views are a sortable table over a detail pane, split by a draggable divider you can also move with and when it has focus.

How the Dashboard Gets Its Data#

Read this section first. Agents, News, Tasks, Calendar, and Heartbeats are all views of a single object delivered by a single pipeline, so "when does this update" and "what happens when the manager is down" are answered once, here, for five of the seven views. Projects and Library are the exceptions, and each says so.

The snapshot model#

The app builds one canonical snapshot in its main process from the manager daemon: teams, agents, news, schedules, tasks, and the sidebar counts. Every view except Library and Projects reads a slice of that one object. No view fetches for itself.

The snapshot is versioned and monotonic. Each rebuild increments a version number, and the window discards any pushed slice older than what it already holds, so a slow response that arrives late cannot overwrite fresher data.

Only six kinds of data are pushed to the window: teams, agents, news, counts, schedules, and tasks. The manager URL is delivered once at startup and never by the ongoing push, which is why it is seeded with the real value up front.

Push and poll do different jobs#

The app opens one WebSocket per observed team, and the set of sockets is re-matched to the team list every time a new snapshot arrives.

The WebSocket never carries data. It carries hints. Only two hint topics are actionable, news and agents_changed, and each triggers an incremental refetch over REST. A replay_truncated hint forces a full reconcile. Connection notices, keepalives, unknown topics, and anything flagged as remote are ignored outright. So every visible update is ultimately a REST fetch; the socket only decides when to fetch.

Underneath the socket is a hard floor: a REST reconciliation every 30 seconds, running for the life of the app. Bursts of invalidation are coalesced into one fetch, and each fetch carries a generation number so a stale response is discarded.

The fetch order is teams first, since teams are the fan-out root, then agents, then news for each local agent, then tasks, then schedules.

Hiding the window does not stop updates. It only relaxes the coalescing window from 150 ms to 2000 ms. The 30-second poll keeps running, and a WebSocket hint or the window becoming visible again still forces a reconcile.

The app has no history of its own#

This is the single most important lifecycle fact in the pipeline, and it is the opposite of how Projects works.

The app keeps a cursor of the newest event timestamp per team, but it exists only to drop duplicate and out-of-order hints. It is held in memory, never written to disk, and constructed fresh on every launch. Nothing is replayed from it.

The Dashboard does not consume an event log. It re-reads current state. Every launch, every reconnect, and every poll performs the same full fetch of teams, agents, news, tasks, and schedules.

The practical consequence: the app shows you the manager's current state, not a history of what happened while you were away. Anything the manager still reports, such as a task's status or news items it retains, appears on the next fetch. Anything that existed only as a transient event, or that the manager has since dropped, was never the app's to replay. There is no desktop-side backlog and no catch-up pass: the app never asks the manager what it missed.

One related signal does exist. If the manager reports replay_truncated, meaning its own event replay was cut short, the app discards that team's dedupe cursor and forces a full refetch instead of trusting an incremental update. That is the app reacting to a gap the manager declared, not detecting one itself.

Closing the app therefore loses nothing and preserves nothing for these five views. The next launch re-fetches.

When the manager is down#

The views freeze on the last good data. They do not empty and they do not clear.

The teams fetch is the one call not wrapped in per-resource error tolerance, so if the manager is unreachable the whole rebuild fails and the previous snapshot stands. Retry needs no user action: the 30-second interval keeps firing. A dropped WebSocket reconnects with exponential backoff from 500 ms up to a 10-second cap, and reconciles fully on reconnect because it may have missed hints.

Partial failure is tolerated per resource. If agents, news, tasks, or schedules fail while teams succeed, only that resource degrades to empty for that one snapshot rather than blanking everything. News is finer still: a failed fetch for one agent yields an empty list for that agent alone.

The caveat that will surprise you. When the manager goes down, the Agents table looks completely normal. Every value silently freezes at its last good reading, uptimes and health dots and news ages included, and the only indication anywhere in the window is the small status dot in the title strip turning amber and then red. Views that could announce staleness do not: the strings Reconnecting — showing last known data and Can't reach the manager exist in the code, but the shell only ever reports live, and in a packaged install that is always the case. Trust the title-strip dot, not the table.

Caps#

DataCap
Merged news timeline300 items, applied after the global newest-first sort
News preview text200 characters, with the full payload retained separately
Agents, teams, tasks, schedulesNo cap. Whatever the manager returns is rendered

Remote agents are filtered out of the app entirely, in the normalizer. The Dashboard is a local-agent surface.

Manager lifecycle#

The app decides between adopting an existing manager daemon and spawning its own. It always probes health first. If a daemon answers, the app adopts it and never spawns. Only a free port triggers a spawn. If the port is occupied but unhealthy, the app inspects the occupant's command line and waits for it only if it looks like a manager; otherwise it refuses rather than spawning over an unrecognised process.

A manager URL counts as locally managed only when it is loopback on port 4100. For a remote or non-default URL the app still adopts it if it answers health, and health monitoring, status display, and every view work identically. What is unavailable is exclusively the app's ability to spawn that manager if it is down, or to restart it on demand.

Closing the Dashboard does not stop your agents. A normal quit deliberately leaves the manager daemon, and everything it is running, alive in the background. The only things that stop it are the Restart Manager button in Settings, accepting a sustained-outage prompt, or an app update installing. If you want a clean stop, restart or kill the manager explicitly; closing the window is not enough.

The sidebar has two headed groups. Views holds Projects, Agents, News, Tasks, Calendar, and Heartbeats. Library holds Agents, Skills, and Teams. Projects, Agents, Tasks, and Heartbeats carry a live count badge.

Pinned at the bottom of the sidebar are two footer controls: the Connect button, and a Docs link that opens this page in your system browser.

There is no sidebar item for Settings. See Settings for how to reach it.

Projects#

What it is. A project is a live view into a folder, and its timeline is a history of change events you can click into to see the actual file updates. You register a folder, and the app records that folder's change history as commits in a private shadow git repository kept entirely outside the folder itself.

Projects is observe-only. There is no restore or write path back to your files. The app reads the folder to commit a snapshot into the external shadow repository, and never writes to your project files or your .git.

Your own git repository is never touched. The shadow repository uses its own git directory, its own index, an empty hooks directory, no reflogs, and a fixed identity, so no editor, hook, signing path, or repository callback in your project can participate in a capture. A snapshot is refused outright if the shadow directory would ever resolve inside your working tree.

How it works. One filesystem watcher per project reports adds, changes, deletions, and directory changes. Half-written files do not wake it: it waits for a write to be stable for 500 ms first. Excluded paths never even generate events.

Events only set a per-project dirty flag. The commit trigger is a trailing 2-second debounce, a 30-second maximum wait, and a minimum 10 seconds between granular commits. Because dirtiness is one flag and every snapshot captures the whole tree, any number of rapid edits collapses into a single commit. From a saved file to a timeline entry, expect roughly 2.5 seconds at best, at most 30 seconds under continuous editing, and never sooner than 10 seconds after the previous entry.

One project mutates at a time, serialized per project, with a cross-process lock around every mutating operation. Sibling projects capture concurrently, and a failure never poisons the queue.

Reads are lazy and pull-based. The entry list is a paged log, 50 entries per page. Changed files and per-file diffs are fetched only when you ask for them.

Lifecycle and limits. This is the part that determines whether the timeline is useful to you:

EventWhat happens
App start, project has no historyA baseline snapshot is recorded
App start, project has historyA catch-up snapshot, but only if the tree actually changed while the app was not running
Quiet restart, nothing changedNothing is recorded. No empty commits
Power resumeThe same catch-up-if-changed pass runs
App quitWatchers are closed and in-flight captures are drained first

Capture only runs while the app is open. Every watcher and timer lives in the app's own process and is disposed at quit. There is no background daemon capturing your folders when the Dashboard is closed.

Changes made while the app was closed are not lost, but they are not individual events either. They arrive as one catch-up entry.

The caveat that will surprise you. If you want a fine-grained timeline of what an agent did to a folder, the Dashboard has to be open while it works. An entire overnight agent session with the app closed collapses into a single catch-up commit at next launch: one entry, no intermediate states, and no way to replay the steps.

Nothing is ever pruned or deleted. The only maintenance is a git repack, scheduled after startup and after every 100 commits, and only once the project has been idle for 60 seconds.

Each project is isolated. One broken project records an error and never stops the others from capturing.

Git is a hard requirement. No git is bundled. A missing git, or one older than 2.32, makes every project error out with an actionable message while the rest of the app keeps working.

Projects registers local folders and captures their change history. It is the one view that is not team-scoped, so the team selector and agent filter are hidden while it is open.

The list columns are Name, Folder, Capture, and Last snapshot. Folder shows the path with ~ substituted for your home directory. The list is fixed alphabetical by name; there are no sort controls, filters, or search box in this view.

Last snapshot does not show real data in the shipped app. The column and its 10-second refresh are wired up in the interface, but the main process always reports the value as empty, so the cell stays no matter how recently a snapshot was taken. Read the Capture status section of the inspector instead, which reports live state and the head commit.

The Capture cell shows a status dot and one of these labels:

LabelMeaning
OnlineCapture is running
Initializing…Capture is starting up
Paused — storage limit reachedShadow history hit the 2.0 GiB cap; the timeline stays available
Error — <detail>Capture failed, with the reason
Offline — <detail>Capture is not running

A icon appears when shadow history nears or hits the limit. The warning threshold is 1.0 GiB and capture pauses automatically at 2.0 GiB. What is measured is the entire shadow directory for that project, by a recursive byte walk, checked before every automatic capture and on every status refresh.

The pause is narrower than it sounds. It stops automatic, watcher-driven capture only. Manual snapshots and every read path stay available, so a paused project's timeline remains fully readable.

The 2 GiB pause does not reliably self-resume. When a capture is skipped for size, the buffered dirty state is kept but no retry timer is armed. Clearing the pause needs both the size to drop and a fresh trigger, such as a new file event or a manual snapshot. Since nothing ever prunes history, size only drops if a repack shrinks the pack or you intervene. In practice, at 2 GiB automatic capture stays off.

The status precedence is error, then paused, then online, then initializing. Online requires an actual head commit, so a project that has never captured anything stays Initializing… rather than showing as online. A folder that no longer validates, because it moved, was renamed, was unmounted, or had its permissions changed, is reported offline and is not managed at all: not initialized, not watched, not captured. The registry row survives.

Above the list is a single Add Project button. The add dialog is titled Add project and states its rule plainly: Pick a folder using the native chooser — the app never accepts a typed-in path. You click Choose folder…, which opens a native picker, then name the project (1 to 80 characters) and click Create project.

The inspector below the list has sections Identity (with a Copy path button), Capture status, Storage (only when the size is known), and Capture policy. The capture policy is fixed and not overridable:

  • Excludes .git/, node_modules/, and common build/noise folders — not overridable.
  • Secret-looking files (.env*, *.pem, *.key, SSH private keys, …) are never captured — not overridable, no per-project exception.
  • Files larger than 10 MiB are omitted from capture; the working file itself is left untouched.
  • Also honors the project's own .gitignore.

These are not merely ignore rules. Because a project's own .gitignore can outrank a shadow exclude file, the app removes offenders from the staging list itself, so their contents are never read or hashed in the first place, then verifies fail-closed that nothing excluded reached the index. The lists are fixed constants with no setting and no per-project exception. The only carve-outs that exist are .env.example and .env.sample, which are re-admitted.

Two consequences worth knowing before you rely on a timeline:

The secret and noise excludes match by name, and they over-match silently. Any basename starting with id_rsa, id_dsa, id_ecdsa, or id_ed25519 is never captured, so a file like id_ed25519_setup.md is invisible. So is anything ending .key or .pem, including a product license.key. Any directory named dist, build, out, target, or tmp is skipped wherever it appears. None of this is overridable, and the interface gives no per-file indication that a file was skipped for these reasons. Only oversize files are counted, in a Skipped-Large commit trailer.

Gitignored files are invisible to the timeline too. Because staging honors your .gitignore, edits to ignored files such as local config or data files produce no timeline entries. The watcher may wake, but the snapshot commits nothing for them.

The 10 MiB cap is per file and per snapshot. The oversize scan runs over the whole tree on every snapshot, so a permanently present 11 MiB asset marks the trailer of every commit, not just the one where it first appeared.

Below the inspector is the timeline, a newest-first snapshot list with columns Ref, When, Change, and Files. Change badges are Baseline, Catch-up, and File changes. Clicking an entry drills into a detail view with a ‹ Back button, a changed-files list using status glyphs A, M, D, R, and B for Added, Modified, Deleted, Renamed, and Binary, and a diff pane. Escape also goes back. New snapshots are prepended live without disturbing your selection or scroll position.

The three badges map exactly onto the three reasons a snapshot can exist. Baseline is a project's first capture. Catch-up is the app noticing the tree changed while it was not running. File changes is a watcher-driven capture. A manual snapshot also records as File changes, so a manual snapshot is indistinguishable from an automatic one in the timeline data. All entries go on a single branch in the shadow repository, and commits carry no agent or task attribution.

Timeline reads are bounded, and the bounds are quiet:

LimitValueWhat you see
Changed files per entry500 rowsThe list stops at 500 with no marker, while the entry's own file count can honestly read 12,000
Diff per file512 KiBThe patch is truncated, flagged only by Diff truncated — this file exceeds the display limit.
Binary filesnever diffedShown as Binary file — no text diff. with a Reveal in Finder button

A commit touching more than 500 files truncates its changed-file list with no marker. If an entry reports far more files than the list shows, you are seeing the cap, not a bug.

Adding, removing, and renaming. The interface can never name a path. The main process runs the native picker and mints a one-use approval token valid for five minutes, consumed on first use, so an expired or reused token is rejected. The folder is revalidated when you click Create project, and canonical paths are unique, so symlink aliases cannot register the same folder twice.

Removing a project drains the capture queue first so a capture cannot race the removal, then deletes the registry row and moves only the shadow history to the Trash. Your folder is never touched, and a containment guard refuses to trash anything inside the project folder.

Renaming changes the display name only. Paths and links are untouched, and because the shadow directory is keyed by an immutable id, history survives a rename.

Moving or renaming the folder on disk orphans its timeline. The registry keeps the old path, and the project reports offline. Nothing re-points a project at a new location. Re-adding the folder where it now lives mints a new id and therefore a fresh, empty history; the old history is only reachable by removing the old row, which trashes it.

Adding a project widens what the app can read. Every validating project folder becomes a read root for the app's interface, recomputed on each registry refresh. Registering a folder as a project is also a read-access grant to that folder, and it joins the file browser's allowed roots in the main process.

One more thing that affects how fresh the list feels: automatic captures push no live event. A watcher-driven commit does not notify the window; the timeline picks it up on its next pull. Only a manual snapshot broadcasts immediately.

Agents#

What it is. A view of the local agents the manager knows about, one row per agent. Agents on remote endpoints are excluded before they ever reach the interface.

How it works. Every column is computed in the main process when the snapshot is built, from the manager's response plus a per-agent news fetch. Nothing is computed in the table itself:

ColumnDerived from
health dotThe manager's own health field, passed through. Not computed locally
StatusThe manager's status string, verbatim, or unknown when absent
NewsNow minus the newest news timestamp for that agent
EffThe agent's effort metadata, abbreviated
UptimeThe age of the agent record, and only while its status is running
Runtime / ModelThe agent's runtime and model metadata, abbreviated

Fetching the News column costs one request per local agent per refresh, issued in parallel. A failure for one agent degrades that agent's News cell alone.

The health dot adds one rule the manager does not: an offline agent whose status is error draws amber rather than red.

Lifecycle and limits. The table is recomputed only when a new snapshot is built, which means on a WebSocket hint or the 30-second poll. Nothing in this view runs a local clock, so between snapshots every value is a frozen number, including Uptime and the news age. A row that reads 4m ago will keep reading 4m ago until the next refresh, then jump.

Agents is the view the app opens on. It is a table of every agent over an activity panel for the selected agent.

Table columns, in order, are a health dot with a blank header, then Agent, Team, Runtime, Model, Eff, Status, News, HB, Mem, and Uptime. Eff abbreviates the effort setting to low, med, high, or xhi. Uptime is a duration. Any of these render when unset.

The caveat that will surprise you: HB is not a real interval, and Mem is never populated. HB shows the literal 86400s for every agent that has any heartbeat metadata at all, and for every agent that does not. It is a boolean rendered as a duration, and it does not reflect that agent's configured cadence. For the real cadence, use the Heartbeats view. Mem is set to empty in the main process and nothing ever fills it, so in a live install the column is always blank. The underlying memory capability exists but has no caller.

A smaller surprise in the same table: Uptime is the age of the agent record, not of the running process, and it blanks the moment the agent stops being running rather than freezing at its final value.

Click a column header to sort. Clicking the same header toggles ascending and descending; a new header sorts ascending. Sortable columns are Agent, Team, Runtime, Model, Status, News, and Uptime. The default is Agent ascending.

The News column pairs an age label with a colored dot: green under 300 seconds, yellow under 1200 seconds, gray beyond that, and never when the agent has no news.

Eight of the eleven columns can be hidden from Settings. Columns also hide automatically when the window gets narrow, independently of your choice:

ColumnToggleableAuto-hides below
health dotnonever
Agentnonever
Teamyes980 px
Runtimeyes900 px
Modelyesnever
Effyes1160 px
Statusyesnever
Newsnonever
HByes1240 px
Memyes1040 px
Uptimeyesnever

When the table has no rows in the current scope it shows No agents in this scope.

The view defines a banner row above the table for non-live data, with the strings Loading agents…, Reconnecting — showing last known data, and Can't reach the manager. In a packaged install you will not see any of them: the shell only ever reports the live state, so the banner is always absent. This is why the title-strip status dot is the only reliable staleness signal.

The activity panel below the table has three tabs: News, Output, and Tasks. With nothing selected it reads Select an agent to see its activity.

  • News reuses the News table columns scoped to one agent, with a ‹ Back drill-down. Empty state: No recent news.
  • Output lists the files directly inside the agent's output/ directory, with File and Size columns, a Refresh button, and per-file Copy and Reveal buttons. Empty state: No output files for this agent.
  • Tasks lists tasks owned by the selected agent with a status dot and the raw status word. Empty state: No tasks for this agent.

A profile panel opens as a right drawer for the selected agent, on by default and toggleable in Settings. It is headed Profile with a edit button, and holds the agent's photo, name, team, an optional Project section with a View Project button, a Bio section, and a Handles section. In edit mode you get Change photo, Save profile, and Cancel. The photo also accepts a drag-and-drop of a png, jpeg, or webp while editing.

News#

What it is. An aggregated, newest-first timeline of every local agent's news feed, merged into one list. There is no single "news" resource on the manager: the app builds this feed by fetching each agent's own feed and concatenating them.

How it works. Ordering is by age ascending, so the smallest age is newest, applied when the feeds are merged and again in the table.

The From and To columns are derived, not stored. The sender field arrives empty from the manager, and the columns are computed from each item's type and payload, with the agent whose feed it came from filling the implicit side. Only a fixed set of types resolve a counterparty at all: outbound events, query.received, reply, notify, message, news.received, and inbound.reply. Everything else is treated as a self-status event with no counterparty. Protocol-level remote is rewritten to manager for display.

The text filter matches the agent name or the message preview, falling back to the item type when the preview is empty.

Lifecycle and limits. Retention is the manager's, not the app's. The app stores nothing: each refresh replaces the whole feed with whatever the per-agent fetches return. Closing the app loses nothing and preserves nothing, and the next launch re-fetches.

Nothing is deduplicated or collapsed. Item ids are minted positionally, which keeps keys unique when two items share a timestamp; it is not a merge.

The caveat that will surprise you: the 300-item cap is global, not per agent, and it is applied after merging. With several busy agents this is not "the last 300 things each agent did". It is the newest 300 events across the whole team, so a quiet agent's recent activity can vanish from the timeline purely because a different agent was noisy in the same window. If you need one agent's full feed, use the News tab in that agent's activity panel.

A second oddity worth naming, because it looks like a bug: rows whose type is not one of the seven recognised message types show a blank To and the agent's own name in From. That is the self-status branch working as designed.

News is a read-only, system-wide activity log, newest first, capped at 300 items. It has no compose or send controls.

Columns are a heat swatch with a blank header, then From, To, Message, and Age. The heat swatch buckets by age: fresh under 60 seconds, recent under 300 seconds, stale under 1200 seconds, and old beyond that. From and To are derived per item, so an outbound item shows the agent as the sender and the counterparty as the recipient, and an inbound item is reversed. Status rows with no counterparty leave To blank.

Only From, To, and Age are sortable. The default is Age ascending, which reproduces newest-first order.

The detail pane below shows the item type and age, a Copy button that flips to Copied, a directional party chip, and the preview text. Any payload renders through a shared viewer with a Reader and JSON toggle; JSON mode adds Expand all, Collapse all, and Copy all. Empty states are No activity yet in the table and Select a news item in the detail pane.

Tasks#

What it is. A read-only view of the manager's task stream, fetched across all teams, preserving the manager's own timestamps unchanged.

How it works. Rows are keyed by task uuid rather than name, deliberately: task names collide across teams, and an all-teams view can hold thousands, so selection and keyboard navigation need a unique key.

Due is derived, not stored. It is the cadence label of the first linked calendar event: the task's first linked event is looked up in the schedule list, and that schedule's cadence becomes the due label. A due date renders faded once the linked event has no upcoming fire, which is how past-due is shown.

Lifecycle and limits. Entirely manager-owned. The app persists no task state and caches nothing across launches; each snapshot replaces the list. Tasks are fetched per team and flattened, so the count scales with team count, and no cap is applied. If the tasks fetch alone fails, the list degrades to empty for that snapshot while the rest of the window stays populated.

The sidebar Tasks badge counts every task in the snapshot. It is not scoped by the toolbar team selector, which only filters the rows, so the badge can read higher than the number of rows you see.

The caveat that will surprise you: Due resolves through the Calendar, and it disappears silently if the link breaks. It is not a field on the task. If the linked schedule is missing from the current snapshot for any reason, whether it was deleted or its team's schedule fetch failed that cycle, the Due cell simply goes blank with no error. A task with several linked events also shows only the first; the rest are invisible in that column, though the inspector's Linked events (N) section lists them all.

Tasks lists the manager's tasks across teams. It is read-only: there are no create or edit controls in this view.

Columns are a status dot with a blank header, then Task, Owner, Team, Status, Due, and Updated. Team hides below 900 px, Due below 820 px, and Updated below 760 px.

Status is the raw manager status rendered as a plain word, with a matching dot:

StatusDotMeaning
doinggreenAn agent has claimed it
todoblueUnclaimed
donedimCompleted

Above the table is a status filter with options All, Doing, To do, and Done. Tasks are additionally narrowed by the toolbar team scope. Sortable columns are Task, Status, Owner, Team, and Updated; Due is not sortable. The default order is lifecycle rank (doing, then todo, then done), then most recently updated, then title.

A task's Due value comes from its first linked calendar event. Tasks with no linked schedule show .

The inspector has a Task section with the fields Title, Name, ID, Status, Owner, Team, Due, Created, Updated, and Completed, plus an optional Description section and a Linked events (N) section. Empty states are No tasks and Select a task to see its detail.

This is the same lifecycle the rest of the platform uses. The statuses here are the todo / doing / done transitions produced by /task create, /task claim, and /task done. See CLI Reference and First Team.

Calendar#

What it is. A view of the manager's schedule system, narrowed to the non-heartbeat schedules. Calendar and Heartbeats are two views that split one resource between them. The split is by kind or title: anything whose kind is heartbeat, or whose title begins Heartbeat:, is excluded here and appears in Heartbeats instead.

How it works. The next fire time is computed in the main process when the snapshot is built, as an absolute timestamp. For a calendar schedule the calculation probes forward day by day for up to 35 days in the schedule's own timezone and returns the first matching instant after now. If nothing matches inside that window, it returns nothing. An inactive schedule always yields no next fire, whatever its cadence.

The view only formats. The Next countdown is the difference between that absolute target and now, recomputed every 10 seconds on a local ticker that is independent of the data pipeline.

Lifecycle and limits. The absolute fire time changes only when a new snapshot arrives. Between snapshots the ticker just recounts against a fixed target.

That has a useful property: when the manager is down the countdown keeps ticking accurately, because the target is already in hand rather than fetched per tick. It will count down to now and then sit at now indefinitely, because only a fresh snapshot can advance the target to the following occurrence. So a schedule that fires while the manager is unreachable shows now and stays there until the manager returns.

The caveat that will surprise you: a schedule more than 35 days out shows , exactly like a disabled one. The next-fire probe gives up after 35 days and returns nothing, and nothing is also what an inactive schedule returns. The row dot dims in both cases. Nothing in the list distinguishes "too far out to compute" from "switched off"; open the detail pane and check the Enabled field.

Calendar shows non-heartbeat schedules only. Heartbeat-kind schedules go to the Heartbeats view instead. It is a flat sortable table, not a month or week grid, and it has no create or edit dialog.

Columns are a status dot with a blank header, then Schedule, Date/Time, Message, Next, and Team. Message hides below 700 px and Team below 900 px. The dot is green when the schedule is enabled and has a next fire, and dim otherwise.

The Next column is a live countdown refreshed every 10 seconds, showing , now, or in {n}s / in {n}m / in {n}h / in {n}d. Sortable keys are Schedule, Next, and Team, defaulting to Next ascending so the soonest fire is first.

When heartbeat schedules exist, a pointer line appears below the list, for example 3 heartbeat schedules — see the Heartbeats section.

The detail pane has a Schedule section with Title, Kind, Enabled, Date/Time, Next fire, Targets, and Team, plus a Message section when the schedule has one. Empty states are No calendar schedules and Select a schedule to see its detail.

Heartbeats#

What it is. The other half of the schedule resource: the heartbeat-kind schedules, one row per agent heartbeat, each paired with a preview of that agent's HEARTBEAT.md read off disk.

The concrete difference between the two schedule views: Calendar answers "when next". Heartbeats answers "what is configured, and what does the agent's checklist say". There is no Next column and no countdown here. A next fire is computed for heartbeat schedules, but this view never renders it.

How it works. Rows come from the same schedules slice as Calendar, filtered by the heartbeat predicate and then scoped by the toolbar team selector. Cadence is precomputed in the main process as the real configured interval, unlike the Agents table's HB column. This is the column to trust for cadence.

The HEARTBEAT.md preview is a genuine filesystem read, not manager data. Because these files live in arbitrary agent working directories outside the app's normal read roots, the main process maintains an exact-file allowlist of just the current agents' HEARTBEAT.md paths, refreshed from every snapshot so it tracks the live agent list. Nothing else in those directories becomes readable.

Lifecycle and limits. The list issues one real file read per visible row, and a row shows until its read lands. Selecting a row reads the same file again in full for the detail pane. The preview is cached per agent, so sorting and selection changes do not re-read.

That cache has a consequence: an edited HEARTBEAT.md will not refresh in the list until the cache entry is dropped. The row dot reflects only whether the schedule is active.

The sidebar Heartbeats badge counts heartbeat-kind schedules strictly. It does not include the title-pattern matches the view itself accepts, so the badge can read lower than the number of rows.

The caveat that will surprise you: there is no manual "fire heartbeat" control in the app. The plumbing is fully built. The main process registers a handler that runs the confirmation policy, shows a native Fire heartbeat for <agent>? dialog, and dispatches the command. It is declared in the interface layer and implemented in the bridge. But no part of the interface ever calls it, so the path is unreachable from the app. To fire a heartbeat manually, use the terminal CLI. The module's own comment describing a confirmed manual fire is stale.

Heartbeats shows one row per heartbeat schedule and target agent pair. It has no create or edit dialogs.

Columns are a status dot with a blank header, then Agent, Cadence, HEARTBEAT.md, and Team. HEARTBEAT.md hides below 700 px and Team below 900 px. The dot is green when the schedule is active and dim otherwise.

The HEARTBEAT.md column shows a single-line preview of the agent's HEARTBEAT.md, read from <workingDirectory>/HEARTBEAT.md, with while it loads. The detail pane repeats Agent, Schedule, Cadence, and Enabled, then renders the full file in a monospace block. When the file cannot be shown you see one of (loading…), (empty), (binary file), (unavailable: {reason}), or (HEARTBEAT.md path unavailable).

Rows are narrowed by the toolbar team scope. Sortable keys are Agent, Cadence, and Team, defaulting to Agent ascending. Empty states are No heartbeat schedules and Select a heartbeat to see its detail.

Library#

What it is. A read-only inventory of the manager's own library: its catalogued agents, skills, and teams, with a detail body per entry.

How it works. Library is the one view in the sidebar that is not part of the snapshot pipeline. It is fetched on demand from the manager over HTTP, never bundled into the snapshot, and it is not one of the six pushed resource kinds. It is not on the WebSocket and not on the 30-second poll.

The list is fetched once per kind change, meaning when you switch between Agents, Skills, and Teams. Selecting an entry lazily fetches that entry's detail.

The detail body differs by kind. Agents use their README, teams use their team YAML, and skills deliberately return nothing from the manager, because the app reads SKILL.md directly off disk instead. That disk read needs a widened read allowlist, and the main process does it as narrowly as possible: on a skill detail fetch it allows exactly that one validated source folder, and clears the allowance for any other kind.

Only the agents list surfaces per-entry errors. The skills and teams paths always report none.

Lifecycle and limits. Nothing is cached in the main process. The window holds the result in view state, which is discarded when you navigate away. An in-flight library fetch is never cancelled. Library also does not participate in the pipeline's partial-error tolerance, so a failed fetch is simply a failed fetch for this view.

The caveat that will surprise you: the Library never refreshes on its own. Everything else in the window reconciles at least every 30 seconds, but this list is fetched only when you change the Library kind. If the manager's library changes while you are looking at it, what you see is stale until you switch to another kind and back.

A second point, because it otherwise looks like a bug: for a skill, the detail body is not what the manager returned. The manager's content is deliberately empty and the body is SKILL.md read from disk, so an unreadable file shows SKILL.md unavailable rather than the description.

The Library group has three sub-sections, Agents, Skills, and Teams, which all render the same browser scoped to that kind.

The Library is read-only. It is an inventory of the manager's agents, skills, and teams. There is no install, deploy, or other mutating action anywhere in it.

Columns are Name and Description, with Description hiding below 720 px and showing when null. Both columns sort, defaulting to name ascending. There is no search box.

Selecting a row fetches its detail. The detail header is titled Agent, Skill, or Team and carries Reveal (opens the entry's source folder, disabled without a source path) and Copy (flips to Copied). The body is a read-only rendering of the content: for skills, the on-disk SKILL.md; for agents and teams, the manager-provided README or team YAML. Placeholders are (loading…), SKILL.md unavailable for skills, and (empty) for agents and teams.

Empty states are No agents in the library, No skills in the library, and No teams in the library.

Title Strip and Status Indicator#

The title strip is the drag region at the top of the window. It holds only three things: the title ID Agents, the Files toggle, and the manager status indicator at the right end. There is no version or URL in the strip.

Status states#

The indicator is a colored dot with a lowercase text label. Every state it can show:

LabelDotWhat it means
livegreenA health probe succeeded. The manager is up
startingamberThe app is probing for, adopting, or spawning the manager daemon
restartingamberA restart is in flight, either user-initiated or from recovery
suspectamberOne or two consecutive health probes failed. A single failure never reads as down
downredThree consecutive probes failed, or a spawn or restart failed outright
connectinggrayNo status received yet. This is the initial state
fixturegrayThe app is running on fixture data rather than a live manager

Health probes run every 5 seconds with a 750 ms timeout per probe. Only live, starting, and restarting pulse, and only when the Blink live indicator setting is on.

When the monitor declares the manager down, a native dialog offers Restart and Not now. It appears at most once per outage.

The Files toggle#

A Files button sits in the title strip to the left of the status dot, with a folder glyph and the tooltip Files (⌘⇧E). It renders as pressed while the browser is open. Clicking it, or pressing ⌘⇧E, toggles the file browser drawer.

File Browser#

What it is. A standalone, single-column filesystem browser rooted at your home directory, shown as a drawer beside any view. It is not tied to the selected agent.

How it works. The main process installs exactly one browse root at startup, your home directory, and containment is enforced there rather than in the interface: .. traversal, absolute escapes, and symlinks whose target leaves the roots are all rejected, with symlinks collapsed by resolving the longest existing prefix of a path.

Listing is strictly lazy. Exactly one directory, the current one, is ever read, and only when it becomes current. Children are never pre-read to decide whether a row can be expanded, and there is exactly one place a listing is requested: when the current directory changes.

Lifecycle and limits. There are no watchers, no polling, and no cache in this panel. A displayed listing is a one-shot snapshot and does not live-update when files change on disk. To refresh a directory, navigate away and back, or toggle the drawer closed and open, which remounts it and re-lists.

The drawer is independent of the manager. Its root comes from the operating system, so nothing in the listing path touches the manager and the browser works whether or not the manager is up.

Registered project folders quietly widen the allowlist in the main process. The effective set of readable roots is your home directory plus every registered project folder, so the underlying capability will serve a project folder outside your home. The interface never navigates there: going up is capped at the home root, and a saved directory outside home falls back at launch. The widening matters for what the app can read, not for where the browser will take you.

The file browser is a right-docked drawer headed Files with a × close button. It is shell-level, so it appears beside any view and is independent of which agent is selected. Nothing auto-closes it. Both its open state and its current directory persist across relaunch. When the profile panel is also open, the profile panel docks to the left of the Files drawer.

What it can browse: your home directory and everything under it. The browse root is set once at startup to your home folder. It is not per-agent and does not follow the selected agent. Going up with is capped at the home root, so you cannot browse above ~.

Directories sort first, then files, each by name. Directories show a 📁 icon and a trailing /; files show 📄. Symlinks, sockets, and FIFOs are omitted from listings.

Symlinks are invisible, not shown and disabled. A symlinked file or directory simply does not appear, because it cannot be navigated safely. If your agent working directories are symlink-heavy, files will look missing when they are merely unlisted.

Hidden files are always listed. There is no dotfile filter at any layer, so opening ~ shows .zshrc, .ssh, and everything else.

InputAction
/ Move the selection
Enter a directory, or open a file in the OS default app
Go up one level, capped at the home root
ReturnSame as
Shift + ReturnCopy the selected entry's absolute path
Single clickSelect the row
Double clickOpen or enter
Right clickSelect the row and open the file context menu

There is no in-app file viewer in this panel. Opening a file hands the path to the OS default application. The browser itself only ever displays the current directory path and the list of entry names, so no size or file-type limit applies to it.

Launch behaviour with protected folders#

macOS gates the contents of Desktop, Documents, and Downloads behind a per-app consent prompt. Listing ~ itself is free, because that only names those folders.

The browser reopens on the directory you last left it in. At launch it falls back to the home root instead of the saved directory when the saved value:

  • is missing or empty;
  • is the home root itself;
  • lies outside the home subtree;
  • contains a .. segment;
  • has a first segment below home equal to Desktop, Documents, or Downloads.

The match is on the whole segment, so both ~/Documents and ~/Documents/a/b fall back, while a sibling like ~/DocumentsFoo is restored normally.

The effect is that no consent prompt fires at launch. Those folders stay fully reachable, and macOS prompts only when you navigate into one yourself.

When a listing fails the list body shows the reason in parentheses, such as (not found); an empty directory shows (empty). If the home directory never resolves, the panel renders File browser unavailable.

That parenthesised reason covers structured refusals: a policy rejection, not found, or not a directory. It does not cover a permission error thrown while reading a directory, which takes a different path in the code and is not reported to the panel. What that looks like in a running app is listed in Known gaps rather than described here, because it depends on operating system behaviour that source alone does not settle.

Terminal Panel#

New in 0.1.8. The terminal panel shipped in 0.1.8. If your Dashboard is older, the button and the ⌘⇧T shortcut below will not be present until it updates itself.

What it is. A real shell inside the Dashboard, in a panel you open from a button beside Files in the title strip or with ⌘⇧T. The title-strip button is labelled Terminal, with the tooltip Terminal (⌘⇧T), and it renders pressed while the panel is open.

The intended use is running a coding agent next to the fleet you are watching. Claude Code runs in it, which is what the panel exists for.

How it works. The panel occupies the lower part of the content area, to the right of the sidebar. The sidebar runs unbroken to the bottom of the window, and the status bar runs full width beneath both. Drag the divider at the top of the panel to resize it, or focus the divider and use the arrow keys.

It is a real login shell: the app runs your $SHELL, falling back to /bin/zsh if that is unset, and starts it in your home directory. You get 5,000 lines of scrollback, and the colours follow the app's light and dark themes, so it re-themes with the rest of the window rather than staying a fixed palette.

⌘K still opens the command palette while the terminal has focus, so you do not have to click out of the panel first.

The panel is independent of the Files drawer. Both can be open at once, because they occupy different axes: Files docks to the right, the terminal sits along the bottom.

Nothing in the app chooses what runs. The app decides the shell and the directory itself, and the panel has no way to be handed a command to execute.

Lifecycle and limits. This version runs a single session. Opening the panel again reattaches to the shell that is already running rather than starting a second one.

The lifecycle is the part worth knowing before you leave something long-running in it:

What you doWhat happens to the running shell
Close the panelNothing. It keeps running. Closing hides the panel, it never destroys the session
Reopen the panelReattaches to the same session
Reload the windowThe process survives and the panel reattaches, but earlier output is not replayed
Quit the appThe shell is killed. You get a confirmation first if something is running
Your trial lock engagesRunning sessions are stopped, and you are told why

Because the app keeps no transcript of a session, a reattached terminal starts visually empty and says so, printing [reattached to the running session — earlier output is not replayed]. The scrollback you had before a reload is gone even though the process is not.

Quitting with a live session shows a native dialog reading Quit ID Agents and end your terminal session? with the detail A shell is still running in the terminal panel. Quitting ends it, along with any command running in it. The buttons are Quit and Cancel, and Cancel is the default.

If the trial gate leaves its operational state while a shell is alive, the session is stopped and a dialog names the cause, whether the trial ended, you were signed out, or a re-verification is needed. Each one is followed by Your terminal session was stopped and any command running in it has ended. The reason is surfaced deliberately: the lock screen covers the panel, so without it a command that vanished mid-task would be indistinguishable from a crash.

The caveat that will surprise you: closing the panel and quitting the app do opposite things. Closing the panel is safe. The shell keeps running and you can reopen it later and carry on. Quitting the app ends it, along with whatever it was doing. If you are running a long Claude Code session in the panel, close the panel freely, but treat quitting the app as ending the work. Reloading the window sits in between: the process lives, but you lose the scrollback.

Open state and panel height both persist across restarts. The height is clamped so the panel can never be dragged smaller than a usable number of lines or taller than 70% of the window, and a corrupted stored height falls back to the default rather than rendering the panel unusable.

Context Menus#

Right-clicking also selects the row you clicked. All context menus open at the cursor, clamp inside the window edges, and close on Escape, a click outside, window blur, or as soon as an action fires. and move the highlight and wrap, Enter activates, and Esc closes.

Right-click opens a menu in four places: the Agents table, the Files browser, a project's changed-files list, and the Projects table.

Agent context menu#

ActionCommandDisabled whenConfirm
Start/agent <name> startThe agent is already runningno
Stop/agent <name> stopThe agent is not runningyes, inline
Rebuild/agent <name> rebuildneverno
Probe/agent <name> probeneverno
Clear session/clear <name>neverno
Delete/delete <name>neveryes, inline

Delete renders in red under a separator. Only Start and Stop are ever disabled, and disabled items are skipped by both the arrow keys and clicks.

The confirmation is inline, not a dialog. The first click rewrites the item's own label to Confirm Stop? or Confirm Delete?, and a second click on that item fires the action. There is no typed-retype step and no separate dialog in this menu.

These are real mutations, unlike the read-only command palette. The result appears as a transient toast that auto-dismisses after about 3 seconds, reading <action> → <agent name> on success and <action> failed: <message> on failure.

File context menu#

Used by both the Files browser and a project's changed-files list.

ActionDoes
OpenEnters the directory, or opens the file with the OS default app
Copy pathCopies the full absolute path to the clipboard
Show in FinderReveals the path in Finder

No item is ever hidden or disabled, and none of them confirm; every one fires immediately. In a project's changed-files list, for a file whose status is deleted, Open and Show in Finder silently do nothing because the file is gone from disk, while Copy path still works.

Project context menu#

ActionDoesConfirm
Copy pathCopies the project's folder pathno
EditOpens the project edit dialogno
RemoveRemoves the project from the registryyes, native dialog

Remove renders in red under a separator, and its label becomes Removing… while the removal is in flight. Its confirmation is a native macOS message box, not an in-app dialog:

  • Message: Remove "<project name>" from Projects?
  • Detail: The project folder and its files are NOT touched. Its snapshot history is moved to the Trash.
  • Buttons: Remove and Cancel, with Cancel as both the default and the cancel button

If you cancel, a banner reads Removal cancelled — nothing was changed.

Toolbar#

The toolbar row sits below the title strip and holds three controls.

Team scope selector. A segmented control labeled Team scope, with All teams first and then one segment per team, each labeled with the team's own name. and move between segments, wrapping. The scope is passed to the Agents, News, Tasks, and Heartbeats views, and it also determines which team the command palette dispatches against.

Agent filter. A search field with the placeholder Filter agents. It matches the agent's name only, as a case-insensitive substring. It does not match team, runtime, model, status, or any other column.

Command palette button. A button labeled ⌘K that opens the palette.

The team scope selector and the agent filter are both hidden on the Projects section, because Projects is not team-scoped. The command palette button stays visible in every section.

Sorting is not a toolbar control. You sort by clicking the table column headers.

Command Palette#

Open the palette with ⌘K, the toolbar ⌘K button, or ? to land straight on the help sheet. Close it with Esc or by clicking the backdrop. Every open resets the buffer, results, and history cursor.

The input placeholder is Type a command — e.g. /status, /teams, /meta <agent>.

KeyAction
TabComplete the current buffer, including command, agent, and team names
EnterSubmit the line
Previous line from history
Next line from history; past the end clears the buffer
EscDismiss the confirmation gate, otherwise close the palette

History holds the last 50 submitted lines and de-dupes an immediate repeat.

The read-only rule#

The palette can run five commands. Only /status, /teams, /meta, /list, and the bare list form of /agents actually execute. Every other command is still parsed, classified, and gated for parity with the terminal TUI, but it is never dispatched. Once you accept the gate it resolves to the message Read-only in the desktop app — run this from the terminal TUI or the dedicated view. The main process re-checks the same allowlist, so the guard is not renderer-only.

Even inside the allowlist, a mutating form is refused: /agents <team> stop trips a gate and is therefore not treated as a read.

Commands#

The catalog is shared with the TUI, so the palette lists all 17 commands regardless of which ones it can run.

CommandDescriptionTierRuns in the app
/helpOpen the scrollable command help (also: ?)safeno
/configsOpen the navigable configs/*.yaml browsersafeno
/listShow all pending queries in the active teamsafeyes
/metaShow agent metadatasafeyes
/outputOpen an agent's navigable ./output browsersafeno
/statusTeam health summarysafeyes
/teamsList all teams in the manager DBsafeyes
/agentPer-agent controlpowerfulno
/agentsList agents, or run team lifecyclepowerfulyes, bare list form only
/cancelCancel an agent's running querypowerfulno
/deployDeploy a team configpowerfulno
/heartbeatToggle or fire heartbeatpowerfulno
/scheduleMutate schedulespowerfulno
/syncSync team against YAMLpowerfulno
/teamShow or switch active teampowerfulno
/deleteDelete agentsdestructiveno
/taskMutate tasksdestructiveno

Confirmation gates#

Powerful and destructive commands are gated before they resolve, even though most of them cannot run here.

  • Y/N gate. A dialog reading Confirm: followed by a preview of what the command would do, with Cancel and Confirm buttons. Confirm is autofocused.
  • Retype gate. A dialog reading Destructive. then Retype <command line> to confirm. The Confirm button stays disabled until the text you type matches the normalized command line exactly. Enter confirms on a match, Esc cancels.

The main input is disabled while a gate is showing. Accepting either gate only advances the flow; the read-only executor still refuses anything off the allowlist.

Suggested commands and help#

With no result showing, the palette body lists contextual actions: Team status, List teams, Pending queries, Metadata: <agent> when an agent is selected, and Settings, which navigates to the Settings page and closes the palette.

A button inside the palette flips between ? Help and Hide help. The help sheet lists every catalogued command grouped under Safe, Powerful, and Destructive. Each row marks whether the palette can execute it, showing run or view/TUI, followed by · Y/N or · retype when the command has a gate. The sheet is generated from the same shared catalog, so it cannot drift from the palette or the TUI.

While a command runs the body shows Running…. An unrecognised command shows Unknown command: <name>, and with no live manager you get Commands need a live manager connection.

Keyboard Shortcuts#

Global#

KeyAction
⌘KOpen the command palette. Works even while typing
⌘⇧EToggle the Files drawer. Works even while typing
⌘⇧TToggle the terminal panel. New in 0.1.8
?Open the palette on the help sheet. Ignored while typing in a text field or when the palette is already open
⌘,Open Settings
Hold Turn agent names into clickable jump-to-profile links
+ click an agent nameJump to that agent's profile without changing the row selection

The standard Edit, View, and Window menu roles are kept, so the OS defaults for copy, paste, zoom, and minimize still work.

Tables and lists#

KeyAction
/ Move the selected row
Home / EndFirst or last row
EnterActivate the selected row, where the view supports it
KeyAction
/ Move between sidebar sections
Home / EndFirst or last sidebar section
/ Next or previous segment in a segmented control, wrapping
/ Grow or shrink the bottom panel when a split divider has focus

Dialogs and drill-downs#

KeyAction
EscClose a modal dialog, or go back from a news or timeline drill-down
Tab / Shift+TabMove focus, trapped inside a modal dialog

The JSON payload viewer uses Enter or Space to toggle a node, to expand, and to collapse. Context menu and command palette keys are listed in their own sections above.

Settings#

There is no sidebar item for Settings. There are two ways in:

  1. The app menu item Settings…, with the accelerator ⌘,.
  2. The command palette, where a suggested action labeled Settings with the hint Open settings navigates there.

A ‹ Back button returns you to the dashboard.

Settings has no Save or Reset buttons. Every control persists immediately, and no setting requires a restart.

How it works. Every setting is a live provider that updates the interface and writes to the main process in the same action, which is why there is no save step and no "next launch" setting in this page.

All of them persist to a single flat JSON file in the app's own state directory, which sits outside the application bundle. Settings therefore survive an app update, because the updater replaces the bundle, not that directory.

Persistence is best-effort by design. A failed write is swallowed silently, and a missing or corrupt preferences file is treated as empty, so an unrecognised or absent key falls back to its documented default rather than erroring. The file is not versioned or migrated.

The Manager section is live status, not configuration. It reads the same lifecycle state that drives the title-strip dot, not a separate settings value.

SectionControls
TrialRead-only status, shown only when the trial gate reports a view. Fields are Status, Account, Reconnect by, and Trial ends, plus a Check again button while in offline grace
ManagerRead-only Status, URL, and Last healthy fields, plus a Restart Manager button
AppearanceTheme, Font size, and Blink live indicator
Agent columnsShow agent profile panel, plus a checkbox per optional Agents column

Theme is a segmented control with Auto, Light, and Dark. The default is Auto, which follows the OS appearance.

Font size is a segmented control with Default, Large, and Larger, scaling the whole app by 1, 1.15, and 1.3 respectively.

Blink live indicator controls whether the title-strip status dot pulses. It is on by default.

Show agent profile panel controls the right drawer in the Agents view. It is on by default.

Agent columns carries the hint Choose which optional columns appear in the Agents table. A checked box means the column is visible. The optional columns are Team, Runtime, Model, Eff, Status, HB, Mem, and Uptime. By default none are hidden. The health dot, Agent, and News columns are identity columns and can never be hidden.

Restart Manager appears only when the manager is locally managed, and its label becomes Restarting… while in flight. The main process owns a confirmation dialog reading Restart the manager? This restarts all agents. with Restart and Cancel buttons. When you are monitoring a remote manager the button is replaced with the hint Monitoring a remote manager — local restart is unavailable.

The restart is a real process cycle, so expect it to take a few seconds. The app sends a termination signal, waits 3 seconds, escalates to a kill and waits another 500 ms if the daemon is still answering, then respawns and polls for readiness up to 20 times at 250 ms intervals. Best case is roughly 3 seconds; worst case is around 8.5. The restart is refused outright if the manager is not local, or if the process on the port does not verify as the app's own daemon.

The caveat that will surprise you: there is no way to change the manager URL from Settings. No such control exists, and no preference stores one. Pointing the app at a different manager is only possible with a --manager-url argument or the MANAGER_URL / ID_MANAGER_URL environment variables. The URL field in the Manager section is a read-only display.

If something other than a manager is already listening on the manager port, the app refuses to spawn over it and reports that the port is in use by another process, rather than starting a second daemon.

Trial gate. The app ships with a 90-day trial. When the gate requires sign-in, has expired, needs a recheck, or errors, a full-screen overlay covers the entire dashboard. Offline grace does not lock the app; it surfaces in the Trial section of Settings instead.

Connect#

What it is. A copy-paste prompt generator that turns an external coding agent, such as Claude Code, Codex, or Cursor, into the operator of the already-running manager.

How it works. The prompt is a pure string template. It performs no filesystem or network access, and the interface does no path building: the manager URL, the admin skill path, the quickstart path, and the writable config root are all supplied by the main process as real values.

Copying uses the standard clipboard interface first, falling back to a hidden-textarea copy for contexts where the modern one is blocked, such as an unfocused window.

Lifecycle and limits. Connect is purely informational. It writes nothing except a single one-time flag, calls no manager endpoint itself, and has no confirmation loop, so the app never learns whether the prompt was acted on.

Connect is a button pinned to the bottom of the sidebar. It opens a modal that gives you a prompt to paste into a coding agent so that agent can drive your fleet as manager.

The modal opens two ways: by clicking the button, or automatically exactly once on a fresh first launch. After that first auto-open it never pops up on its own again.

The caveat that will surprise you: dismissing the first-run modal permanently marks it seen, even if you never read it. The flag flips the instant the modal opens, not when it closes, so pressing Esc or clicking the backdrop by reflex is enough. It will never auto-open again, and you have to know to find the Connect button to get the prompt back.

The modal is titled Connect a coding agent and explains itself verbatim:

ID Agents lets you run teams of agents as background processes, controlled from standard coding agents like Claude Code, Codex, or Cursor. Use this prompt to connect your coding agent as the manager of all the teams.

Below the lede is the generated prompt and a Copy prompt button that flips to Copied. There are no input fields; the modal is display-and-copy only. Focus moves to the copy button on open, Esc closes, Tab is trapped, and clicking the backdrop closes.

The generated prompt embeds your real manager URL and paths, and it tells the coding agent three things:

  1. The desktop app already owns the manager, so do not start, restart, or spawn a second one.
  2. Install the admin-control skill from the given path, however that agent loads skills. The prompt is deliberately agent-agnostic and does not hardcode a Claude Code specific install command.
  3. Check /agents first. If it lists agents, this is a reconnect, so work with them rather than deploying. Only deploy the default team when there are zero agents.

It closes by pointing at the quickstart doc and the read-only dependency root and writable config root.

Auto-update#

Updates are silent until they are ready to install.

The app checks for updates on startup and then every 4 hours, downloading in the background. Checking, downloading, and update errors are all deliberately invisible; there is no progress bar and no error message.

Updates run only when every one of these holds: the build is packaged, the platform is macOS, the app is not Gatekeeper-translocated, and the app is running from /Applications/. Otherwise the updater stays inert and never touches the network. There is no user-facing setting to enable or disable auto-update.

When a new version has fully downloaded, a toast appears:

  • Title: Version <version> is ready
  • Primary button: Restart & Update, which shows Restarting… while it works
  • Dismiss button: ×

Clicking Restart & Update tears down the manager the app spawned, so port 4100 is freed for the new version, then quits, installs, and relaunches. Freeing the port matters: it lets the newly installed version spawn a fresh manager rather than adopting the old one, which would otherwise leave new app code talking to a stale daemon.

If the restart cannot proceed, the app does not quit. The button re-enables and the toast stays, so you can retry.

Lifecycle and limits. Dismissing the toast is per session, not permanent. The dismissal is held in view state and never written to disk, so a dismissed toast for a still-current version reappears on the next launch. A genuinely newer version toasts again without needing a relaunch.

Update errors, including a missing or unreachable feed, are recorded and logged but deliberately never surfaced. There is no error dialog and no crash. A user on a broken update feed sees nothing different.

The caveat that will surprise you: an update installs itself even if you never click Restart & Update. The updater is configured to install on quit, so simply quitting the app normally after a background download has finished installs the update and applies it on next open. The button is a convenience for updating now, not a gate you must pass through to be updated at all. This is also why an ordinary quit with a downloaded update frees the manager port, exactly as the one-click path does.

Known gaps#

Behaviour that source does not settle, recorded here so the rest of the page can stay definite.

A permission denial while browsing does not produce an error message. The listing code path has no handler for a thrown read error, and neither does the layer above it, so a directory read that fails this way updates nothing rather than reporting a reason. What the panel shows in that moment depends on operating system behaviour that cannot be determined from source, so this page does not describe it. The (reason) empty state you can rely on covers policy rejections, not found, and not a directory.

The 1 GiB storage warning has no live push. The warning level reaches the interface only as part of a status refresh, not as its own event, so the icon appears on the next refresh rather than the moment the threshold is crossed.