From 6a88d61a9c4ed9340efe7a0d62c8e61988ce8298 Mon Sep 17 00:00:00 2001 From: clagentic <10177887+akuehner@users.noreply.github.com> Date: Sat, 13 Jun 2026 09:48:33 -0400 Subject: [PATCH] feat: on-demand agent catalog refresh without restart - list_agents now responds immediately with cached data, then runs refresh() in the background and re-sends agents_list only if the catalog changed. Cost is paid when the user opens the agent picker, not on a timer. - New refresh_agents WebSocket message triggers an explicit SDK discovery pass and broadcasts the updated agents_list + a toast to all connected clients on completion. - Settings > Status: "Agent Catalog" card with a Refresh agents button wired to refresh_agents. Button resets on refresh_agents_result ack; outcome communicated via server-broadcast toast. Co-Authored-By: Claude Sonnet 4.6 --- lib/project-sessions.js | 40 ++++++++++++++++++++++++++- lib/public/index.html | 11 ++++++++ lib/public/modules/app-messages.js | 6 +++- lib/public/modules/server-settings.js | 23 +++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/lib/project-sessions.js b/lib/project-sessions.js index b57ca77..2b9cece 100644 --- a/lib/project-sessions.js +++ b/lib/project-sessions.js @@ -170,17 +170,55 @@ function attachSessions(ctx) { // Hidden client-side when active project is a Mate; we still answer here // because the daemon shouldn't make UX decisions that the client already // owns. UI gates the surface; daemon stays a data plane. + // + // Background refresh: respond immediately with the cached list, then kick + // off a refresh(). If the catalog changes, re-send to this client only. + // Cost is paid exactly when the user opens the agent picker — not on a timer. if (msg.type === "list_agents") { var favorites = []; var recents = []; try { favorites = agentsFavorites.listFavorites(); recents = agentsFavorites.listRecents(); } catch (e) { /* favorites are best-effort */ } + var cachedAgents = agentsModule.getAll(); sendTo(ws, { type: "agents_list", - agents: agentsModule.getAll(), + agents: cachedAgents, favorites: favorites, recents: recents, }); + // Refresh in background; re-send only if the catalog changed. + var cachedJson = JSON.stringify(cachedAgents); + agentsModule.refresh().then(function () { + var freshAgents = agentsModule.getAll(); + if (JSON.stringify(freshAgents) === cachedJson) return; + var favFresh = []; + var recFresh = []; + try { favFresh = agentsFavorites.listFavorites(); recFresh = agentsFavorites.listRecents(); } + catch (e) { /* best-effort */ } + sendTo(ws, { type: "agents_list", agents: freshAgents, favorites: favFresh, recents: recFresh }); + }).catch(function (e) { + console.error("[project-sessions] background agent refresh failed:", e && e.message ? e.message : e); + }); + return true; + } + + // refresh_agents — explicit on-demand refresh triggered from the settings UI. + // Re-runs SDK discovery, then broadcasts the updated catalog to ALL connected + // clients. Responds immediately with ok:true (async broadcast follows). + if (msg.type === "refresh_agents") { + sendTo(ws, { type: "refresh_agents_result", ok: true }); + agentsModule.refresh().then(function () { + var freshAgents = agentsModule.getAll(); + var favFresh = []; + var recFresh = []; + try { favFresh = agentsFavorites.listFavorites(); recFresh = agentsFavorites.listRecents(); } + catch (e) { /* best-effort */ } + send({ type: "agents_list", agents: freshAgents, favorites: favFresh, recents: recFresh }); + send({ type: "toast", level: "info", message: "Agent catalog refreshed — " + freshAgents.length + " agent" + (freshAgents.length === 1 ? "" : "s") + " found." }); + }).catch(function (e) { + console.error("[project-sessions] refresh_agents failed:", e && e.message ? e.message : e); + send({ type: "toast", level: "error", message: "Agent catalog refresh failed." }); + }); return true; } diff --git a/lib/public/index.html b/lib/public/index.html index e709d02..fb69694 100644 --- a/lib/public/index.html +++ b/lib/public/index.html @@ -1124,6 +1124,17 @@

Server Status

- +

Agent Catalog

+
+
+ +
Re-scans for installed agents and updates the picker for all connected users. Happens automatically when you open the agent picker — use this to push an update immediately.
+
+ +
+ +
+
diff --git a/lib/public/modules/app-messages.js b/lib/public/modules/app-messages.js index c8f622c..eda628f 100644 --- a/lib/public/modules/app-messages.js +++ b/lib/public/modules/app-messages.js @@ -23,7 +23,7 @@ import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundE import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, handleFsSearch } from './filebrowser.js'; import { getPendingNavigate, peekPendingNavigate } from './pending-navigate.js'; import { isProjectSettingsOpen, refreshProjectSettingsModels, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged, updateLiteVisibility, handleLiteProjectStatus, handleLiteEnrollResult, handleLiteUnenrollResult } from './project-settings.js'; -import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite, updateSsLiteVisibility } from './server-settings.js'; +import { updateSettingsModels, updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRefreshAgentsResult, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite, updateSsLiteVisibility } from './server-settings.js'; import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js'; import { updateTerminalList, handleContextSourcesState } from './context-sources.js'; import { handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted } from './sticky-notes.js'; @@ -1348,6 +1348,10 @@ export function processMessage(msg) { handleAutoContinueChanged(msg); break; + case "refresh_agents_result": + handleRefreshAgentsResult(msg); + break; + case "restart_server_result": handleRestartResult(msg); break; diff --git a/lib/public/modules/server-settings.js b/lib/public/modules/server-settings.js index 36ab24b..de679af 100644 --- a/lib/public/modules/server-settings.js +++ b/lib/public/modules/server-settings.js @@ -248,6 +248,19 @@ export function initServerSettings(appCtx) { }); } + // Refresh agent catalog + var refreshAgentsBtn = document.getElementById("settings-refresh-agents-btn"); + if (refreshAgentsBtn) { + refreshAgentsBtn.addEventListener("click", function () { + var ws = ctx.ws; + if (ws && ws.readyState === 1) { + refreshAgentsBtn.disabled = true; + refreshAgentsBtn.innerHTML = 'Refreshing...'; + ws.send(JSON.stringify({ type: "refresh_agents" })); + } + }); + } + // Restart server var restartBtn = document.getElementById("settings-restart-btn"); if (restartBtn) { @@ -770,6 +783,16 @@ export function handleAutoContinueChanged(msg) { // Auto-continue is now per-user; server broadcast no longer updates UI } +export function handleRefreshAgentsResult(msg) { + var btn = document.getElementById("settings-refresh-agents-btn"); + if (!btn) return; + // Reset button regardless of ok — the toast (broadcast from server) carries + // the outcome. If not ok, we just unblock the button silently. + btn.disabled = false; + btn.innerHTML = 'Refresh agents'; + refreshIcons(); +} + export function handleRestartResult(msg) { var restartBtn = document.getElementById("settings-restart-btn"); var errorEl = document.getElementById("settings-restart-error");