docs/agents/AGENTS.modals.md
openModal/closeModal)This document covers the stacked modal system used by Agent Zero’s frontend, implemented in:
webui/js/modals.jswebui/css/modals.cssIt also defines conventions for writing modal components in webui/components/modals/ and webui/components/settings/.
Legacy note: there is an older overlay / teleport modal style in the codebase (e.g.
x-teleport+.modal-overlay). Avoid it for new work. This doc is about the stacked modal system.
$storeThe UI uses Alpine.js for reactivity (x-show, x-if, x-on, x-model, …). Stores are registered via createStore() and read in HTML via $store.<storeName>.
webui/js/initFw.jswebui/js/AlpineStore.js<x-component> and importComponent()Agent Zero uses a custom <x-component path="..."> tag that is populated by the component loader.
The modal system does not rely on <x-component> directly; instead it calls the same loader function:
importComponent(path, targetElement) from webui/js/components.jsThat loader fetches component HTML, injects its <style> and <script> tags, and supports nested components.
Agent Zero can open modals from many places (sidebar, settings, message actions), and sometimes a modal opens another modal.
The stacked system provides:
<style> + <script type="module"> the same way <x-component> doesFile: webui/js/modals.js
openModal(modalPath): Promise<void>importComponent).Edge cases / failure behavior (current implementation):
modalPath does not reject the returned promise. Instead, the modal remains open and shows an error message inside the modal body; the promise still resolves when the user closes the modal.openModal() multiple times with the same modalPath creates multiple modal instances (no deduping).Modal paths are component paths, e.g.:
modals/file-browser/file-browser.htmlmodals/history/history.htmlsettings/settings.htmlcloseModal(modalPath?: string): voidmodalPath is passed, finds and closes that modal in the stack.Edge cases / failure behavior:
closeModal() is a no-op.modalPath is provided but not found, it is a no-op.scrollModal(id: string): voidScrolls within the top modal’s .modal-scroll to an element by id.
modals.js creates)When openModal() runs, it creates this outer shell:
.modal (full-screen fixed overlay container)
.modal-inner
.modal-header (title + close button).modal-scroll
.modal-bd (where the component HTML is imported).modal-footer-slot (used when the modal provides a footer)The component HTML is imported into .modal-bd.
The title is taken from <title> inside the imported document (fallback: the modalPath).
File: webui/css/modals.css
.modal is full-screen fixed positioning..modal-inner is centered and constrained:
width: 90%max-width: 960pxmax-height: 90vh.modal-scroll (overflow-y: auto)..modal-footer-slot is pinned under the scroll area (the body scrolls; the footer doesn’t).data-modal-footer)Some modals want a footer that stays fixed while the body scrolls.
Pattern:
data-modal-footer.modals.js will move that element out of the scroll area and into .modal-footer-slot.modals.js adds .modal-with-footer to .modal-inner so CSS can lay out the scroll area correctly.Example patterns in the repo:
webui/components/settings/settings.html (Save / Cancel footer)webui/components/modals/file-browser/file-browser.htmlwebui/components/modals/scheduler/scheduler-modal.htmlmodals.js uses a requestAnimationFrame to let Alpine mount first.File: webui/css/modals.css
Modals should reuse shared CSS classes rather than redefining common styles. The modal system provides base classes for buttons, footers, and layout that work consistently across all modals.
Use these standard button classes for modal actions:
| Class | Use case | Visual style |
|---|---|---|
btn btn-ok | Positive/confirmatory actions (Save, Create, Confirm) | Solid blue background, white text |
btn btn-cancel | Dismissive/negative actions (Cancel, Close, Delete) | Transparent with accent border |
Footer button order convention: positive action first (left), negative action second (right).
Example footer markup:
<div class="modal-footer" data-modal-footer>
<button class="btn btn-ok" @click="$store.myStore.save()">Save</button>
<button class="btn btn-cancel" @click="window.closeModal('...')">Cancel</button>
</div>
Before writing component-specific CSS, check webui/css/modals.css for:
.btn, .btn-ok, .btn-cancel, .btn-field — button styles.modal-footer — footer container layout.section, .section-title, .section-description — content sections.loading — shimmer loading placeholder.toolbar-button, .toolbar-group — editor toolbar elementsAdd styles in your component's <style> tag only when:
Avoid redefining .btn, .modal-footer, or other shared classes in component CSS—this creates inconsistency and maintenance burden.
The shell provides a close button (.modal-close) that always closes the top modal.
Escape closes the top modal only (stack semantics).
To avoid accidental close during drag/select, the shell only closes on click-outside when:
mousedown and mouseup occurred on the modal container itself (.modal), not on inner content.File: webui/js/modals.js
.modal-backdrop is used for all modals.3000base + index*20Outcome:
Use .modal-floating on the outer .modal when a modal should behave like a floating utility panel instead of a blocking dialog. This is for special live surfaces such as the browser panel where the user should keep seeing and interacting with the chat or dashboard behind the panel.
Working contract:
.modal-floating suppresses the shared .modal-backdrop for that modal..modal-floating makes the full-screen .modal shell pointer-transparent..modal-floating .modal-inner remains pointer-active, so the floating panel itself still receives clicks, keyboard focus, drag handlers, resize handles, and form input.Good to know:
.modal-no-backdrop only when a component needs backdrop suppression without click-through floating behavior. Prefer .modal-floating for utility panels..modal-floating for destructive confirmations, settings forms, auth, import/export, or workflows that require the user to finish or dismiss the dialog before interacting with the rest of the app.Prefer:
webui/components/modals/<name>/<name>.html (+ optional *-store.js)Settings uses:
webui/components/settings/settings.html and nested settings components.Use this structure:
<head><title>...</title><script type="module"> that imports your store (so it registers before Alpine evaluates bindings)<body> with:
x-create/x-init + x-destroy) to run open/cleanup logic<style> tag at the bottom of the component file containing all modal-specific stylingGood examples:
webui/components/modals/history/history.htmlwebui/components/modals/context/context.htmlwebui/components/modals/scheduler/scheduler-modal.htmlcreateStore() from webui/js/AlpineStore.js.$store.<storeName>.open() / openModal() to open the modaldestroy() / cleanup() to reset transient statedocument.addEventListener('alpine:init', ...) blocks inside component HTML — it conflicts with initFw.js ordering and can register handlers twice when components are reloaded..modal-inner or .modal-scroll structure — modals.js owns the shell and footer-slot mechanics; manual changes create layout/scroll bugs.Escape/close behavior, z-index, and focus should all be “top modal wins”.requestAnimationFrame if needed.open() rehydrates and destroy()/cleanup() resets transient state.openModal() is correct and resolves under webui/components/.<script type="module" src="..."> in the component, ensure the URL is correct.data-modal-footer attribute.x-if, ensure the store is available).Escape closes only the top modal.closeModal('some/path.html') closes that modal wherever it is in the stack.$store.someStore is undefined<script type="module"> block (so the store registers before Alpine evaluates bindings).createStore("history", ...) is accessed as $store.history).<template x-if="$store.someStore">, verify you’re using the correct store key and not a stale name.closeModal() removes the modal element immediately; if your store keeps polling or timers alive, the UI may still feel active—ensure you have x-destroy cleanup (destroy()/cleanup()).document.querySelectorAll('.modal').forEach(m => m.remove())) and reset any store state that assumes the modal is open..modal elements (multiple means you have a stack)style="z-index: ..." on .modal-inner.modal-backdrop exists and sits between modals when stackeddata-modal-footer and that .modal-inner has .modal-with-footer.