Imperal Docs
Guides

Build a panel layout

Build panel layouts for the Imperal Panel: four React surface patterns, from master-detail to hub-only, covering the vast majority of extension UI shapes.

This guide walks through the four panel layouts that cover ~95% of extension UI shapes seen in production. Each layout has a When to use it section to help you pick the right one, then a complete wire-up walkthrough with a working code example.

Before starting, make sure you have read Panels — the mental model. The lifecycle section there (batch discovery → auto_action → slot replace) is assumed knowledge throughout this guide.

The layouts:

LayoutSlots usedReference extension
1 — Master-detailleft + centernotes, sql-db, tasks
2 — Center-overlay-editorleft + center (overlay)notes (editor open path)
3 — Multi-tab right panelleft + right (kwarg-branched)tasks (details), mail
4 — Hub-only-centercenter onlycalculator-style tools

Layout 1 — Master-detail

When to use it

Master-detail is the right layout when your extension has a navigable list of items (notes, queries, tasks, tickets) and a content area that shows the selected item. The user browses the list in the left sidebar and the detail view fills the center.

Production examples: the Notes extension (folder/note list → rich editor), the SQL extension (saved queries list → query editor + results), and the Tasks extension (project/task list → task board).

The two decorators

panels.py — decorator declarations
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,
)

@ext.panel(
    "sidebar",
    slot="left",
    title="Items",
    icon="📜",
    default_width=280,
    min_width=200,
    max_width=500,
)
async def sidebar(ctx: object, **kwargs: object) -> object: ...

@ext.panel(
    "editor",
    slot="center",
    title="Editor",
    icon="✏️",
)
async def editor(ctx: object, **kwargs: object) -> object: ...

Two panels, two separate Python functions. The sidebar occupies the left slot; editor occupies center. The center panel is not called at session init — only left and right panels are called eagerly when the page loads. The editor is only reached via an explicit ui.Call action or through auto_action.

Wire-up

Step 1 — Editor returns ui.Empty() at batch discovery.

The editor is not called at session init, but it will be called whenever ui.Call("__panel__editor", ...) is dispatched (including from auto_action). At that point, if no item_id kwarg is supplied, return ui.Empty() — not None — so the slot stays non-null and renders a visible placeholder:

panels.py — editor with empty-state guard
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,
)

@ext.panel("editor", slot="center", center_overlay=True, title="Editor", icon="✏️")
async def editor(ctx: object, item_id: str = "", **kwargs: object) -> object:
    if not item_id:
        # Canonical empty-state. Do NOT return None here:
        # None leaves the center slot empty and blocks auto_action from firing.
        return ui.Empty(message="Select an item", icon="📄")

    item = {"id": item_id, "title": "Item", "body": "Content here"}  # replace with your fetch
    return ui.Stack(children=[
        ui.Header(item["title"]),
        ui.Text(item["body"]),
    ])

Step 2 — Sidebar drives the editor via on_click and seeds auto_action for first load.

panels.py — full master-detail wire-up
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,
)

@ext.panel(
    "sidebar",
    slot="left",
    title="Items",
    icon="📜",
    default_width=280,
    min_width=200,
    max_width=500,
)
async def sidebar(ctx: object, folder_id: str = "", active_item_id: str = "",
                  **kwargs: object) -> object:
    # Replace with your real data fetch.
    items = [
        {"id": "item-1", "title": "First item"},
        {"id": "item-2", "title": "Second item"},
    ]

    list_items = [
        ui.ListItem(
            id=item["id"],
            title=item["title"],
            selected=(active_item_id == item["id"]),
            on_click=ui.Call("__panel__editor", item_id=item["id"]),
        )
        for item in items
    ]
    root = ui.Stack(children=[ui.List(items=list_items)], gap=2)

    # Seed auto_action only when no item is already active.
    # This fires once after the panel loads (see Pitfall 2 below).
    if not active_item_id and items:
        root.props["auto_action"] = ui.Call(
            "__panel__editor", item_id=items[0]["id"]
        )

    return root


@ext.panel("editor", slot="center", title="Editor", icon="✏️")
async def editor(ctx: object, item_id: str = "", **kwargs: object) -> object:
    if not item_id:
        return ui.Empty(message="Select an item", icon="📄")

    item = {"id": item_id, "title": "Item", "body": "Content here"}  # replace with your fetch
    return ui.Stack(children=[
        ui.Header(item["title"]),
        ui.Text(item["body"]),
    ])

