Skip to main content

Global Launcher

<AiAssistantLauncher> is a reusable component that renders an "AI" button in the backoffice topbar, opens a Cmd-K-style picker over every agent the caller can launch, and embeds the chosen agent's <AiChat> in a right-side sheet. It is mounted unconditionally in AppShell and self-hides when the AI runtime is not configured.

A single keystroke — Cmd/Ctrl+L — opens the picker from anywhere in the admin.

Global AI assistants picker with search input, three registered agents, and Can write badges

Why a global launcher

Per-page triggers (the People list Ask AI button, the deal-detail header trigger, the merchandising-assistant sheet) are great when the operator is already on the right page. The global launcher covers the other case: the operator wants an agent regardless of where they are. It also scales: with 20 agents, a popover stops being usable — the search dialog handles arbitrary counts.

What ships

  • A rounded-rectangle pill button in the topbar that mirrors the global-search trigger styling (Sparkles icon + "AI" label + ⌘L kbd hint).
  • A modal dialog with a search input and arrow-key navigation listing every typed agent the caller is allowed to invoke.
  • Per-row "Can write" badge for agents whose effective mutationPolicy is confirm-required or destructive-confirm-required.
  • An <AiChat> right-side sheet opened on agent select.
  • A global keyboard binding for Cmd/Ctrl+L (preventing the browser's default focus-address-bar binding when an Open Saasframe page has focus).
  • Self-hiding behaviour: the launcher renders nothing when /api/ai_assistant/health returns non-2xx or /api/ai_assistant/ai/agents returns zero accessible agents.

Component contract

import { AiAssistantLauncher } from '@saasframe/ui/ai'

<AiAssistantLauncher />

That is the entire surface the topbar uses. Every prop is optional:

PropDefaultPurpose
variant'topbar'Reserved for future placements; both values render the same trigger today
agentsEndpoint/api/ai_assistant/ai/agentsOverride for tests or custom dispatchers
healthEndpoint/api/ai_assistant/healthOverride for tests or hosts that gate visibility differently
skipHealthCheckfalseSet when the host already knows AI is configured (e.g., feature-flag gating in tests)
disableGlobalShortcutfalseDisable Cmd/Ctrl+L for nested launchers in dialogs
classNameExtra classes merged onto the trigger

Where it is mounted

packages/ui/src/backend/AppShell.tsx includes the launcher next to the existing topbar action group, before the profile dropdown:

{renderedTopbarInjectedActions}
<AiAssistantLauncher variant="topbar" />
{rightHeaderSlot ? rightHeaderSlot : <span>...</span>}

You should rarely need to mount the launcher directly. It is exported so:

  • Standalone apps with custom chrome can drop it into their own header.
  • Per-page launchers (e.g. on a public marketing surface) can reuse the same dialog without rebuilding the picker.

How visibility is decided

The launcher fetches both endpoints once when it mounts:

GET /api/ai_assistant/health → { healthy: true } (or 4xx/5xx)
GET /api/ai_assistant/ai/agents → { agents: [...] } (already filtered by ACL)
ResultOutcome
Health 2xx + ≥1 agentTrigger renders, shortcut is bound
Health non-2xxTrigger never renders; shortcut is not bound
Health 2xx + 0 agentsTrigger never renders (the user has access to no agents)
Network error on eitherTrigger never renders

This is intentional: an operator without ai_assistant.view (or a tenant whose admin removed every agent feature) should not see a dead AI button.

Keyboard shortcut

Cmd/Ctrl+L opens the picker from any backoffice page.

ComboUsed byNotes
Cmd/Ctrl+KGlobal searchReserved — never reuse
Cmd/Ctrl+JOpenCode command paletteReserved — used by the legacy Code Mode chat
Cmd/Ctrl+LAI launcherNew — preventDefault()'d against the browser address-bar binding when an Open Saasframe page has focus

The launcher ignores the shortcut when focus is inside an <input>, <textarea>, <select>, or any contenteditable region — typing L while composing a message never opens the dialog.

Picker UI

Inside the dialog:

  • Search input — filters by id, label, description, moduleId, or keywords.
  • Arrow keys — navigate the highlighted row.
  • Enter — launch the highlighted agent.
  • Escape — close.
  • Mouse hover — highlights the row under the cursor (so click-to-launch is unambiguous).
  • Bottom hint bar — shows the navigation kbd hints + Cmd/Ctrl+L.

The "Can write" pill on a row reflects the agent's code-declared policy. A per-tenant downgrade to read-only does not change the pill (the pill describes intent, not the current envelope) — but the runtime still strips every isMutation: true tool when the override is read-only.

Adapting for other surfaces

Want a launcher inside a customer portal page, or scoped to a specific module? Use the lower-level pieces:

import { AiAssistantLauncher } from '@saasframe/ui/ai'

<AiAssistantLauncher
agentsEndpoint="/api/portal/ai/agents"
healthEndpoint="/api/portal/ai/health"
disableGlobalShortcut
/>

The launcher's <AiChat> sheet uses an empty pageContext={{}} because the picker is intentionally page-agnostic. Per-page triggers continue to embed <AiChat> directly with their own pageContext={{ entityType, recordId }} — those are unaffected by the launcher.

See also