summaryrefslogtreecommitdiffhomepage
path: root/src/features/plugins/logic/last-provider.ts
blob: 279df4dcd115313865625a5a67f5b268c618d286 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import type { PluginNode } from "./types";

/**
 * Whether unloading `targetId` would remove the LAST loaded provider — the
 * §8 hard lock (the one unload the server refuses outright, never force-able).
 * The UI disables the control with an explanatory tooltip; the server still
 * enforces the rule. Pure input → output.
 */
export function isLastProvider(targetId: string, nodes: readonly PluginNode[]): boolean {
  const target = nodes.find((n) => n.id === targetId);
  if (target === undefined || !target.loaded || target.tier !== "providers") return false;
  const loadedProviders = nodes.filter((n) => n.loaded && n.tier === "providers");
  return loadedProviders.length <= 1;
}

/**
 * Why the row's load/unload control is disabled, or null when it is
 * interactive. Composes the two client-mirrored locks:
 * - tier "core" (`locked`) → the server refuses to unload it (§3/§5);
 * - the last loaded provider → the §8 hard lock.
 * Missing rows have no control at all (handled separately by the UI).
 */
export function unloadBlockReason(targetId: string, nodes: readonly PluginNode[]): string | null {
  const target = nodes.find((n) => n.id === targetId);
  if (target === undefined) return null;
  if (target.locked) return "Core plugins cannot be unloaded — they are part of the runtime.";
  if (isLastProvider(targetId, nodes)) {
    return "Cannot unload the last provider — Dispatch needs at least one to pick models.";
  }
  return null;
}