@ext.menu_item reference
@ext.menu_item reference — contribute an entry to the Panel's top-right user menu: navigation links vs dispatched handlers, sections and ordering.
@ext.menu_item contributes an entry to the user menu behind the avatar in the Panel's top right — the menu that holds profile, theme and sign-out.
Use it for a destination the user reaches occasionally and deliberately: a workspace, a settings screen, an admin tool. It is the opposite of a tray item: the tray is glanceable state, the menu is a deliberate destination.
New in SDK 5.10.0
Before 5.10.0 this menu was a hardcoded list of four links with no way in. It is now composed: the host renders its own entries and every declared one through the same path. Requires kernel support for the published menu surface; older kernels ignore the key.
Where it lives
@ext.menu_item is a method on the Extension instance, not on ChatExtension.
from imperal_sdk import Extension
ext = Extension(
"my-app",
version="1.0.0",
display_name="My App",
description="My App — AI-powered tool for managing your resources.",
icon="icon.svg",
actions_explicit=True,
)
@ext.menu_item("workspace", label="My Workspace", icon="LayoutGrid", path="/ext/my-app")
async def menu_workspace(ctx, **kwargs):
return NoneSignature
def menu_item(
self,
item_id: str,
label: str = "",
icon: str = "",
section: str = "main",
path: str = "",
order: int = 100,
danger: bool = False,
) -> Callable:
...Two shapes
The presence of path decides whether your handler runs at all. This is the
single most important thing on this page.
1. Navigation link — path= set
The host routes straight to path. The handler is never called. Give it a
body of return None; it exists only to carry the declaration.
@ext.menu_item("workspace", label="My Workspace", icon="LayoutGrid", path="/ext/my-app")
async def menu_workspace(ctx, **kwargs):
return None2. Dispatched action — no path
Clicking calls __menu__{item_id}, and the result is handled like any other
panel action: return a UINode to open it, or an action to navigate or refresh.
@ext.menu_item("quick_report", label="Today's report", icon="FileText")
async def menu_quick_report(ctx, **kwargs):
rows = await ctx.store.query("events", where={"day": "today"})
return ui.Modal(
title="Today",
content=ui.Stack([
ui.Stat(label="Events", value=str(len(rows))),
ui.Button("Open full report", on_click=ui.Navigate("/ext/my-app/report")),
]),
)A handler with a path is dead code
If you set path= and also write real logic in the handler, that logic never
runs — and nothing warns you. Pick one shape per item.
Kwargs reference
item_id
Prop
Type
label
Prop
Type
icon
Prop
Type
section
Prop
Type
The schema knows four sections — main, admin, account, footer — but only
the first two accept contributions:
| Section | Contributable | What it is |
|---|---|---|
main | ✅ Default | Ordinary destinations |
admin | ✅ | Admin-only tools; inherits the host's admin gate |
account | ❌ Rejected | Platform identity entries |
footer | ❌ Rejected | Platform theme and sign-out |
account and footer are refused in two places
Passing either raises ValueError from the decorator and fails manifest
schema validation. Two gates on purpose: a manifest can be hand-edited, and the
consequence is not cosmetic — an item appearing there shoves Sign out sideways
under a cursor already moving toward it.
path
Prop
Type
order
Prop
Type
danger
Prop
Type
Handler signature
async def handler(ctx, **kwargs) -> UINode | dict | None:
...Accept **kwargs — the host may pass context the SDK does not model yet.
If the return value has a .to_dict() method, the decorator serialises it to
{"ui": ..., "item_id": item_id}. Anything else is passed through unchanged, so
returning None (the correct body for a path= link) is fine.
Complete example
# Ordinary destination.
@ext.menu_item("workspace", label="My Workspace", icon="LayoutGrid",
path="/ext/my-app", order=10)
async def menu_workspace(ctx, **kwargs):
return None
# Admin-gated tool — hidden entirely for non-admins.
@ext.menu_item("audit", label="Audit log", icon="ScrollText",
section="admin", path="/ext/my-app/audit")
async def menu_audit(ctx, **kwargs):
return None
# Dispatched action with a confirmation, marked destructive.
@ext.menu_item("purge_cache", label="Purge cached data", icon="Trash2", danger=True)
async def menu_purge_cache(ctx, **kwargs):
return ui.Dialog(
title="Purge cached data?",
content=ui.Text("Cached results will be rebuilt on next use."),
confirm_label="Purge",
on_confirm=ui.Call("do_purge_cache"),
destructive=True,
)Anti-patterns
Do not put frequent actions here. The menu costs two clicks and is hidden by default. Something used often belongs in a panel or a tray item.
Do not reach for account or footer. They are refused, by design, in two
places.
Do not mark everything danger=True. If three entries are red, none of them
reads as dangerous.
Manifest output
{
"menu": [
{
"item_id": "workspace",
"label": "My Workspace",
"icon": "LayoutGrid",
"section": "main",
"path": "/ext/my-app",
"order": 10,
"danger": false
}
]
}An extension that declares no menu items emits no menu key at all, so every
manifest written before 5.10.0 rebuilds byte-identical.
A menu entry alone is enough to publish
Since 5.10.0 an extension contributing only a menu entry — no panel — publishes its UI surface. Previously the publisher returned early and the declaration was dropped silently.
Cross-references
@ext.tray reference
@ext.tray reference — contribute an item to the Panel's system tray: the three zones, ordering, badges, dropdown panels, and the TrayResponse envelope.
Lifecycle hooks reference
Lifecycle hooks reference — install, enable, disable, uninstall, upgrade, and health-check hook signatures for the platform-driven extension lifecycle.