What happens at runtime:

  1. Session init: host calls __panel__sidebar with params: {}. Sidebar returns the list with auto_action set to the first item.
  2. auto_action fires: the platform dispatches ui.Call("__panel__editor", item_id="item-1") and the editor renders in the center slot. (This Layout 1 example does not set center_overlay=True on the editor, so it mounts inline rather than as an overlay — see Layout 2 for the overlay path.)
  3. User clicks "Second item": on_click = ui.Call("__panel__editor", item_id="item-2"). The editor content is replaced, not stacked.

Params accumulate across calls. The platform remembers the parameters each panel was last called with, merging new params over old ones per panel. If your sidebar was called with folder_id="folder-1" and then the user clicks a note, the subsequent editor call does not disturb the sidebar's remembered folder_id. When a refresh_panels=["sidebar"] triggers a re-fetch, the sidebar is re-called with its last-used folder_id preserved — you do not need to thread folder context through every action result.

Common pitfalls

Pitfall 1 — returning None instead of ui.Empty() from the editor.

If editor returns None when item_id is empty, the center slot stays empty. auto_action then fires, calls __panel__editor with no item_id, and the slot stays empty again. The user sees a blank center region with no visual feedback.

Always return ui.Empty(message="...", icon="...") as the empty-state from a center panel handler.

Pitfall 2 — not guarding auto_action on re-fetches.

If sidebar always sets auto_action regardless of active_item_id, every event-triggered sidebar re-fetch fires the auto-open again — overwriting whatever the user has open in the center. Guard with if not active_item_id and items:.

Pitfall 3 — using the __panel__ prefix in refresh_panels.

In ActionResult, refresh_panels takes bare panel_id strings: ["sidebar"], not ["__panel__sidebar"]. Do not add the __panel__ prefix yourself — the platform handles that, and a prefixed value will not match any panel.

Where to extend

  • Multi-folder navigation: add a folder list above the notes list in the sidebar. Clicking a folder calls ui.Call("__panel__sidebar", folder_id=f["id"]) — same panel_id, new params. The platform re-fetches the sidebar with the new folder context.
  • Breadcrumbs in the editor: pass folder_id alongside item_id when the user clicks an item so the editor can render a breadcrumb header.
  • Auto-open on first load: for the isolated auto_action pattern see recipes/panel-auto-action-on-load.
  • Write actions: when a save button calls a @chat.function that returns ActionResult(refresh_panels=["sidebar"]), the sidebar is re-fetched with its remembered parameters. See recipes/panel-refresh-after-write.

Layout 2 — Center-overlay-editor

Opting into the center overlay is declarative.

To make a center panel render as an overlay (an editor that expands over the main chat area instead of mounting in a right-column slot), declare it with center_overlay=True:

panels.py - no-check: bare decorator fragment, not a runnable module
@ext.panel("editor", slot="center", center_overlay=True, title="Editor", icon="✏️")

That single field is the opt-in. You are not restricted to any fixed list of panel names, you may name your panel and its kwargs whatever you like, and no frontend change is required — the platform reads the flag and renders the panel as a center overlay. (center_overlay=True is available since SDK v4.1.8.)

When to use it

Center-overlay is the right layout when you want an immersive editing experience that expands over the main content area — for example, editing a note in a rich-text editor with the left sidebar still visible. The overlay opens on item selection and closes when the user navigates back to the list.

This layout is a specialization of Layout 1. The difference is that the center panel is declared with center_overlay=True, so opening it expands a full-width center overlay instead of mounting the editor in a right-column slot.

You choose the panel name and the kwarg that carries item identity. The example below uses panel_id="editor" with a note_id kwarg (matching the production Notes extension), but any names work as long as your on_click and handler agree.

The decorator

panels_editor.py — center-overlay editor declaration
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="A notes extension with center-overlay editor.",
    actions_explicit=True,
)

