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).
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.
Cross-links
- Build a panel layout — master-detail — full guide where
auto_actionis one component of the complete wire-up. - Concepts: four ways a panel updates — auto_action — full contract, firing conditions, and reset rules.
- Troubleshoot: auto_action does not fire after navigating away and back — workaround for the once-per-session limitation.
- Center-overlay recipe —
auto_actionused to open a full-bleed overlay instead of a side-slot editor.