From c757a07708b14c0431ee3795f2453b8e55ffdf78 Mon Sep 17 00:00:00 2001 From: HenkDz Date: Fri, 3 Apr 2026 17:30:45 +0100 Subject: [PATCH] fix(adapters): stable sort order, npm/local icons, reinstall dialog, HMR polling on WSL - Sort GET /api/adapters alphabetically by type (reload no longer shuffles) - Show red Package icon for npm adapters, amber FolderOpen for local path - Add reinstall confirmation dialog with current vs latest npm version - Enable Vite polling when running on /mnt/ (WSL inotify doesn't work on NTFS) --- server/src/routes/adapters.ts | 2 +- ui/src/pages/AdapterManager.tsx | 110 +++++++++++++++++++++++++++++++- ui/vite.config.ts | 2 + 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/server/src/routes/adapters.ts b/server/src/routes/adapters.ts index 49e76c25..fdeb64d2 100644 --- a/server/src/routes/adapters.ts +++ b/server/src/routes/adapters.ts @@ -175,7 +175,7 @@ export function adapterRoutes() { const result: AdapterInfo[] = registeredAdapters.map((adapter) => buildAdapterInfo(adapter, externalRecords.get(adapter.type), disabledSet), - ); + ).sort((a, b) => a.type.localeCompare(b.type)); res.json(result); }); diff --git a/ui/src/pages/AdapterManager.tsx b/ui/src/pages/AdapterManager.tsx index 39df6e75..e41b9ca3 100644 --- a/ui/src/pages/AdapterManager.tsx +++ b/ui/src/pages/AdapterManager.tsx @@ -63,6 +63,11 @@ function AdapterRow({ {adapter.label || getAdapterLabel(adapter.type)} {adapter.source === "external" ? "External" : "Built-in"} + {adapter.source === "external" && ( + adapter.isLocalPath + ? + : + )} { + return fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { + signal: AbortSignal.timeout(5000), + }) + .then((res) => res.json()) + .then((data) => (typeof data?.version === "string" ? (data.version as string) : null)) + .catch(() => null); +} + +function ReinstallDialog({ + adapter, + open, + isReinstalling, + onConfirm, + onCancel, +}: { + adapter: AdapterInfo | null; + open: boolean; + isReinstalling: boolean; + onConfirm: () => void; + onCancel: () => void; +}) { + const { data: latestVersion, isLoading: isFetchingVersion } = useQuery({ + queryKey: ["npm-latest-version", adapter?.packageName], + queryFn: () => { + if (!adapter?.packageName) return null; + return fetchNpmLatestVersion(adapter.packageName); + }, + enabled: open && !!adapter?.packageName, + staleTime: 60_000, + }); + + const isUpToDate = adapter?.version && latestVersion && adapter.version === latestVersion; + + return ( + { if (!o) onCancel(); }}> + + + Reinstall Adapter + + This will pull the latest version of{" "} + {adapter?.packageName} from npm and hot-swap + the running adapter module. Existing agents will use the new + version on their next run. + + + +
+
+ Package + {adapter?.packageName} +
+
+ Current + + {adapter?.version ? `v${adapter.version}` : "unknown"} + +
+
+ Latest on npm + + {isFetchingVersion + ? "checking..." + : latestVersion + ? `v${latestVersion}` + : "unavailable"} + +
+ {isUpToDate && ( +

+ Already on the latest version. +

+ )} +
+ + + + + +
+
+ ); +} + export function AdapterManager() { const { selectedCompany } = useCompany(); const { setBreadcrumbs } = useBreadcrumbs(); @@ -151,6 +244,7 @@ export function AdapterManager() { const [isLocalPath, setIsLocalPath] = useState(false); const [installDialogOpen, setInstallDialogOpen] = useState(false); const [removeType, setRemoveType] = useState(null); + const [reinstallTarget, setReinstallTarget] = useState(null); useEffect(() => { setBreadcrumbs([ @@ -411,7 +505,7 @@ export function AdapterManager() { onToggle={(type, disabled) => toggleMutation.mutate({ type, disabled })} onRemove={(type) => setRemoveType(type)} onReload={(type) => reloadMutation.mutate(type)} - onReinstall={!adapter.isLocalPath ? (type) => reinstallMutation.mutate(type) : undefined} + onReinstall={!adapter.isLocalPath ? (type) => setReinstallTarget(adapter) : undefined} isToggling={toggleMutation.isPending} isReloading={reloadMutation.isPending} isReinstalling={reinstallMutation.isPending} @@ -481,6 +575,20 @@ export function AdapterManager() { + {/* Reinstall confirmation */} + { + if (reinstallTarget) { + reinstallMutation.mutate(reinstallTarget.type, { + onSettled: () => setReinstallTarget(null), + }); + } + }} + onCancel={() => setReinstallTarget(null)} + /> ); } diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 56d8a4db..395eebb9 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -13,6 +13,8 @@ export default defineConfig({ }, server: { port: 5173, + // WSL2 /mnt/ drives don't support inotify — fall back to polling so HMR works + watch: process.cwd().startsWith("/mnt/") ? { usePolling: true, interval: 1000 } : undefined, proxy: { "/api": { target: "http://localhost:3100",