# center_overlay=True is the opt-in for the center-overlay surface.
# Pair it with slot="center". You pick the panel name ("editor" here) and
# the kwarg that carries item identity ("note_id" here) — any names work,
# as long as your on_click and handler use the same ones.
@ext.panel("editor", slot="center", center_overlay=True, title="Editor", icon="✏️")
async def notes_editor(ctx: object, note_id: str = "", **kwargs: object) -> object: ...

Wire-up

The sidebar is identical to Layout 1, with one difference: the on_click action passes the same kwarg the editor handler reads (note_id=item["id"] here), so the selected item flows into the overlay editor.

panels.py + panels_editor.py — center-overlay wire-up
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="A notes extension with center-overlay editor.",
    actions_explicit=True,
)

# ── Left sidebar ──────────────────────────────────────────────────────────

@ext.panel(
    "sidebar",
    slot="left",
    title="Notes",
    icon="🗒️",
    default_width=280,
    min_width=200,
    max_width=500,
    refresh="on_event:notes.created,notes.updated,notes.deleted",
)
async def sidebar(ctx: object, active_note_id: str = "", **kwargs: object) -> object:
    # Replace with your real data fetch.
    notes = [
        {"id": "note-1", "title": "Meeting notes"},
        {"id": "note-2", "title": "Ideas"},
    ]

    items = [
        ui.ListItem(
            id=n["id"],
            title=n["title"],
            selected=(active_note_id == n["id"]),
            # Pass the same kwarg the editor handler reads (note_id here).
            on_click=ui.Call("__panel__editor", note_id=n["id"]),
        )
        for n in notes
    ]
    root = ui.Stack(children=[ui.List(items=items)], gap=2)

    # auto_action: open the most-recent note on first load.
    # Guard: only when no note is already active, and not in trash view.
    if not active_note_id and notes:
        root.props["auto_action"] = ui.Call(
            "__panel__editor", note_id=notes[0]["id"]
        )

    return root


# ── Center overlay editor ─────────────────────────────────────────────────

@ext.panel("editor", slot="center", center_overlay=True, title="Editor", icon="✏️")
async def editor(ctx: object, note_id: str = "", **kwargs: object) -> object:
    if not note_id:
        # Canonical empty-state — do NOT return None.
        return ui.Empty(message="Select a note to edit", icon="📄")

    if note_id == "new":
        # Create a new note stub; redirect to real ID after creation.
        return ui.Stack(children=[
            ui.Input(label="Title", placeholder="e.g. Meeting notes",
                     param_name="title",
                     on_submit=ui.Call("create_note", title="{{value}}")),
        ])

    note = {"id": note_id, "title": "Meeting notes", "content": "<p>Hello</p>"}  # fetch yours
    return ui.Stack(children=[
        ui.Stack(children=[
            ui.Header(note["title"]),
            ui.Button(
                "Back to list", icon="⬅️", variant="ghost", size="sm",
                # Navigating back to the sidebar closes the overlay.
                on_click=ui.Call("__panel__sidebar", view=""),
            ),
        ], direction="h"),
        ui.RichEditor(
            content=note["content"],
            on_change=ui.Call("save_note", note_id=note_id),
        ),
    ])

What happens at runtime:

  1. Batch discovery: __panel__sidebar called with {}. Sidebar returns the note list with auto_action = ui.Call("__panel__editor", note_id="note-1").
  2. auto_action fires. Because the editor panel declared center_overlay=True, the editor opens as an overlay over the center chat area.
  3. User clicks "Back to list": ui.Call("__panel__sidebar", view=""). Navigating back to the sidebar closes the overlay, then the sidebar is refreshed.
  4. User clicks another note: ui.Call("__panel__editor", note_id="note-2"). The overlay re-renders with the new note.

Pitfalls

Pitfall 1 — forgetting center_overlay=True on the center panel.

The overlay surface is opt-in. A slot="center" panel without center_overlay=True mounts inline (Layout 1) rather than as an overlay. Declare center_overlay=True to get the overlay behavior.

Pitfall 2 — mismatched kwarg names between on_click and the handler.

The kwarg you pass in ui.Call("__panel__editor", note_id="abc") must match the parameter your editor handler reads (note_id here). If you send doc_id but the handler reads note_id, the handler receives an empty note_id and falls into its empty-state branch. Pick a kwarg name and use it consistently on both sides.

Pitfall 3 — always setting auto_action regardless of active note.

