Repository navigation
Description
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:
PluginSidebarThreadIndicatorcarriesunread-errorandunread-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
pixexid commentedon Aug 19, 2026
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:
da5c6041c6beec2bda5c6041c6beec2bbb plugin reload322e5591f2341d0aT2 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.lightand.darkmoved in lockstep at every step, so the logo is not an escape hatch either.The mechanism, from the bundle:
loadPluginBrandingAssetsruns only inloadOne/populateIdentity, retains the file bytes in memory, and embeds?h=sha256[0:16]in the asset URL;getBrandingAssetserves that cached buffer. There is no watcher, nostat, 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:
PluginNavPanelRegistration— the original filing.headerContentrenders inside the opened panel, so it cannot signal to someone who has not opened it.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.
Paint inbox unread on the nav row, and make it announce its own death
Answer the cold review: transform-aware switch, retained counts, one log
Inbox unread on the nav row, under a narrow dead-man exception (#281)
SawyerHood commentedon Sep 9, 2026
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.