update docs, AGENTS.md and skill

Alessandro committed Feb 20, 2026 at 08:27 UTC a13b81a9a15cdf7e8369dc6099340db4cd05ba98
14 files changed +1787 -152
AGENTS.md new
+113
@@ -0,0 +1,113 @@
1 +# Agent Zero — Full-Stack Agent & Plugin Architecture
2 +
3 +This document bridges the gap between the **Python Backend** (AgentContext, LLM loop) and the **Frontend Component System** (Alpine.js, Modals). Use this as the canonical reference for building deep integrations.
4 +
5 +---
6 +
7 +## 1. The Core Concept: `AgentContext`
8 +
9 +Every conversation in Agent Zero is an `AgentContext`. It owns the message history, the LLM state, the tool definitions, and the log queue.
10 +
11 +### Backend: Managing Contexts
12 +When building a plugin, you must interact with the context system correctly:
13 +
14 +```python
15 +from agent import AgentContext, AgentContextType, initialize_agent
16 +from python.helpers.messages import UserMessage
17 +
18 +# 1. Access an existing context (e.g., from a stored ID)
19 +context = AgentContext.use(context_id)
20 +
21 +# 2. Or create a new one
22 +config = {} # use defaults
23 +context = AgentContext(config=config, type=AgentContextType.USER)
24 +await initialize_agent(context)
25 +
26 +# 3. Communicate (send a message and wait for completion)
27 +task = context.communicate(UserMessage("Hello Agent!"))
28 +response_text = await task.result()
29 +```
30 +
31 +### The Glue: `MessageQueue` (mq)
32 +The frontend listens to a WebSocket. To make messages appear in the WebUI from your plugin, use the log helper:
33 +
34 +```python
35 +# In your ApiHandler or bridge
36 +from python.helpers.messages import mq
37 +
38 +# This makes the user message appear in the UI immediately
39 +mq.log_user_message(context.id, "Incoming message from WhatsApp", source="WhatsApp")
40 +```
41 +
42 +---
43 +
44 +## 2. The Frontend: Component System
45 +
46 +Agent Zero uses a custom **Component Loader** that fetches HTML, extracts `<style>` and `<script type="module">`, and injects them into the DOM.
47 +
48 +### The "Golden Rules" of Frontend Components
49 +
50 +1. **Store Gating (Critical)**: Always wrap your component content in a template that waits for the store. This prevents "undefined" errors during the loading race.
51 + ```html
52 + <div x-data>
53 + <template x-if="$store.myStore">
54 + <div class="content">...</div>
55 + </template>
56 + </div>
57 + ```
58 +2. **Separate Store Files**: Never put store registration logic directly inside the HTML `alpine:init` block. Use a separate `*-store.js` file and import it via `<script type="module" src="...">`.
59 +3. **createStore Proxy**: Use `createStore` from `/js/AlpineStore.js`. It ensures the store is available to the module even before Alpine fully boots.
60 +
61 +---
62 +
63 +## 3. The Modal System
64 +
65 +Modals in A0 are "stacked" and loaded dynamically via `openModal(path)`.
66 +
67 +### Directory Convention
68 +- `webui/components/modals/<feature>/<feature>.html`
69 +- `webui/components/modals/<feature>/<feature>-store.js`
70 +
71 +### Plugin Settings
72 +
73 +Plugins get a dedicated settings modal with **Project** and **Agent profile** context selectors. To enable it:
74 +
75 +1. Add `webui/settings.html` to your plugin (auto-detected).
76 +2. Set `"settings_sections": ["agent"]` in `plugin.json` - this places a subsection with a Settings button in the chosen tab.
77 +
78 +Your `settings.html` binds to `$store.pluginSettings.settings` (a plain object persisted as `settings.json`). The modal's Save/Cancel footer handles persistence automatically. See `plugins/README.md` for the full contract and settings resolution priority chain.
79 +
80 +For plugins that surface **existing core settings** (e.g. wrapping `settings/agent/memory.html`), set `$store.pluginSettings.saveMode = 'core'` in `x-init` to route Save through the core settings API instead.
81 +
82 +---
83 +
84 +## 4. Lifecycle Synchronization
85 +
86 +| Action | Backend Extension | Frontend Lifecycle |
87 +|---|---|---|
88 +| **Initialization** | `agent_init` | `init()` in Store |
89 +| **Mounting** | N/A | `x-create` directive |
90 +| **Processing** | `monologue_start/end` | UI loading state |
91 +| **Cleanup** | `context_deleted` | `x-destroy` directive |
92 +
93 +---
94 +
95 +## 5. Directory Mapping (Plugin Layout)
96 +
97 +> [!IMPORTANT]
98 +> **Always create new plugins in `usr/plugins/`.** The root `/plugins/` folder is reserved for core Agent Zero plugins and may be overwritten during framework updates.
99 +
100 +```text
101 +usr/plugins/my-plugin/
102 +├── plugin.json # Required manifest (name, version, settings_sections)
103 +├── api/ # ApiHandler (python.helpers.api)
104 +├── extensions/
105 +│ ├── python/agent_init/ # Auto-start logic
106 +│ └── webui/ # sidebar-quick-actions-main-start/
107 +└── webui/
108 + ├── settings.html # Optional: plugin settings UI
109 + └── my-modal.html # Full plugin pages + stores
110 +```
111 +
112 +Refer to `docs/agents/AGENTS.components.md` for deep UI technicals and `docs/agents/AGENTS.modals.md` for modal-specific CSS classes (`btn-ok`, `btn-cancel`).
113 +
docs/agents/AGENTS.components.md new
+648
@@ -0,0 +1,648 @@
1 +# Agent Zero Component System
2 +
3 +> Generated from codebase reconnaissance on 2026-01-10
4 +> Scope: `webui/components/` - Self-contained Alpine.js component architecture
5 +
6 +## Quick Reference
7 +
8 +| Aspect | Value |
9 +|--------|-------|
10 +| **Tech Stack** | Alpine.js, ES Modules, CSS Variables |
11 +| **Component Tag** | `<x-component path="...">` |
12 +| **State Management** | `createStore(name, model)` from `/js/AlpineStore.js` |
13 +| **Modals** | `openModal(path)` / `closeModal()` from `/js/modals.js` |
14 +| **API Layer** | `callJsonApi()` / `fetchApi()` from `/js/api.js` |
15 +
16 +---
17 +
18 +## Table of Contents
19 +
20 +1. [Architecture Overview](#1-architecture-overview)
21 +2. [Component Structure](#2-component-structure)
22 +3. [Store Pattern](#3-store-pattern)
23 +4. [Lifecycle Management](#4-lifecycle-management)
24 +5. [Integration Layer](#5-integration-layer)
25 +6. [Alpine.js Directives](#6-alpinejs-directives)
26 +7. [Patterns and Conventions](#7-patterns-and-conventions)
27 +8. [Pitfalls and Anti-Patterns](#8-pitfalls-and-anti-patterns)
28 +9. [Porting Guide](#9-porting-guide)
29 +
30 +---
31 +
32 +## 1. Architecture Overview
33 +
34 +### Core Files (Integration Layer)
35 +
36 +| File | Purpose |
37 +|------|---------|
38 +| `/js/components.js` | Component loader - hydrates `<x-component>` tags |
39 +| `/js/AlpineStore.js` | Store factory with Alpine proxy |
40 +| `/js/modals.js` | Modal stack management |
41 +| `/js/initFw.js` | Bootstrap: loads Alpine, registers custom directives |
42 +| `/js/api.js` | CSRF-protected API client (`callJsonApi`, `fetchApi`) |
43 +
44 +### Component Resolution
45 +
46 +```
47 +<x-component path="sidebar/left-sidebar.html">
48 + ↓
49 + Resolves to: components/sidebar/left-sidebar.html
50 + ↓
51 + Loader: importComponent() fetches, parses, injects
52 +```
53 +
54 +- Path auto-prefixes `components/` if not present
55 +- Component HTML cached after first fetch
56 +- Module scripts cached by virtual URL
57 +- MutationObserver auto-loads dynamically inserted components
58 +
59 +### Data Flow
60 +
61 +```
62 +Component HTML
63 + ↓
64 +imports Store module
65 + ↓
66 +createStore() registers with Alpine
67 + ↓
68 +Template binds via $store.name
69 + ↓
70 +User actions → store methods → state updates → reactive UI
71 +```
72 +
73 +---
74 +
75 +## 2. Component Structure
76 +
77 +### Anatomy of a Component
78 +
79 +```html
80 +<html>
81 +<head>
82 + <!-- Module imports MUST be in <head> -->
83 + <script type="module">
84 + import { store } from "/components/feature/feature-store.js";
85 + </script>
86 +</head>
87 +
88 +<body>
89 + <!-- Store gate: prevents render until store registered -->
90 + <div x-data>
91 + <template x-if="$store.featureStore">
92 + <!-- Single root element inside template (mandatory) -->
93 + <div class="feature-container">
94 + <p x-text="$store.featureStore.value"></p>
95 + <button @click="$store.featureStore.action()">Do Thing</button>
96 + </div>
97 + </template>
98 + </div>
99 +
100 + <!-- Inline styles scoped to component -->
101 + <style>
102 + .feature-container {
103 + color: var(--color-text);
104 + }
105 + </style>
106 +</body>
107 +</html>
108 +```
109 +
110 +### Key Rules
111 +
112 +| Rule | Rationale |
113 +|------|-----------|
114 +| Scripts in `<head>`, content in `<body>` | Loader extracts separately |
115 +| Use `type="module"` for scripts | Enables ES imports, caching |
116 +| Wrap with `x-data` + `x-if="$store.X"` | Prevents render before store ready |
117 +| `<template>` has ONE root element | Alpine limitation |
118 +| Styles inline in component | Self-contained, no global CSS files |
119 +
120 +### Nesting Components
121 +
122 +```html
123 +<div class="parent-container">
124 + <x-component path="child/child-component.html"></x-component>
125 +</div>
126 +```
127 +
128 +Components can nest other components. Loader recursively processes `x-component` tags.
129 +
130 +---
131 +
132 +## 3. Store Pattern
133 +
134 +### Creating a Store
135 +
136 +```javascript
137 +// /components/feature/feature-store.js
138 +import { createStore } from "/js/AlpineStore.js";
139 +
140 +const model = {
141 + // State
142 + items: [],
143 + loading: false,
144 + _initialized: false,
145 +
146 + // Lifecycle (called once by Alpine when store registers)
147 + init() {
148 + if (this._initialized) return;
149 + this._initialized = true;
150 + this.load();
151 + },
152 +
153 + // Actions
154 + async load() {
155 + this.loading = true;
156 + // ... fetch data
157 + this.loading = false;
158 + },
159 +
160 + // Computed-like getters (Alpine reactivity works)
161 + get itemCount() {
162 + return this.items.length;
163 + }
164 +};
165 +
166 +export const store = createStore("featureStore", model);
167 +```
168 +
169 +### Store Proxy Behavior
170 +
171 +`createStore()` returns a proxy that:
172 +- Before Alpine boots: reads/writes directly to `model` object
173 +- After Alpine boots: reads/writes through `Alpine.store(name)`
174 +
175 +This enables safe module-level initialization before Alpine loads.
176 +
177 +### Store Access
178 +
179 +| Context | Syntax |
180 +|---------|--------|
181 +| Template (Alpine) | `$store.featureStore.prop` |
182 +| Module import | `import { store } from "./feature-store.js"; store.prop` |
183 +| Global (avoid) | `Alpine.store("featureStore").prop` |
184 +
185 +**Prefer module imports over global lookups.**
186 +
187 +### Persistence Helpers
188 +
189 +```javascript
190 +import { saveState, loadState } from "/js/AlpineStore.js";
191 +
192 +// Save to localStorage (exclude functions automatically)
193 +const snapshot = saveState(store, [], ["transientField"]);
194 +localStorage.setItem("myStore", JSON.stringify(snapshot));
195 +
196 +// Restore
197 +const saved = JSON.parse(localStorage.getItem("myStore"));
198 +loadState(store, saved);
199 +```
200 +
201 +---
202 +
203 +## 4. Lifecycle Management
204 +
205 +### Custom Alpine Directives
206 +
207 +Registered in `/js/initFw.js`:
208 +
209 +| Directive | When Fires | Use Case |
210 +|-----------|------------|----------|
211 +| `x-create` | Once on mount | Initialize, subscribe to events |
212 +| `x-destroy` | On unmount/cleanup | Unsubscribe, clear timers |
213 +| `x-every-second` | Every 1s while mounted | Polling, countdowns |
214 +| `x-every-minute` | Every 60s while mounted | Low-frequency updates |
215 +| `x-every-hour` | Every 3600s while mounted | Rare periodic tasks |
216 +
217 +### Usage Pattern
218 +
219 +```html
220 +<div
221 + x-create="$store.myStore.onOpen()"
222 + x-destroy="$store.myStore.cleanup()"
223 +>
224 + <!-- content -->
225 +</div>
226 +```
227 +
228 +### Store Lifecycle Pattern
229 +
230 +```javascript
231 +const model = {
232 + _initialized: false,
233 + resizeHandler: null,
234 +
235 + init() {
236 + // Guard: runs only once per app lifetime
237 + if (this._initialized) return;
238 + this._initialized = true;
239 + // Global setup: event listeners, intervals
240 + this.resizeHandler = () => this.handleResize();
241 + window.addEventListener("resize", this.resizeHandler);
242 + },
243 +
244 + // Called via x-create when component mounts (can run multiple times)
245 + onOpen() {
246 + this.loadData();
247 + },
248 +
249 + // Called via x-destroy when component unmounts
250 + cleanup() {
251 + // Clear component-specific state, not global listeners
252 + },
253 +
254 + // For full teardown (rarely needed)
255 + destroy() {
256 + if (this.resizeHandler) {
257 + window.removeEventListener("resize", this.resizeHandler);
258 + this.resizeHandler = null;
259 + }
260 + this._initialized = false;
261 + }
262 +};
263 +```
264 +
265 +**Key distinction:**
266 +- `init()` → once per app load (store registration)
267 +- `onOpen()` → each time component mounts (modal opens, etc.)
268 +- `cleanup()`/`destroy()` → teardown resources
269 +
270 +---
271 +
272 +## 5. Integration Layer
273 +
274 +### API Calls
275 +
276 +```javascript
277 +import { callJsonApi, fetchApi } from "/js/api.js";
278 +
279 +// JSON POST with CSRF
280 +const result = await callJsonApi("/endpoint", { key: "value" });
281 +
282 +// Raw fetch with CSRF
283 +const response = await fetchApi("/endpoint", {
284 + method: "GET",
285 + headers: { "Accept": "application/json" }
286 +});
287 +```
288 +
289 +- `callJsonApi`: JSON-in, JSON-out, throws on non-2xx
290 +- `fetchApi`: Adds CSRF header, handles 403 retry, redirects to `/login`
291 +
292 +### Modals
293 +
294 +```javascript
295 +import { openModal, closeModal } from "/js/modals.js";
296 +
297 +// Open (returns Promise that resolves when modal closes)
298 +await openModal("feature/feature-modal.html");
299 +
300 +// Close topmost modal
301 +closeModal();
302 +
303 +// Close specific modal by path
304 +closeModal("feature/feature-modal.html");
305 +```
306 +
307 +Modal component receives title from `<title>` tag:
308 +```html
309 +<head>
310 + <title>My Modal Title</title>
311 +</head>
312 +```
313 +
314 +Modal footer (outside scroll area):
315 +```html
316 +<div data-modal-footer>
317 + <button @click="closeModal()">Close</button>
318 +</div>
319 +```
320 +
321 +### Attribute Inheritance
322 +
323 +Parent `x-component` attributes accessible via `globalThis.xAttrs(element)`:
324 +
325 +```html
326 +<!-- Parent -->
327 +<x-component path="child.html" mydata='{"id": 123}'></x-component>
328 +
329 +<!-- Child can access -->
330 +<script type="module">
331 + const attrs = globalThis.xAttrs(document.currentScript);
332 + console.log(attrs.mydata.id); // 123
333 +</script>
334 +```
335 +
336 +---
337 +
338 +## 6. Alpine.js Directives
339 +
340 +### Common Patterns
341 +
342 +| Pattern | Syntax |
343 +|---------|--------|
344 +| Reactive text | `x-text="$store.s.value"` |
345 +| Conditional render | `x-if="$store.s.condition"` |
346 +| Visibility toggle | `x-show="$store.s.visible"` |
347 +| Class binding | `:class="{'active': $store.s.isActive}"` |
348 +| Event handler | `@click="$store.s.action()"` |
349 +| Two-way bind | `x-model="$store.s.inputValue"` |
350 +| List iteration | `<template x-for="item in $store.s.items">` |
351 +| Init expression | `x-init="$store.s.load()"` |
352 +
353 +### Store Gating (Critical Pattern)
354 +
355 +```html
356 +<div x-data>
357 + <template x-if="$store.myStore">
358 + <!-- Renders only when store exists -->
359 + </template>
360 +</div>
361 +```
362 +
363 +**Always gate components that depend on stores.** Prevents errors during initial load race.
364 +
365 +---
366 +
367 +## 7. Patterns and Conventions
368 +
369 +### ✅ DO
370 +
371 +| Pattern | Example |
372 +|---------|---------|
373 +| Self-contained components | All HTML/CSS/JS in one component folder |
374 +| Module imports with absolute paths | `import { store } from "/components/..."` |
375 +| CSS variables for theming | `color: var(--color-text)` |
376 +| Guard `init()` with `_initialized` | Prevents duplicate setup |
377 +| Use `display: contents` for flex chains | Wrapper doesn't break parent flex |
378 +| Inline component styles | `<style>` in component `<body>` |
379 +| Import stores in `<head>` | Ensures registration before render |
380 +| Name stores uniquely | `createStore("featureStore", ...)` |
381 +
382 +### CSS Variable Theming
383 +
384 +```css
385 +.component {
386 + background: var(--color-panel);
387 + color: var(--color-text);
388 + border: 1px solid var(--color-border);
389 + padding: var(--spacing-md);
390 + transition: all var(--transition-speed) ease-in-out;
391 + font-size: var(--font-size-normal);
392 +}
393 +```
394 +
395 +### Flex Chain Preservation
396 +
397 +When `x-component` wrapper would break flex layout:
398 +
399 +```css
400 +#parent-container > x-component,
401 +#parent-container > x-component > div[x-data] {
402 + display: contents;
403 +}
404 +```
405 +
406 +---
407 +
408 +## 8. Pitfalls and Anti-Patterns
409 +
410 +### 🚫 DON'T
411 +
412 +| Anti-Pattern | Why | Fix |
413 +|--------------|-----|-----|
414 +| Global CSS files | Breaks encapsulation | Inline styles per component |
415 +| `window.Alpine.store()` lookups | Timing issues, coupling | Import store module directly |
416 +| Call `init()` from `x-init` | Runs multiple times | Use guard, or use `x-create` for per-mount |
417 +| Multiple roots in `<template>` | Alpine breaks | Wrap in single `<div>` |
418 +| `.catch(() => null)` for errors | Hides bugs | Let errors surface, use notifications |
419 +| Scripts outside `<head>` | May not load before template | Move to `<head>` with `type="module"` |
420 +| Hardcoded colors | Breaks theming | Use CSS variables |
421 +| Relative imports `./file.js` | Path resolution issues | Use absolute `/components/...` |
422 +
423 +### Common Mistakes
424 +
425 +**Race condition: store not ready**
426 +```html
427 +<!-- ❌ BAD: No gate -->
428 +<div x-data>
429 + <p x-text="$store.myStore.value"></p>
430 +</div>
431 +
432 +<!-- ✅ GOOD: Store gate -->
433 +<div x-data>
434 + <template x-if="$store.myStore">
435 + <p x-text="$store.myStore.value"></p>
436 + </template>
437 +</div>
438 +```
439 +
440 +**Duplicate initialization**
441 +```javascript
442 +// ❌ BAD: Runs every time store accessed
443 +init() {
444 + window.addEventListener("resize", this.handler);
445 +}
446 +
447 +// ✅ GOOD: Guard pattern
448 +init() {
449 + if (this._initialized) return;
450 + this._initialized = true;
451 + window.addEventListener("resize", this.handler);
452 +}
453 +```
454 +
455 +**Leaking listeners**
456 +```javascript
457 +// ❌ BAD: No cleanup
458 +init() {
459 + this.interval = setInterval(() => this.tick(), 1000);
460 +}
461 +
462 +// ✅ GOOD: With cleanup
463 +init() {
464 + this.interval = setInterval(() => this.tick(), 1000);
465 +},
466 +destroy() {
467 + clearInterval(this.interval);
468 +}
469 +```
470 +
471 +---
472 +
473 +## 9. Porting Guide
474 +
475 +### Minimum Requirements for External Apps
476 +
477 +1. **Files to copy:**
478 + ```
479 + /js/components.js # Component loader
480 + /js/AlpineStore.js # Store factory
481 + /js/modals.js # Modal system (optional)
482 + /js/initFw.js # Alpine bootstrap + directives
483 + ```
484 +
485 +2. **Dependencies:**
486 + - Alpine.js (vendor or CDN)
487 + - CSS variables (define your theme)
488 +
489 +3. **Bootstrap sequence:**
490 + ```javascript
491 + // initFw.js pattern:
492 + await import("path/to/alpine.min.js");
493 +
494 + // Register custom directives
495 + Alpine.directive("destroy", ...);
496 + Alpine.directive("create", ...);
497 + // etc.
498 + ```
499 +
500 +4. **HTML entry point:**
501 + ```html
502 + <script type="module" src="/js/initFw.js"></script>
503 + <x-component path="app/root.html"></x-component>
504 + ```
505 +
506 +### Adaptation Checklist
507 +
508 +- [ ] Define CSS variables for theming (`--color-*`, `--spacing-*`, etc.)
509 +- [ ] Set up component directory structure
510 +- [ ] Configure build tool to serve `/components/` path (or adjust loader)
511 +- [ ] Create API wrapper matching your backend (replace `api.js`)
512 +- [ ] Test MutationObserver behavior with your router/SPA framework
513 +- [ ] Verify module caching behavior in production build
514 +
515 +### Integration with Frameworks
516 +
517 +| Framework | Consideration |
518 +|-----------|--------------|
519 +| **Vanilla/Static** | Works directly, include initFw.js |
520 +| **Electron** | Works, may need CSP adjustments for Blob URLs |
521 +| **React/Vue** | Mount Alpine in specific container, avoid conflicts |
522 +| **SPA Routers** | MutationObserver handles dynamic inserts |
523 +
524 +---
525 +
526 +## Directory Structure Reference
527 +
528 +```
529 +webui/components/
530 +├── _examples/ # Reference implementations
531 +│ ├── _example-component.html
532 +│ └── _example-store.js
533 +├── chat/
534 +│ ├── input/
535 +│ │ ├── chat-bar.html
536 +│ │ └── input-store.js
537 +│ └── ...
538 +├── sidebar/
539 +│ ├── sidebar-store.js
540 +│ ├── left-sidebar.html
541 +│ └── chats/
542 +│ └── chats-list.html
543 +├── modals/
544 +│ └── file-browser/
545 +│ ├── file-browser.html
546 +│ └── file-browser-store.js
547 +├── notifications/
548 +│ ├── notification-store.js
549 +│ └── notification-toast-stack.html
550 +└── settings/
551 + └── ...
552 +```
553 +
554 +**Naming conventions:**
555 +- Components: `feature-name.html`
556 +- Stores: `feature-store.js` or `feature-name-store.js`
557 +- Modals: placed in `modals/` or feature folder
558 +
559 +---
560 +
561 +## Key Exports Summary
562 +
563 +### `/js/components.js`
564 +```javascript
565 +export async function importComponent(path, targetElement)
566 +export async function loadComponents(roots)
567 +export function getParentAttributes(el)
568 +// Global: globalThis.xAttrs
569 +```
570 +
571 +### `/js/AlpineStore.js`
572 +```javascript
573 +export function createStore(name, initialState)
574 +export function getStore(name)
575 +export function saveState(store, include, exclude)
576 +export function loadState(store, state, include, exclude)
577 +```
578 +
579 +### `/js/modals.js`
580 +```javascript
581 +export function openModal(modalPath)
582 +export function closeModal(modalPath?)
583 +export function scrollModal(id)
584 +// Globals: globalThis.openModal, closeModal, scrollModal
585 +```
586 +
587 +### `/js/api.js`
588 +```javascript
589 +export async function callJsonApi(endpoint, data)
590 +export async function fetchApi(url, request)
591 +```
592 +
593 +---
594 +
595 +## Addendum: Additional Patterns
596 +
597 +### Alpine Transitions
598 +
599 +Use `x-transition` for enter/leave animations:
600 +
601 +```html
602 +<div x-show="visible"
603 + x-transition:enter="fade-enter"
604 + x-transition:leave="fade-leave">
605 +```
606 +
607 +### Two-Click Confirmation (`$confirmClick`)
608 +
609 +Magic helper for destructive actions:
610 +
611 +```html
612 +<button @click="$confirmClick($event, () => $store.myStore.delete(item.id))">
613 + <span class="material-symbols-outlined">delete</span>
614 +</button>
615 +```
616 +
617 +First click arms (icon changes to checkmark), second click confirms. Auto-resets after 2s.
618 +
619 +### Device Detection
620 +
621 +Body receives `device-touch` or `device-mouse` class via `/js/initializer.js`. Use for input-type-specific styling:
622 +
623 +```css
624 +.device-touch .hover-only { display: none; }
625 +```
626 +
627 +### CSS Variables Reference
628 +
629 +Defined in `/webui/index.css`:
630 +
631 +| Variable | Purpose |
632 +|----------|---------|
633 +| `--color-background` | Page background |
634 +| `--color-text` | Primary text |
635 +| `--color-primary` | Headings, emphasis |
636 +| `--color-panel` | Card/sidebar backgrounds |
637 +| `--color-border` | Borders, dividers |
638 +| `--color-accent` | Highlights, actions |
639 +| `--color-input` | Form field backgrounds |
640 +| `--spacing-xs/sm/md/lg` | Consistent spacing scale |
641 +| `--font-size-small/normal/large` | Typography scale |
642 +| `--transition-speed` | Animation duration (0.3s) |
643 +
644 +Theme switching via `.light-mode` class on root element.
645 +
646 +---
647 +
648 +*End of Component System Documentation*
docs/agents/AGENTS.modals.md new
+307
@@ -0,0 +1,307 @@
1 +# Agent Zero — Frontend Modals (stacked `openModal/closeModal`)
2 +
3 +This document covers the stacked modal system used by Agent Zero’s frontend, implemented in:
4 +
5 +- `webui/js/modals.js`
6 +- `webui/css/modals.css`
7 +
8 +It also defines conventions for writing modal components in `webui/components/modals/` and `webui/components/settings/`.
9 +
10 +> 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.
11 +
12 +---
13 +
14 +## Prerequisites & concepts (quick orientation)
15 +
16 +### Alpine.js and `$store`
17 +
18 +The 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>`.
19 +
20 +- Alpine initialization + lifecycle directives: `webui/js/initFw.js`
21 +- Store wrapper: `webui/js/AlpineStore.js`
22 +
23 +### `<x-component>` and `importComponent()`
24 +
25 +Agent Zero uses a custom `<x-component path="...">` tag that is populated by the component loader.
26 +
27 +The modal system does not rely on `<x-component>` directly; instead it calls the same loader function:
28 +
29 +- `importComponent(path, targetElement)` from `webui/js/components.js`
30 +
31 +That loader fetches component HTML, injects its `<style>` and `<script>` tags, and supports nested components.
32 +
33 +---
34 +
35 +## Why a “stacked modal system”?
36 +
37 +Agent Zero can open modals from many places (sidebar, settings, message actions), and sometimes a modal opens another modal.
38 +
39 +The stacked system provides:
40 +
41 +- A single modal container structure for consistent UI
42 +- A stack so nested modals work predictably
43 +- A single backdrop that always sits behind the active modal
44 +- A content loader that supports component HTML + `<style>` + `<script type="module">` the same way `<x-component>` does
45 +
46 +---
47 +
48 +## Core API
49 +
50 +File: `webui/js/modals.js`
51 +
52 +### `openModal(modalPath): Promise<void>`
53 +
54 +- Creates a new modal element and pushes it onto a stack.
55 +- Loads modal contents into the modal body by calling the component loader (`importComponent`).
56 +- Resolves the returned promise when the modal element is removed from the DOM.
57 +
58 +Edge cases / failure behavior (current implementation):
59 +
60 +- Invalid `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.
61 +- Calling `openModal()` multiple times with the same `modalPath` creates multiple modal instances (no deduping).
62 +
63 +Modal paths are component paths, e.g.:
64 +
65 +- `modals/file-browser/file-browser.html`
66 +- `modals/history/history.html`
67 +- `settings/settings.html`
68 +
69 +### `closeModal(modalPath?: string): void`
70 +
71 +- If no path is passed, closes the top modal.
72 +- If `modalPath` is passed, finds and closes that modal in the stack.
73 +
74 +Edge cases / failure behavior:
75 +
76 +- If the stack is empty, `closeModal()` is a no-op.
77 +- If `modalPath` is provided but not found, it is a no-op.
78 +
79 +### `scrollModal(id: string): void`
80 +
81 +Scrolls within the top modal’s `.modal-scroll` to an element by id.
82 +
83 +---
84 +
85 +## Modal DOM structure (what `modals.js` creates)
86 +
87 +When `openModal()` runs, it creates this outer shell:
88 +
89 +- `.modal` (full-screen fixed overlay container)
90 + - `.modal-inner`
91 + - `.modal-header` (title + close button)
92 + - `.modal-scroll`
93 + - `.modal-bd` (where the component HTML is imported)
94 + - `.modal-footer-slot` (used when the modal provides a footer)
95 +
96 +The component HTML is imported into `.modal-bd`.
97 +
98 +The title is taken from `<title>` inside the imported document (fallback: the modalPath).
99 +
100 +### Sizing and scrolling (CSS behavior)
101 +
102 +File: `webui/css/modals.css`
103 +
104 +- `.modal` is full-screen fixed positioning.
105 +- `.modal-inner` is centered and constrained:
106 + - `width: 90%`
107 + - `max-width: 960px`
108 + - `max-height: 90vh`
109 +- Tall modals scroll inside `.modal-scroll` (`overflow-y: auto`).
110 +- If the modal provides a footer, `.modal-footer-slot` is pinned under the scroll area (the body scrolls; the footer doesn’t).
111 +
112 +---
113 +
114 +## Footer support (`data-modal-footer`)
115 +
116 +Some modals want a footer that stays fixed while the body scrolls.
117 +
118 +Pattern:
119 +
120 +- In your modal component HTML, include a footer element marked with `data-modal-footer`.
121 +- `modals.js` will move that element out of the scroll area and into `.modal-footer-slot`.
122 +- `modals.js` adds `.modal-with-footer` to `.modal-inner` so CSS can lay out the scroll area correctly.
123 +
124 +Example patterns in the repo:
125 +
126 +- `webui/components/settings/settings.html` (Save / Cancel footer)
127 +- `webui/components/modals/file-browser/file-browser.html`
128 +- `webui/components/modals/scheduler/scheduler-modal.html`
129 +
130 +### Practical advice
131 +
132 +- The footer element must exist *after the component is loaded*, but `modals.js` uses a `requestAnimationFrame` to let Alpine mount first.
133 +- If you conditionally render the footer, keep it stable (don't constantly create/destroy it) or you'll fight the relocation.
134 +
135 +---
136 +
137 +## Shared CSS and button conventions
138 +
139 +File: `webui/css/modals.css`
140 +
141 +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.
142 +
143 +### Button classes
144 +
145 +Use these standard button classes for modal actions:
146 +
147 +| Class | Use case | Visual style |
148 +|-------|----------|--------------|
149 +| `btn btn-ok` | Positive/confirmatory actions (Save, Create, Confirm) | Solid blue background, white text |
150 +| `btn btn-cancel` | Dismissive/negative actions (Cancel, Close, Delete) | Transparent with accent border |
151 +
152 +Footer button order convention: **positive action first** (left), **negative action second** (right).
153 +
154 +Example footer markup:
155 +
156 +```html
157 +<div class="modal-footer" data-modal-footer>
158 + <button class="btn btn-ok" @click="$store.myStore.save()">Save</button>
159 + <button class="btn btn-cancel" @click="window.closeModal('...')">Cancel</button>
160 +</div>
161 +```
162 +
163 +### Available shared classes
164 +
165 +Before writing component-specific CSS, check `webui/css/modals.css` for:
166 +
167 +- `.btn`, `.btn-ok`, `.btn-cancel`, `.btn-field` — button styles
168 +- `.modal-footer` — footer container layout
169 +- `.section`, `.section-title`, `.section-description` — content sections
170 +- `.loading` — shimmer loading placeholder
171 +- `.toolbar-button`, `.toolbar-group` — editor toolbar elements
172 +- Range input styling for sliders
173 +
174 +### When to add component-specific CSS
175 +
176 +Add styles in your component's `<style>` tag only when:
177 +
178 +- The style is truly unique to that component's layout/behavior
179 +- No existing shared class covers the use case
180 +- You need to override a shared class for a specific context (use sparingly)
181 +
182 +Avoid redefining `.btn`, `.modal-footer`, or other shared classes in component CSS—this creates inconsistency and maintenance burden.
183 +
184 +---
185 +
186 +## Closing behavior (what users expect)
187 +
188 +### Close by button
189 +
190 +The shell provides a close button (`.modal-close`) that always closes the top modal.
191 +
192 +### Close by Escape
193 +
194 +`Escape` closes the top modal only (stack semantics).
195 +
196 +### Close by click-outside
197 +
198 +To avoid accidental close during drag/select, the shell only closes on click-outside when:
199 +
200 +- both `mousedown` and `mouseup` occurred on the modal container itself (`.modal`), not on inner content.
201 +
202 +---
203 +
204 +## Stacking + backdrop + z-index
205 +
206 +File: `webui/js/modals.js`
207 +
208 +- A single `.modal-backdrop` is used for all modals.
209 +- Z-index logic:
210 + - Base z-index for modals is `3000`
211 + - Each modal gets `base + index*20`
212 + - Backdrop sits below the top modal, and between the top two when multiple modals are open.
213 +
214 +Outcome:
215 +
216 +- Nested modals don’t “flatten” into each other.
217 +- The backdrop always darkens the page behind the active modal without hiding lower modals incorrectly.
218 +
219 +---
220 +
221 +## Writing a modal component (conventions)
222 +
223 +### File location
224 +
225 +Prefer:
226 +
227 +- `webui/components/modals/<name>/<name>.html` (+ optional `*-store.js`)
228 +
229 +Settings uses:
230 +
231 +- `webui/components/settings/settings.html` and nested settings components.
232 +
233 +### Recommended HTML skeleton
234 +
235 +Use this structure:
236 +
237 +- `<head><title>...</title>`
238 +- A `<script type="module">` that imports your store (so it registers before Alpine evaluates bindings)
239 +- `<body>` with:
240 + - a single root wrapper
241 + - Alpine lifecycle hooks (`x-create`/`x-init` + `x-destroy`) to run open/cleanup logic
242 +- A `<style>` tag at the bottom of the component file containing all modal-specific styling
243 +
244 +Good examples:
245 +
246 +- `webui/components/modals/history/history.html`
247 +- `webui/components/modals/context/context.html`
248 +- `webui/components/modals/scheduler/scheduler-modal.html`
249 +
250 +### Store conventions
251 +
252 +- Define stores via `createStore()` from `webui/js/AlpineStore.js`.
253 +- UI reads state through `$store.<storeName>`.
254 +- Prefer a small public API:
255 + - `open()` / `openModal()` to open the modal
256 + - `destroy()` / `cleanup()` to reset transient state
257 +
258 +---
259 +
260 +## “Don’t” list (keeps the system sane)
261 +
262 +- Don’t add `document.addEventListener('alpine:init', ...)` blocks inside component HTML — it conflicts with `initFw.js` ordering and can register handlers twice when components are reloaded.
263 +- Don’t manually manipulate `.modal-inner` or `.modal-scroll` structure — `modals.js` owns the shell and footer-slot mechanics; manual changes create layout/scroll bugs.
264 +- Don’t introduce a new modal overlay system; keep the stack, mixed systems break z-index/backdrop expectations and make nested modals unreliable.
265 +- Don’t assume a modal is the only one open; always write logic that behaves correctly when stacked — `Escape`/close behavior, z-index, and focus should all be “top modal wins”.
266 +- Don’t open a new modal synchronously from another modal’s close handler — it can race stack removal and produce transient z-index/backdrop glitches; schedule via `requestAnimationFrame` if needed.
267 +- Don’t store sensitive data in modal stores without explicit cleanup — stores often outlive a modal’s DOM and can leak values across reopen.
268 +- Don’t rely on modal state persisting across close/reopen cycles — design stores so `open()` rehydrates and `destroy()`/`cleanup()` resets transient state.
269 +
270 +---
271 +
272 +## Debugging checklist
273 +
274 +### Modal opens but content never appears
275 +
276 +- Verify the path passed to `openModal()` is correct and resolves under `webui/components/`.
277 +- Check browser console for fetch/import errors.
278 +- If you use `<script type="module" src="...">` in the component, ensure the URL is correct.
279 +
280 +### Footer is inside the scroll area (not pinned)
281 +
282 +- Ensure your footer element has the `data-modal-footer` attribute.
283 +- Ensure it exists after the component loads (if you gate it behind `x-if`, ensure the store is available).
284 +
285 +### Closing behavior is weird with nested modals
286 +
287 +- Remember `Escape` closes only the top modal.
288 +- `closeModal('some/path.html')` closes that modal wherever it is in the stack.
289 +
290 +### Modal opens but `$store.someStore` is undefined
291 +
292 +- Ensure the modal HTML imports its store in a `<script type="module">` block (so the store registers before Alpine evaluates bindings).
293 +- Ensure the store name matches what the markup reads (store registered as `createStore("history", ...)` is accessed as `$store.history`).
294 +- If you guard with `<template x-if="$store.someStore">`, verify you’re using the correct store key and not a stale name.
295 +
296 +### Modal won’t close / UI feels “stuck”
297 +
298 +- `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()`).
299 +- As a last resort, inspect and remove leftover modal nodes in DevTools (`document.querySelectorAll('.modal').forEach(m => m.remove())`) and reset any store state that assumes the modal is open.
300 +
301 +### Inspecting stack/z-index issues
302 +
303 +- In DevTools Elements:
304 + - inspect `.modal` elements (multiple means you have a stack)
305 + - check the `style="z-index: ..."` on `.modal-inner`
306 + - verify `.modal-backdrop` exists and sits between modals when stacked
307 +- If the footer is not pinned, confirm your footer element has `data-modal-footer` and that `.modal-inner` has `.modal-with-footer`.
plugins/README.md
+72 -139
@@ -1,6 +1,9 @@
1 # Agent Zero Plugins
2
3 -This directory contains default plugins shipped with Agent Zero and is the source of truth for the plugin system.
3 +This directory contains default plugins. For a full-stack development guide, see [docs/AGENTS.md](../docs/AGENTS.md).
4 +
5 +> [!TIP]
6 +> While Agent Zero looks for plugins in both `usr/plugins/` and `plugins/`, you should **always develop new plugins in `usr/plugins/`**. This ensures your work is isolated from core system files and persists through framework updates.
7
8 ## Architecture
9
@@ -38,8 +41,9 @@ Agent Zero uses a convention-over-configuration plugin model:
41 ## File Structure
42
43 ```text
41 -plugins/
42 - <plugin_id>/
44 +usr/plugins/
45 + <plugin_name>/
46 + plugin.json # Required manifest (enables discovery)
47 api/ # API handlers (ApiHandler subclasses)
48 tools/ # Agent tools (Tool subclasses)
49 helpers/ # Shared Python helpers
@@ -48,26 +52,41 @@ plugins/
52 extensions/
53 python/<extension_point>/ # Python lifecycle extensions
54 webui/<extension_point>/ # WebUI HTML/JS hook contributions
51 - webui/ # Full plugin-owned UI pages/components
52 -
53 -usr/plugins/<plugin_id>/ # User overrides (higher priority)
55 + webui/
56 + settings.html # Optional: plugin settings UI
57 + ... # Full plugin-owned UI pages/components
58 ```
59
60 ## Directory Conventions
61
58 -Each plugin lives in `plugins/<plugin_id>/` (or `usr/plugins/<plugin_id>/` for overrides).
62 +Each plugin lives in `usr/plugins/<plugin_name>/`.
63
64 Capability discovery is based on these paths:
65
62 -- `api/*.py` - API handlers (`ApiHandler` subclasses), exposed under `/api/plugins/<plugin_id>/<handler>`
66 +- `plugin.json` - **required** manifest; a directory without it is not recognized as a plugin
67 +- `api/*.py` - API handlers (`ApiHandler` subclasses), exposed under `/api/plugins/<plugin_name>/<handler>`
68 - `tools/*.py` - agent tools (`Tool` subclasses)
69 - `helpers/*.py` - shared Python helpers
70 - `extensions/python/<extension_point>/*.py` - backend lifecycle extensions
71 - `extensions/webui/<extension_point>/*` - WebUI extension assets (HTML/JS)
72 - `webui/**` - full plugin-owned UI pages/components (loaded directly by path)
73 +- `webui/settings.html` - if present, a Settings button appears for this plugin in the relevant settings tabs
74 - `prompts/**/*.md` - prompt templates
75 - `agents/` - agent profiles
76
77 +### `plugin.json` format
78 +
79 +```json
80 +{
81 + "name": "My Plugin",
82 + "description": "What this plugin does.",
83 + "version": "1.0.0",
84 + "settings_sections": ["agent"]
85 +}
86 +```
87 +
88 +`settings_sections` controls which Settings tabs show a subsection for this plugin. Current valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Leave empty (`[]`) for no subsection.
89 +
90 ## Frontend Extensions
91
92 ### HTML insertion via breakpoints
@@ -78,57 +97,6 @@ Core UI defines insertion points like:
97 <x-extension id="sidebar-quick-actions-main-start"></x-extension>
98 ```
99
81 -Current sidebar surfaces:
82 -
83 -- `sidebar-start`
84 -- `sidebar-end`
85 -- `sidebar-top-wrapper-start`
86 -- `sidebar-top-wrapper-end`
87 -- `sidebar-quick-actions-main-start`
88 -- `sidebar-quick-actions-main-end`
89 -- `sidebar-quick-actions-dropdown-start`
90 -- `sidebar-quick-actions-dropdown-end`
91 -- `sidebar-chats-list-start`
92 -- `sidebar-chats-list-end`
93 -- `sidebar-tasks-list-start`
94 -- `sidebar-tasks-list-end`
95 -- `sidebar-bottom-wrapper-start`
96 -- `sidebar-bottom-wrapper-end`
97 -
98 -Current input surfaces:
99 -
100 -- `chat-input-start`
101 -- `chat-input-end`
102 -- `chat-input-progress-start`
103 -- `chat-input-progress-end`
104 -- `chat-input-box-start`
105 -- `chat-input-box-end`
106 -- `chat-input-bottom-actions-start`
107 -- `chat-input-bottom-actions-end`
108 -
109 -Current chat surfaces:
110 -
111 -- `chat-top-start`
112 -- `chat-top-end`
113 -
114 -Current welcome surfaces:
115 -
116 -- `welcome-screen-start`
117 -- `welcome-screen-end`
118 -- `welcome-actions-start`
119 -- `welcome-actions-end`
120 -- `welcome-banners-start`
121 -- `welcome-banners-end`
122 -
123 -Current modal surfaces:
124 -
125 -- `modal-shell-start`
126 -- `modal-shell-end`
127 -
128 -Placement pattern:
129 -- keep wrapper-level anchors in parent composition files
130 -- keep section anchors in their owning component files, inside local `x-data` scope
131 -
100 Resolution flow:
101
102 1. `webui/js/extensions.js` finds `x-extension` nodes.
@@ -137,21 +105,6 @@ Resolution flow:
105 4. `extensions.js` injects returned entries as `<x-component path="...">`.
106 5. `components.js` loads each component using the standard component pipeline.
107
140 -Baseline extension template (project convention):
141 -
142 -```html
143 -<div x-data>
144 - <button
145 - x-move-after=".config-button#dashboard"
146 - class="config-button"
147 - id="my-plugin-button"
148 - @click="openModal('../plugins/my-plugin/webui/my-modal.html')"
149 - title="My Plugin">
150 - <span class="material-symbols-outlined">extension</span>
151 - </button>
152 -</div>
153 -```
154 -
108 Required baseline for HTML UI extensions in this repository:
109 - include a root `x-data` scope
110 - include one explicit `x-move-*` placement directive
@@ -166,22 +119,6 @@ Runtime code calls:
119
120 `callJsExtensions("<extension_point>", contextObject)`
121
169 -JS hook convention:
170 -- pass one mutable context object when extensions are expected to influence behavior
171 -- that object is passed by reference, so mutations are visible to subsequent hooks in the same flow
172 -- hooks that support cancellation expose a `cancel: false` or `skip: false` field; set it to `true` to abort the operation
173 -
174 -Current JS hook points:
175 -
176 -| Hook | File | Context fields | skip/cancel |
177 -|---|---|---|---|
178 -| `set_messages_before_loop` | messages.js | `messages, history, scrollerOptions, massRender, results` | - |
179 -| `set_messages_after_loop` | messages.js | same as above | - |
180 -| `send_message_before` | index.js | `message, attachments, context, cancel` | `cancel` |
181 -| `apply_snapshot_before` | index.js | `snapshot, willUpdateMessages, skip` | `skip` |
182 -| `open_modal_before` | modals.js | `modalPath, modal, cancel` | `cancel` |
183 -| `close_modal_before` | modals.js | `modalPath, modal, cancel` | `cancel` |
184 -
122 ### Fine placement helpers
123
124 `initFw.js` provides Alpine move directives for plugin markup:
@@ -192,75 +129,71 @@ Current JS hook points:
129 - `x-move-before`
130 - `x-move-after`
131
195 -Placement behavior:
196 -- `x-move-to-start`, `x-move-to-end`, and `x-move-to` resolve a parent selector and insert the extension element as that parent's child.
197 -- `x-move-before` and `x-move-after` resolve a reference selector and insert the extension element as a sibling in the reference element's parent.
198 -- This structural difference can produce different visual results when parent and sibling styling differ (for example dropdown spacing/padding).
199 -- Example anchor selector for placing after the first dropdown item: `x-move-after=".quick-actions-dropdown .dropdown-header + .dropdown-item"`.
200 -
201 -## Plugin Author Flow
202 -
203 -1. Create `plugins/<plugin_id>/`.
204 -2. Add backend capabilities by convention (`api/`, `tools/`, `helpers/`, `extensions/python/`, `prompts/`, `agents/`).
205 -3. Pick a WebUI breakpoint or JS hook extension point.
206 -4. For HTML UI entries: place files under `extensions/webui/<extension_point>/`, use root `x-data` + one `x-move-*` directive.
207 -5. For JS hooks: place `*.js` files under `extensions/webui/<extension_point>/`, export a default async function.
208 -6. Place full plugin pages/components in `webui/` and open them directly by path.
132 +## Plugin Settings
133
210 -### Python extension example
134 +If your plugin needs user-configurable settings:
135
212 -```python
213 -# plugins/my-plugin/extensions/python/monologue_end/_50_my_extension.py
214 -from python.helpers.extension import Extension
136 +1. Add `webui/settings.html` to your plugin. The system detects this file automatically.
137 +2. Declare which settings tabs should show a subsection for your plugin via `settings_sections` in `plugin.json`.
138 +3. The plugin settings modal provides **Project** and **Agent profile** context selectors (same as the Skills list). Settings are scoped per-project and per-agent.
139
216 -class MyExtension(Extension):
217 - async def execute(self, **kwargs):
218 - pass
219 -```
140 +### Settings HTML contract
141
221 -### HTML WebUI extension example
142 +Your `settings.html` receives context from `$store.pluginSettings`:
143
144 ```html
224 -<!-- plugins/my-plugin/extensions/webui/sidebar-quick-actions-main-start/my-button.html -->
225 -<div x-data>
226 - <button
227 - x-move-after=".config-button#dashboard"
228 - class="config-button"
229 - id="my-plugin-button"
230 - @click="openModal('../plugins/my-plugin/webui/my-modal.html')"
231 - title="My Plugin">
232 - <span class="material-symbols-outlined">extension</span>
233 - </button>
234 -</div>
145 +<html>
146 +<head>
147 + <title>My Plugin Settings</title>
148 + <script type="module">
149 + import { store } from "/components/plugins/plugin-settings-store.js";
150 + </script>
151 +</head>
152 +<body>
153 + <div x-data>
154 + <!-- bind fields to $store.pluginSettings.settings -->
155 + <input x-model="$store.pluginSettings.settings.my_key" />
156 + </div>
157 +</body>
158 +</html>
159 ```
160
237 -### JS hook example
161 +- `$store.pluginSettings.settings` - plain object loaded from `settings.json`, save-scoped to the selected project/agent.
162 +- The modal's **Save** button calls `POST /plugins` (`action: save_settings`) automatically.
163 +- For plugins that surface **core settings** (like memory), set `saveMode = 'core'` in `x-init` so Save delegates to the core settings API instead.
164
239 -```js
240 -// plugins/my-plugin/extensions/webui/send_message_before/transform.js
241 -export default async function(ctx) {
242 - // prepend a tag to every outgoing message
243 - ctx.message = "[my-plugin] " + ctx.message;
244 -}
165 +### Settings resolution priority (highest first)
166 +
167 +```
168 +project/.a0proj/agents/<profile>/plugins/<name>/settings.json
169 +project/.a0proj/plugins/<name>/settings.json
170 +usr/agents/<profile>/plugins/<name>/settings.json
171 +agents/<profile>/plugins/<name>/settings.json
172 +usr/plugins/<name>/settings.json
173 +plugins/<name>/settings.json
174 ```
175
247 -### Full plugin UI page
176 +## Plugin Author Flow
177
249 -```html
250 -<!-- opened via openModal() or x-component -->
251 -<x-component path="../plugins/my-plugin/webui/my-modal.html"></x-component>
252 -```
178 +1. Create `usr/plugins/<plugin_name>/`.
179 +2. Add `plugin.json` manifest (required for discovery).
180 +3. Add backend capabilities by convention (`api/`, `tools/`, `helpers/`, `extensions/python/`, `prompts/`, `agents/`).
181 +4. Pick a WebUI breakpoint or JS hook extension point.
182 +5. For HTML UI entries: place files under `extensions/webui/<extension_point>/`, use root `x-data` + one `x-move-*` directive.
183 +6. For JS hooks: place `*.js` files under `extensions/webui/<extension_point>/`, export a default async function.
184 +7. Place full plugin pages/components in `webui/` and open them directly by path.
185 +8. Optionally add `webui/settings.html` and set `settings_sections` in `plugin.json` to expose settings in the UI.
186
187 ## Routes
188
256 -- Plugin static assets: `GET /plugins/<plugin_id>/<path>`
257 -- Plugin APIs: `POST /api/plugins/<plugin_id>/<handler>`
189 +- Plugin static assets: `GET /plugins/<plugin_name>/<path>`
190 +- Plugin APIs: `POST /api/plugins/<plugin_name>/<handler>`
191 - WebUI extension discovery: `POST /api/load_webui_extensions`
192 +- Plugin management (list, get/save settings): `POST /plugins`
193
194 ## Notes
195
196 - User plugins in `usr/plugins/` override repo plugins by plugin ID.
197 - Runtime behavior is fully convention-driven from directory structure.
198 - Extension point ordering between multiple plugins is currently implicit (filesystem order).
265 -- Project-specific plugin roots are not yet active (commented out in `get_plugin_roots()`).
199 - When you need a new extension point for your plugin, submit a PR - we are actively expanding coverage based on community needs.
plugins/example_agent/plugin.json
+5
@@ -2,4 +2,9 @@
2 "description": "Example Agent Plugin",
3 "per_project_config": true,
4 "per_agent_config": false
5 +}
6 + "name": "Example Agent",
7 + "description": "Example agent plugin demonstrating the Agent Zero plugin system.",
8 + "version": "1.0.0",
9 + "settings_sections": []
10 }
plugins/memory/plugin.json
+6 -1
@@ -2,4 +2,9 @@
2 "description": "Memory Plugin",
3 "per_project_config": true,
4 "per_agent_config": true
5 -}
\ No newline at end of file
5 +}
6 + "name": "Memory",
7 + "description": "Provides persistent memory capabilities to Agent Zero agents.",
8 + "version": "1.0.0",
9 + "settings_sections": ["agent"]
10 +}
plugins/memory/webui/settings.html new
+21
@@ -0,0 +1,21 @@
1 +<html>
2 +<head>
3 + <title>Memory Settings</title>
4 + <script type="module">
5 + import { store } from "/components/settings/settings-store.js";
6 + </script>
7 +</head>
8 +<body>
9 + <!--
10 + This file surfaces the existing core memory settings inside the plugin settings modal.
11 + It sets saveMode='core' so the modal's Save button delegates to $store.settings.saveSettings().
12 + -->
13 + <div x-data
14 + x-init="
15 + $store.pluginSettings.saveMode = 'core';
16 + if ($store.settings && !$store.settings.settings) $store.settings.onOpen();
17 + ">
18 + <x-component path="settings/agent/memory.html"></x-component>
19 + </div>
20 +</body>
21 +</html>
python/api/plugins.py new
+40
@@ -0,0 +1,40 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import plugins
3 +
4 +
5 +class Plugins(ApiHandler):
6 + """
7 + Core plugin management API.
8 + Actions: list, get_settings, save_settings
9 + """
10 +
11 + async def process(self, input: dict, request: Request) -> dict | Response:
12 + action = input.get("action", "list")
13 +
14 + if action == "list":
15 + tab = input.get("tab") # optional: filter by settings_tab
16 + data = plugins.list_plugins_with_metadata(tab_filter=tab or None)
17 + return {"ok": True, "data": data}
18 +
19 + if action == "get_settings":
20 + plugin_name = input.get("plugin_name", "")
21 + project_name = input.get("project_name", "")
22 + agent_profile = input.get("agent_profile", "")
23 + if not plugin_name:
24 + return Response(status=400, response="Missing plugin_name")
25 + settings = plugins.get_plugin_settings(plugin_name,
26 + project_name=project_name,
27 + agent_profile=agent_profile)
28 + return {"ok": True, "data": settings or {}}
29 +
30 + if action == "save_settings":
31 + plugin_name = input.get("plugin_name", "")
32 + project_name = input.get("project_name", "")
33 + agent_profile = input.get("agent_profile", "")
34 + settings = input.get("settings", {})
35 + if not plugin_name:
36 + return Response(status=400, response="Missing plugin_name")
37 + plugins.save_plugin_settings(plugin_name, project_name, agent_profile, settings)
38 + return {"ok": True}
39 +
40 + return Response(status=400, response=f"Unknown action: {action}")
run_ui.py
+1 -1
@@ -11,7 +11,7 @@ import asyncio
11 from pathlib import Path
12 import urllib.request
13 import urllib.error
14 -from regex.regex import F
14 +from regex import F
15 import uvicorn
16 from flask import Flask, request, Response, session, redirect, url_for, render_template_string
17 from werkzeug.wrappers.response import Response as BaseResponse
skills/a0-create-plugin/SKILL.md new
+149
@@ -0,0 +1,149 @@
1 +---
2 +name: a0-create-plugin
3 +description: Create, extend, or modify Agent Zero plugins. Follows strict full-stack conventions (usr/plugins, plugin.json, Store Gating, AgentContext, plugin settings). Use for UI hooks, API handlers, lifecycle extensions, or plugin settings UI.
4 +---
5 +
6 +# Agent Zero Plugin Development
7 +
8 +> [!IMPORTANT]
9 +> **Always create new plugins in `usr/plugins/<plugin_name>/`.** The root `/plugins` directory is reserved for core system plugins.
10 +
11 +Primary references:
12 +- `/a0/AGENTS.md` (Full-stack architecture & AgentContext)
13 +- `/a0/docs/agents/AGENTS.components.md` (Component system deep dive)
14 +- `/a0/docs/agents/AGENTS.modals.md` (Modal system & CSS conventions)
15 +- `/a0/plugins/README.md` (Extension points, plugin.json, settings system)
16 +
17 +## 📋 Plugin Manifest (`plugin.json`)
18 +
19 +Every plugin **must** have a `plugin.json` or it will not be discovered:
20 +
21 +```json
22 +{
23 + "name": "My Plugin",
24 + "description": "What this plugin does.",
25 + "version": "1.0.0",
26 + "settings_sections": ["agent"]
27 +}
28 +```
29 +
30 +`settings_sections` controls which Settings tabs show a subsection for this plugin. Valid values: `agent`, `external`, `mcp`, `developer`, `backup`. Use `[]` for no subsection.
31 +
32 +## 🛠️ Mandatory Frontend Patterns
33 +
34 +### 1. The "Store Gate" Template
35 +To avoid race conditions and "undefined" errors, every component must use this wrapper:
36 +```html
37 +<div x-data>
38 + <template x-if="$store.myPluginStore">
39 + <div x-init="$store.myPluginStore.onOpen()" x-destroy="$store.myPluginStore.cleanup()">
40 + <!-- Content goes here -->
41 + </div>
42 + </template>
43 +</div>
44 +```
45 +
46 +### 2. Separate Store Module
47 +Place store logic in a separate `.js` file. Do NOT use `alpine:init` listeners inside HTML.
48 +```javascript
49 +// webui/my-store.js
50 +import { createStore } from "/js/AlpineStore.js";
51 +export const store = createStore("myPluginStore", {
52 + status: 'idle',
53 + init() { ... },
54 + onOpen() { ... }
55 +});
56 +```
57 +Import it in the HTML `<head>`:
58 +```html
59 +<head>
60 + <script type="module" src="/plugins/<plugin_name>/webui/my-store.js"></script>
61 +</head>
62 +```
63 +
64 +## ⚙️ Plugin Settings
65 +
66 +If your plugin needs user-configurable settings, add `webui/settings.html`. The system detects it automatically and shows a Settings button in the relevant tabs (per `settings_sections` in `plugin.json`).
67 +
68 +### Settings modal contract
69 +
70 +The modal provides Project + Agent profile context selectors. Your `settings.html` binds to `$store.pluginSettings.settings`:
71 +
72 +```html
73 +<html>
74 +<head>
75 + <title>My Plugin Settings</title>
76 + <script type="module">
77 + import { store } from "/components/plugins/plugin-settings-store.js";
78 + </script>
79 +</head>
80 +<body>
81 + <div x-data>
82 + <input x-model="$store.pluginSettings.settings.my_key" />
83 + <input type="checkbox" x-model="$store.pluginSettings.settings.feature_enabled" />
84 + </div>
85 +</body>
86 +</html>
87 +```
88 +
89 +The modal's Save button persists `$store.pluginSettings.settings` to `settings.json` in the correct scope (project/agent/global).
90 +
91 +### Surfacing core settings (e.g. memory pattern)
92 +
93 +If your plugin exposes **existing core settings** rather than plugin-specific ones, set `saveMode = 'core'` so Save delegates to the core settings API:
94 +
95 +```html
96 +<div x-data x-init="
97 + $store.pluginSettings.saveMode = 'core';
98 + if ($store.settings && !$store.settings.settings) $store.settings.onOpen();
99 +">
100 + <x-component path="settings/agent/memory.html"></x-component>
101 +</div>
102 +```
103 +
104 +### Sidebar Button (sidebar entry point)
105 +- **Extension point**: `sidebar-quick-actions-main-start`
106 +- **Class**: `class="config-button"`
107 +- **Placement**: `x-move-after=".config-button#dashboard"`
108 +- **Action**: `@click="openModal('/plugins/<plugin_name>/webui/my-modal.html')"`
109 +
110 +## 🐍 Backend API & Context
111 +
112 +### Import Paths
113 +- **Correct**: `from agent import AgentContext` (not python.helpers.agent)
114 +
115 +### Sending Messages Proactively
116 +```python
117 +from agent import AgentContext
118 +from python.helpers.messages import UserMessage
119 +
120 +context = AgentContext.use(context_id)
121 +task = context.communicate(UserMessage("Message text"))
122 +response = await task.result()
123 +```
124 +
125 +### Reading Plugin Settings (backend)
126 +```python
127 +from python.helpers.plugins import get_plugin_settings
128 +
129 +# Runtime (with running agent - resolves project/profile from context)
130 +settings = get_plugin_settings("my-plugin", agent=agent)
131 +
132 +# UI path (explicit strings, no agent instance needed)
133 +settings = get_plugin_settings("my-plugin", project_name="my-project", agent_profile="default")
134 +```
135 +
136 +## 📁 Directory Layout
137 +```
138 +usr/plugins/<name>/
139 + plugin.json # Required manifest
140 + api/ # API Handlers (ApiHandler base class)
141 + tools/ # Tool subclasses
142 + extensions/
143 + python/agent_init/ # Python lifecycle extensions
144 + webui/<point>/ # HTML/JS hook extensions
145 + webui/
146 + settings.html # Optional: plugin settings UI
147 + my-modal.html # Full plugin pages
148 + my-store.js # Alpine stores
149 +```
webui/components/plugins/plugin-settings-store.js new
+151
@@ -0,0 +1,151 @@
1 +import { createStore } from "/js/AlpineStore.js";
2 +
3 +const fetchApi = globalThis.fetchApi;
4 +
5 +const model = {
6 + // which plugin this modal is showing
7 + pluginName: null,
8 + pluginMeta: null,
9 +
10 + // context selectors (mirrors skills list pattern)
11 + projects: [],
12 + agentProfiles: [],
13 + projectName: "",
14 + agentProfileKey: "",
15 +
16 + // plugin settings data (plugins bind their fields here)
17 + settings: {},
18 +
19 + // 'plugin' = save to plugin settings API
20 + // 'core' = save via $store.settings.saveSettings() (for plugins that surface core settings)
21 + saveMode: 'plugin',
22 +
23 + isLoading: false,
24 + isSaving: false,
25 + error: null,
26 +
27 + // Called by the subsection button before openModal()
28 + async open(pluginName) {
29 + this.pluginName = pluginName;
30 + this.pluginMeta = null;
31 + this.settings = {};
32 + this.error = null;
33 + this.saveMode = 'plugin';
34 + this.projectName = "";
35 + this.agentProfileKey = "";
36 + await Promise.all([this.loadProjects(), this.loadAgentProfiles()]);
37 + await this.loadSettings();
38 + },
39 +
40 + // Called by x-create inside the modal on every open
41 + async onModalOpen() {
42 + if (this.pluginName) await this.loadSettings();
43 + },
44 +
45 + async loadAgentProfiles() {
46 + try {
47 + const response = await fetchApi("/agents", {
48 + method: "POST",
49 + headers: { "Content-Type": "application/json" },
50 + body: JSON.stringify({ action: "list" }),
51 + });
52 + const data = await response.json().catch(() => ({}));
53 + this.agentProfiles = data.ok ? (data.data || []) : [];
54 + } catch {
55 + this.agentProfiles = [];
56 + }
57 + },
58 +
59 + async loadProjects() {
60 + try {
61 + const response = await fetchApi("/projects", {
62 + method: "POST",
63 + headers: { "Content-Type": "application/json" },
64 + body: JSON.stringify({ action: "list_options" }),
65 + });
66 + const data = await response.json().catch(() => ({}));
67 + this.projects = data.ok ? (data.data || []) : [];
68 + } catch {
69 + this.projects = [];
70 + }
71 + },
72 +
73 + async loadSettings() {
74 + if (!this.pluginName) return;
75 + this.isLoading = true;
76 + this.error = null;
77 + try {
78 + const response = await fetchApi("/plugins", {
79 + method: "POST",
80 + headers: { "Content-Type": "application/json" },
81 + body: JSON.stringify({
82 + action: "get_settings",
83 + plugin_name: this.pluginName,
84 + project_name: this.projectName || "",
85 + agent_profile: this.agentProfileKey || "",
86 + }),
87 + });
88 + const result = await response.json().catch(() => ({}));
89 + this.settings = result.ok ? (result.data || {}) : {};
90 + if (!result.ok) this.error = result.error || "Failed to load settings";
91 + } catch (e) {
92 + this.error = e?.message || "Failed to load settings";
93 + this.settings = {};
94 + } finally {
95 + this.isLoading = false;
96 + }
97 + },
98 +
99 + async save() {
100 + if (!this.pluginName) return;
101 +
102 + // Core-backed plugins (e.g. memory) delegate to the settings store
103 + if (this.saveMode === 'core') {
104 + const coreStore = Alpine.store('settings');
105 + if (coreStore?.saveSettings) {
106 + const ok = await coreStore.saveSettings();
107 + if (ok) window.closeModal?.();
108 + }
109 + return;
110 + }
111 +
112 + // Plugin-specific settings: persist to plugin settings API
113 + this.isSaving = true;
114 + this.error = null;
115 + try {
116 + const response = await fetchApi("/plugins", {
117 + method: "POST",
118 + headers: { "Content-Type": "application/json" },
119 + body: JSON.stringify({
120 + action: "save_settings",
121 + plugin_name: this.pluginName,
122 + project_name: this.projectName || "",
123 + agent_profile: this.agentProfileKey || "",
124 + settings: this.settings,
125 + }),
126 + });
127 + const result = await response.json().catch(() => ({}));
128 + if (!result.ok) this.error = result.error || "Save failed";
129 + else window.closeModal?.();
130 + } catch (e) {
131 + this.error = e?.message || "Save failed";
132 + } finally {
133 + this.isSaving = false;
134 + }
135 + },
136 +
137 + cleanup() {
138 + this.pluginName = null;
139 + this.pluginMeta = null;
140 + this.settings = {};
141 + this.error = null;
142 + },
143 +
144 + // Reactive URL for the plugin's settings component (used with x-html injection)
145 + get settingsComponentHtml() {
146 + if (!this.pluginName) return "";
147 + return `<x-component path="/plugins/${this.pluginName}/webui/settings.html"></x-component>`;
148 + },
149 +};
150 +
151 +export const store = createStore("pluginSettings", model);
webui/components/plugins/plugin-settings.html new
+152
@@ -0,0 +1,152 @@
1 +<html>
2 +<head>
3 + <title>Plugin Settings</title>
4 + <script type="module">
5 + import { store } from "/components/plugins/plugin-settings-store.js";
6 + </script>
7 +</head>
8 +<body>
9 + <div x-data>
10 + <template x-if="$store.pluginSettings">
11 + <div x-create="$store.pluginSettings.onModalOpen()"
12 + x-destroy="$store.pluginSettings.cleanup()">
13 +
14 + <!-- Context toolbar: Project + Agent profile (mirrors skills list) -->
15 + <div class="plugin-settings-toolbar">
16 + <div class="plugin-settings-toolbar-row">
17 +
18 + <label class="plugin-settings-toolbar-item">
19 + <span class="plugin-settings-toolbar-label">Project</span>
20 + <select x-model="$store.pluginSettings.projectName"
21 + @change="$store.pluginSettings.loadSettings()">
22 + <option value="">Global</option>
23 + <template x-for="project in $store.pluginSettings.projects" :key="project.key">
24 + <option :value="project.key" x-text="project.label"></option>
25 + </template>
26 + </select>
27 + </label>
28 +
29 + <label class="plugin-settings-toolbar-item">
30 + <span class="plugin-settings-toolbar-label">Agent profile</span>
31 + <select x-model="$store.pluginSettings.agentProfileKey"
32 + @change="$store.pluginSettings.loadSettings()">
33 + <option value="">All profiles</option>
34 + <template x-for="profile in $store.pluginSettings.agentProfiles" :key="profile.key">
35 + <option :value="profile.key" x-text="profile.label"></option>
36 + </template>
37 + </select>
38 + </label>
39 +
40 + </div>
41 + </div>
42 +
43 + <!-- Error -->
44 + <div x-show="$store.pluginSettings.error" class="plugin-settings-error">
45 + <span class="material-symbols-outlined">error</span>
46 + <span x-text="$store.pluginSettings.error"></span>
47 + </div>
48 +
49 + <!-- Loading -->
50 + <div x-show="$store.pluginSettings.isLoading" class="plugin-settings-loading">
51 + <span class="material-symbols-outlined spinning">progress_activity</span>
52 + <span>Loading settings...</span>
53 + </div>
54 +
55 + <!-- Plugin settings body: plugin provides /plugins/<name>/webui/settings.html -->
56 + <div x-show="!$store.pluginSettings.isLoading"
57 + class="plugin-settings-body"
58 + x-html="$store.pluginSettings.settingsComponentHtml">
59 + </div>
60 +
61 + </div>
62 + </template>
63 + </div>
64 +
65 + <!-- Footer (pinned outside scroll area) -->
66 + <div class="modal-footer" data-modal-footer>
67 + <button class="btn btn-ok"
68 + @click="$store.pluginSettings.save()"
69 + :disabled="$store.pluginSettings?.isSaving || $store.pluginSettings?.isLoading">
70 + Save
71 + </button>
72 + <button class="btn btn-cancel"
73 + @click="window.closeModal?.()">
74 + Cancel
75 + </button>
76 + </div>
77 +
78 + <style>
79 + .plugin-settings-toolbar {
80 + margin-bottom: 1rem;
81 + }
82 +
83 + .plugin-settings-toolbar-row {
84 + display: flex;
85 + align-items: center;
86 + gap: 0.75rem;
87 + flex-wrap: wrap;
88 + }
89 +
90 + .plugin-settings-toolbar-item {
91 + display: flex;
92 + align-items: center;
93 + gap: 0.5rem;
94 + flex: 1 1 18rem;
95 + min-width: 12rem;
96 + margin: 0;
97 + }
98 +
99 + .plugin-settings-toolbar-label {
100 + font-weight: 600;
101 + color: var(--color-text-secondary);
102 + white-space: nowrap;
103 + }
104 +
105 + .plugin-settings-toolbar-item select {
106 + flex: 1 1 auto;
107 + min-width: 0;
108 + }
109 +
110 + @media (max-width: 640px) {
111 + .plugin-settings-toolbar-item {
112 + flex-basis: 100%;
113 + min-width: 0;
114 + }
115 + }
116 +
117 + .plugin-settings-error {
118 + display: flex;
119 + align-items: center;
120 + gap: 0.5rem;
121 + color: var(--color-error, #e74c3c);
122 + background: var(--color-error-bg, #fdecea);
123 + border-radius: 4px;
124 + padding: 0.5rem 0.75rem;
125 + margin-bottom: 0.75rem;
126 + font-size: var(--font-size-small);
127 + }
128 +
129 + .plugin-settings-loading {
130 + display: flex;
131 + align-items: center;
132 + justify-content: center;
133 + gap: 0.5rem;
134 + padding: 2rem;
135 + color: var(--color-text-secondary);
136 + }
137 +
138 + .plugin-settings-body {
139 + min-height: 4rem;
140 + }
141 +
142 + .spinning {
143 + animation: spin 1s linear infinite;
144 + }
145 +
146 + @keyframes spin {
147 + from { transform: rotate(0deg); }
148 + to { transform: rotate(360deg); }
149 + }
150 + </style>
151 +</body>
152 +</html>
webui/components/settings/agent/agent-settings.html
+4 -11
@@ -39,12 +39,6 @@
39 <span>Embedding Model</span>
40 </a>
41 </li>
42 - <li>
43 - <a href="#section-memory">
44 - <img src="/public/memory.svg" alt="Memory" />
45 - <span>Memory</span>
46 - </a>
47 - </li>
42 <li>
43 <a href="#section-speech">
44 <img src="/public/speech.svg" alt="Speech" />
@@ -81,10 +75,6 @@
75 <x-component path="settings/agent/embed_model.html"></x-component>
76 </div>
77
84 - <div id="section-memory" class="section">
85 - <x-component path="settings/agent/memory.html"></x-component>
86 - </div>
87 -
78 <div id="section-speech" class="section">
79 <x-component path="settings/agent/speech.html"></x-component>
80 </div>
@@ -93,7 +83,10 @@
83 <x-component path="settings/agent/workdir.html"></x-component>
84 </div>
85
96 -
86 + <!-- Plugin settings subsection: shows plugins tagged with "agent" -->
87 + <div id="section-agent-plugins" class="section">
88 + <x-component path="settings/plugins/plugins-subsection.html" data-tab="agent"></x-component>
89 + </div>
90
91 </div>
92 </template>
webui/components/settings/plugins/plugins-subsection.html new
+118
@@ -0,0 +1,118 @@
1 +<html>
2 +<head>
3 + <script type="module">
4 + import { store } from "/components/plugins/plugin-settings-store.js";
5 + </script>
6 +</head>
7 +<body>
8 + <!--
9 + Reusable plugin subsection for a settings tab.
10 + Parent must pass data-tab attribute on the x-component tag, e.g.:
11 + <x-component path="settings/plugins/plugins-subsection.html" data-tab="agent"></x-component>
12 + The tab value is read at init time from the x-component element's dataset.
13 + -->
14 + <div x-data="{
15 + tab: '',
16 + plugins: [],
17 + loading: false,
18 + async init() {
19 + // find closest x-component ancestor to read the data-tab attribute
20 + const host = this.$el.closest('x-component') || this.$el.parentElement?.closest('x-component');
21 + this.tab = host?.getAttribute('data-tab') || '';
22 + await this.load();
23 + },
24 + async load() {
25 + if (!this.tab) return;
26 + this.loading = true;
27 + try {
28 + const r = await globalThis.fetchApi('/plugins', {
29 + method: 'POST',
30 + headers: { 'Content-Type': 'application/json' },
31 + body: JSON.stringify({ action: 'list', tab: this.tab }),
32 + });
33 + const data = await r.json().catch(() => ({}));
34 + this.plugins = (data.ok ? data.data || [] : []).filter(p => p.has_settings_ui);
35 + } catch {
36 + this.plugins = [];
37 + } finally {
38 + this.loading = false;
39 + }
40 + },
41 + async openSettings(pluginName) {
42 + await Alpine.store('pluginSettings')?.open(pluginName);
43 + window.openModal?.('components/plugins/plugin-settings.html');
44 + }
45 + }" x-init="init()">
46 +
47 + <template x-if="loading || plugins.length > 0">
48 + <div class="plugin-subsection-wrapper">
49 + <div class="section-title">Plugins</div>
50 + <div class="section-description">
51 + Plugin-specific settings for this tab.
52 + </div>
53 +
54 + <div x-show="loading" class="plugin-subsection-loading">
55 + <span>Loading...</span>
56 + </div>
57 +
58 + <template x-for="plugin in plugins" :key="plugin.name">
59 + <div class="plugin-subsection-row">
60 + <div class="plugin-subsection-info">
61 + <span class="plugin-subsection-name" x-text="plugin.display_name || plugin.name"></span>
62 + <span class="plugin-subsection-desc" x-text="plugin.description"></span>
63 + </div>
64 + <button class="button" @click="openSettings(plugin.name)">
65 + <span class="icon material-symbols-outlined">settings</span>
66 + Settings
67 + </button>
68 + </div>
69 + </template>
70 + </div>
71 + </template>
72 + </div>
73 +
74 + <style>
75 + .plugin-subsection-wrapper {
76 + margin-top: 0.5rem;
77 + }
78 +
79 + .plugin-subsection-loading {
80 + color: var(--color-text-secondary);
81 + font-size: var(--font-size-small);
82 + margin-top: 0.5rem;
83 + }
84 +
85 + .plugin-subsection-row {
86 + display: flex;
87 + align-items: center;
88 + justify-content: space-between;
89 + gap: 1rem;
90 + padding: 0.6rem 0.75rem;
91 + border: 1px solid var(--color-border);
92 + border-radius: 4px;
93 + margin-top: 0.5rem;
94 + background: var(--color-bg-primary);
95 + }
96 +
97 + .plugin-subsection-info {
98 + display: flex;
99 + flex-direction: column;
100 + gap: 0.2rem;
101 + min-width: 0;
102 + }
103 +
104 + .plugin-subsection-name {
105 + font-weight: 600;
106 + font-size: var(--font-size-normal);
107 + }
108 +
109 + .plugin-subsection-desc {
110 + font-size: var(--font-size-small);
111 + color: var(--color-text-secondary);
112 + white-space: nowrap;
113 + overflow: hidden;
114 + text-overflow: ellipsis;
115 + }
116 + </style>
117 +</body>
118 +</html>