If sidebar sets auto_action unconditionally, every event-triggered sidebar re-fetch re-opens the first note in the overlay, overwriting whatever the user has open. Guard: if not active_note_id and notes:.

Pitfall 4 — expecting return None to suppress the overlay.

If editor returns None when note_id is empty, the first auto_action dispatch (before any note is selected) leaves the overlay empty and it never opens. Return ui.Empty() to keep the slot usable.

Where to extend

  • Back button: always add a "Back to list" button in the overlay editor that navigates back to the sidebar (ui.Call("__panel__sidebar", view="")); navigating away from the overlay panel closes it — there is no explicit ui.Close() action in the SDK.
  • New-item creation: ui.Call("__panel__editor", note_id="new") is the common pattern. Your editor handles the "new" sentinel value and creates the item inline.
  • Trash / archive views: these are sub-views of the sidebar, not separate panels. Pass a view="trash" kwarg to the sidebar and branch inside the sidebar handler.

Layout 3 — Multi-tab right panel

How right-panel "tabs" actually work — an important clarification.

The platform does not render a tab strip for the right panel for you. The right slot holds a single UINode at a time. Multiple @ext.panel declarations with slot="right" do not produce a tab bar; each right-panel call fully replaces the previous right-panel content (same replace-not-stack behavior as the left slot).

Tabs in a right panel are achieved by one of two approaches:

  1. Single @ext.panel(slot="right") handler that branches internally on a kwarg (e.g., active_tab="documents"). The tab UI is rendered inside your UINode tree.
  2. Multiple @ext.panel declarations where only one is discovered at session init, and the others are reached via ui.Call(...) actions that replace the slot.

Do not attempt to declare tabs in the manifest — there is no manifest tab field; tab navigation lives entirely in your UINode tree.

When to use it

Multi-tab right panel is the right layout when your extension has a main content view in the left sidebar or center and a secondary panel on the right that shows different aspects of the selected item — for example, a task board on the left with a detail panel on the right that switches between Comments, Attachments, and History tabs.

Production examples: tasks extension (board view + task detail panel), mail extension (inbox list + email viewer / accounts / compose panels).

The pattern — single handler with kwarg branching

The most maintainable approach is a single @ext.panel(slot="right") handler that reads an active_tab (or equivalent) kwarg and branches:

panels.py — multi-tab right panel via kwarg branching
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="An extension with a tabbed right panel.",
    actions_explicit=True,
)

@ext.panel(
    "sidebar",
    slot="left",
    title="Items",
    icon="📜",
    default_width=280,
    min_width=200,
    max_width=500,
)
async def sidebar(ctx: object, **kwargs: object) -> object:
    items = [{"id": "item-1", "title": "First item"}]
    list_items = [
        ui.ListItem(
            id=item["id"],
            title=item["title"],
            # Open details panel with default tab on click.
            on_click=ui.Call(
                "__panel__details",
                item_id=item["id"],
                active_tab="overview",
            ),
        )
        for item in items
    ]
    return ui.Stack(children=[ui.List(items=list_items)], gap=2)


@ext.panel(
    "details",
    slot="right",
    title="Details",
    icon="🔳",
    default_width=320,
    min_width=240,
    max_width=480,
)
async def details(ctx: object, item_id: str = "", active_tab: str = "overview",
                  **kwargs: object) -> object:
    if not item_id:
        return ui.Empty(message="Select an item to see details", icon="ℹ️")

    item = {"id": item_id, "title": "First item"}  # replace with your fetch

    # Tab bar rendered inside the UINode — not by the host shell.
    tab_buttons = ui.Stack(
        children=[
            ui.Button(
                "Overview",
                variant="secondary" if active_tab == "overview" else "ghost",
                size="sm",
                on_click=ui.Call("__panel__details",
                                 item_id=item_id, active_tab="overview"),
            ),
            ui.Button(
                "Activity",
                variant="secondary" if active_tab == "activity" else "ghost",
                size="sm",
                on_click=ui.Call("__panel__details",
                                 item_id=item_id, active_tab="activity"),
            ),
            ui.Button(
                "Notes",
                variant="secondary" if active_tab == "notes" else "ghost",
                size="sm",
                on_click=ui.Call("__panel__details",
                                 item_id=item_id, active_tab="notes"),
            ),
        ],
        direction="h",
        gap=1,
        sticky=True,
    )

    if active_tab == "overview":
        tab_content: object = ui.Stack(children=[
            ui.Header(item["title"], level=3),
            ui.Text("Overview content here"),
        ])
    elif active_tab == "activity":
        tab_content = ui.Text("Activity feed here")
    else:
        tab_content = ui.Text("Notes content here")

    return ui.Stack(children=[tab_buttons, tab_content], gap=3)

