align plugin schema, config API actions, and plugin list UI
Alessandro committed
Feb 20, 2026 at 08:32 UTC
604078376f6f67c57e862b7bab0d6bf9d43ae87a
15 files changed
+413
-252
AGENTS.md
+3
-3
@@ -72,10 +72,10 @@ Modals in A0 are "stacked" and loaded dynamically via `openModal(path)`.
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).
75
+1. Add `webui/config.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.
78
+Your `config.html` binds to `$store.pluginSettings.settings` (a plain object persisted as `config.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
@@ -105,7 +105,7 @@ usr/plugins/my-plugin/
105
│ ├── python/agent_init/ # Auto-start logic
106
│ └── webui/ # sidebar-quick-actions-main-start/
107
└── webui/
108
- ├── settings.html # Optional: plugin settings UI
108
+ ├── config.html # Optional: plugin settings UI
109
└── my-modal.html # Full plugin pages + stores
110
```
111
plugins/README.md
+16
-16
@@ -53,7 +53,7 @@ usr/plugins/
53
python/<extension_point>/ # Python lifecycle extensions
54
webui/<extension_point>/ # WebUI HTML/JS hook contributions
55
webui/
56
- settings.html # Optional: plugin settings UI
56
+ config.html # Optional: plugin settings UI
57
... # Full plugin-owned UI pages/components
58
```
59
@@ -70,7 +70,7 @@ Capability discovery is based on these paths:
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
73
+- `webui/config.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
@@ -133,18 +133,18 @@ Runtime code calls:
133
134
If your plugin needs user-configurable settings:
135
136
-1. Add `webui/settings.html` to your plugin. The system detects this file automatically.
136
+1. Add `webui/config.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
140
-### Settings HTML contract
140
+### Config HTML contract
141
142
-Your `settings.html` receives context from `$store.pluginSettings`:
142
+Your `config.html` receives context from `$store.pluginSettings`:
143
144
```html
145
<html>
146
<head>
147
- <title>My Plugin Settings</title>
147
+ <title>My Plugin Config</title>
148
<script type="module">
149
import { store } from "/components/plugins/plugin-settings-store.js";
150
</script>
@@ -158,19 +158,19 @@ Your `settings.html` receives context from `$store.pluginSettings`:
158
</html>
159
```
160
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.
161
+- `$store.pluginSettings.settings` - plain object loaded from `config.json`, save-scoped to the selected project/agent.
162
+- The modal's **Save** button calls `POST /plugins` (`action: save_config`) 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
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
168
+project/.a0proj/agents/<profile>/plugins/<name>/config.json
169
+project/.a0proj/plugins/<name>/config.json
170
+usr/agents/<profile>/plugins/<name>/config.json
171
+agents/<profile>/plugins/<name>/config.json
172
+usr/plugins/<name>/config.json
173
+plugins/<name>/config.json
174
```
175
176
## Plugin Author Flow
@@ -182,14 +182,14 @@ plugins/<name>/settings.json
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.
185
+8. Optionally add `webui/config.html` and set `settings_sections` in `plugin.json` to expose settings in the UI.
186
187
## Routes
188
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`
192
+- Plugin management (get/save config): `POST /plugins`
193
194
## Notes
195
plugins/example_agent/plugin.json
+3
-5
@@ -1,10 +1,8 @@
1
{
2
- "description": "Example Agent Plugin",
3
- "per_project_config": true,
4
- "per_agent_config": false
5
-}
2
"name": "Example Agent",
3
"description": "Example agent plugin demonstrating the Agent Zero plugin system.",
4
"version": "1.0.0",
9
- "settings_sections": []
5
+ "settings_sections": [],
6
+ "per_project_config": true,
7
+ "per_agent_config": false
8
}
plugins/memory/plugin.json
+3
-5
@@ -1,10 +1,8 @@
1
{
2
- "description": "Memory Plugin",
3
- "per_project_config": true,
4
- "per_agent_config": true
5
-}
2
"name": "Memory",
3
"description": "Provides persistent memory capabilities to Agent Zero agents.",
4
"version": "1.0.0",
9
- "settings_sections": ["agent"]
5
+ "settings_sections": ["agent"],
6
+ "per_project_config": true,
7
+ "per_agent_config": true
8
}
plugins/memory/webui/config.html
+21
@@ -1,3 +1,24 @@
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>
22
<!DOCTYPE html>
23
<html lang="en">
24
<head>
plugins/memory/webui/settings.html
deleted
-21
@@ -1,21 +0,0 @@
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
+24
-14
@@ -1,40 +1,50 @@
1
from python.helpers.api import ApiHandler, Request, Response
2
-from python.helpers import plugins
2
+from python.helpers import plugins, files
3
4
5
class Plugins(ApiHandler):
6
"""
7
Core plugin management API.
8
- Actions: list, get_settings, save_settings
8
+ Actions: get_config, save_config
9
"""
10
11
async def process(self, input: dict, request: Request) -> dict | Response:
12
- action = input.get("action", "list")
12
+ action = input.get("action", "get_config")
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":
14
+ # Accept legacy aliases during migration.
15
+ if action in {"get_config", "get_settings"}:
16
plugin_name = input.get("plugin_name", "")
17
project_name = input.get("project_name", "")
18
agent_profile = input.get("agent_profile", "")
19
if not plugin_name:
20
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)
21
+
22
+ settings = None
23
+ if project_name or agent_profile:
24
+ file_path = plugins.determine_plugin_asset_path(
25
+ plugin_name,
26
+ project_name,
27
+ agent_profile,
28
+ plugins.CONFIG_FILE_NAME,
29
+ )
30
+ if files.exists(file_path):
31
+ settings = files.read_file_json(file_path)
32
+
33
+ if settings is None:
34
+ settings = plugins.get_plugin_config(plugin_name, agent=None)
35
+
36
return {"ok": True, "data": settings or {}}
37
30
- if action == "save_settings":
38
+ if action in {"save_config", "save_settings"}:
39
plugin_name = input.get("plugin_name", "")
40
project_name = input.get("project_name", "")
41
agent_profile = input.get("agent_profile", "")
42
settings = input.get("settings", {})
43
if not plugin_name:
44
return Response(status=400, response="Missing plugin_name")
37
- plugins.save_plugin_settings(plugin_name, project_name, agent_profile, settings)
45
+ if not isinstance(settings, dict):
46
+ return Response(status=400, response="settings must be an object")
47
+ plugins.save_plugin_config(plugin_name, project_name, agent_profile, settings)
48
return {"ok": True}
49
50
return Response(status=400, response=f"Unknown action: {action}")
python/helpers/plugins.py
+21
-4
@@ -5,7 +5,7 @@ from pathlib import Path
5
from typing import Any, Dict, List, Optional, TYPE_CHECKING
6
7
from python.helpers import files, print_style
8
-from pydantic import BaseModel
8
+from pydantic import BaseModel, Field
9
10
if TYPE_CHECKING:
11
from agent import Agent
@@ -23,13 +23,24 @@ ENABLED_FILE_NAME = ".enabled"
23
24
25
class PluginMetadata(BaseModel):
26
+ name: str = ""
27
description: str = ""
28
+ version: str = ""
29
+ settings_sections: List[str] = Field(default_factory=list)
30
+ per_project_config: bool = False
31
+ per_agent_config: bool = False
32
33
34
class PluginListItem(BaseModel):
35
name: str
36
path: str
37
+ display_name: str = ""
38
description: str = ""
39
+ version: str = ""
40
+ settings_sections: List[str] = Field(default_factory=list)
41
+ per_project_config: bool = False
42
+ per_agent_config: bool = False
43
+ is_custom: bool = False
44
has_main_screen: bool = False
45
has_config_screen: bool = False
46
@@ -64,7 +75,7 @@ def get_enhanced_plugins_list(
75
"""Discover plugins by directory convention. First root wins on ID conflict."""
76
results = []
77
67
- def load_plugins(root_path: str):
78
+ def load_plugins(root_path: str, is_custom: bool):
79
for d in sorted(Path(root_path).iterdir(), key=lambda p: p.name):
80
try:
81
if not d.is_dir() or d.name.startswith("."):
@@ -78,7 +89,13 @@ def get_enhanced_plugins_list(
89
PluginListItem(
90
name=d.name,
91
path=str(d),
92
+ display_name=meta.name or d.name,
93
description=meta.description,
94
+ version=meta.version,
95
+ settings_sections=meta.settings_sections,
96
+ per_project_config=meta.per_project_config,
97
+ per_agent_config=meta.per_agent_config,
98
+ is_custom=is_custom,
99
has_main_screen=has_main_screen,
100
has_config_screen=has_config_screen,
101
)
@@ -87,9 +104,9 @@ def get_enhanced_plugins_list(
104
pass
105
106
if custom:
90
- load_plugins(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR))
107
+ load_plugins(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR), True)
108
if builtin:
92
- load_plugins(files.get_abs_path(files.PLUGINS_DIR))
109
+ load_plugins(files.get_abs_path(files.PLUGINS_DIR), False)
110
return results
111
112
skills/a0-create-plugin/SKILL.md
+14
-9
@@ -63,11 +63,11 @@ Import it in the HTML `<head>`:
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`).
66
+If your plugin needs user-configurable settings, add `webui/config.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`:
70
+The modal provides Project + Agent profile context selectors. Your `config.html` binds to `$store.pluginSettings.settings`:
71
72
```html
73
<html>
@@ -86,7 +86,7 @@ The modal provides Project + Agent profile context selectors. Your `settings.htm
86
</html>
87
```
88
89
-The modal's Save button persists `$store.pluginSettings.settings` to `settings.json` in the correct scope (project/agent/global).
89
+The modal's Save button persists `$store.pluginSettings.settings` to `config.json` in the correct scope (project/agent/global).
90
91
### Surfacing core settings (e.g. memory pattern)
92
@@ -124,13 +124,18 @@ response = await task.result()
124
125
### Reading Plugin Settings (backend)
126
```python
127
-from python.helpers.plugins import get_plugin_settings
127
+from python.helpers.plugins import get_plugin_config, save_plugin_config
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")
130
+settings = get_plugin_config("my-plugin", agent=agent) or {}
131
+
132
+# Explicit write target (project/profile scope)
133
+save_plugin_config(
134
+ "my-plugin",
135
+ project_name="my-project",
136
+ agent_profile="default",
137
+ settings=settings,
138
+)
139
```
140
141
## 📁 Directory Layout
@@ -143,7 +148,7 @@ usr/plugins/<name>/
148
python/agent_init/ # Python lifecycle extensions
149
webui/<point>/ # HTML/JS hook extensions
150
webui/
146
- settings.html # Optional: plugin settings UI
151
+ config.html # Optional: plugin settings UI
152
my-modal.html # Full plugin pages
153
my-store.js # Alpine stores
154
```
webui/components/plugins/list/plugin-list.html
+97
-162
@@ -9,138 +9,95 @@
9
<body>
10
<div x-data>
11
<template x-if="$store.pluginListStore">
12
- <div x-init="$store.pluginListStore.loadPluginList && $store.pluginListStore.loadPluginList({ builtin: false, custom: true, search: '' })">
12
+ <div x-init="$store.pluginListStore.setTab && $store.pluginListStore.setTab('custom')">
13
14
<ul class="nav nav-tabs" id="plugin-list-tabs" role="tablist">
15
<li class="nav-item" role="presentation">
16
- <button class="nav-link active" id="plugins-custom-tab" data-bs-toggle="tab"
17
- data-bs-target="#plugins-custom" type="button" role="tab"
18
- aria-controls="plugins-custom" aria-selected="true"
19
- @click="$store.pluginListStore.loadPluginList && $store.pluginListStore.loadPluginList({ builtin: false, custom: true, search: '' })">
16
+ <button class="nav-link"
17
+ :class="{ active: $store.pluginListStore.activeTab === 'custom' }"
18
+ id="plugins-custom-tab"
19
+ type="button"
20
+ role="tab"
21
+ aria-controls="plugins-custom"
22
+ :aria-selected="$store.pluginListStore.activeTab === 'custom'"
23
+ @click="$store.pluginListStore.setTab('custom')">
24
Custom
25
</button>
26
</li>
27
<li class="nav-item" role="presentation">
24
- <button class="nav-link" id="plugins-builtin-tab" data-bs-toggle="tab"
25
- data-bs-target="#plugins-builtin" type="button" role="tab"
26
- aria-controls="plugins-builtin" aria-selected="false"
27
- @click="$store.pluginListStore.loadPluginList && $store.pluginListStore.loadPluginList({ builtin: true, custom: false, search: '' })">
28
+ <button class="nav-link"
29
+ :class="{ active: $store.pluginListStore.activeTab === 'builtin' }"
30
+ id="plugins-builtin-tab"
31
+ type="button"
32
+ role="tab"
33
+ aria-controls="plugins-builtin"
34
+ :aria-selected="$store.pluginListStore.activeTab === 'builtin'"
35
+ @click="$store.pluginListStore.setTab('builtin')">
36
Builtin
37
</button>
38
</li>
39
</ul>
40
33
- <div class="tab-content" id="plugin-list-tabs-content">
34
- <div class="tab-pane fade show active" id="plugins-custom" role="tabpanel"
35
- aria-labelledby="plugins-custom-tab">
36
- <div>
37
- <div class="plugins-toolbar">
38
- <div class="plugins-toolbar-actions">
39
- <button type="button" class="button confirm" title="Refresh"
40
- @click="$store.pluginListStore.loadPluginList && $store.pluginListStore.loadPluginList({ builtin: false, custom: true, search: '' })"
41
- :disabled="$store.pluginListStore.loading">
42
- <span class="icon material-symbols-outlined">refresh</span> Refresh
43
- </button>
44
- </div>
45
- </div>
46
-
47
- <div x-show="$store.pluginListStore.loading" class="loading">
48
- <span>Loading plugins...</span>
49
- </div>
50
-
51
- <div class="plugins-list">
52
- <template x-for="plugin in ($store.pluginListStore.plugins || [])" :key="plugin.path || plugin.name">
53
- <div class="plugin-card">
54
- <div class="plugin-header">
55
- <div class="plugin-heading">
56
- <div class="plugin-title" x-text="plugin.name || '(unnamed plugin)'"></div>
57
- <code class="plugin-path" x-text="plugin.path"></code>
58
- </div>
59
- <div class="plugin-actions">
60
- <template x-if="plugin.has_main_screen">
61
- <button type="button" class="button" title="Open"
62
- @click="$store.pluginListStore.openPlugin && $store.pluginListStore.openPlugin(plugin)">
63
- <span class="icon material-symbols-outlined">assistant_on_hub</span> Open
64
- </button>
65
- </template>
66
- <template x-if="plugin.has_config_screen">
67
- <button type="button" class="button" title="Config"
68
- @click="$store.pluginListStore.openPluginConfig && $store.pluginListStore.openPluginConfig(plugin)">
69
- <span class="icon material-symbols-outlined">settings</span> Config
70
- </button>
71
- </template>
72
- <button type="button" class="button" title="Info"
73
- @click="$store.pluginListStore.openPluginInfo && $store.pluginListStore.openPluginInfo(plugin)">
74
- <span class="icon material-symbols-outlined">info</span> Info
75
- </button>
76
- <button type="button" class="button cancel icon-button" title="Delete"
77
- @click="$confirmClick($event, () => $store.pluginListStore.deletePlugin && $store.pluginListStore.deletePlugin(plugin))">
78
- <span class="icon material-symbols-outlined">delete</span>
79
- </button>
80
- </div>
81
- </div>
82
- <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
83
- </div>
84
- </template>
85
- </div>
86
- </div>
41
+ <div class="plugins-toolbar">
42
+ <div class="plugins-toolbar-actions">
43
+ <button type="button"
44
+ class="button confirm"
45
+ title="Refresh"
46
+ @click="$store.pluginListStore.refresh()"
47
+ :disabled="$store.pluginListStore.loading">
48
+ <span class="icon material-symbols-outlined">refresh</span> Refresh
49
+ </button>
50
</div>
51
+ </div>
52
89
- <div class="tab-pane fade" id="plugins-builtin" role="tabpanel" aria-labelledby="plugins-builtin-tab">
90
- <div>
91
- <div class="plugins-toolbar">
92
- <div class="plugins-toolbar-actions">
93
- <button type="button" class="button confirm" title="Refresh"
94
- @click="$store.pluginListStore.loadPluginList && $store.pluginListStore.loadPluginList({ builtin: true, custom: false, search: '' })"
95
- :disabled="$store.pluginListStore.loading">
96
- <span class="icon material-symbols-outlined">refresh</span> Refresh
53
+ <div x-show="$store.pluginListStore.loading" class="loading">
54
+ <span>Loading plugins...</span>
55
+ </div>
56
+
57
+ <div class="plugins-list">
58
+ <template x-for="plugin in ($store.pluginListStore.plugins || [])" :key="plugin.path || plugin.name">
59
+ <div class="plugin-card">
60
+ <div class="plugin-header">
61
+ <div class="plugin-heading">
62
+ <div class="plugin-title" x-text="plugin.display_name || plugin.name || '(unnamed plugin)'"></div>
63
+ <code class="plugin-path" x-text="plugin.path"></code>
64
+ </div>
65
+ <div class="plugin-actions">
66
+ <template x-if="plugin.has_main_screen">
67
+ <button type="button"
68
+ class="button"
69
+ title="Open"
70
+ @click="$store.pluginListStore.openPlugin(plugin)">
71
+ <span class="icon material-symbols-outlined">folder_open</span> Open
72
+ </button>
73
+ </template>
74
+ <template x-if="plugin.has_config_screen">
75
+ <button type="button"
76
+ class="button"
77
+ title="Config"
78
+ @click="$store.pluginListStore.openPluginConfig(plugin)">
79
+ <span class="icon material-symbols-outlined">settings</span> Config
80
+ </button>
81
+ </template>
82
+ <button type="button"
83
+ class="button"
84
+ title="Info"
85
+ @click="$store.pluginListStore.openPluginInfo(plugin)">
86
+ <span class="icon material-symbols-outlined">info</span> Info
87
+ </button>
88
+ <button type="button"
89
+ class="button cancel icon-button"
90
+ :title="plugin.is_custom ? 'Delete' : 'Builtin plugins cannot be deleted'"
91
+ :disabled="!plugin.is_custom"
92
+ @click="$confirmClick($event, () => $store.pluginListStore.deletePlugin(plugin))">
93
+ <span class="icon material-symbols-outlined">delete</span>
94
</button>
95
</div>
96
</div>
100
-
101
- <div x-show="$store.pluginListStore.loading" class="loading">
102
- <span>Loading plugins...</span>
103
- </div>
104
-
105
- <div class="plugins-list">
106
- <template x-for="plugin in ($store.pluginListStore.plugins || [])" :key="plugin.path || plugin.name">
107
- <div class="plugin-card">
108
- <div class="plugin-header">
109
- <div class="plugin-heading">
110
- <div class="plugin-title" x-text="plugin.name || '(unnamed plugin)'"></div>
111
- <code class="plugin-path" x-text="plugin.path"></code>
112
- </div>
113
- <div class="plugin-actions">
114
- <template x-if="plugin.has_main_screen">
115
- <button type="button" class="button" title="Open"
116
- @click="$store.pluginListStore.openPlugin && $store.pluginListStore.openPlugin(plugin)">
117
- <span class="icon material-symbols-outlined">assistant_on_hub</span> Open
118
- </button>
119
- </template>
120
- <template x-if="plugin.has_config_screen">
121
- <button type="button" class="button" title="Config"
122
- @click="$store.pluginListStore.openPluginConfig && $store.pluginListStore.openPluginConfig(plugin)">
123
- <span class="icon material-symbols-outlined">settings</span> Config
124
- </button>
125
- </template>
126
- <button type="button" class="button" title="Info"
127
- @click="$store.pluginListStore.openPluginInfo && $store.pluginListStore.openPluginInfo(plugin)">
128
- <span class="icon material-symbols-outlined">info</span> Info
129
- </button>
130
- <button type="button" class="button cancel icon-button" title="Delete"
131
- @click="$confirmClick($event, () => $store.pluginListStore.deletePlugin && $store.pluginListStore.deletePlugin(plugin))">
132
- <span class="icon material-symbols-outlined">delete</span>
133
- </button>
134
- </div>
135
- </div>
136
- <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
137
- </div>
138
- </template>
139
- </div>
97
+ <div class="plugin-description" x-text="plugin.description || 'No description provided.'"></div>
98
</div>
141
- </div>
99
+ </template>
100
</div>
143
-
101
</div>
102
</template>
103
</div>
@@ -172,24 +129,12 @@
129
background: var(--color-bg-primary);
130
}
131
175
- .tab-content {
176
- padding-top: 0.75rem;
177
- }
178
-
179
- .tab-pane {
180
- display: none;
181
- }
182
-
183
- .tab-pane.active {
184
- display: block;
185
- }
186
-
132
.plugins-toolbar {
133
display: flex;
134
align-items: center;
135
gap: 0.75rem;
136
flex-wrap: wrap;
192
- margin-top: 0.25rem;
137
+ margin-top: 0.5rem;
138
margin-bottom: 0.75rem;
139
}
140
@@ -238,47 +183,13 @@
183
align-items: center;
184
gap: 0.5rem;
185
padding-bottom: var(--spacing-sm);
241
- flex-wrap: nowrap;
186
+ flex-wrap: wrap;
187
min-width: 0;
243
- flex: 0 0 auto;
188
+ flex: 0 1 11.5rem;
189
max-width: 100%;
190
justify-content: flex-end;
191
}
192
248
- @media (max-width: 640px) {
249
- .plugin-header {
250
- flex-direction: column;
251
- align-items: stretch;
252
- }
253
-
254
- .plugin-heading {
255
- width: 100%;
256
- flex: 0 0 auto;
257
- }
258
-
259
- .plugin-actions {
260
- width: 100%;
261
- justify-content: flex-start;
262
- padding-bottom: 0;
263
- flex-wrap: wrap;
264
- }
265
-
266
- .plugin-path {
267
- white-space: normal;
268
- word-break: break-word;
269
- }
270
- }
271
-
272
- .plugin-actions .button {
273
- padding: 0.25rem 0.5rem;
274
- font-size: 0.85rem;
275
- line-height: 1.1;
276
- }
277
-
278
- .plugin-actions .button .icon {
279
- font-size: 1.1rem;
280
- }
281
-
193
.plugin-description {
194
margin-top: 0.35rem;
195
color: var(--color-text-secondary);
@@ -303,6 +214,30 @@
214
margin-bottom: 0.75rem;
215
color: var(--color-secondary);
216
}
217
+
218
+ @media (max-width: 640px) {
219
+ .plugin-header {
220
+ flex-direction: column;
221
+ align-items: stretch;
222
+ }
223
+
224
+ .plugin-heading {
225
+ width: 100%;
226
+ flex: 0 0 auto;
227
+ }
228
+
229
+ .plugin-actions {
230
+ width: 100%;
231
+ justify-content: flex-start;
232
+ padding-bottom: 0;
233
+ flex-wrap: wrap;
234
+ }
235
+
236
+ .plugin-path {
237
+ white-space: normal;
238
+ word-break: break-word;
239
+ }
240
+ }
241
</style>
242
</body>
243
webui/components/plugins/list/pluginListStore.js
+72
-2
@@ -1,5 +1,6 @@
1
import { createStore } from "/js/AlpineStore.js";
2
import * as api from "/js/api.js";
3
+import "/components/plugins/plugin-settings-store.js";
4
import {
5
store as notificationStore,
6
defaultPriority,
@@ -9,19 +10,88 @@ import {
10
const model = {
11
loading: false,
12
plugins: [],
13
+ selectedPlugin: null,
14
+ activeTab: "custom",
15
16
async loadPluginList(filter) {
17
this.loading = true;
15
- this.plugins = [];
18
+ this.selectedPlugin = null;
19
try {
20
const response = await api.callJsonApi("plugins_list", { filter });
18
- this.plugins = response.plugins;
21
+ this.plugins = Array.isArray(response.plugins) ? response.plugins : [];
22
} catch (e) {
23
+ this.plugins = [];
24
showErrorNotification(e, "Failed to load plugins list");
25
} finally {
26
this.loading = false;
27
}
28
},
29
+
30
+ async setTab(tab) {
31
+ this.activeTab = tab === "builtin" ? "builtin" : "custom";
32
+ const filter =
33
+ this.activeTab === "builtin"
34
+ ? { builtin: true, custom: false, search: "" }
35
+ : { builtin: false, custom: true, search: "" };
36
+ await this.loadPluginList(filter);
37
+ },
38
+
39
+ async refresh() {
40
+ await this.setTab(this.activeTab);
41
+ },
42
+
43
+ openPlugin(plugin) {
44
+ if (!plugin?.name || !plugin?.has_main_screen) return;
45
+ window.openModal?.(`/plugins/${plugin.name}/webui/main.html`);
46
+ },
47
+
48
+ async openPluginConfig(plugin) {
49
+ if (!plugin?.name || !plugin?.has_config_screen) return;
50
+ try {
51
+ const pluginSettingsStore = Alpine.store("pluginSettings");
52
+ if (!pluginSettingsStore?.open) {
53
+ throw new Error("Plugin settings store is unavailable.");
54
+ }
55
+ await pluginSettingsStore.open(plugin.name);
56
+ window.openModal?.("components/plugins/plugin-settings.html");
57
+ } catch (e) {
58
+ showErrorNotification(e, "Failed to open plugin config");
59
+ }
60
+ },
61
+
62
+ openPluginInfo(plugin) {
63
+ if (!plugin) return;
64
+ this.selectedPlugin = plugin;
65
+ window.openModal?.("components/plugins/plugin-info.html");
66
+ },
67
+
68
+ async deletePlugin(plugin) {
69
+ if (!plugin?.path) return;
70
+
71
+ if (!plugin.is_custom) {
72
+ showErrorNotification(
73
+ new Error("Only custom plugins can be deleted from this modal."),
74
+ "Delete blocked",
75
+ );
76
+ return;
77
+ }
78
+
79
+ try {
80
+ const response = await api.callJsonApi("delete_work_dir_file", {
81
+ path: plugin.path,
82
+ currentPath: "/",
83
+ });
84
+ if (response?.error) {
85
+ throw new Error(response.error);
86
+ }
87
+ if (window.toastFrontendSuccess) {
88
+ window.toastFrontendSuccess("Plugin deleted", "Plugins");
89
+ }
90
+ await this.refresh();
91
+ } catch (e) {
92
+ showErrorNotification(e, "Failed to delete plugin");
93
+ }
94
+ },
95
};
96
97
function showErrorNotification(error, heading) {
webui/components/plugins/plugin-info.html
new
+119
@@ -0,0 +1,119 @@
1
+<html>
2
+<head>
3
+ <title>Plugin Info</title>
4
+ <script type="module">
5
+ import { store } from "/components/plugins/list/pluginListStore.js";
6
+ </script>
7
+</head>
8
+<body>
9
+ <div x-data>
10
+ <template x-if="$store.pluginListStore && $store.pluginListStore.selectedPlugin">
11
+ <div class="plugin-info-view">
12
+ <div class="plugin-info-header">
13
+ <h2 class="plugin-info-title"
14
+ x-text="$store.pluginListStore.selectedPlugin.display_name || $store.pluginListStore.selectedPlugin.name"></h2>
15
+ <div class="plugin-info-source"
16
+ x-text="$store.pluginListStore.selectedPlugin.is_custom ? 'Custom plugin' : 'Builtin plugin'"></div>
17
+ </div>
18
+
19
+ <div class="plugin-info-description"
20
+ x-text="$store.pluginListStore.selectedPlugin.description || 'No description provided.'"></div>
21
+
22
+ <div class="plugin-info-grid">
23
+ <div class="plugin-info-label">Plugin ID:</div>
24
+ <div class="plugin-info-value">
25
+ <code x-text="$store.pluginListStore.selectedPlugin.name"></code>
26
+ </div>
27
+
28
+ <div class="plugin-info-label">Version:</div>
29
+ <div class="plugin-info-value"
30
+ x-text="$store.pluginListStore.selectedPlugin.version || 'n/a'"></div>
31
+
32
+ <div class="plugin-info-label">Path:</div>
33
+ <div class="plugin-info-value">
34
+ <code x-text="$store.pluginListStore.selectedPlugin.path"></code>
35
+ </div>
36
+
37
+ <div class="plugin-info-label">Settings sections:</div>
38
+ <div class="plugin-info-value"
39
+ x-text="($store.pluginListStore.selectedPlugin.settings_sections || []).length ? $store.pluginListStore.selectedPlugin.settings_sections.join(', ') : 'None'"></div>
40
+
41
+ <div class="plugin-info-label">Per-project config:</div>
42
+ <div class="plugin-info-value"
43
+ x-text="$store.pluginListStore.selectedPlugin.per_project_config ? 'Yes' : 'No'"></div>
44
+
45
+ <div class="plugin-info-label">Per-agent config:</div>
46
+ <div class="plugin-info-value"
47
+ x-text="$store.pluginListStore.selectedPlugin.per_agent_config ? 'Yes' : 'No'"></div>
48
+
49
+ <div class="plugin-info-label">Main screen:</div>
50
+ <div class="plugin-info-value"
51
+ x-text="$store.pluginListStore.selectedPlugin.has_main_screen ? 'Available' : 'Not available'"></div>
52
+
53
+ <div class="plugin-info-label">Config screen:</div>
54
+ <div class="plugin-info-value"
55
+ x-text="$store.pluginListStore.selectedPlugin.has_config_screen ? 'Available' : 'Not available'"></div>
56
+ </div>
57
+ </div>
58
+ </template>
59
+ </div>
60
+
61
+ <div class="modal-footer" data-modal-footer>
62
+ <button class="btn btn-cancel" @click="window.closeModal?.()">Close</button>
63
+ </div>
64
+
65
+ <style>
66
+ .plugin-info-header {
67
+ display: flex;
68
+ flex-wrap: wrap;
69
+ align-items: baseline;
70
+ justify-content: space-between;
71
+ gap: 0.75rem;
72
+ margin-bottom: 0.5rem;
73
+ }
74
+
75
+ .plugin-info-title {
76
+ margin: 0;
77
+ font-size: 1.1rem;
78
+ }
79
+
80
+ .plugin-info-source {
81
+ color: var(--color-text-secondary);
82
+ font-size: var(--font-size-small);
83
+ }
84
+
85
+ .plugin-info-description {
86
+ margin-bottom: 1rem;
87
+ color: var(--color-text-secondary);
88
+ }
89
+
90
+ .plugin-info-grid {
91
+ display: grid;
92
+ grid-template-columns: 12rem 1fr;
93
+ gap: 0.5rem 0.75rem;
94
+ }
95
+
96
+ .plugin-info-label {
97
+ font-weight: 600;
98
+ color: var(--color-text-secondary);
99
+ }
100
+
101
+ .plugin-info-value {
102
+ min-width: 0;
103
+ word-break: break-word;
104
+ overflow-wrap: anywhere;
105
+ }
106
+
107
+ @media (max-width: 640px) {
108
+ .plugin-info-grid {
109
+ grid-template-columns: 1fr;
110
+ gap: 0.25rem;
111
+ }
112
+
113
+ .plugin-info-label {
114
+ margin-top: 0.5rem;
115
+ }
116
+ }
117
+ </style>
118
+</body>
119
+</html>
webui/components/plugins/plugin-settings-store.js
+3
-3
@@ -79,7 +79,7 @@ const model = {
79
method: "POST",
80
headers: { "Content-Type": "application/json" },
81
body: JSON.stringify({
82
- action: "get_settings",
82
+ action: "get_config",
83
plugin_name: this.pluginName,
84
project_name: this.projectName || "",
85
agent_profile: this.agentProfileKey || "",
@@ -117,7 +117,7 @@ const model = {
117
method: "POST",
118
headers: { "Content-Type": "application/json" },
119
body: JSON.stringify({
120
- action: "save_settings",
120
+ action: "save_config",
121
plugin_name: this.pluginName,
122
project_name: this.projectName || "",
123
agent_profile: this.agentProfileKey || "",
@@ -144,7 +144,7 @@ const model = {
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>`;
147
+ return `<x-component path="/plugins/${this.pluginName}/webui/config.html"></x-component>`;
148
},
149
};
150
webui/components/plugins/plugin-settings.html
+1
-1
@@ -52,7 +52,7 @@
52
<span>Loading settings...</span>
53
</div>
54
55
- <!-- Plugin settings body: plugin provides /plugins/<name>/webui/settings.html -->
55
+ <!-- Plugin settings body: plugin provides /plugins/<name>/webui/config.html -->
56
<div x-show="!$store.pluginSettings.isLoading"
57
class="plugin-settings-body"
58
x-html="$store.pluginSettings.settingsComponentHtml">
webui/components/settings/plugins/plugins-subsection.html
+16
-7
@@ -25,21 +25,30 @@
25
if (!this.tab) return;
26
this.loading = true;
27
try {
28
- const r = await globalThis.fetchApi('/plugins', {
28
+ const r = await globalThis.fetchApi('/plugins_list', {
29
method: 'POST',
30
headers: { 'Content-Type': 'application/json' },
31
- body: JSON.stringify({ action: 'list', tab: this.tab }),
31
+ body: JSON.stringify({
32
+ filter: { custom: true, builtin: true },
33
+ }),
34
});
35
const data = await r.json().catch(() => ({}));
34
- this.plugins = (data.ok ? data.data || [] : []).filter(p => p.has_settings_ui);
36
+ const plugins = data.ok ? (data.plugins || []) : [];
37
+ this.plugins = plugins.filter((plugin) => {
38
+ const sections = Array.isArray(plugin.settings_sections)
39
+ ? plugin.settings_sections
40
+ : [];
41
+ return plugin.has_config_screen && sections.includes(this.tab);
42
+ });
43
} catch {
44
this.plugins = [];
45
} finally {
46
this.loading = false;
47
}
48
},
41
- async openSettings(pluginName) {
42
- await Alpine.store('pluginSettings')?.open(pluginName);
49
+ async openSettings(plugin) {
50
+ if (!plugin?.name) return;
51
+ await Alpine.store('pluginSettings')?.open(plugin.name);
52
window.openModal?.('components/plugins/plugin-settings.html');
53
}
54
}" x-init="init()">
@@ -55,13 +64,13 @@
64
<span>Loading...</span>
65
</div>
66
58
- <template x-for="plugin in plugins" :key="plugin.name">
67
+ <template x-for="plugin in plugins" :key="plugin.path || plugin.name">
68
<div class="plugin-subsection-row">
69
<div class="plugin-subsection-info">
70
<span class="plugin-subsection-name" x-text="plugin.display_name || plugin.name"></span>
71
<span class="plugin-subsection-desc" x-text="plugin.description"></span>
72
</div>
64
- <button class="button" @click="openSettings(plugin.name)">
73
+ <button class="button" @click="openSettings(plugin)">
74
<span class="icon material-symbols-outlined">settings</span>
75
Settings
76
</button>