Add force refresh support and state management improvements to plugin installer and model config
- Add force parameter to plugin index fetch with cache-busting headers and timestamp - Add openIndexView and reloadIndex methods to pluginInstallStore for explicit refresh - Add request sequence tracking to prevent race conditions in concurrent index loads - Move models summary state from component to store with loading/caching support - Add refreshModelsSummary, ensureModelsSummaryLoaded, and modal
frdel committed
Mar 26, 2026 at 17:24 UTC
c7a983638eead8068b69ba456715d5be04e6df8f
6 files changed
+183
-62
plugins/_model_config/webui/model-config-store.js
+60
@@ -1,4 +1,5 @@
1
import { createStore } from "/js/AlpineStore.js";
2
+import { store as pluginSettingsStore } from "/components/plugins/plugin-settings-store.js";
3
4
5
export const MODEL_SECTIONS = [
@@ -60,6 +61,12 @@ export const store = createStore("modelConfig", {
61
globalPresets: [],
62
_presetsLoaded: false,
63
64
+ // Settings include summary state
65
+ modelsSummary: [],
66
+ modelsSummaryLoading: false,
67
+ _modelsSummaryLoaded: false,
68
+ _modelsSummaryPromise: null,
69
+
70
// Switcher state
71
switcherAllowed: false,
72
switcherOverride: null,
@@ -464,6 +471,59 @@ export const store = createStore("modelConfig", {
471
].map(s => ({ icon: s.icon, title: s.title, provider: label(s.pList, s.cfg?.provider), name: s.cfg?.name || '\u2014' }));
472
},
473
474
+ async refreshModelsSummary() {
475
+ if (this._modelsSummaryPromise) return await this._modelsSummaryPromise;
476
+
477
+ this.modelsSummaryLoading = true;
478
+ this._modelsSummaryPromise = (async () => {
479
+ try {
480
+ const models = await this.loadModelsSummary();
481
+ this.modelsSummary = models;
482
+ this._modelsSummaryLoaded = true;
483
+ return models;
484
+ } catch (e) {
485
+ console.error('Failed to load models summary:', e);
486
+ this.modelsSummary = [];
487
+ this._modelsSummaryLoaded = true;
488
+ return [];
489
+ }
490
+ })();
491
+
492
+ try {
493
+ return await this._modelsSummaryPromise;
494
+ } finally {
495
+ this._modelsSummaryPromise = null;
496
+ this.modelsSummaryLoading = false;
497
+ }
498
+ },
499
+
500
+ async ensureModelsSummaryLoaded() {
501
+ if (this._modelsSummaryLoaded) return this.modelsSummary;
502
+ return await this.refreshModelsSummary();
503
+ },
504
+
505
+ async openConfigFromSummary() {
506
+ try {
507
+ await pluginSettingsStore.openConfig('_model_config');
508
+ } finally {
509
+ await this.refreshModelsSummary();
510
+ }
511
+ },
512
+
513
+ async openPresetsFromSummary() {
514
+ await window.openModal?.('/plugins/_model_config/webui/main.html');
515
+ },
516
+
517
+ async openApiKeysFromSummary() {
518
+ try {
519
+ await window.openModal?.('/plugins/_model_config/webui/api-keys.html');
520
+ } finally {
521
+ await this.refreshApiKeyStatus().catch((e) => {
522
+ console.error('Failed to refresh API key status:', e);
523
+ });
524
+ }
525
+ },
526
+
527
// Switcher high-level methods
528
async refreshSwitcher(contextId) {
529
this.switcherLoading = true;
plugins/_model_config/webui/models-summary.html
+8
-12
@@ -7,7 +7,6 @@
7
<body>
8
<script type="module">
9
import { store } from "/plugins/_model_config/webui/model-config-store.js";
10
- import { store as pluginListStore } from "/components/plugins/list/pluginListStore.js";
10
</script>
11
12
<div x-data>
@@ -19,21 +18,18 @@
18
</div>
19
20
<!-- Read-only model config summary -->
22
- <div x-data="{ loading: true, models: [] }" x-init="
23
- models = await $store.modelConfig.loadModelsSummary().catch(() => []);
24
- loading = false;
25
- " class="model-summary">
21
+ <div x-init="$store.modelConfig.refreshModelsSummary()" class="model-summary">
22
<div class="model-summary-header">
23
<div class="field-title">Current Models</div>
24
<div class="field-description">Active model configuration resolved from global, project, and agent profile
25
scopes.</div>
26
</div>
31
- <div x-show="loading" style="text-align:center; padding:12px;">
27
+ <div x-show="$store.modelConfig.modelsSummaryLoading" style="text-align:center; padding:12px;">
28
<span class="material-symbols-outlined spinning" style="font-size:18px;">progress_activity</span>
29
</div>
34
- <template x-if="!loading && models.length">
30
+ <template x-if="!$store.modelConfig.modelsSummaryLoading && $store.modelConfig.modelsSummary.length">
31
<div class="model-summary-grid">
36
- <template x-for="m in models" :key="m.title">
32
+ <template x-for="m in $store.modelConfig.modelsSummary" :key="m.title">
33
<div class="model-summary-row">
34
<span class="material-symbols-outlined model-summary-icon" x-text="m.icon"></span>
35
<span class="model-summary-label" x-text="m.title"></span>
@@ -49,13 +45,13 @@
45
</div>
46
47
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;">
52
- <button class="btn btn-field" @click="$store.pluginListStore.openPluginConfig({ name: '_model_config', has_config_screen: true })">
48
+ <button class="btn btn-field" @click="$store.modelConfig.openConfigFromSummary()">
49
Configure Models
50
</button>
55
- <button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/main.html')">
51
+ <button class="btn btn-field" @click="$store.modelConfig.openPresetsFromSummary()">
52
Configure Presets
53
</button>
58
- <button class="btn btn-field" @click="openModal('/plugins/_model_config/webui/api-keys.html')">
54
+ <button class="btn btn-field" @click="$store.modelConfig.openApiKeysFromSummary()">
55
Configure API Keys
56
</button>
57
</div>
@@ -146,4 +142,4 @@
142
</style>
143
</body>
144
149
-</html>
\ No newline at end of file
145
+</html>
plugins/_plugin_installer/api/plugin_install.py
+8
-1
@@ -56,4 +56,11 @@ class PluginInstall(ApiHandler):
56
return update_from_git(input.get("plugin_name", ""))
57
58
def _fetch_index(self, input: dict) -> dict:
59
- return {"success": True, **get_plugin_hub_index()}
59
+ force_raw = input.get("force", False)
60
+ force = force_raw if isinstance(force_raw, bool) else str(force_raw).strip().lower() in {
61
+ "1",
62
+ "true",
63
+ "yes",
64
+ "on",
65
+ }
66
+ return {"success": True, **get_plugin_hub_index(force=force)}
plugins/_plugin_installer/helpers/install.py
+13
-4
@@ -286,9 +286,9 @@ def run_install_hook(plugin_name: str):
286
def run_pre_update_hook(plugin_name: str):
287
return plugins.call_plugin_hook(plugin_name, "pre_update")
288
289
-def get_plugin_hub_index() -> dict[str, Any]:
289
+def get_plugin_hub_index(force: bool = False) -> dict[str, Any]:
290
"""Return the plugin index plus installed Plugin Hub keys."""
291
- index_data = fetch_plugin_index()
291
+ index_data = fetch_plugin_index(force=force)
292
if not isinstance(index_data, dict):
293
raise ValueError("Plugin index response was not a JSON object")
294
@@ -339,10 +339,19 @@ def get_plugin_hub_index() -> dict[str, Any]:
339
return {"index": index_data, "installed_plugins": installed_keys}
340
341
342
-def fetch_plugin_index() -> dict:
342
+def fetch_plugin_index(force: bool = False) -> dict:
343
"""Download the plugin index from GitHub releases."""
344
index_url = "https://github.com/agent0ai/a0-plugins/releases/download/generated-index/index.json"
345
- req = urllib.request.Request(index_url, headers={"User-Agent": "AgentZero"})
345
+ if force:
346
+ separator = "&" if "?" in index_url else "?"
347
+ index_url = f"{index_url}{separator}ts={time.time_ns()}"
348
+
349
+ headers = {"User-Agent": "AgentZero"}
350
+ if force:
351
+ headers["Cache-Control"] = "no-cache"
352
+ headers["Pragma"] = "no-cache"
353
+
354
+ req = urllib.request.Request(index_url, headers=headers)
355
with urllib.request.urlopen(req, timeout=30) as resp:
356
data = json.loads(resp.read().decode())
357
return data
plugins/_plugin_installer/webui/install-index.html
+24
-1
@@ -8,7 +8,7 @@
8
<body>
9
<div x-data>
10
<template x-if="$store.pluginInstallStore">
11
- <div class="pi-browse-shell">
11
+ <div class="pi-browse-shell" x-create="$store.pluginInstallStore.openIndexView()">
12
13
<section class="pi-browse-hero">
14
<div class="pi-browse-copy">
@@ -39,6 +39,15 @@
39
<option value="name">Name</option>
40
</select>
41
</label>
42
+
43
+ <button type="button"
44
+ class="button icon-button pi-reload-button"
45
+ aria-label="Reload Plugin Hub"
46
+ title="Reload Plugin Hub"
47
+ @click="$store.pluginInstallStore.reloadIndex()"
48
+ :disabled="$store.pluginInstallStore.loading">
49
+ <span class="material-symbols-outlined">refresh</span>
50
+ </button>
51
</div>
52
53
<div class="pi-filter-row" x-show="$store.pluginInstallStore.browseFilters.length > 1">
@@ -268,6 +277,20 @@
277
justify-content: space-between;
278
}
279
280
+ .pi-reload-button {
281
+ flex-shrink: 0;
282
+ display: inline-flex;
283
+ align-items: center;
284
+ justify-content: center;
285
+ min-width: 0;
286
+ min-height: 0;
287
+ padding: 0.45rem 0.6rem;
288
+ }
289
+
290
+ .pi-reload-button .material-symbols-outlined {
291
+ font-size: 1.1rem;
292
+ }
293
+
294
.pi-field-label {
295
font-size: 0.8rem;
296
font-weight: 600;
plugins/_plugin_installer/webui/pluginInstallStore.js
+70
-44
@@ -36,6 +36,7 @@ const model = {
36
// Index state
37
index: { authors: {}, plugins: {} },
38
indexLoadPromise: null,
39
+ indexLoadSeq: 0,
40
installedPlugins: [],
41
installedPluginDetails: {},
42
search: "",
@@ -64,10 +65,6 @@ const model = {
65
setTab(tab) {
66
this.activeTab = tab;
67
this.result = null;
67
- if (tab === "store") {
68
- this.resetIndex();
69
- void this.fetchIndex();
70
- }
68
},
69
70
setBrowseFilter(filter) {
@@ -281,62 +278,91 @@ const model = {
278
},
279
280
async fetchIndex(options = {}) {
281
+ const force = !!options?.force;
282
const background = !!options?.background;
283
const suppressErrors = !!options?.suppressErrors;
286
- if (this.indexLoadPromise) {
287
- return this.indexLoadPromise;
284
+ if (!force && this.indexLoadPromise) {
285
+ if (background) {
286
+ return this.indexLoadPromise;
287
+ }
288
+
289
+ this.loading = true;
290
+ this.loadingMessage = "Loading plugin index...";
291
+ try {
292
+ return await this.indexLoadPromise;
293
+ } finally {
294
+ this.loading = false;
295
+ this.loadingMessage = "";
296
+ }
297
}
298
299
+ const requestSeq = ++this.indexLoadSeq;
300
const loadPromise = (async () => {
291
- try {
292
- if (!background) {
293
- this.loading = true;
294
- this.loadingMessage = "Loading plugin index...";
295
- }
301
+ try {
302
+ if (!background) {
303
+ this.loading = true;
304
+ this.loadingMessage = "Loading plugin index...";
305
+ }
306
297
- const data = await api.callJsonApi(PLUGIN_API, {
298
- action: "fetch_index",
299
- });
307
+ const data = await api.callJsonApi(PLUGIN_API, {
308
+ action: "fetch_index",
309
+ force,
310
+ });
311
301
- if (!data.success) {
302
- if (!suppressErrors) {
303
- void toastFrontendError(data.error || "Failed to load index", "Plugin Installer");
312
+ if (!data.success) {
313
+ if (!suppressErrors && requestSeq === this.indexLoadSeq) {
314
+ void toastFrontendError(data.error || "Failed to load index", "Plugin Installer");
315
+ }
316
+ return false;
317
}
305
- return false;
306
- }
318
308
- this.index = data.index;
309
- this.installedPlugins = data.installed_plugins || [];
310
- const installedResponse = await api.callJsonApi("plugins_list", {
311
- filter: { custom: true, builtin: false, search: "" },
312
- });
313
- const installedList = Array.isArray(installedResponse.plugins) ? installedResponse.plugins : [];
314
- this.installedPluginDetails = Object.fromEntries(
315
- installedList.map((plugin) => [plugin.name, plugin])
316
- );
317
- this.page = 1;
318
- return true;
319
- } catch (e) {
320
- const message = e instanceof Error ? e.message : String(e);
321
- if (!suppressErrors) {
322
- void toastFrontendError(`Failed to load plugin index: ${message}`, "Plugin Installer");
323
- }
324
- return false;
325
- } finally {
326
- if (!background) {
327
- this.loading = false;
328
- this.loadingMessage = "";
319
+ const installedResponse = await api.callJsonApi("plugins_list", {
320
+ filter: { custom: true, builtin: false, search: "" },
321
+ });
322
+ const installedList = Array.isArray(installedResponse.plugins) ? installedResponse.plugins : [];
323
+
324
+ if (requestSeq !== this.indexLoadSeq) {
325
+ return false;
326
+ }
327
+
328
+ this.index = data.index;
329
+ this.installedPlugins = data.installed_plugins || [];
330
+ this.installedPluginDetails = Object.fromEntries(
331
+ installedList.map((plugin) => [plugin.name, plugin])
332
+ );
333
+ this.page = 1;
334
+ return true;
335
+ } catch (e) {
336
+ const message = e instanceof Error ? e.message : String(e);
337
+ if (!suppressErrors && requestSeq === this.indexLoadSeq) {
338
+ void toastFrontendError(`Failed to load plugin index: ${message}`, "Plugin Installer");
339
+ }
340
+ return false;
341
+ } finally {
342
+ if (!background && requestSeq === this.indexLoadSeq) {
343
+ this.loading = false;
344
+ this.loadingMessage = "";
345
+ }
346
}
330
- }
347
})();
348
333
- this.indexLoadPromise = loadPromise.finally(() => {
334
- if (this.indexLoadPromise === loadPromise) {
349
+ const trackedPromise = loadPromise.finally(() => {
350
+ if (this.indexLoadPromise === trackedPromise) {
351
this.indexLoadPromise = null;
352
}
353
});
354
+ this.indexLoadPromise = trackedPromise;
355
+
356
+ return trackedPromise;
357
+ },
358
+
359
+ async openIndexView() {
360
+ this.resetIndex();
361
+ return this.fetchIndex({ force: true });
362
+ },
363
339
- return this.indexLoadPromise;
364
+ async reloadIndex() {
365
+ return this.fetchIndex({ force: true });
366
},
367
368
async ensureIndexLoaded(options = {}) {