What happens at runtime:

  1. Batch discovery: __panel__sidebar called with {}. __panel__details is also called with {} if it is configured as config.panels.right.panel_id. With item_id="", details returns ui.Empty(...).
  2. User clicks "First item": ui.Call("__panel__details", item_id="item-1", active_tab="overview"). The right slot is replaced with the details panel showing the Overview tab.
  3. User clicks "Activity" tab: ui.Call("__panel__details", item_id="item-1", active_tab="activity"). Same panel_id, new params — the platform updates the details panel's remembered params with the new active_tab, and the right slot is replaced with the Activity view.

Wire-up alternative — multiple @ext.panel declarations (slot replace)

If your tabs have very different data requirements, you can declare them as separate panel handlers and use ui.Call(...) to switch between them. Only one can be the config.panels.right.panel_id for batch discovery; the others are reached via explicit action dispatch.

panels.py — separate panel declarations per tab (slot replace pattern)
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="An extension with separate right-panel handlers per tab.",
    actions_explicit=True,
)

# This one is configured as config.panels.right.panel_id — called at batch discovery.
@ext.panel("inbox", slot="right", title="Inbox", icon="📥",
           default_width=320, min_width=240, max_width=480)
async def inbox(ctx: object, **kwargs: object) -> object:
    return ui.Stack(children=[
        ui.Stack(children=[
            ui.Button("Inbox", variant="secondary", size="sm",
                      on_click=ui.Call("__panel__inbox")),
            ui.Button("Accounts", variant="ghost", size="sm",
                      on_click=ui.Call("__panel__accounts")),
        ], direction="h", gap=1, sticky=True),
        ui.Text("Inbox messages here"),
    ])


# Not discovered at init — only reached via ui.Call from inbox tab bar.
@ext.panel("accounts", slot="right", title="Accounts", icon="👤",
           default_width=320, min_width=240, max_width=480)
async def accounts(ctx: object, **kwargs: object) -> object:
    return ui.Stack(children=[
        ui.Stack(children=[
            ui.Button("Inbox", variant="ghost", size="sm",
                      on_click=ui.Call("__panel__inbox")),
            ui.Button("Accounts", variant="secondary", size="sm",
                      on_click=ui.Call("__panel__accounts")),
        ], direction="h", gap=1, sticky=True),
        ui.Text("Accounts list here"),
    ])

Note: switching between inbox and accounts fully replaces the right-slot UINode each time — there is no tab strip from the host and no partial-update mechanism.

Pitfalls

Pitfall 1 — declaring two slot="right" panels and expecting both to appear simultaneously.

The right slot holds one panel at a time. The second right-panel call unconditionally replaces the first (replace, not stack). Both panels cannot be visible at the same time.

Pitfall 2 — trying to configure tabs in the manifest.

There is no manifest field for right-panel tabs. Tab navigation must be wired entirely within your UINode tree.

Pitfall 3 — not passing item_id when switching tabs.

If your "Activity" tab button calls ui.Call("__panel__details", active_tab="activity") without item_id, the handler receives item_id="" and returns the empty state. Always include all the context kwargs your handler needs to reconstruct the full view.

Where to extend

  • Persisting the active tab across refreshes: the platform remembers each panel's last-used params, so a refresh_panels=["details"] re-fetch reuses the last-known active_tab. See concepts/panels — slot ownership for how panels keep their parameters across refreshes.
  • Deep-link to a specific tab from chat: a @chat.function can return ActionResult(refresh_panels=["details"]) and the re-fetch will use the last accumulated active_tab. To force a specific tab from chat, use a custom @chat.function that instructs the user to click.

Layout 4 — Hub-only-center

When to use it

