Master-detail panel
Build a master-detail panel in Imperal Cloud: a list on the left drives an editor in the center via ui.Call, with the newest item auto-opened on first load.
The most common multi-panel layout: a list on the left, an editor in the center. The list drives the editor via ui.Call. The editor returns ui.Empty() on first load so the center slot stays renderable. auto_action on the sidebar root then claims the center slot for the most-recent item on first load.
from __future__ import annotations
from imperal_sdk import Extension, ui
ext = Extension(
"my-ext",
display_name="My Extension",
description="A master-detail extension for managing items.",
actions_explicit=True,
)
# ── Inline stub data — replace with your real data fetch in production ─────
_ITEMS = [
{"id": "item-1", "title": "First item", "body": "Content for item one."},
{"id": "item-2", "title": "Second item", "body": "Content for item two."},
{"id": "item-3", "title": "Third item", "body": "Content for item three."},
]
def _get_item(item_id: str) -> dict | None:
return next((i for i in _ITEMS if i["id"] == item_id), None)
# ── Left sidebar — discovery-safe, sets auto_action on root ────────────────
@ext.panel(
"sidebar",
slot="left",
title="Items",
icon="📜",
default_width=280,
min_width=200,
max_width=500,
)
async def sidebar(ctx: object, active_item_id: str = "", **kwargs: object) -> object:
items = _ITEMS # replace with: items = await fetch_items(ctx)
root = ui.Stack(
children=[
ui.List(
items=[
ui.ListItem(
id=item["id"],
title=item["title"],
selected=(item["id"] == active_item_id),
on_click=ui.Call(
"__panel__editor",
item_id=item["id"],
),
)
for item in items
]
)
],
gap=2,
)
# Set auto_action only when there is content to show and no item is
# already active (first load). The host fires this once per discovery
# cycle to claim the center slot automatically.
if items and not active_item_id:
root.props["auto_action"] = ui.Call(
"__panel__editor", item_id=items[0]["id"]
)
return root
# ── Center editor — returns ui.Empty() at batch discovery ─────────────────
@ext.panel(
"editor",
slot="center",
center_overlay=True,
title="Editor",
icon="✏️",
)
async def editor(ctx: object, item_id: str = "", **kwargs: object) -> object:
# On first load the platform calls this with no item_id.
# Return ui.Empty() so the slot is held but visually indicates "select an item".
# This keeps the center slot renderable while the sidebar's auto_action
# claims it with the first real item.
if not item_id:
return ui.Empty(message="Select an item", icon="🖱️")
item = _get_item(item_id)
if item is None:
return ui.Error(message="Item not found")
return ui.Stack(
children=[
ui.Header(item["title"]),
ui.RichEditor(
content=item["body"],
on_change=ui.Call("save_item", item_id=item_id),
),
],
gap=4,
)Walk-through
Why ui.Empty() and not return None?
Returning ui.Empty() (instead of None) keeps the center slot renderable with a visible placeholder and lets the sidebar's auto_action claim the center with the first real item on load. Returning None from a center panel is appropriate only in edge cases where you do not want the slot populated at all (for example, a center panel that should only ever appear as a center overlay — see the center-overlay recipe).
Why auto_action on the sidebar root, not a child node?
auto_action is read only from the root UINode of the left panel — not from nested children or the right panel. The root is the ui.Stack returned from the sidebar handler — set root.props["auto_action"] after building the root, before returning.
Why the if items and not active_item_id guard?
auto_action fires once after the panel loads. The not active_item_id guard prevents the sidebar from re-declaring auto_action when a specific item is already active (e.g., when the sidebar is refreshed after a list update), so it won't overwrite whatever the user already has open. The items guard prevents setting auto_action when there is nothing to open — opening an empty editor is worse UX than showing the empty-state placeholder.
How item_id flows from click to editor handler.
ui.Call("__panel__editor", item_id=item["id"]) dispatches an action carrying item_id. The platform calls __panel__editor with that value, and the editor handler receives item_id as a kwarg and branches on it. The platform also remembers the parameters a panel was last called with, so a later refresh_panels=["editor"] re-renders with the same item_id — you do not need to thread it through every action result.
Cross-links
- Build a panel layout — master-detail — full guide with pitfalls and extension patterns.
- Concepts: slot ownership — who renders what — the mental model for replace semantics and batch discovery.
- Concepts: auto_action prop — when it fires, what resets it, what it dispatches.
Recipe — extension that calls the LLM
Call an LLM from inside your Webbee extension with ctx.ai.complete — it routes through the user's BYOLLM provider automatically, or the platform LLM otherwise.
Center-overlay panel
Build a modal-style center-overlay panel in Imperal Cloud: declare center_overlay on the panel and open it from the sidebar for a focused full-bleed editor.