summaryrefslogtreecommitdiffhomepage
path: root/src/features/plugins/logic/types.ts
blob: e64b553f247ad39ba4bf4b61791a8740fb39ca3f (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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import type { PluginTier, PluginTrustLevel } from "@dispatch/transport-contract";

/** Which manager table a plugin row belongs to (the response's row kinds). */
export type PluginSection = "loaded" | "discovered" | "missing";

/**
 * The manager's normalized view of one plugin — a single shape across the
 * `GET /plugins` response's row kinds (loaded / disabledWithReason /
 * discovered / missing) so the pure graph logic (cascade / prereqs /
 * highlight / last-provider) and the manager tables work over ONE node type.
 * Built by `pluginNodes` (`logic/view-model`) from a `PluginListResponse`.
 */
export interface PluginNode {
  readonly id: string;
  readonly name: string;
  readonly version: string;
  readonly tier: PluginTier;
  /** Trust level, when the row carries one (missing rows don't). */
  readonly trust: PluginTrustLevel | null;
  /** Loaded and live — the manager's checkbox reflects this. */
  readonly loaded: boolean;
  /** True for tier "core" — the server refuses to unload it (locked control). */
  readonly locked: boolean;
  /** Declared dependencies (plugin ids), possibly empty. */
  readonly dependsOn: readonly string[];
  /** Why the plugin is not loaded (disabled-with-reason) or gone (missing). */
  readonly reason: string | null;
  /**
   * The frontend-plugin (plan 03) issue line for the manager row, when the
   * plugin's FE half is incompatible ("incompatible frontend plugin") or
   * failed (`frontend failed: <error>`). Absent = the FE half is fine (or
   * the plugin declares no frontend section).
   */
  readonly frontendIssue?: string;
  readonly section: PluginSection;
}

/** Display labels for the classification tiers (backend-canonical values). */
export function tierLabel(tier: PluginTier): string {
  switch (tier) {
    case "core":
      return "Core";
    case "providers":
      return "Provider";
    case "standard":
      return "Standard";
  }
}

/** Display order of the tiers within a manager table (Core first). */
export const TIER_ORDER: readonly PluginTier[] = ["core", "providers", "standard"];

/**
 * The layout port (plan 04 §4.4) the workspace plugins modal needs for the
 * "Reset layout to default" affordance — the modal is the natural home (it
 * is where users manage the layout plugins themselves). Wired by the
 * composition root over the layout store; absent → the section doesn't
 * render (tests).
 */
export interface WorkspaceLayoutPort {
  /**
   * Whether a layout provider is active (the reset button's gate). False →
   * the button is DISABLED with the reason shown ("no layout plugin
   * enabled") — there is nothing to reset and no persistence to re-seed.
   */
  readonly providerActive: boolean;
  /**
   * Reset the workspace's layout to the default seed (`DELETE
   * /workspaces/:id/layout` → re-seed → broadcast). Resolves to `null` on
   * success or to the failure reason to display.
   */
  readonly reset: (workspaceId: string) => Promise<string | null>;
  /**
   * The workspace's PER-WORKSPACE PLUGIN STATE changed (the modal just
   * toggled a plugin's enablement): the layout plugin that serves THIS
   * workspace may have changed (the per-workspace activation rule) — the
   * composition root re-probes the layout so the newly-enabled provider's
   * tree (or the no-provider degrade) lands without a reload. Only the
   * ACTIVE workspace's toggle re-renders; others are ignored by the store.
   */
  readonly refresh: (workspaceId: string) => void;
}

/**
 * Runtime-normalize a tier value to the canonical THREE: anything that is not
 * exactly `core` | `providers` | `standard` — a missing tier, or a value
 * outside the contract ("kernel" is NOT a tier; the kernel hosts plugins, it
 * is not one) — folds into `"standard"`, the safe catch-all classification.
 * The wire contract pins the three values, so this only ever fires on a
 * contract violation (the backend rejects tierless externals at boot and
 * directory-scanned plugins fail manifest validation); folding rather than
 * surfacing the raw value keeps every tier-keyed consumer total — e.g.
 * `tierLabel`'s exhaustive switch (an out-of-contract value would render an
 * empty badge) and the manager table's keyed grouping (a non-canonical key
 * would create a second, colliding group).
 */
export function normalizeTier(tier: unknown): PluginTier {
  return tier === "core" || tier === "providers" || tier === "standard" ? tier : "standard";
}