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";
}
|