Surface plugin update availability on the plugins page (#421)

* Surface plugin update availability on the plugins page

The plugin manager page already showed each installed plugin's version and
had an Update button, but nothing told users an update actually existed —
they had to guess. The store manager already compares the installed
manifest version against the registry's latest_version for its reinstall
decision; this surfaces that same signal in the UI.

- api_v3 /plugins/installed now returns `latest_version` (from the registry
  cache, no extra network call) and an `update_available` flag computed by a
  new semver-aware helper `_is_plugin_update_available`. A locally modified
  plugin whose version is ahead of the registry is not flagged.
- The installed-plugin card shows "vX.Y.Z available" next to the installed
  version, and the Update button becomes emphasized ("Update to vX.Y.Z" with
  a gentle pulse) when a newer version is published — mirroring the app's own
  update banner styling.
- Added tests for the helper and the endpoint fields.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EW8yDbsk8EceDpq6Hjgqj

* Catch InvalidVersion specifically in plugin update comparison

Address review feedback: the version comparison caught a blind `except
Exception` (ruff BLE001). Split the two failure modes and catch each
specifically — ImportError for a missing `packaging` (a core dependency)
and InvalidVersion for an unparseable version string — while preserving
the existing "surface the mismatch" fallback behavior. Added a test for
the unparseable-version path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012EW8yDbsk8EceDpq6Hjgqj

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-07-21 08:06:36 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 989162d28f
commit 3872a68ff7
4 changed files with 115 additions and 7 deletions
+15
View File
@@ -1334,6 +1334,21 @@ button.bg-white {
color: #93c5fd;
}
/* Installed-plugin "Update" button when a newer version is published.
A gentle pulsing ring draws the eye without being noisy. */
.plugin-update-available {
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.6);
animation: plugin-update-pulse 2s ease-out infinite;
}
@keyframes plugin-update-pulse {
0% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.55); }
70% { box-shadow: 0 0 0 6px rgba(59, 130, 246, 0); }
100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
}
@media (prefers-reduced-motion: reduce) {
.plugin-update-available { animation: none; }
}
/* Under-voltage / throttling warning banner */
.power-warning-banner {
background-color: #fef2f2;
+5 -4
View File
@@ -1454,7 +1454,7 @@ function renderInstalledPlugins(plugins) {
</div>
<div class="text-sm text-gray-600 space-y-1.5 mb-3">
<p class="flex items-center"><i class="fas fa-user mr-2 text-gray-400 w-4"></i>${escapeHtml(plugin.author || 'Unknown')}</p>
${plugin.version ? `<p class="flex items-center"><i class="fas fa-tag mr-2 text-gray-400 w-4"></i>v${escapeHtml(plugin.version)}</p>` : ''}
${plugin.version ? `<p class="flex items-center flex-wrap gap-1.5"><i class="fas fa-tag mr-2 text-gray-400 w-4"></i>v${escapeHtml(plugin.version)}${plugin.update_available && plugin.latest_version ? `<span class="badge badge-info" title="Installed v${escapeAttr(plugin.version)} → latest v${escapeAttr(plugin.latest_version)}"><i class="fas fa-arrow-circle-up mr-1"></i>v${escapeHtml(plugin.latest_version)} available</span>` : ''}</p>` : ''}
<p class="flex items-center"><i class="fas fa-folder mr-2 text-gray-400 w-4"></i>${escapeHtml(plugin.category || 'General')}</p>
</div>
<p class="text-sm text-gray-700 leading-relaxed">${escapeHtml(plugin.description || 'No description available')}</p>
@@ -1501,11 +1501,12 @@ function renderInstalledPlugins(plugins) {
<i class="fas fa-cog mr-2"></i>Configure
</button>
<div style="display: flex; gap: 0.5rem;">
<button class="btn bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-2 rounded-md text-sm font-semibold"
<button class="btn ${plugin.update_available ? 'bg-blue-600 hover:bg-blue-700 plugin-update-available' : 'bg-yellow-600 hover:bg-yellow-700'} text-white px-4 py-2 rounded-md text-sm font-semibold"
style="flex: 1;"
data-plugin-id="${escapedPluginId}"
data-action="update">
<i class="fas fa-sync mr-2"></i>Update
data-action="update"
title="${plugin.update_available && plugin.latest_version ? 'Update to v' + escapeAttr(plugin.latest_version) : 'Reinstall the latest published version'}">
<i class="fas ${plugin.update_available ? 'fa-arrow-circle-up' : 'fa-sync'} mr-2"></i>${plugin.update_available && plugin.latest_version ? 'Update to v' + escapeHtml(plugin.latest_version) : 'Update'}
</button>
<button class="btn bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-md text-sm font-semibold"
style="flex: 1;"