Agent Zero AI framework
Move preset editor behavior into Alpine store
Create local preset drafts and handle add, remove, and save actions through modelConfig.createPresetEditor so the HTML remains declarative. Document the project-wide Alpine expression boundary and keep the plugin contract specific to preset editor ownership.
Alessandro committed
Jul 10, 2026 at 14:19 UTC
db7b90823e2892f5e122077760adf365ea073dd7
5 files changed
+42
-17
AGENTS.md
+1
@@ -131,6 +131,7 @@ Key Files:
131
</div>
132
```
133
- Store Registration: Use createStore from /js/AlpineStore.js.
134
+- Template Expression Scope: Keep Alpine attributes declarative: property reads, bindings, guards, and short method calls. Put object or array construction, cloning, multi-step mutation, async workflows, and reusable behavior in named JavaScript store or component methods; simple toggles and assignments may stay inline.
135
- Modals: Use openModal(path) and closeModal() from /js/modals.js.
136
137
### Plugin Architecture
plugins/_model_config/AGENTS.md
+1
-1
@@ -20,7 +20,7 @@
20
- `model_config_get` exposes `model_configured` as a derived chat-model readiness flag from provider, model name, and API-key availability.
21
- Applying a model preset may inherit durable tuning such as context windows and rate limits, but must replace or clear per-slot `kwargs` so provider-specific extra params never leak across model providers.
22
- Repair provider-specific model-config aliases at the model-config read/build boundary; keep provider-specific repairs out of provider-agnostic core wrappers such as `models.py`.
23
-- Preset editor rows use stable UI-only keys; never key them by array position or editable preset name because deleting or renaming a row must not rebind nested model fields to another preset.
23
+- `modelConfig.createPresetEditor()` owns local preset drafts, row actions, and stable UI-only row keys so deletion or renaming cannot rebind nested model fields.
24
25
## Work Guidance
26
plugins/_model_config/webui/main.html
+4
-14
@@ -15,10 +15,7 @@
15
await $store.modelConfig.refreshApiKeyStatus();
16
">
17
<template x-if="$store.modelConfig._loaded && $store.modelConfig._presetsLoaded">
18
- <div class="presets-page" x-data="{
19
- presets: JSON.parse(JSON.stringify($store.modelConfig.globalPresets)).map((preset, idx) => ({ ...preset, _key: idx })),
20
- nextPresetKey: $store.modelConfig.globalPresets.length
21
- }">
18
+ <div class="presets-page" x-data="$store.modelConfig.createPresetEditor()">
19
<div class="presets-header">
20
<div class="field-title" style="font-size:1rem;">Model Presets</div>
21
<div class="field-description">Global presets shared across all projects and agents. Used by the model switcher in the chat area.</div>
@@ -34,7 +31,7 @@
31
<span class="preset-card-summary" x-show="!expanded"
32
x-text="(preset.chat?.provider ? preset.chat.provider + '/' : '') + (preset.chat?.name || '')"></span>
33
<button class="text-button preset-delete-btn"
37
- @click.stop="$confirmClick($event, () => { presets.splice(idx, 1); presets = [...presets]; })"
34
+ @click.stop="$confirmClick($event, () => removePreset(idx))"
35
title="Remove preset">
36
<span class="material-symbols-outlined" style="font-size:16px;">close</span>
37
</button>
@@ -64,14 +61,7 @@
61
</template>
62
63
<div class="presets-actions">
67
- <button class="text-button preset-add-btn"
68
- @click="
69
- presets = [...presets, {
70
- _key: nextPresetKey++,
71
- name: 'Preset ' + (presets.length + 1),
72
- chat: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' },
73
- utility: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' }
74
- }]">
64
+ <button class="text-button preset-add-btn" @click="addPreset()">
65
<span class="material-symbols-outlined">add</span>
66
<span>Add Preset</span>
67
</button>
@@ -89,7 +79,7 @@
79
</div>
80
81
<div class="presets-footer">
92
- <button class="button" @click="(async () => { await $store.modelConfig.persistAllDirtyApiKeys(); await $store.modelConfig.saveGlobalPresets(presets); })()">
82
+ <button class="button" @click="savePresets()">
83
<span class="icon material-symbols-outlined">save</span> Save Presets
84
</button>
85
</div>
plugins/_model_config/webui/model-config-store.js
+29
@@ -268,6 +268,35 @@ export const store = createStore("modelConfig", {
268
},
269
270
// Global presets
271
+ createPresetEditor() {
272
+ const store = this;
273
+ let nextPresetKey = 0;
274
+ const presets = clonePlain(this.globalPresets).map(preset => ({
275
+ ...preset,
276
+ _key: nextPresetKey++,
277
+ }));
278
+
279
+ return {
280
+ presets,
281
+ addPreset() {
282
+ this.presets = [...this.presets, {
283
+ _key: nextPresetKey++,
284
+ name: `Preset ${this.presets.length + 1}`,
285
+ chat: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' },
286
+ utility: { provider: '', name: '', api_key: '', api_base: '', kwargs: {}, _kwargs_text: '' },
287
+ }];
288
+ },
289
+ removePreset(index) {
290
+ this.presets.splice(index, 1);
291
+ this.presets = [...this.presets];
292
+ },
293
+ async savePresets() {
294
+ await store.persistAllDirtyApiKeys();
295
+ await store.saveGlobalPresets(this.presets);
296
+ },
297
+ };
298
+ },
299
+
300
async loadGlobalPresets() {
301
try {
302
const res = await fetchApi(`${API_BASE}/model_presets`, {
tests/test_model_config_ui.py
+7
-2
@@ -24,8 +24,13 @@ def test_model_config_text_buttons_have_shared_primitive() -> None:
24
25
def test_model_preset_rows_keep_stable_identity_after_middle_delete() -> None:
26
preset_modal = read("plugins", "_model_config", "webui", "main.html")
27
+ preset_store = read("plugins", "_model_config", "webui", "model-config-store.js")
28
29
+ assert 'x-data="$store.modelConfig.createPresetEditor()"' in preset_modal
30
assert ':key="preset._key"' in preset_modal
29
- assert "_key: idx" in preset_modal
30
- assert "_key: nextPresetKey++" in preset_modal
31
+ assert "createPresetEditor()" in preset_store
32
+ assert "_key: nextPresetKey++" in preset_store
33
+ assert "removePreset(index)" in preset_store
34
assert ':key="idx"' not in preset_modal
35
+ assert "JSON.parse(JSON.stringify" not in preset_modal
36
+ assert "presets.splice" not in preset_modal