---
title: Header widgets
---

# Header widgets

## What it is

**Header widgets** are the named controls in Omniscio's top titlebar. Clicking a widget either triggers an action directly or opens an inline popover panel.

The header's right side is a **drag-to-reorder row of blocks**. The **Shortcuts block** is the rounded pill of pinned toolbar icons (which still reorder *inside* the pill). The **`account`** (Balancer) and **`hardcore`** widgets are pulled out of the pill and render as standalone **bare blocks** (no pill background) alongside it. The user drags blocks left/right to arrange them; the arrangement persists in `headerBlockOrder`.

Header widgets are not hardcoded — they live in the toolbar registry and render through the same pinned-item path (`ToolbarPinnedItem`) as every other toolbar control, so users own their header layout completely.

## Where to find it

### The named widgets

| Widget | Label | What it does |
|--------|-------|--------------|
| `account` | Account | Opens the account-switcher popover — shows usage, lets the user switch or add accounts, and links to Settings → Accounts. Always visible. |
| `hardcore` | Hardcore Mode | Shows the skull icon and look count while hardcore mode is both revealed and switched on. Hidden automatically when the mode is off. |
| `cpu-burst` | CPU Burst | Performance mode toggle. Visible only in dev builds (`import.meta.env.DEV`). |
| `helpdesk` | Get Help | Help desk console. In-development, visible only when the `helpdesk` unreleased feature is enabled. |
| `land-queue` | Land Queue | Two counts: branches waiting on the auto-lander, and worktrees the ledger is holding open. Opt-in — it is not pinned by default, so it sits in the overflow until you add it. |

## How it behaves

### How users manage widgets

There are two levels of arrangement:

