TUI¶
List tabs¶
The five list tabs — Tools, Dots, Agents, Status and Groups — share one table, one navigation model and one hit model, so a key, a click or a wheel tick behaves the same on every tab. Rows are budgeted against the gaps the layout actually renders, so the scroll window matches what is on screen.
| Key | Movement |
|---|---|
j / k, ↓ / ↑ |
One row. Wraps at either end. |
ctrl+d / ctrl+u |
Half a screen, clamped. |
pgdown / ctrl+f, pgup / ctrl+b |
A full screen, clamped. |
home |
First row. |
G / end |
Last row. |
While a filter query has focus the arrows and the ctrl chords still move the
selection; j and k stay text and reach the input.
Every list tab puts the same thing in the same place. The header counts what the
tab holds. The body opens with the source of truth the tab reads — the dotfiles
repo, ~/.apm/apm.yml — followed by that source's own state: ✓ clean or
✗ dirty with the pending changes, a workspace that is not ready with what to
do about it. The footer carries what is happening right now: the command running
and how it ended. Short-lived messages go to the status bar instead.
A left click selects the row under the pointer on any of the five tabs, and on
the Tools tab it also picks the provider and group filter pills. Clicking the
tab bar switches tabs. A click below the last row, or anywhere while an overlay
is open, reaches nothing. The wheel moves the selection one row, exactly as
j/k do, except over an open trace log or dots preview, which it scrolls.
Dots at launch¶
Starting the TUI runs a dotfile link repair before the first frame settles —
missing links are created and a local file newer than its repo source is
adopted into the repo. The footer reads Syncing dots… while it runs, row keys
are ignored until it finishes, and conflicts are left for u/l. The rules are
in Dotfiles.
Agent status¶
The Agents view is a navigable per-package list: one row per declared or locked
package with its author, version and agents, in the column order the Tools tab
uses. Status is the leading glyph rather than a column, and is one of installed,
drifted, unavailable, missing or orphaned. Rows come from reading
~/.apm/apm.yml and ~/.apm/apm.lock.yaml directly, never from parsing APM's
table output. installed means the entry is present in the lockfile, either
because the manifest declares it or because a package the manifest declares
resolved it; orphaned means nothing in the manifest leads to it at all. An
author the package does not declare falls back to the owner its source is
published under. A package can also be
drifted when a bundled MCP/LSP child conflicts, has multiple owners, or has
degraded runtime health.
Below the packages, two more sections list the manifest's mcp and lsp
servers, joined to the lockfile's mcp_servers / lsp_servers by name. Their
targets come from the manifest's top-level targets: (LSP intersected with the
only targets APM deploys it to, claude and copilot) — never from the
lockfile, whose mcp_target_servers records only the last install. A locked
entry whose command binary is not on PATH is reported unavailable: APM writes
such entries to the lockfile without ever checking that they can run. An empty
section is omitted.
Ownership and runtime health¶
Package-owned MCP/LSP children render under the selected package as
provides:, sorted by kind and name, instead of appearing again as top-level
service rows. issues: shows duplicate, conflict, ambiguity, unavailable
ownership evidence, or child-health problems and degrades the package row.
Unavailable ownership evidence appears only when standalone MCP/LSP entries
make ownership relevant; a package-only workspace is not degraded. Search
matches provided child names and issue text.
An exact standalone duplicate is hidden from the top-level service section and
reported on its owner package. A conflicting standalone declaration remains
visible with conflicts with package <owner> while the package shows the same
problem. Multi-owner ambiguity marks every involved package. Independent and
unmanaged services remain top-level. Package-owned child health never improves
an already missing, orphaned, unavailable, or drifted package.
Those two sections also read the deployed harness files — ~/.claude.json
(mcpServers, lspServers) and ~/.codex/config.toml ([mcp_servers.<name>])
— which is the one documented exception to "the TUI hard-codes no client names":
APM records no per-entry deployment anywhere, so nothing else can see them. A
name in a harness file that APM neither declares nor locks is orphaned with
detail unmanaged and the harnesses it was found in as its targets; APM prunes
only names it locked itself, so nothing else will ever report it. A codex entry
with enabled = false still counts — it is deployed config APM cannot see.
An MCP row whose lockfile mcp_configs entry disagrees with what
~/.claude.json actually deploys is drifted. Only command, url, and args
are compared; headers and env are not, because they carry secrets and the two
harnesses store them differently. Lock values are expanded through the
environment first, and an entry referencing an unset variable is skipped rather
than reported as drift. Drift is a status, never a value diff — no deployed
value is ever rendered. LSP entries never drift: APM rewrites them from
lsp_configs on every install. Codex-side value drift is not detected; codex
gets orphan detection only.
Sync fails closed on two APM 0.31.0 hazards, before APM is invoked at all
(dry runs included, so a preview that would fail for real says so): a --frozen
sync whose manifest declares an LSP server missing from the lockfile is refused,
because --frozen does not check LSP entries and would silently install and lock
it; and declaring lsp entries whose targets: intersect neither claude nor
copilot is refused, because APM deploys and locks everything else before it
fails on that combination, leaving the sync half done.
Sync also fails closed on exact package-child duplicates, differing definitions, multi-owner ambiguity, and unavailable package evidence when the same template declares standalone MCP/LSP services. These checks run before the live manifest is materialized, including for dry-run.
Items APM does not manage¶
A final section, Not managed by APM, lists the native Claude and Codex
plugins, MCP servers and marketplaces this host has installed outside APM. It
is the same inventory omni agents drift prints, so a row here means the
artifact exists on the host but no APM manifest declares it. Rows read
unavailable, except an ignored one, which reads orphaned: it is deliberately
outside APM, not damaged. The tab header counts them as N native, beside the
per-status rollup it carries for the packages and services. The
section is omitted when there is nothing to report and when the clients cannot
be read.
The row's detail block carries the client, the kind, the state, the file or CLI
record it was read from, and its install root. The state is one of
not declared in the host template, retained with the classifier's reason
(the migration classifier will not import it — a marketplace with no APM
source, say), or ignored with the recorded reason.
Three keys act on the selected native row, and the ones a row can offer appear in its own detail block:
| Key | Action |
|---|---|
x |
Ignore the artifact, or unignore it when it is already ignored. Writes only agents.ignored in settings.json; the host template is never touched. |
a |
Adopt: declare the artifact in the host template. Offered only for a row the classifier can import, and refused while APM is running, because adopt writes the template sync reads. |
d |
Remove the artifact through its own client CLI, after a second press. |
x and a report select a row under Not managed by APM first when the
selection is elsewhere; d falls through to the package uninstall it also
serves. An ignored row offers neither adopt nor remove — the entry exists to
say this one stays — and says to press x first.
Removal runs the client's own command by identity (claude plugin uninstall,
claude mcp remove -s user, claude plugin marketplace remove, and the Codex
equivalents), so no filesystem path is ever derived from a package-supplied
name. Adoption writes the manifest entry only: the artifact is deployed by the
next omni agents sync, and the status line says so. Neither key runs APM.
Navigation and package actions¶
/ filters every section by name and by source or transport, hides the
sections it empties, and reports how many rows are showing; the arrow keys keep
moving the selection while the query has focus. The selected row always carries
its own detail block beneath it, as the tools and dots rows do: the package's
description first, then its source, author and deployed-file count; for a
service, transport, command basename, URL host, and the harnesses it is deployed
to. Descriptions
come from the package's own apm.yml under ~/.apm/apm_modules/, so a package
without one says no description available. Only a URL's host and a command's
basename are shown, so credentials, header values, and install paths never reach
the screen.
u updates the selected package and d uninstalls it after a second press;
both keys, and the reason a row cannot offer them, appear in that row's own
detail block rather than the footer, which carries only tab-wide actions. A
failed row op reports between the row and its details.
Omni does not decide for APM which rows it will accept: both keys dispatch and
APM answers, and a refusal reports on the row where it can be read. Only facts
about the row itself withhold a key — a pinned row has no version picker yet
(change the pin in the host template), a local path is never part of an update
plan, a row that is not installed has nothing to act on, and mcp/lsp entries
are removed by editing ~/.apm/apm.yml and re-running S. A key pressed while
the tab is checking for updates is queued rather than refused, because that check
is an APM command of its own and two cannot run at once; it runs when the check
finishes. Queueing sits behind the uninstall confirmation, so it never turns one
press into a removal. Both keys act on the live workspace only, so the
host-template hint sits under the workspace path: a package the template still declares comes back on the
next sync, and a package installed without being declared is lost at the next
one. An uninstall re-deploys the surviving packages and can drop their trusted
bin/ executables; the repair is apm install -g --trust-bin <pkg>. A row op
still runs APM's scope-wide passes, so its workspace-wide summary counts are
dropped from the footer — the full output stays in the trace log.
Registry and tab actions¶
i opens the registry: every plugin the registered marketplaces offer, read
from ~/.apm/marketplaces.json and the catalogs under
~/.apm/cache/marketplace/. APM files a catalog under the registered
marketplace name, except a URL marketplace, whose filename it hashes, so a
catalog that does not match by name is resolved by the name it carries inside.
Type to filter and use the arrow keys (or ctrl-n/ctrl-p, home/end) to
move the selection while typing continues — j and k stay text. enter
installs the selected plugin after a confirm, esc leaves. Entries already
present in the lockfile are marked and cannot be re-installed. Sync never
refreshes an existing cache, so a marketplace with no resolvable catalog says
to run apm marketplace update.
It also exposes sync/install (S), update (U), refresh (R), and the trace
log (e). S runs the same lifecycle as omni agents sync, so it materializes
the host template before installing and reports the divergence warning below
the list. The TUI takes no flags; use the CLI for --frozen, --dry-run, or
--force-template. U dispatches APM. The view checks for package updates on
startup; R reloads the manifest and lockfile and runs that check again.
Available versions appear in a separate Updates Available section. A failed
check leaves the package rows usable and offers R to retry. Full APM output
goes to the trace log, not the pane.
Agent desired/runtime state is owned by APM (~/.apm/apm.yml, lockfile, and
~/.apm/marketplaces.json). Targets come from APM, including targets added after
Omni was built; the harness readers above are the only place a client name is
hard-coded, and every one of them is read-only.