Admin wiring¶
This page covers how the sidebar is built and attached to the admin: the context the template needs, the route map and page map the agent uses to navigate, and the two attachment paths.
The sidebar context¶
build_sidebar_context() assembles everything the sidebar template renders:
| Key | Source | Used for |
|---|---|---|
endpoint |
reverse("<namespace>:endpoint") |
The AG-UI endpoint the chat POSTs to. |
title |
DJANGO_ADMIN_AGENT["TITLE"] |
The chat panel header. |
auto_confirm |
DJANGO_ADMIN_AGENT["AUTO_CONFIRM"] |
Surfaced to the component as autoConfirm. |
tool_display |
DJANGO_ADMIN_AGENT["TOOL_DISPLAY"] |
The data-tool-display attribute (minimal / compact / full). |
theme, density, placement, text_animation |
the matching DJANGO_ADMIN_AGENT keys |
Themeable presentation attributes (theme / density / placement / data-text-animation), rendered only when set. See Configuration → Presentation. |
theme_toggle |
DJANGO_ADMIN_AGENT["THEME_TOGGLE"] |
The data-theme-toggle attribute — opts into the built-in light⇄dark header toggle (off by default). |
launcher_drag |
DJANGO_ADMIN_AGENT["LAUNCHER_DRAG"] |
The data-launcher-drag attribute, rendered as "false" only when dragging is turned off — it governs both the collapsed bubble and the open panel's header. |
side |
DJANGO_ADMIN_AGENT["SIDE"] |
The data-side attribute for placement="sidebar" (left / right), rendered only when set. |
icon_url |
DJANGO_ADMIN_AGENT["ICON_URL"] |
The data-icon-url attribute (header/launcher icon), rendered only when set. |
strings_json |
DJANGO_ADMIN_AGENT["STRINGS"] |
Localized UI-string overrides serialized to JSON for the data-strings attribute, rendered only when set. |
skills |
DJANGO_ADMIN_AGENT["SKILLS"] or build_skills() |
The skill catalog (chips + /-command palette), embedded as a json_script. See Configuration → Skills. |
tools_url |
reverse("<namespace>:tools") (or None) |
URL of the server-tool label catalog, rendered as data-tools-url; the component fetches it to label server-tool cards. None when the server wasn't mounted. |
threads_url |
reverse("<namespace>:threads") (or None) |
URL of the owner-scoped thread index, rendered as data-threads-url; the component's history drawer fetches it. None when no conversation store is configured (the sub-view is unmounted). |
attachments_url |
reverse("<namespace>:attachments") (or None) |
URL of the upload endpoint, rendered as data-attachments-url; the composer posts files to it. None when no attachment store is configured (the sub-view is unmounted). |
attachment_max_bytes, attachment_accept |
DJANGO_AG_UI upload guards |
When uploads are mounted, the server-side ATTACHMENT_MAX_BYTES / ATTACHMENT_ALLOWED_TYPES mirrored onto data-attachment-max-bytes / data-attachment-accept so the composer can reject files before upload. None (attribute omitted) when the corresponding limit is unset. |
transcribe_url |
reverse("<namespace>:transcribe") (or None) |
URL of the transcription endpoint, rendered as data-transcribe-url; the mic button posts audio to it. None when no transcription backend is configured (the sub-view is unmounted). |
user_key |
request.user.pk (or None) |
The user-key attribute, which scopes the component's per-tab conversation to the signed-in principal. sessionStorage outlives the navigation a logout is, so without it the next person to sign in at the same desk lands on the previous one's transcript; changing the value purges everything the previous principal left behind. pk rather than username, because a renamed account is the same principal. None (attribute omitted) for an anonymous or absent user. |
bootstrap_url |
static("django_admin_agent/admin_agent.js") |
The ES-module entry point. |
admin_base_url |
reverse("admin:index") (or /) |
Lets the frontend nav.* tools build changelist / changeform URLs without reversing named routes in the browser. |
route_map |
build_route_map() |
The navigable-route manifest (see below). |
The same helper backs both attachment paths, so the rendered sidebar is identical whichever you choose.
The sidebar template¶
templates/django_admin_agent/sidebar.html renders the <ag-ui-chat> custom
element with the endpoint, title, auto-confirm, tool-display, and admin-base
data attributes (plus data-slash-commands="true", and the optional theme /
density / placement / data-text-animation attributes when their settings
are set, plus data-tools-url when the catalog endpoint is mounted); embeds the
route map and the skill catalog as two json_script blocks
(#django-admin-agent-routes, #django-admin-agent-skills); and loads the
bootstrap module. The bootstrap (static/django_admin_agent/admin_agent.js)
then:
- Defines the
<ag-ui-chat>custom element from the vendored bundle. - Attaches the CSRF token as an
X-CSRFTokenheader so the endpoint accepts POSTs under the logged-in admin session. - Reads the auto-confirm flag and the route map off the element / page, setting
el.routeMap. - Reads the embedded skill catalog and calls
el.setSkills(...), and setsel.skillContextto a provider that derives{path}/{selected_ids}placeholder values from the current page. - Calls
registerAdminTools(el)to register the frontend tools.
Server-tool card labels are not embedded — the component fetches them from
data-tools-url (the <prefix>tools/ catalog endpoint, named
<namespace>:tools), whose labels come from each tool's @tool(summary=).
The themeable attributes (theme / density / placement /
data-text-animation) and data-tool-display are read by the Web Component
itself; this template is the seam where the DJANGO_ADMIN_AGENT settings become
element attributes.
The route map¶
build_route_map() walks admin.site._registry and emits, per registered
model, a changelist route, an add route (when available), and a dynamic
change route, each shaped for the Web Component's routeMap:
The change route's path is a :pk template, e.g.
/admin/app/model/:pk/change/ — build_route_map reverses the change URL with
a sentinel pk and swaps it for the :pk placeholder. When the agent calls
navigate_to_route, the Web Component substitutes the :pk segment from the
call's params, so the agent can edit a specific record by intent.
The agent calls the component's list_routes to discover destinations and
navigate_to_route to jump to one, instead of guessing admin URL shapes. URLs
are reverse-resolved (admin:<app>_<model>_changelist / _add / _change), so
models without a resolvable route are simply skipped.
The page map¶
registerAdminTools also wires el.getPageMap — a per-run provider that
returns a compact snapshot of the current page (field names/types/labels and
button labels/handles, no values). The Web Component auto-injects it into each
run's context, so the agent knows the page's surface without first calling
get_form_state / get_visible_buttons. Exact live values are still fetched on
demand via the ui_read.* tools.
Attaching the sidebar¶
Template tag¶
@register.inclusion_tag("django_admin_agent/sidebar.html")
def django_admin_agent_sidebar() -> dict[str, Any]: ...
{% load django_admin_agent %} then {% django_admin_agent_sidebar %} in your
admin/base_site.html renders the sidebar. It is self-contained — it computes
its own context via build_sidebar_context(), so the admin site does not need
swapping. This is the common path.
The tag renders nothing at all for a signed-out visitor. base_site.html is
also what the admin's login page renders, and the endpoint behind the launcher
refuses anyone who is not active staff — so a sidebar there could only answer
401. A context with no request (or a request that never met
AuthenticationMiddleware) still renders, because "nobody to refuse" is not the
same answer as "refused".
SidebarAdminSite¶
SidebarAdminSite is a drop-in AdminSite whose each_context adds the
sidebar context under the django_admin_agent key, so a base template can
render the chat from {{ django_admin_agent }} without the tag. It calls
super().each_context() first, so all standard admin context keys pass through
unchanged. Use this when you already swap the admin site; otherwise prefer the
template tag.
each_context runs for the login page too, so the signed-out case reaches this
path as well. It cannot render nothing the way the tag does — the markup is
yours — so it withholds the content instead: an anonymous request gets an
empty django_admin_agent, which is falsy. {% if django_admin_agent %} is
therefore the natural guard, and a template that renders unconditionally is no
worse off than before, because its launcher was already inert for a visitor the
endpoint refuses.
What that empty context prevents is worth stating plainly: a populated one
carries the route manifest, and build_route_map() walks the admin registry
without filtering by permission — so it names every registered model and its
admin URL. A host rendering unconditionally used to publish that on its login
page.
Unfold compatibility shim¶
The frontend tool handlers work against Django Unfold unchanged because Unfold
preserves Django's structural DOM contracts. Two Unfold-only quirks are smoothed
by static/django_admin_agent/unfold_shim.js, which no-ops on vanilla admin —
see Unfold & vanilla support. There is no Python Unfold
dependency: Unfold is detected at runtime in the browser, never imported at
module load.