Skip to content

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:

  1. Defines the <ag-ui-chat> custom element from the vendored bundle.
  2. Attaches the CSRF token as an X-CSRFToken header so the endpoint accepts POSTs under the logged-in admin session.
  3. Reads the auto-confirm flag and the route map off the element / page, setting el.routeMap.
  4. Reads the embedded skill catalog and calls el.setSkills(...), and sets el.skillContext to a provider that derives {path} / {selected_ids} placeholder values from the current page.
  5. 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:

{ "id": "app.model.changelist", "path": "/admin/app/model/",
  "title": "Models", "group": "app" }

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.