fix: synced plugin updates failing on desktop and server-only plugins shown as stopped

This commit is contained in:
LukeGus committed 2026-10-11 12:07:22 -05:00
1 parent e92db4e9f8
commit 91e1ee94fa
12 files changed
+261 -12

No files matched your search

+2
View File
@@ -221,6 +221,8 @@ export interface PluginSummary {
enabled: boolean;
/** enabled | disabled | blocked | failed, or the loader's live state. */
state: string;
/** A linked desktop leaves this plugin to its server. */
runsOnServer?: boolean;
contributes: PluginContributions | null;
/** Lucide icon name from the manifest. */
icon?: string;
+5 -2
View File
@@ -1875,12 +1875,15 @@
"running": "Running",
"disabled": "Disabled",
"failed": "Stopped unexpectedly",
"blocked": "Waiting on another plugin"
"blocked": "Waiting on another plugin",
"server": "Runs on your server"
},
"groups": {
"running": "Running",
"notRunning": "Not running"
"notRunning": "Not running",
"onServer": "On your server"
},
"runsOnServerHint": "This plugin only runs on your server, so it stays off in the desktop app. It still works there.",
"sort": {
"popular": "Most popular",
"name": "Name",
+10
View File
@@ -11,6 +11,7 @@ import {
Lightbulb,
Pin,
RotateCcw,
Server,
Settings,
Trash2,
TriangleAlert,
@@ -225,6 +226,15 @@ export function PluginDetail({
</div>
)}
{plugin.status === "server" && (
<div className="flex items-start gap-2 border border-border bg-muted/30 px-3 py-2.5">
<Server className="mt-px size-3.5 shrink-0 text-muted-foreground" />
<span className="text-[11px] leading-snug text-muted-foreground">
{t("plugins.manager.runsOnServerHint")}
</span>
</div>
)}
<div className="grid grid-cols-1 gap-2 lg:grid-cols-3">
<div className="flex flex-col gap-2 lg:col-span-2">
<PluginVideo videoId={plugin.videoId} name={plugin.name} />
+22 -7
View File
@@ -483,7 +483,10 @@ function InstalledSection({
sort,
);
const running = rows.filter((p) => p.status === "running");
const stopped = rows.filter((p) => p.status !== "running");
const onServer = rows.filter((p) => p.status === "server");
const stopped = rows.filter(
(p) => p.status !== "running" && p.status !== "server",
);
const pending = plugins.filter((p) => p.updateAvailable && !p.pinnedVersion);
const listColumns: DataColumn<PluginEntry>[] = [
@@ -587,6 +590,14 @@ function InstalledSection({
{collection(running)}
</Group>
)}
{onServer.length > 0 && (
<Group
title={t("plugins.manager.groups.onServer")}
count={onServer.length}
>
{collection(onServer)}
</Group>
)}
{stopped.length > 0 && (
<Group
title={t("plugins.manager.groups.notRunning")}
@@ -717,7 +728,9 @@ function InstalledCard({
<div className="flex items-center gap-2 border-t border-border px-3 py-2">
<span className="size-1.5 shrink-0 bg-muted-foreground/40" />
<span className="text-[11px] text-muted-foreground">
{t("plugins.manager.status.disabled")}
{plugin.status === "server"
? t("plugins.manager.status.server")
: t("plugins.manager.status.disabled")}
</span>
</div>
) : (
@@ -806,11 +819,13 @@ function StatusCell({ plugin }: { plugin: PluginEntry }) {
? t("plugins.manager.status.blocked")
: failed
? t("plugins.manager.status.failed")
: !running
? t("plugins.manager.status.disabled")
: plugin.pinnedVersion
? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })
: t("plugins.manager.status.running");
: plugin.status === "server"
? t("plugins.manager.status.server")
: !running
? t("plugins.manager.status.disabled")
: plugin.pinnedVersion
? t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion })
: t("plugins.manager.status.running");
return (
<span
className="flex min-w-0 items-center gap-1.5 text-muted-foreground"
+7 -2
View File
@@ -24,7 +24,8 @@ import {
normalizeGitHubRepo,
} from "@/lib/issue-url";
export type PluginStatus = "running" | "disabled" | "failed" | "blocked";
export type PluginStatus =
"running" | "disabled" | "failed" | "blocked" | "server";
/** One row of the Plugins tab: what is installed merged with the registry. */
export interface PluginEntry {
@@ -100,6 +101,7 @@ export function pluginSource(summary: PluginSummary): PluginSource {
}
export function pluginStatus(summary: PluginSummary): PluginStatus {
if (summary.runsOnServer) return "server";
if (summary.state === "failed") return "failed";
if (summary.state === "blocked") return "blocked";
if (!summary.enabled) return "disabled";
@@ -136,7 +138,10 @@ export function mergePlugins(
latestBeta: entry?.latestBeta ?? null,
channel: summary.channel ?? entry?.channel ?? "stable",
isBeta: isPrerelease(summary.version),
updateAvailable: entry?.updateAvailable ?? false,
// The server keeps its own copy up to date.
updateAvailable: summary.runsOnServer
? false
: (entry?.updateAvailable ?? false),
addedCapabilities: entry?.addedCapabilities ?? [],
versions: entry?.versions ?? [],
capabilities: summary.capabilities ?? [],
+11
View File
@@ -125,6 +125,14 @@ describe("mergePlugins", () => {
);
});
it("offers no update for a plugin a linked desktop leaves to its server", () => {
const [row] = mergePlugins(
[summary({ enabled: false, state: "disabled", runsOnServer: true })],
[entry()],
);
expect(row).toMatchObject({ status: "server", updateAvailable: false });
});
it("keeps installed plugins when the registry is unreachable", () => {
const rows = mergePlugins([summary()], null);
expect(rows).toHaveLength(1);
@@ -140,6 +148,9 @@ describe("pluginStatus", () => {
"disabled",
);
expect(pluginStatus(summary())).toBe("running");
expect(pluginStatus(summary({ state: "failed", runsOnServer: true }))).toBe(
"server",
);
});
});