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>