Skip to content

Placements & Navigation

Placements control where your app’s UI appears in the NimbleBrain shell layout. Each placement maps a ui:// resource to a slot in the shell and optionally registers a route for navigation.

The shell layout has the following slots:

Slot Where it renders Description
sidebar.<group> Left sidebar, under the focused workspace’s Apps heading, and the workspace overview’s app grid Your app’s entry in the workspace. Every sidebar.<group> placement renders in the one Apps group, whatever the group name. Give it a route.
sidebar No nav entry for a connector Reserved for the host’s own views (Conversations, Automations, Files). A connector’s sidebar placement with a route still registers that route.
sidebar.bottom The bottom of the mobile navigation drawer A pinned item. It does not render in the desktop sidebar.
main A route, with no nav entry A full-page view. Requires a route. Reachable by URL or when your app navigates to it.
settings The connector’s settings page Your connector’s settings component, rendered under a Settings heading on Settings → Connectors → your connector, after its connection sections and before tool permissions. See Settings sections.

An unknown slot is dropped, never fatal.

Add a placements array to your manifest’s _meta["ai.nimblebrain/host"]:

manifest.json
{
"_meta": {
"ai.nimblebrain/host": {
"host_version": "1.0",
"name": "Tasks",
"icon": "check-square",
"placements": [
{
"slot": "sidebar.apps",
"resourceUri": "ui://dashboard",
"priority": 30,
"label": "Tasks",
"icon": "check-square",
"route": "tasks"
},
{
"slot": "settings",
"resourceUri": "ui://settings"
}
]
}
}
}

Within a slot, placements sort by priority, ascending, then by label (or route) alphabetically. The default priority is 100.

priority: 10 → appears first
priority: 50 → appears second
priority: 100 → appears third (default)

The sidebar shows the first four apps under Apps, then a View all link to the workspace overview, which lists every app. There is no grouping by priority band.

A placement with a route registers a URL inside the workspace, under /w/<slug>/app/:

route value URL
"tasks" /w/<slug>/app/tasks
"my-app/reports" /w/<slug>/app/my-app/reports

Clicking the app in the sidebar opens that URL and loads the placement’s resourceUri in the main area, beside the docked chat.

The virtual primary resource path resolves to the first declared placement’s resourceUri, so the shell can load your main view without knowing its URI ahead of time. See UI Resources for how primary resolves.

The schema also accepts a primaryView object for back-compat, but the host does not consume it: placements are the source of truth for what is registered and routed. Declare a placement, not primaryView.

Under the focused workspace, the sidebar shows:

  1. The host’s views: Conversations, Automations, Files, and the Inbox.
  2. Apps: sidebar.<group> placements, sorted by priority, capped at four with a View all link.
  3. Connectors: a link to the workspace’s connectors.

The group name in sidebar.<group> is not shown as a heading; every group renders under Apps. A main placement never appears here. To give your app a nav entry, declare a sidebar.apps placement with a route.

On narrow screens the sidebar becomes a drawer. sidebar.bottom placements render at its foot, linking to their route. A sidebar.bottom placement with route: "settings" is not rendered: declare connector settings with the settings slot.

Specify icons using Lucide names in kebab-case:

{ "icon": "check-square" }
{ "icon": "message-square" }
{ "icon": "database" }
{ "icon": "settings" }

The resolver accepts kebab-case or PascalCase ("message-square" and "MessageSquare" both resolve to the MessageSquare component). If the name is not found, CircleDot is used as the fallback.

The size field ("compact", "full", "auto") is accepted as a hint. The shell does not read it. A main or sidebar.<group> view fills the main area; a settings component is sized to its content.

The PlacementRegistry is an in-memory store that tracks all active placements. It is updated when connectors are installed or uninstalled.

Every entry is either ambient (no wsId — platform apps like Conversations, Files, Automations; always present inside any workspace) or workspace-scoped (installed connectors, visible only to members of that workspace). The registry exposes a single read method, forWorkspace(wsId), which returns ambient + scoped entries merged and sorted. There is deliberately no “return everything” accessor — in a multi-tenant host, no legitimate caller wants placements unrelated to a workspace.

Registration — When a connector starts, its declared placements are registered via register(serverName, placements, wsId) against the installing workspace’s wsId. Platform apps register ambient (no wsId). The virtual primary resource path then resolves to the first registered placement’s resourceUri.

Querying — forWorkspace(wsId) returns ambient entries plus entries scoped to wsId, sorted by slot then priority (lower first).

Unregistration — When a connector is uninstalled, its placements for that workspace are removed. Other workspaces’ entries for the same connector are untouched.

// Register explicit placements, scoped to a workspace
registry.register(
"my-app",
[
{ slot: "sidebar", resourceUri: "ui://nav", priority: 50, label: "My App" },
{ slot: "main", resourceUri: "ui://dashboard", route: "my-app" },
],
"ws_engineering",
);
// Read the merged ambient + workspace-scoped list
const items = registry.forWorkspace("ws_engineering");
// → sorted by slot then priority
manifest.json
{
"_meta": {
"ai.nimblebrain/host": {
"host_version": "1.0",
"name": "CRM",
"icon": "users",
"placements": [
{
"slot": "sidebar.apps",
"resourceUri": "ui://dashboard",
"route": "crm",
"label": "CRM",
"icon": "users",
"priority": 50
}
]
}
}
}

This adds CRM under the workspace’s Apps heading and to the workspace overview, and loads ui://dashboard at /w/<slug>/app/crm when clicked.