Imperal Docs
Recipes

Auto-open the most-recent item on panel mount

Use auto_action on the sidebar root to claim the center slot when the user first lands in your extension

auto_action is a prop you set on the left panel's root UINode after building it. The host reads it once after batch discovery completes and fires the embedded ui.Call automatically — no user click required. Use it to open the most-recent or most-relevant item in the center so the extension lands in a useful state rather than an empty editor.

The prop is read from leftPanel.props.auto_action only. It fires once per discovery cycle, guarded by the discovery-once guard. Navigating away and returning to the same extension within the same browser session does NOT re-fire it (see troubleshoot case 3 for the workaround).


panels.py — auto_action pattern (direct adaptation of notes/panels.py:148-152)
from __future__ import annotations

from imperal_sdk import Extension, ui

ext = Extension(
    "my-ext",
    display_name="My Extension",
    description="An extension that auto-opens the most-recent item on load.",
    actions_explicit=True,
)

# ── Inline stub data — replace with your real data fetch in production ─────
_NOTES = [
    {"id": "note-1", "title": "Most recent note", "body": "Body one."},
    {"id": "note-2", "title": "Older note",        "body": "Body two."},
]


def _get_note(note_id: str) -> dict | None:
    return next((n for n in _NOTES if n["id"] == note_id), None)


# ── Left sidebar — sets auto_action on root when conditions are met ────────

@ext.panel(
    "sidebar",
    slot="left",
    title="Notes",
    icon="🗒️",
    default_width=280,
    min_width=200,
    max_width=500,
)
async def sidebar(
    ctx: object,
    active_note_id: str = "",
    view: str = "notes",
    **kwargs: object,
) -> object:
    notes = _NOTES  # replace with: notes = await fetch_notes(ctx)

    # Build the root UINode first — auto_action is set as a prop on it
    # after it is constructed, not passed as a constructor argument.
    root = ui.Stack(
        children=[
            ui.List(
                items=[
                    ui.ListItem(
                        id=note["id"],
                        title=note["title"],
                        selected=(note["id"] == active_note_id),
                        on_click=ui.Call(
                            "__panel__editor", note_id=note["id"]
                        ),
                    )
                    for note in notes
                ]
            )
        ],
        gap=2,
    )

    # ── auto_action: the production pattern from notes/panels.py:149-150 ──
    #
    # Three conditions must ALL be true before setting auto_action:
    #   1. `not active_note_id` — no item is already active (first-load guard).
    #      Avoids re-claiming center when the sidebar refreshes while an editor
    #      is already open.
    #   2. `notes` — the list is non-empty. Opening an empty editor when there
    #      is nothing to show is poor UX.
    #   3. `view != "trash"` — the trash view has different semantics; do not
    #      auto-open a trashed note as the first thing a user sees.
    #
    # The kwarg passed to ui.Call must be one the editor handler recognizes.
    # Here: note_id=notes[0]["id"] — the most-recent item (first in list).
    if not active_note_id and notes and view != "trash":
        root.props["auto_action"] = ui.Call(
            "__panel__editor", note_id=notes[0]["id"]
        )

    return root


# ── Center editor — returns ui.Empty() when called without a note_id ──────

@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:
        # ui.Empty() holds the slot visually while leaving it available
        # for auto_action to claim. This is the correct pattern — not
        # `return None`, which would leave leftPanel as null and prevent
        # auto_action from firing.
        return ui.Empty(message="Select a note to edit", icon="📄")

    note = _get_note(note_id)
    if note is None:
        return ui.Error(message="Note not found")

    return ui.Stack(
        children=[
            ui.Text(note["title"], variant="h2"),
            ui.RichEditor(content=note["body"]),
        ],
        gap=4,
    )

Walk-through

Why is auto_action set on the root after building it, not passed as a constructor argument? ui.Stack (and all UINode constructors) do not accept auto_action as a named parameter. props is a free dict on the UINode dataclass — you mutate it directly: root.props["auto_action"] = ui.Call(...). This is the production pattern from notes/panels.py and is the only supported form.

Why only from leftPanel.props.auto_action? The host's auto_action useEffect reads (leftPanel as any)?.props?.auto_action. There is no code path that reads auto_action from the right panel, from the center overlay, or from any nested child node. If you set auto_action on a right-panel root, it is silently ignored.

Why does the conditional matter? The the discovery-once guard means auto_action fires at most once per discovery cycle. If you always set auto_action unconditionally, the first discovery fires it (good). But when the sidebar is re-fetched later — for example because an SSE event triggers refreshAll() — the updated sidebar root will have auto_action set again. Because the discovery-once guard is already true, the host will not re-fire it. The conditional not active_note_id is therefore a belt-and-suspenders guard that also prevents the sidebar from declaring auto_action when the user has explicitly navigated to a different note — avoiding a confusing snap back to the first note on the next refresh.

When auto_action re-fires. the discovery-once guard is reset to false at the beginning of a new discovery cycle (when extId, leftId, or rightId changes ). In practice this means: reloading the page, switching to a different extension and returning, or changing the extension configuration. Within a single uninterrupted session on the same extension page, auto_action fires exactly once.


On this page