Hub-only-center is the right layout for extensions that are a single self-contained tool — a calculator, a whiteboard, a code playground, a standalone dashboard. There is no navigable list, no editor overlay — just one persistent canvas in the center slot.

This is the simplest layout to implement and the only one where you declare a single @ext.panel with no sidebar counterpart.

The decorator

panels.py — hub-only-center declaration
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="A standalone hub tool with a single center panel.",
    actions_explicit=True,
)

@ext.panel(
    "hub",
    slot="center",
    center_overlay=True,
    title="My Tool",
    icon="🪟",
)
async def hub(ctx: object, **kwargs: object) -> object: ...

No slot="left" companion. With no left or right panel declared, nothing is discovered at session init, so there is no left panel. Because there is no left panel, auto_action never fires — you do not need to worry about the auto_action contract in this layout.

The hub panel is only reachable via explicit ui.Call(...) dispatch from within the panel's own UI (buttons, inputs, on_change handlers).

Wire-up

panels.py — hub-only-center full working example
from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="A standalone hub tool with a single center panel.",
    actions_explicit=True,
)

@ext.panel(
    "hub",
    slot="center",
    center_overlay=True,
    title="Calculator",
    icon="🧮",
)
async def hub(ctx: object, expression: str = "", result: str = "",
              **kwargs: object) -> object:
    # State is passed back as kwargs on each ui.Call dispatch.
    # For richer state, use ctx.store for cross-session persistence.
    return ui.Stack(
        children=[
            ui.Header("Calculator"),
            ui.Input(
                label="Expression",
                placeholder="e.g. 2 + 2",
                param_name="expression",
                value=expression,
                on_submit=ui.Call("__panel__hub",
                                  expression="{{value}}",
                                  result=""),
            ),
            ui.Stack(
                children=[
                    ui.Button(
                        "Evaluate",
                        variant="primary",
                        on_click=ui.Call("evaluate_expression",
                                         expression=expression),
                    ),
                    ui.Button(
                        "Clear",
                        variant="ghost",
                        on_click=ui.Call("__panel__hub",
                                         expression="",
                                         result=""),
                    ),
                ],
                direction="h",
                gap=2,
            ),
            ui.Text(f"Result: {result}" if result else "", variant="code"),
        ],
        gap=4,
    )

State management options:

ApproachWhen to useHow
Kwarg pass-throughShort-lived ephemeral state (form values, toggle states)Pass state as kwargs in ui.Call — the platform remembers a panel's last-used params
ctx.storeState that should survive page refresh or be shared across sessionsUse SDK ctx.store.get / ctx.store.set inside the handler
ActionResult.refresh_panelsAfter a write action, re-render the hub with new dataReturn refresh_panels=["hub"] — host re-fetches with accumulated params

Pitfalls

Pitfall 1 — using slot="bottom" for a footer strip.

slot="bottom" passes SDK validation but is not rendered today — it has no visible surface. If you want a footer inside your hub panel, render it as a child UINode inside your slot="center" handler's return tree. Use ui.Stack(children=[main_content, footer], gap=0) with footer as a pinned bottom child.

Pitfall 2 — registering a slot="left" panel alongside the hub without intending to.

If you accidentally register a slot="left" panel (e.g., copied from a master-detail extension), it is discovered at session init and adds a left panel. If that left panel has auto_action set, it fires and may load an unexpected panel into the center. In the hub-only layout, keep the declaration list to a single slot="center" panel.

Pitfall 3 — using slot="chat-sidebar" for a persistent sidebar.

Like slot="bottom", slot="chat-sidebar" passes SDK validation but is not rendered today. Use slot="left" for a left sidebar or combine into a single slot="center" panel with an internal sidebar column.

Where to extend

  • Persist user settings between visits: use ctx.store in your hub handler to read/write user preferences. The store survives page reloads and is scoped per user+extension.
  • Event-driven refresh: if your hub displays live data (e.g., a real-time dashboard), add refresh="on_event:your_scope.event_name" to the @ext.panel decorator. On matching events the platform re-calls your hub handler with its remembered params.
  • Wiring to chat: a @chat.function that updates data can return refresh_panels=["hub"] to trigger a hub re-render after a write. See concepts/panels — Four ways a panel updates for the full set of panel-update paths and their caveats.

Where next

On this page