- **In the header (drag)** — drag a whole block to reorder the blocks (the Shortcut bar via its hover **grip**, or a widget block directly — a tap still opens the widget's panel; a drag reorders). The block order saves to `headerBlockOrder`. Inside the Shortcut bar, drag individual icons to reorder them within the pill (unchanged).
- **In Settings → Widgets** (formerly "Toolbar") — **show/hide** any of the named widgets and reorder the icons *within* the Shortcut bar. Note: reordering a widget here no longer moves its header *block* position — the header drag does that.

Hiding a widget removes its block from the header (it stays in the registry — re-add it any time from the removed-items section). The only widget that can never be hidden is the **Settings gear** (`settings`).

### Existing users

When the `account` and `hardcore` widgets shipped, a one-time startup migration (`runToolbarPinMigration`, flag `headerWidgetsPinMigrationCompleted`) back-filled both IDs into the `pinnedToolbarItems` setting for users whose stored list predates these registry IDs. New installs see them in the default pinned set from the start.

## For agents

### The WidgetShell building block

Interactive widgets (those that open an inline popover) use the shared `WidgetShell` component ([src/renderer/src/components/ui/WidgetShell.tsx](../../src/renderer/src/components/ui/WidgetShell.tsx)). It handles all the common plumbing:

- Uncontrolled or controlled open/close state
- Click-outside and Escape key dismissal
- Focus trap inside the panel
- Opaque elevated panel container (correct surface and shadow via `ToolbarPopoverPanel`)
- Optional viewport flip (panel opens upward when it would clip the bottom of the viewport)
- Optional lazy panel (content is not mounted until the first open, avoiding unnecessary IPC calls)
- Full ARIA wiring (`aria-haspopup`, `aria-expanded` on the trigger; `aria-label` on the panel)

A widget author provides the trigger visuals (a default icon button, or a custom `renderTrigger`) and the panel content. All interaction logic lives in `WidgetShell`.

Example:

```tsx
<WidgetShell
  ariaLabel="Notifications"
  panelAriaLabel="Notification panel"
  tooltip="Notifications"
  icon={Bell}
  dataUiAnchor="app-notification-bell"
  panelClassName="w-80"
>
  {({ close }) => <NotificationList onDismiss={close} />}
</WidgetShell>
```

### How plugins contribute a header widget

Plugins contribute header buttons via the **existing, already-shipped** `toolbar.setItems()` API (part of the plugin UI contributions surface). This API:

- Requires the `chrome` plugin permission (gated in Main via `requirePluginUiAccess()`).
- Is desktop-only — the phone/web bridge blocks it.
- Uses replace-semantics per plugin (a new `setItems` call replaces the plugin's previous set).
- Routes click events back to the plugin's own webview as `command:<id>` messages.
- Accepts an icon by **name** (a registered icon identifier), not a URL or arbitrary component.

No new permission or plugin capability was introduced for header widgets. Plugins cannot inject arbitrary React components into the header — all rendering stays within Omniscio's own `ToolbarPinnedItem` render path.

Full details: [plugin-ui-contributions-contract.md](../../.claude/memory/contracts/plugin-ui-contributions-contract.md).

### Persisted keys

The setting keys `pinnedToolbarItems` and `hiddenToolbarItems` and the individual widget IDs (`account`, `hardcore`, `cpu-burst`, `helpdesk`) are byte-stable. The **`headerBlockOrder`** setting (added for the block row — entries are widget ids plus the `'shortcuts'` sentinel; default `['account','hardcore','shortcuts']`) is additive and defaults cleanly for existing users, so no data migration was needed.

### Where to look in the code

- **Registry** — `TOOLBAR_ITEMS`, `DEFAULT_PINNED_TOOLBAR_ITEMS`, `NON_REMOVABLE_TOOLBAR_IDS`, `HEADER_WIDGET_BLOCK_IDS` (the `{account, hardcore}` single source of truth for which ids become bare blocks): [src/renderer/src/features/toolbar/toolbar-catalog.ts](../../src/renderer/src/features/toolbar/toolbar-catalog.ts)
- **Types and helpers** — `ToolbarItemDef`, `getPinnedItems`, `getVisibleItems`, `getPinnedEntries(…, excludeWidgetBlocks)`, `getHeaderBlocks` (resolves the ordered block row), `SHORTCUTS_BLOCK_ID`: [src/renderer/src/features/toolbar/toolbar-items.ts](../../src/renderer/src/features/toolbar/toolbar-items.ts)
- **Block render + drag** — the outer block row (`useDragReorder`, horizontal) with the Shortcut pill + bare widget blocks: [src/renderer/src/features/toolbar/AppToolbar.tsx](../../src/renderer/src/features/toolbar/AppToolbar.tsx)
- **Render path** — pinned items rendered via `renderPinnedItem`; block reorder writes `headerBlockOrder`: [src/renderer/src/app/AppTitlebar.tsx](../../src/renderer/src/app/AppTitlebar.tsx)
- **Widget renderer** — `ToolbarPinnedItem` (where `AccountIndicator`, `HardcoreIndicator`, etc. live): [src/renderer/src/features/toolbar/ToolbarPinnedItem.tsx](../../src/renderer/src/features/toolbar/ToolbarPinnedItem.tsx)
- **WidgetShell building block**: [src/renderer/src/components/ui/WidgetShell.tsx](../../src/renderer/src/components/ui/WidgetShell.tsx)
- **Pin migration** — startup task `Header widgets pin migration`: [src/main/startup/registry.ts](../../src/main/startup/registry.ts)
- **Settings pane** — the "Widgets" manager: [src/renderer/src/features/toolbar/ToolbarSettings.tsx](../../src/renderer/src/features/toolbar/ToolbarSettings.tsx)
- **Guard test**: [tests/unit/lint/header-widget-contract.test.ts](../../tests/unit/lint/header-widget-contract.test.ts)
- **Contract**: [.claude/memory/contracts/header-widget-contract.md](../../.claude/memory/contracts/header-widget-contract.md)

## Related

[Hub jumper](hub-jumper.md) covers the keyboard-first route to the same destinations the toolbar's icons open.
