Skip to content

Plugin nav entries cannot show an unread badge or count, so a plugin inbox is invisible until opened #1852

Closed

Description

@pixexid

What we hit

We built an operator message inbox as a plugin nav panel. It works. But there is no way to tell the operator that messages are waiting without them opening it.

The concrete failure: the operator's original complaint was that agent-directed messages to them were invisible. We built an inbox to fix that. The inbox itself is now invisible in the same way — one level up. A message arrives, nothing in the UI changes, and the operator finds out when they happen to look.

What the SDK offers today

PluginNavPanelRegistration (0.4.1) exposes:

interface PluginNavPanelRegistration {
  id: string;
  title: string;
  icon: string;
  path: string;
  component: ComponentType<PluginNavPanelProps>;
  headerContent?: ComponentType<PluginNavPanelProps>;
}

headerContent is documented as rendering "on the right side of the shared title bar", which is the opened panel's title bar. It renders a count for someone already looking at the panel and does nothing for someone who has not opened it, so it does not address this.

We checked the obvious near-misses before filing:

  • PluginSidebarThreadIndicator carries unread-error and unread-success, which looked promising, but it belongs to the experimental thread-list registration rather than to plugin nav entries.
  • DOM or CSS injection and shadow-nav approaches are explicitly rejected by our own capability notes and we did not attempt them; we would rather have a supported contract than a fragile one.

What would solve it

Some way for a plugin to declare a count or indicator on its nav entry, host-rendered. A minimal version would be enough — an optional numeric count, or even a boolean "has attention", updated by the plugin:

interface PluginNavPanelRegistration {
  // ...
  /** Optional host-rendered badge on the nav entry. */
  badge?: ComponentType<PluginNavPanelProps> | { count: number } | { indicator: "attention" | "none" };
}

We are not attached to the shape. The property we need is that it renders where the operator looks before deciding to open anything, and that the host owns the rendering so it matches the rest of the chrome.

Why this generalizes

Any plugin surfacing asynchronous inbound state has this problem: a review queue, a notification feed, a pending-approval list, an inbox. All of them can render their state perfectly once opened and none of them can ask for attention. The nav entry is the only place a plugin appears when its panel is closed, and it is currently static.

What we are doing meanwhile

Nothing, deliberately. We considered a count in the panel header and labelled it a partial that does not solve the problem, and we declined the injection workarounds. We would rather carry the gap visibly than ship something that looks like a fix and is not.

Activity

  1. pixexid commented on Aug 19, 2026

    @pixexid
    Author

    Measured: plugin-owned branding assets are read once at load, so they cannot carry live state

    We filed this asking for a nav-entry badge. Before waiting on it we tested whether a plugin could approximate one by rewriting its own icon file at runtime — the plugin directory is plugin-writable, so the file can change. The question was whether the renderer would notice.

    It does not. Observed at the HTTP layer rather than by eye:

    registered icon hash bytes actually served
    T0, square glyph da5c6041c6beec2b square
    T1, circle written to disk, no reload, +5s da5c6041c6beec2b square
    T2, same file, after bb plugin reload 322e5591f2341d0a circle

    T2 is the control: the same bytes the host ignored at T1 are picked up at T2, so T1 is the host declining to re-read rather than the write failing. branding.logo.light and .dark moved in lockstep at every step, so the logo is not an escape hatch either.

    The mechanism, from the bundle: loadPluginBrandingAssets runs only in loadOne / populateIdentity, retains the file bytes in memory, and embeds ?h=sha256[0:16] in the asset URL; getBrandingAsset serves that cached buffer. There is no watcher, no stat, and no revalidation.

    We did not check the rendered glyph visually and did not need to — a plugin-owned icon paints as mask-image: url(<iconUrl>), and at T1 both the URL and the bytes behind it are byte-identical to T0, so the pixels cannot differ. The server-level check is strictly stronger than a visual one.

    Why this strengthens the original request

    The workaround space is now empirically closed rather than merely unattractive:

    • No badge or count field exists on PluginNavPanelRegistration — the original filing.
    • headerContent renders inside the opened panel, so it cannot signal to someone who has not opened it.
    • Branding assets are load-scope, so a self-rewriting icon signals only after a reload.

    A signal that becomes true only after a plugin reload is worse than no signal — it is wrong for an unbounded interval, and an operator who learns to trust it learns something false. We are not shipping it, and we would rather carry the gap visibly than paper over it.

    What we would still use

    Anything host-rendered that a plugin can update: a count, a boolean attention flag, or a revalidation path for branding assets. The first two remain our preference as filed. The third would be a smaller change on your side and would also make plugin-owned icons useful for other state, but it is strictly the weaker option since it makes every plugin invent its own signalling vocabulary.

    We have left a regression test in our repo that fails if any of these surfaces appears, so we will notice when one lands rather than polling this issue.

  2. added
    pluginsPlugin SDK, runtime, marketplace
    uiApp shell, sidebar, composer, rendering
    on Aug 19, 2026
  3. SawyerHood commented on Sep 9, 2026

    @SawyerHood
    Collaborator

    Closing as addressed by #1362. Plugins can contribute desktop sidebar badges and indicators through experimental_sidebarAccessory. The maintainer has accepted desktop coverage as sufficient for this request; compact navigation does not currently render the same accessory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    pluginsPlugin SDK, runtime, marketplaceuiApp shell, sidebar, composer, rendering

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions