@termix-ssh/plugin-sdk 1.0.11 → 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/PLUGIN-GUIDE.md +178 -54
  3. package/cli/commands/build.mjs +36 -22
  4. package/cli/commands/pack.mjs +12 -2
  5. package/cli/commands/validate.mjs +63 -3
  6. package/cli/lib/plugin-dir.mjs +11 -0
  7. package/cli/lib/tmxplug.mjs +9 -0
  8. package/dist/backend.d.ts +404 -0
  9. package/dist/capabilities.js +7 -0
  10. package/dist/db.js +1 -0
  11. package/dist/desktop.d.ts +0 -243
  12. package/dist/frontend.d.ts +175 -2
  13. package/dist/frontend.js +7 -0
  14. package/dist/host/PluginsTab-OXQQ7M4I.js +3266 -0
  15. package/dist/host/chunk-54KCK6IZ.js +3167 -0
  16. package/dist/host/chunk-CALNJCHN.js +10435 -0
  17. package/dist/host/{chunk-7RRTYD6B.js → chunk-CN5UBTBP.js} +1412 -14
  18. package/dist/host/{chunk-RLQBKJRK.js → chunk-ERGN4YW6.js} +9204 -11492
  19. package/dist/host/{chunk-LSWB3ZZV.js → chunk-T2PZWXFJ.js} +6 -1
  20. package/dist/host/chunk-YGYBM7SQ.js +58 -0
  21. package/dist/host/core-classes.json +1 -1
  22. package/dist/host/{linked-server-UCW74T2V.js → linked-server-5VFO4XQB.js} +2 -2
  23. package/dist/host/{rbac-api-6UYMFIFV.js → rbac-api-5YMBJAO5.js} +2 -2
  24. package/dist/host/testing-host.js +123 -57
  25. package/dist/host/types/src/types/onboarding.d.ts +0 -2
  26. package/dist/host/types/src/types/ui-types.d.ts +2 -1
  27. package/dist/host/types/src/ui/api/audit-log-api.d.ts +36 -0
  28. package/dist/host/types/src/ui/api/credentials-api.d.ts +0 -22
  29. package/dist/host/types/src/ui/api/host-defaults-api.d.ts +42 -0
  30. package/dist/host/types/src/ui/api/host-tags-api.d.ts +2 -0
  31. package/dist/host/types/src/ui/api/look-api.d.ts +40 -0
  32. package/dist/host/types/src/ui/api/open-tabs-api.d.ts +4 -0
  33. package/dist/host/types/src/ui/api/plugins-api.d.ts +30 -0
  34. package/dist/host/types/src/ui/api/private-endpoints-api.d.ts +6 -0
  35. package/dist/host/types/src/ui/api/settings-api.d.ts +0 -6
  36. package/dist/host/types/src/ui/api/ssh-host-management-api.d.ts +0 -28
  37. package/dist/host/types/src/ui/api/tls-api.d.ts +25 -0
  38. package/dist/host/types/src/ui/auth/ElectronLoginForm.d.ts +9 -0
  39. package/dist/host/types/src/ui/components/command.d.ts +78 -0
  40. package/dist/host/types/src/ui/components/folder-style.d.ts +16 -0
  41. package/dist/host/types/src/ui/components/theme-provider.d.ts +2 -1
  42. package/dist/host/types/src/ui/components/version-badge.d.ts +7 -0
  43. package/dist/host/types/src/ui/hooks/use-page-visible-interval.d.ts +10 -0
  44. package/dist/host/types/src/ui/lib/activity-types.d.ts +15 -0
  45. package/dist/host/types/src/ui/lib/app-version.d.ts +2 -0
  46. package/dist/host/types/src/ui/lib/apply-ui-preset.d.ts +19 -0
  47. package/dist/host/types/src/ui/lib/create-id.d.ts +2 -0
  48. package/dist/host/types/src/ui/lib/database-transfer-url.d.ts +8 -0
  49. package/dist/host/types/src/ui/lib/desktop-updates.d.ts +29 -0
  50. package/dist/host/types/src/ui/lib/host-connection-tabs.d.ts +14 -0
  51. package/dist/host/types/src/ui/lib/host-to-ssh-host.d.ts +4 -0
  52. package/dist/host/types/src/ui/lib/issue-url.d.ts +21 -0
  53. package/dist/host/types/src/ui/lib/local-adaptive-preferences.d.ts +9 -0
  54. package/dist/host/types/src/ui/lib/pane-nav-hotkey.d.ts +7 -0
  55. package/dist/host/types/src/ui/lib/plugin-i18n.d.ts +4 -3
  56. package/dist/host/types/src/ui/lib/role-label.d.ts +3 -0
  57. package/dist/host/types/src/ui/look/LookSettings.d.ts +8 -0
  58. package/dist/host/types/src/ui/look/layout-registry.d.ts +34 -0
  59. package/dist/host/types/src/ui/look/look-store.d.ts +29 -0
  60. package/dist/host/types/src/ui/look/resolve-look.d.ts +18 -0
  61. package/dist/host/types/src/ui/look/theme-css.d.ts +7 -0
  62. package/dist/host/types/src/ui/look/theme-registry.d.ts +41 -0
  63. package/dist/host/types/src/ui/look/use-look-theme.d.ts +12 -0
  64. package/dist/host/types/src/ui/main-axios.d.ts +2 -2
  65. package/dist/host/types/src/ui/manage/manage-requests.d.ts +34 -0
  66. package/dist/host/types/src/ui/onboarding/onboarding-registry.d.ts +3 -1
  67. package/dist/host/types/src/ui/plugin-host/loader.d.ts +2 -0
  68. package/dist/host/types/src/ui/plugin-host/scope.d.ts +6 -0
  69. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +101 -0
  70. package/dist/host/types/src/ui/plugin-host/view-ownership.d.ts +1 -1
  71. package/dist/host/types/src/ui/plugins/PluginConsentPrompt.d.ts +20 -0
  72. package/dist/host/types/src/ui/plugins/PluginDetail.d.ts +7 -0
  73. package/dist/host/types/src/ui/plugins/PluginReleaseNotes.d.ts +14 -0
  74. package/dist/host/types/src/ui/plugins/PluginsTab.d.ts +1 -0
  75. package/dist/host/types/src/ui/plugins/RegistriesSection.d.ts +4 -0
  76. package/dist/host/types/src/ui/plugins/plugin-bits.d.ts +23 -0
  77. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +136 -0
  78. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +78 -0
  79. package/dist/host/types/src/ui/settings/appearance/appearance-controls.d.ts +53 -0
  80. package/dist/host/types/src/ui/settings/sync/LinkServerDialog.d.ts +12 -0
  81. package/dist/host/types/src/ui/settings/sync/ProxySettingsFields.d.ts +15 -0
  82. package/dist/host/types/src/ui/settings/sync/saved-servers.d.ts +3 -0
  83. package/dist/host/types/src/ui/shell/controller/ShellControllerProvider.d.ts +20 -0
  84. package/dist/host/types/src/ui/shell/controller/plugin-shell.d.ts +7 -0
  85. package/dist/host/types/src/ui/shell/controller/shell-store.d.ts +4 -0
  86. package/dist/host/types/src/ui/shell/controller/types.d.ts +156 -0
  87. package/dist/host/types/src/ui/shell/controller/use-hosts.d.ts +10 -0
  88. package/dist/host/types/src/ui/shell/controller/use-regions.d.ts +5 -0
  89. package/dist/host/types/src/ui/shell/controller/use-shell-keybindings.d.ts +12 -0
  90. package/dist/host/types/src/ui/shell/controller/use-shell-lifecycle.d.ts +17 -0
  91. package/dist/host/types/src/ui/shell/controller/use-split.d.ts +21 -0
  92. package/dist/host/types/src/ui/shell/controller/use-tabs.d.ts +70 -0
  93. package/dist/host/types/src/ui/shell/controller/use-workspace-layout.d.ts +4 -0
  94. package/dist/host/types/src/ui/shell/reconnect-tabs.d.ts +6 -0
  95. package/dist/host/types/src/ui/shell/shell-layout.d.ts +46 -0
  96. package/dist/host/types/src/ui/shell/split/EmptyPanePicker.d.ts +32 -0
  97. package/dist/host/types/src/ui/shell/split/SplitDropOverlay.d.ts +9 -0
  98. package/dist/host/types/src/ui/shell/split/SplitLayoutMenu.d.ts +16 -0
  99. package/dist/host/types/src/ui/shell/split/SplitView.d.ts +31 -0
  100. package/dist/host/types/src/ui/shell/split/split-drag.d.ts +58 -0
  101. package/dist/host/types/src/ui/shell/split/split-presets.d.ts +28 -0
  102. package/dist/host/types/src/ui/shell/split/split-tabs.d.ts +95 -0
  103. package/dist/host/types/src/ui/shell/split/split-targets.d.ts +30 -0
  104. package/dist/host/types/src/ui/shell/split/split-tree.d.ts +89 -0
  105. package/dist/host/types/src/ui/shell/stage/TabStage.d.ts +9 -0
  106. package/dist/host/types/src/ui/shell/stage/tab-stage-store.d.ts +24 -0
  107. package/dist/host/types/src/ui/shell/tab-close-guards.d.ts +9 -0
  108. package/dist/host/types/src/ui/shell/tab-handles.d.ts +8 -0
  109. package/dist/host/types/src/ui/shell/tabUtils.d.ts +15 -0
  110. package/dist/host/types/src/ui/sidebar/AuthEnrollmentSections.d.ts +5 -0
  111. package/dist/host/types/src/ui/sidebar/HostManagerData.d.ts +4 -0
  112. package/dist/host/types/src/ui/sidebar/build-host-tree.d.ts +25 -0
  113. package/dist/host/types/src/ui/sidebar/host-contributions.d.ts +10 -0
  114. package/dist/host/ui.js +6045 -4055
  115. package/dist/host/{use-permissions-GYCBY2AU.js → use-permissions-ISEDRY3J.js} +2 -2
  116. package/dist/manifest.d.ts +50 -1
  117. package/dist/manifest.js +104 -1
  118. package/dist/ssh-keys.d.ts +29 -0
  119. package/dist/ssh-keys.js +85 -0
  120. package/dist/testing/setup.js +1 -0
  121. package/dist/testing.d.ts +32 -15
  122. package/dist/testing.js +218 -1
  123. package/dist/theme-css.d.ts +11 -0
  124. package/dist/theme-css.js +142 -0
  125. package/dist/vitest-preset.d.ts +18 -0
  126. package/dist/vitest-preset.js +14 -3
  127. package/package.json +5 -1
  128. package/schema/manifest.schema.json +121 -8
  129. package/tsconfig.plugin.json +1 -0
  130. package/dist/host/chunk-YC2MAKSP.js +0 -14
@@ -0,0 +1,3266 @@
1
+ const __termixNoGlob = () => ({});
2
+ import {
3
+ AddButton,
4
+ Card,
5
+ DataView,
6
+ EmptyState,
7
+ FormFooter,
8
+ ListBadge,
9
+ ListRow,
10
+ ListRowAction,
11
+ betaFeedbackUrl,
12
+ betaToTry,
13
+ capabilityRisk,
14
+ categoriesOf,
15
+ describeContributions,
16
+ formatBytes,
17
+ formatCount,
18
+ groupChanges,
19
+ isSevere,
20
+ kindsOf,
21
+ matchesQuery,
22
+ mergePlugins,
23
+ mergeReleaseNotes,
24
+ orderByRisk,
25
+ pageOf,
26
+ reportIssueUrl,
27
+ requestFeatureUrl,
28
+ require_semver,
29
+ sortPlugins,
30
+ uploadEntry,
31
+ youtubeEmbedUrl
32
+ } from "./chunk-54KCK6IZ.js";
33
+ import {
34
+ ArrowDownWideNarrow,
35
+ BookOpen,
36
+ Bug,
37
+ Button,
38
+ Check,
39
+ ChevronLeft,
40
+ ChevronRight,
41
+ Columns3,
42
+ DEVELOPER_MODE_CHANGED_EVENT,
43
+ Database,
44
+ DocsLink,
45
+ Download,
46
+ ExternalLink,
47
+ Facts,
48
+ FakeSwitch,
49
+ FlaskConical,
50
+ Globe,
51
+ GroupHeading,
52
+ InlineView,
53
+ Input,
54
+ Library,
55
+ Lightbulb,
56
+ ListFilter,
57
+ PANEL,
58
+ PanePrompt,
59
+ PanelShell,
60
+ Pin,
61
+ PluginIcon,
62
+ Puzzle,
63
+ RefreshCw,
64
+ RotateCcw,
65
+ Search,
66
+ SectionCard,
67
+ Select,
68
+ SelectContent,
69
+ SelectItem,
70
+ SelectTrigger,
71
+ SelectValue,
72
+ Server,
73
+ Settings,
74
+ SurfaceScope,
75
+ TabStrip,
76
+ Trash,
77
+ TriangleAlert,
78
+ Upload,
79
+ ViewToggle,
80
+ X,
81
+ addRegistry,
82
+ cn,
83
+ deletePluginData,
84
+ docsUrl,
85
+ getDeveloperMode,
86
+ getPluginChangelog,
87
+ getPluginData,
88
+ getPluginRegistry,
89
+ getPlugins,
90
+ getRegistries,
91
+ hasSettingsPage,
92
+ installPlugin,
93
+ installUploadedPlugin,
94
+ openPluginSettings,
95
+ pluginDescription,
96
+ pluginName,
97
+ previewPluginState,
98
+ previewRegistry,
99
+ removeRegistry,
100
+ retryPlugin,
101
+ setAllPluginChannels,
102
+ setPluginOptions,
103
+ setPluginState,
104
+ setRegistryEnabled,
105
+ uninstallPlugin,
106
+ updateAllPlugins,
107
+ updatePlugin,
108
+ uploadPlugin,
109
+ useConfirm
110
+ } from "./chunk-ERGN4YW6.js";
111
+ import "./chunk-CN5UBTBP.js";
112
+ import {
113
+ __toESM,
114
+ define_import_meta_env_default,
115
+ init_define_import_meta_env
116
+ } from "./chunk-YGYBM7SQ.js";
117
+
118
+ // src/ui/plugins/PluginsTab.tsx
119
+ init_define_import_meta_env();
120
+ import { useEffect as useEffect5, useRef as useRef2, useState as useState5 } from "react";
121
+ import { useTranslation as useTranslation7 } from "react-i18next";
122
+
123
+ // src/ui/plugins/plugin-bits.tsx
124
+ init_define_import_meta_env();
125
+ import { useTranslation } from "react-i18next";
126
+ import { jsx, jsxs } from "react/jsx-runtime";
127
+ function PluginIconBox({
128
+ name,
129
+ size = "md",
130
+ muted
131
+ }) {
132
+ const box = size === "lg" ? "size-10" : size === "sm" ? "size-7" : "size-9";
133
+ const glyph = size === "lg" ? "size-5" : size === "sm" ? "size-3.5" : "size-4";
134
+ return /* @__PURE__ */ jsx(
135
+ "div",
136
+ {
137
+ className: cn(
138
+ box,
139
+ "flex shrink-0 items-center justify-center border border-border bg-muted",
140
+ muted && "opacity-40"
141
+ ),
142
+ children: /* @__PURE__ */ jsx(PluginIcon, { name, className: cn(glyph, "text-accent-brand") })
143
+ }
144
+ );
145
+ }
146
+ function CapabilityRow({
147
+ capability,
148
+ showId,
149
+ className
150
+ }) {
151
+ const { t } = useTranslation();
152
+ const severe = isSevere(capability);
153
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-start gap-2.5", className), children: [
154
+ /* @__PURE__ */ jsx("span", { className: "mt-0.5 flex size-4 shrink-0 items-center justify-center", children: severe && /* @__PURE__ */ jsx(
155
+ "span",
156
+ {
157
+ className: "flex size-4 items-center justify-center border border-destructive/50 bg-destructive/10 text-[10px] font-bold text-destructive",
158
+ title: t(`plugins.manager.risk.${capabilityRisk(capability)}`),
159
+ children: "!"
160
+ }
161
+ ) }),
162
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [
163
+ /* @__PURE__ */ jsx("span", { className: "text-xs font-medium leading-snug", children: t(`plugins.capabilities.${capability}.title`, {
164
+ defaultValue: capability,
165
+ nsSeparator: false
166
+ }) }),
167
+ /* @__PURE__ */ jsx("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t(`plugins.capabilities.${capability}.consequence`, {
168
+ defaultValue: "",
169
+ nsSeparator: false
170
+ }) })
171
+ ] }),
172
+ showId && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-[10px] text-muted-foreground/50", children: capability })
173
+ ] });
174
+ }
175
+ function UnverifiedBadge() {
176
+ const { t } = useTranslation();
177
+ return /* @__PURE__ */ jsx(
178
+ "span",
179
+ {
180
+ className: "shrink-0 border border-warning/40 bg-warning/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning",
181
+ title: t("plugins.manager.unverifiedHint"),
182
+ children: t("plugins.manager.unverified")
183
+ }
184
+ );
185
+ }
186
+ function SourceBadge({
187
+ source,
188
+ registryName
189
+ }) {
190
+ const { t } = useTranslation();
191
+ if (source !== "community" && source !== "custom") return null;
192
+ const label = source === "community" ? t("plugins.manager.registries.communityBadge") : registryName ?? t("plugins.manager.registries.customBadge");
193
+ return /* @__PURE__ */ jsx(
194
+ "span",
195
+ {
196
+ className: "max-w-32 shrink-0 truncate border border-border bg-muted/40 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-muted-foreground",
197
+ title: t(`plugins.manager.registries.${source}BadgeHint`, {
198
+ name: label
199
+ }),
200
+ children: label
201
+ }
202
+ );
203
+ }
204
+ function BetaBadge() {
205
+ const { t } = useTranslation();
206
+ return /* @__PURE__ */ jsx(
207
+ "span",
208
+ {
209
+ className: "shrink-0 border border-accent-brand/40 bg-accent-brand/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-accent-brand",
210
+ title: t("plugins.manager.beta.badgeHint"),
211
+ children: t("plugins.manager.beta.badge")
212
+ }
213
+ );
214
+ }
215
+ function DownloadCount({ count }) {
216
+ const { t } = useTranslation();
217
+ if (count === null || count <= 0) return null;
218
+ return /* @__PURE__ */ jsx("span", { title: t("plugins.manager.count.downloadsHint"), children: t("plugins.manager.count.downloads", {
219
+ count,
220
+ formatted: formatCount(count)
221
+ }) });
222
+ }
223
+
224
+ // src/ui/plugins/RegistriesSection.tsx
225
+ init_define_import_meta_env();
226
+ import { useCallback, useEffect, useState } from "react";
227
+ import { useTranslation as useTranslation2 } from "react-i18next";
228
+ import { toast } from "sonner";
229
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
230
+ function errorOf(error) {
231
+ const data = error?.response?.data;
232
+ return {
233
+ message: typeof data?.error === "string" ? data.error : error instanceof Error ? error.message : String(error),
234
+ code: typeof data?.code === "string" ? data.code : void 0,
235
+ data: data ?? {}
236
+ };
237
+ }
238
+ function RegistriesSection({ manager }) {
239
+ const { t } = useTranslation2();
240
+ const confirm = useConfirm();
241
+ const [state, setState] = useState(null);
242
+ const [adding, setAdding] = useState(false);
243
+ const [busy, setBusy] = useState(null);
244
+ const load = useCallback(async () => {
245
+ try {
246
+ setState(await getRegistries());
247
+ } catch (error) {
248
+ toast.error(
249
+ t("plugins.manager.registries.loadFailed", {
250
+ error: errorOf(error).message
251
+ })
252
+ );
253
+ }
254
+ }, [t]);
255
+ useEffect(() => {
256
+ void load();
257
+ }, [load]);
258
+ const statusOf = (id) => manager.registries.find((entry) => entry.id === id);
259
+ const toggle = async (id, enabled) => {
260
+ if (enabled && id === "community") {
261
+ const ok = await confirm({
262
+ title: t("plugins.manager.registries.communityConfirmTitle"),
263
+ description: t("plugins.manager.registries.communityConfirmBody"),
264
+ confirmLabel: t("plugins.manager.registries.communityConfirm"),
265
+ destructive: false
266
+ });
267
+ if (!ok) return;
268
+ }
269
+ setBusy(id);
270
+ try {
271
+ await setRegistryEnabled(id, enabled);
272
+ await load();
273
+ await manager.refresh();
274
+ } catch (error) {
275
+ toast.error(
276
+ t("plugins.manager.registries.changeFailed", {
277
+ error: errorOf(error).message
278
+ })
279
+ );
280
+ } finally {
281
+ setBusy(null);
282
+ }
283
+ };
284
+ const remove = async (id, name) => {
285
+ const ok = await confirm({
286
+ title: t("plugins.manager.registries.removeTitle", { name }),
287
+ description: t("plugins.manager.registries.removeBody"),
288
+ confirmLabel: t("plugins.manager.registries.remove")
289
+ });
290
+ if (!ok) return;
291
+ setBusy(id);
292
+ try {
293
+ await removeRegistry(id);
294
+ toast.success(t("plugins.manager.registries.removed", { name }));
295
+ await load();
296
+ await manager.refresh();
297
+ } catch (error) {
298
+ const info = errorOf(error);
299
+ toast.error(
300
+ info.code === "REGISTRY_IN_USE" ? t("plugins.manager.registries.inUse", {
301
+ name,
302
+ list: (info.data.plugins ?? []).join(", ")
303
+ }) : t("plugins.manager.registries.changeFailed", {
304
+ error: info.message
305
+ })
306
+ );
307
+ } finally {
308
+ setBusy(null);
309
+ }
310
+ };
311
+ const readOnly = manager.managedByServer || state?.managedByServer;
312
+ const meta = (id, url, extra) => {
313
+ const status = statusOf(id);
314
+ const parts = [url];
315
+ if (status && !status.error) {
316
+ parts.push(
317
+ t("plugins.manager.registries.pluginCount", { count: status.plugins })
318
+ );
319
+ }
320
+ if (extra) parts.push(extra);
321
+ return parts.join(" | ");
322
+ };
323
+ const errorLine = (id) => {
324
+ const error = statusOf(id)?.error;
325
+ return error ? /* @__PURE__ */ jsx2("span", { className: "text-[11px] leading-snug text-destructive", children: t("plugins.manager.registries.unreachable", { error }) }) : null;
326
+ };
327
+ return /* @__PURE__ */ jsxs2("div", { className: `flex flex-col ${PANEL.gap} ${PANEL.body}`, children: [
328
+ /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap items-center gap-2", children: [
329
+ /* @__PURE__ */ jsxs2("p", { className: "min-w-0 flex-1 text-[11px] leading-snug text-muted-foreground", children: [
330
+ t("plugins.manager.registries.intro"),
331
+ " ",
332
+ /* @__PURE__ */ jsx2(DocsLink, { core: "hostRegistry", className: "text-[11px]", children: t("plugins.manager.registries.hostYourOwn") })
333
+ ] }),
334
+ !readOnly && /* @__PURE__ */ jsx2(
335
+ AddButton,
336
+ {
337
+ label: t("plugins.manager.registries.add"),
338
+ onClick: () => setAdding(true)
339
+ }
340
+ )
341
+ ] }),
342
+ state && /* @__PURE__ */ jsxs2("div", { className: "flex flex-col border-t border-border/40", children: [
343
+ /* @__PURE__ */ jsx2(
344
+ ListRow,
345
+ {
346
+ stripe: 0,
347
+ icon: /* @__PURE__ */ jsx2(Library, {}),
348
+ title: t("plugins.manager.registries.officialName"),
349
+ badges: /* @__PURE__ */ jsx2(ListBadge, { tone: "brand", children: t("plugins.manager.registries.alwaysOn") }),
350
+ meta: meta("official", state.official.url),
351
+ children: errorLine("official")
352
+ }
353
+ ),
354
+ /* @__PURE__ */ jsxs2(
355
+ ListRow,
356
+ {
357
+ stripe: 1,
358
+ icon: /* @__PURE__ */ jsx2(Library, {}),
359
+ tone: state.community.enabled ? "brand" : "muted",
360
+ dimmed: !state.community.enabled,
361
+ title: t("plugins.manager.registries.communityName"),
362
+ meta: meta("community", state.community.url),
363
+ trailing: /* @__PURE__ */ jsx2(
364
+ FakeSwitch,
365
+ {
366
+ checked: state.community.enabled,
367
+ disabled: readOnly || busy === "community",
368
+ onChange: (next) => void toggle("community", next)
369
+ }
370
+ ),
371
+ children: [
372
+ /* @__PURE__ */ jsxs2("span", { className: "text-[11px] leading-snug text-muted-foreground", children: [
373
+ t("plugins.manager.registries.communityHint"),
374
+ " ",
375
+ /* @__PURE__ */ jsx2(DocsLink, { core: "communityRegistry", className: "text-[11px]", children: t("plugins.manager.communitySubmit") })
376
+ ] }),
377
+ state.community.enabled && errorLine("community")
378
+ ]
379
+ }
380
+ ),
381
+ state.custom.map((entry, index) => /* @__PURE__ */ jsx2(
382
+ ListRow,
383
+ {
384
+ stripe: index,
385
+ icon: /* @__PURE__ */ jsx2(Library, {}),
386
+ tone: entry.enabled ? "warning" : "muted",
387
+ dimmed: !entry.enabled,
388
+ title: entry.name,
389
+ badges: /* @__PURE__ */ jsx2(ListBadge, { tone: "warning", children: t("plugins.manager.registries.customBadge") }),
390
+ meta: meta(
391
+ entry.id,
392
+ entry.url,
393
+ t("plugins.manager.registries.keyFingerprint", {
394
+ keyId: entry.keyId
395
+ })
396
+ ),
397
+ trailing: /* @__PURE__ */ jsx2(
398
+ FakeSwitch,
399
+ {
400
+ checked: entry.enabled,
401
+ disabled: readOnly || busy === entry.id,
402
+ onChange: (next) => void toggle(entry.id, next)
403
+ }
404
+ ),
405
+ actions: readOnly ? void 0 : /* @__PURE__ */ jsx2(
406
+ ListRowAction,
407
+ {
408
+ label: t("plugins.manager.registries.remove"),
409
+ tone: "destructive",
410
+ disabled: busy === entry.id,
411
+ onClick: () => void remove(entry.id, entry.name),
412
+ children: /* @__PURE__ */ jsx2(Trash, {})
413
+ }
414
+ ),
415
+ children: entry.enabled && errorLine(entry.id)
416
+ },
417
+ entry.id
418
+ ))
419
+ ] }),
420
+ /* @__PURE__ */ jsx2(
421
+ AddRegistryView,
422
+ {
423
+ open: adding,
424
+ onOpenChange: setAdding,
425
+ onAdded: async () => {
426
+ setAdding(false);
427
+ await load();
428
+ await manager.refresh();
429
+ }
430
+ }
431
+ )
432
+ ] });
433
+ }
434
+ function AddRegistryView({
435
+ open,
436
+ onOpenChange,
437
+ onAdded
438
+ }) {
439
+ const { t } = useTranslation2();
440
+ const [url, setUrl] = useState("");
441
+ const [preview, setPreview] = useState(null);
442
+ const [error, setError] = useState(null);
443
+ const [working, setWorking] = useState(false);
444
+ useEffect(() => {
445
+ if (!open) {
446
+ setUrl("");
447
+ setPreview(null);
448
+ setError(null);
449
+ }
450
+ }, [open]);
451
+ const check = async () => {
452
+ setWorking(true);
453
+ setError(null);
454
+ setPreview(null);
455
+ try {
456
+ setPreview(await previewRegistry(url.trim()));
457
+ } catch (failure) {
458
+ setError(errorOf(failure).message);
459
+ } finally {
460
+ setWorking(false);
461
+ }
462
+ };
463
+ const add = async () => {
464
+ if (!preview) return;
465
+ setWorking(true);
466
+ setError(null);
467
+ try {
468
+ const added = await addRegistry(preview.url, preview.keyId);
469
+ toast.success(
470
+ t("plugins.manager.registries.added", { name: added.name })
471
+ );
472
+ await onAdded();
473
+ } catch (failure) {
474
+ const info = errorOf(failure);
475
+ if (info.code === "KEY_MISMATCH") setPreview(null);
476
+ setError(info.message);
477
+ } finally {
478
+ setWorking(false);
479
+ }
480
+ };
481
+ return /* @__PURE__ */ jsx2(
482
+ InlineView,
483
+ {
484
+ open,
485
+ onOpenChange,
486
+ title: t("plugins.manager.registries.addTitle"),
487
+ icon: /* @__PURE__ */ jsx2(Library, { className: "size-4" }),
488
+ footer: /* @__PURE__ */ jsx2(
489
+ FormFooter,
490
+ {
491
+ onCancel: () => onOpenChange(false),
492
+ onSave: () => void (preview ? add() : check()),
493
+ saveLabel: preview ? t("plugins.manager.registries.addConfirm") : t("plugins.manager.registries.check"),
494
+ saving: working,
495
+ disabled: !url.trim()
496
+ }
497
+ ),
498
+ children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-3 p-3", children: [
499
+ /* @__PURE__ */ jsx2("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.registries.addHint") }),
500
+ /* @__PURE__ */ jsx2(
501
+ Input,
502
+ {
503
+ value: url,
504
+ onChange: (e) => {
505
+ setUrl(e.target.value);
506
+ setPreview(null);
507
+ setError(null);
508
+ },
509
+ onKeyDown: (e) => {
510
+ if (e.key === "Enter" && url.trim() && !working) {
511
+ e.preventDefault();
512
+ void (preview ? add() : check());
513
+ }
514
+ },
515
+ placeholder: "https://example.com/termix/index.json",
516
+ className: "h-8 text-xs",
517
+ "aria-label": t("plugins.manager.registries.urlLabel"),
518
+ autoFocus: true
519
+ }
520
+ ),
521
+ error && /* @__PURE__ */ jsx2("p", { className: "text-[11px] leading-snug text-destructive", children: error }),
522
+ preview && /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1.5 border border-warning/40 bg-warning/5 p-3", children: [
523
+ /* @__PURE__ */ jsx2("span", { className: "text-sm font-semibold", children: preview.name }),
524
+ /* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: t("plugins.manager.registries.pluginCount", {
525
+ count: preview.plugins
526
+ }) }),
527
+ /* @__PURE__ */ jsx2("span", { className: "text-[11px] text-muted-foreground", children: t("plugins.manager.registries.keyFingerprint", {
528
+ keyId: preview.keyId
529
+ }) }),
530
+ /* @__PURE__ */ jsx2("p", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.registries.customWarning") })
531
+ ] })
532
+ ] })
533
+ }
534
+ );
535
+ }
536
+
537
+ // src/ui/plugins/PluginConsentPrompt.tsx
538
+ init_define_import_meta_env();
539
+ import { useTranslation as useTranslation3 } from "react-i18next";
540
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
541
+ function PluginConsentPrompt({
542
+ request,
543
+ busy,
544
+ onCancel,
545
+ onConfirm
546
+ }) {
547
+ const { t } = useTranslation3();
548
+ if (!request) return null;
549
+ const { plugin, mode, version } = request;
550
+ const ordered = orderByRisk(request.capabilities);
551
+ const notable = ordered.filter((c) => capabilityRisk(c) !== "low");
552
+ const minor = ordered.filter((c) => capabilityRisk(c) === "low");
553
+ return /* @__PURE__ */ jsx3(
554
+ PanePrompt,
555
+ {
556
+ open: true,
557
+ className: "max-w-md",
558
+ onCancel: busy ? void 0 : onCancel,
559
+ icon: /* @__PURE__ */ jsx3(PluginIconBox, { name: plugin.icon, size: "sm" }),
560
+ title: mode === "enable" ? t("plugins.manager.consent.enableTitle", { name: plugin.name }) : mode === "update" ? t("plugins.manager.consent.updateTitle", { name: plugin.name }) : mode === "upload" ? t("plugins.manager.consent.uploadTitle", { name: plugin.name }) : t("plugins.manager.consent.installTitle", { name: plugin.name }),
561
+ description: /* @__PURE__ */ jsxs3(Facts, { children: [
562
+ plugin.author && /* @__PURE__ */ jsx3("span", { className: "truncate", children: plugin.author }),
563
+ /* @__PURE__ */ jsx3("span", { className: "shrink-0", children: (mode === "update" || mode === "upload") && plugin.version ? t("plugins.manager.versionChange", {
564
+ from: plugin.version,
565
+ to: version
566
+ }) : version })
567
+ ] }),
568
+ actions: /* @__PURE__ */ jsxs3(Fragment, { children: [
569
+ /* @__PURE__ */ jsx3(Button, { variant: "ghost", onClick: onCancel, disabled: busy, children: t("common.cancel") }),
570
+ /* @__PURE__ */ jsx3(
571
+ Button,
572
+ {
573
+ variant: "outline",
574
+ onClick: onConfirm,
575
+ disabled: busy,
576
+ className: "border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
577
+ children: mode === "enable" ? t("plugins.manager.enable") : mode === "update" ? t("plugins.manager.consent.agreeUpdate") : t("plugins.manager.install")
578
+ }
579
+ )
580
+ ] }),
581
+ children: /* @__PURE__ */ jsxs3("div", { className: "flex flex-col gap-3", children: [
582
+ mode === "upload" && /* @__PURE__ */ jsxs3("div", { className: "flex items-start gap-2 border border-warning/40 bg-warning/10 px-3 py-2", children: [
583
+ /* @__PURE__ */ jsx3(TriangleAlert, { className: "mt-px size-3.5 shrink-0 text-warning" }),
584
+ /* @__PURE__ */ jsx3("span", { className: "text-[11px] leading-snug text-foreground", children: t("plugins.manager.consent.uploadWarning") })
585
+ ] }),
586
+ mode === "update" && /* @__PURE__ */ jsx3("p", { className: "text-xs leading-relaxed text-foreground", children: t("plugins.manager.consent.updateExplain") }),
587
+ /* @__PURE__ */ jsxs3("div", { className: "flex flex-col", children: [
588
+ /* @__PURE__ */ jsx3("span", { className: "pb-2 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground", children: mode === "update" ? t("plugins.manager.consent.newInVersion") : t("plugins.manager.whatItCanDo") }),
589
+ /* @__PURE__ */ jsxs3("div", { className: "divide-y divide-border border-y border-border", children: [
590
+ notable.map((capability) => /* @__PURE__ */ jsx3(
591
+ CapabilityRow,
592
+ {
593
+ capability,
594
+ className: "py-2.5"
595
+ },
596
+ capability
597
+ )),
598
+ minor.length > 0 && /* @__PURE__ */ jsx3("div", { className: "py-2.5 text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.consent.minor") }),
599
+ ordered.length === 0 && /* @__PURE__ */ jsx3("div", { className: "py-2.5 text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.consent.none") })
600
+ ] })
601
+ ] })
602
+ ] })
603
+ }
604
+ );
605
+ }
606
+
607
+ // src/ui/plugins/PluginDetail.tsx
608
+ init_define_import_meta_env();
609
+ import { useEffect as useEffect3, useState as useState3 } from "react";
610
+ import { useTranslation as useTranslation5 } from "react-i18next";
611
+
612
+ // src/ui/plugins/PluginReleaseNotes.tsx
613
+ init_define_import_meta_env();
614
+ import { useEffect as useEffect2, useMemo, useState as useState2 } from "react";
615
+ import { useTranslation as useTranslation4 } from "react-i18next";
616
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
617
+ var MAX_CHANGES = 8;
618
+ function formatDate(value) {
619
+ if (!value) return "";
620
+ const date = new Date(value);
621
+ return Number.isNaN(date.getTime()) ? value : date.toLocaleDateString();
622
+ }
623
+ function InlineText({ text }) {
624
+ return /* @__PURE__ */ jsx4(Fragment2, { children: text.split(/(`[^`]+`)/).map(
625
+ (part, i) => part.length > 2 && part.startsWith("`") && part.endsWith("`") ? /* @__PURE__ */ jsx4(
626
+ "code",
627
+ {
628
+ className: "bg-muted px-1 py-px font-mono text-[10px] text-foreground",
629
+ children: part.slice(1, -1)
630
+ },
631
+ i
632
+ ) : /* @__PURE__ */ jsx4("span", { children: part }, i)
633
+ ) });
634
+ }
635
+ function Release({
636
+ row,
637
+ installedVersion
638
+ }) {
639
+ const { t } = useTranslation4();
640
+ const [full, setFull] = useState2(false);
641
+ const all = groupChanges(row.changes);
642
+ const hidden = full ? 0 : Math.max(0, row.changes.length - MAX_CHANGES);
643
+ let budget = full ? Infinity : MAX_CHANGES;
644
+ const groups = all.map((group) => {
645
+ const items = group.items.slice(0, Math.max(0, budget));
646
+ budget -= items.length;
647
+ return { ...group, items };
648
+ }).filter((group) => group.items.length > 0);
649
+ return /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-2 px-4 py-3", children: [
650
+ /* @__PURE__ */ jsxs4("div", { className: "flex flex-wrap items-center gap-2", children: [
651
+ /* @__PURE__ */ jsx4("span", { className: "text-xs font-semibold", children: row.version }),
652
+ row.version === installedVersion && /* @__PURE__ */ jsx4("span", { className: "border border-accent-brand/40 bg-accent-brand/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-accent-brand", children: t("plugins.manager.installedBadge") }),
653
+ !row.compatible && /* @__PURE__ */ jsx4("span", { className: "border border-warning/40 bg-warning/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning", children: t("plugins.manager.incompatible") }),
654
+ /* @__PURE__ */ jsx4("span", { className: "ml-auto text-[10px] text-muted-foreground", children: formatDate(row.date) })
655
+ ] }),
656
+ row.summary && /* @__PURE__ */ jsx4(
657
+ "p",
658
+ {
659
+ className: `whitespace-pre-line text-[11px] leading-relaxed text-muted-foreground ${full ? "" : "line-clamp-4"}`,
660
+ children: /* @__PURE__ */ jsx4(InlineText, { text: row.summary })
661
+ }
662
+ ),
663
+ groups.map((group) => /* @__PURE__ */ jsxs4("div", { className: "flex flex-col gap-1", children: [
664
+ /* @__PURE__ */ jsx4("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-muted-foreground/70", children: t(`plugins.manager.changeTypes.${group.type}`) }),
665
+ /* @__PURE__ */ jsx4("ul", { className: "flex list-disc flex-col gap-0.5 pl-4 text-[11px] leading-snug", children: group.items.map((item, i) => /* @__PURE__ */ jsx4("li", { className: "whitespace-pre-line", children: /* @__PURE__ */ jsx4(InlineText, { text: item }) }, i)) })
666
+ ] }, group.type)),
667
+ hidden > 0 && /* @__PURE__ */ jsx4(
668
+ "button",
669
+ {
670
+ type: "button",
671
+ onClick: () => setFull(true),
672
+ className: "w-fit text-[11px] text-muted-foreground hover:text-accent-brand",
673
+ children: t("plugins.manager.showMoreChanges", { count: hidden })
674
+ }
675
+ ),
676
+ groups.length === 0 && !row.summary && /* @__PURE__ */ jsx4("span", { className: "text-[11px] text-muted-foreground", children: t("plugins.manager.noReleaseNotes") }),
677
+ row.releaseNotesUrl && /* @__PURE__ */ jsx4(
678
+ "a",
679
+ {
680
+ href: row.releaseNotesUrl,
681
+ target: "_blank",
682
+ rel: "noreferrer",
683
+ className: "w-fit text-[11px] text-muted-foreground hover:text-accent-brand",
684
+ children: t("plugins.manager.viewRelease")
685
+ }
686
+ )
687
+ ] });
688
+ }
689
+ function PluginReleaseNotes({ plugin }) {
690
+ const { t } = useTranslation4();
691
+ const [local, setLocal] = useState2([]);
692
+ const [showOlder, setShowOlder] = useState2(false);
693
+ useEffect2(() => {
694
+ let cancelled = false;
695
+ setLocal([]);
696
+ if (!plugin.installed) return;
697
+ getPluginChangelog(plugin.id).then((releases) => {
698
+ if (!cancelled) setLocal(releases);
699
+ }).catch(() => {
700
+ });
701
+ return () => {
702
+ cancelled = true;
703
+ };
704
+ }, [plugin.id, plugin.installed, plugin.version]);
705
+ const rows = useMemo(
706
+ () => mergeReleaseNotes(plugin.versions, local),
707
+ [plugin.versions, local]
708
+ );
709
+ if (rows.length === 0) return null;
710
+ const [latest, ...older] = rows;
711
+ return /* @__PURE__ */ jsx4(
712
+ SectionCard,
713
+ {
714
+ title: t("plugins.manager.releaseNotes"),
715
+ icon: null,
716
+ action: older.length > 0 ? /* @__PURE__ */ jsx4(
717
+ Button,
718
+ {
719
+ variant: "ghost",
720
+ size: "xs",
721
+ className: "text-muted-foreground",
722
+ onClick: () => setShowOlder((v) => !v),
723
+ children: showOlder ? t("plugins.manager.hideOlderReleases") : t("plugins.manager.showOlderReleases", {
724
+ count: older.length
725
+ })
726
+ }
727
+ ) : void 0,
728
+ children: /* @__PURE__ */ jsxs4("div", { className: "-mx-3 -my-1 md:-mx-4", children: [
729
+ /* @__PURE__ */ jsx4(Release, { row: latest, installedVersion: plugin.version }),
730
+ showOlder && older.length > 0 && /* @__PURE__ */ jsx4("div", { className: "max-h-96 divide-y divide-border overflow-y-auto border-t border-border", children: older.map((row) => /* @__PURE__ */ jsx4(
731
+ Release,
732
+ {
733
+ row,
734
+ installedVersion: plugin.version
735
+ },
736
+ row.version
737
+ )) })
738
+ ] })
739
+ }
740
+ );
741
+ }
742
+ function PluginVideo({
743
+ videoId,
744
+ name
745
+ }) {
746
+ const { t } = useTranslation4();
747
+ const src = youtubeEmbedUrl(videoId);
748
+ if (!src) return null;
749
+ return /* @__PURE__ */ jsx4("div", { className: "aspect-video w-full border border-border bg-black", children: /* @__PURE__ */ jsx4(
750
+ "iframe",
751
+ {
752
+ src,
753
+ title: t("plugins.manager.videoTitle", { name }),
754
+ className: "size-full",
755
+ loading: "lazy",
756
+ referrerPolicy: "strict-origin-when-cross-origin",
757
+ sandbox: "allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox",
758
+ allow: "encrypted-media; picture-in-picture; fullscreen",
759
+ allowFullScreen: true
760
+ }
761
+ ) });
762
+ }
763
+
764
+ // src/ui/plugins/PluginDetail.tsx
765
+ import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
766
+ var LINK_ROW = "flex items-center gap-2 border border-border bg-card px-3 py-2.5 text-left text-[11px] text-muted-foreground transition-colors hover:border-accent-brand/40 hover:text-foreground";
767
+ function PluginDetail({
768
+ plugin,
769
+ manager,
770
+ onUninstalled
771
+ }) {
772
+ const { t } = useTranslation5();
773
+ const busy = manager.busy.has(plugin.id) || manager.busy.has("*");
774
+ const locked = manager.managedByServer;
775
+ const running = plugin.status === "running";
776
+ const failed = plugin.status === "failed" || plugin.status === "blocked";
777
+ const compatible = plugin.versions.filter((v) => v.compatible);
778
+ const [picked, setPicked] = useState3(
779
+ plugin.version ?? plugin.latestVersion ?? ""
780
+ );
781
+ useEffect3(() => {
782
+ setPicked(plugin.version ?? plugin.latestVersion ?? "");
783
+ }, [plugin.version, plugin.latestVersion]);
784
+ const contributions = describeContributions(plugin.contributes);
785
+ const hasSettings = plugin.installed && hasSettingsPage(plugin, true);
786
+ const issueUrl = reportIssueUrl(
787
+ plugin,
788
+ define_import_meta_env_default.VITE_APP_VERSION || void 0
789
+ );
790
+ const featureUrl = requestFeatureUrl(plugin);
791
+ const feedbackUrl = plugin.isBeta ? betaFeedbackUrl(plugin, define_import_meta_env_default.VITE_APP_VERSION || void 0) : null;
792
+ const tryableBeta = betaToTry(plugin);
793
+ return /* @__PURE__ */ jsxs5(
794
+ "div",
795
+ {
796
+ className: `mx-auto flex w-full max-w-5xl flex-col ${PANEL.gap} ${PANEL.body}`,
797
+ children: [
798
+ /* @__PURE__ */ jsxs5(Card, { className: "flex-row flex-wrap items-start gap-4 px-4 py-4", children: [
799
+ /* @__PURE__ */ jsx5(
800
+ PluginIconBox,
801
+ {
802
+ name: plugin.icon,
803
+ size: "lg",
804
+ muted: plugin.installed && !running
805
+ }
806
+ ),
807
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-1 flex-col gap-1.5", children: [
808
+ /* @__PURE__ */ jsx5("h1", { className: "text-lg font-bold tracking-tight", children: plugin.name }),
809
+ /* @__PURE__ */ jsx5("span", { className: "text-xs leading-relaxed text-muted-foreground", children: plugin.description }),
810
+ /* @__PURE__ */ jsxs5(Facts, { className: "text-[11px] text-muted-foreground/70", children: [
811
+ plugin.unverified && /* @__PURE__ */ jsx5(UnverifiedBadge, {}),
812
+ /* @__PURE__ */ jsx5(
813
+ SourceBadge,
814
+ {
815
+ source: plugin.source,
816
+ registryName: plugin.registryName
817
+ }
818
+ ),
819
+ plugin.isBeta && /* @__PURE__ */ jsx5(BetaBadge, {}),
820
+ plugin.author && /* @__PURE__ */ jsx5("span", { children: plugin.author }),
821
+ plugin.version ? /* @__PURE__ */ jsxs5("span", { children: [
822
+ "v",
823
+ plugin.version
824
+ ] }) : plugin.latestVersion && /* @__PURE__ */ jsxs5("span", { children: [
825
+ "v",
826
+ plugin.latestVersion
827
+ ] }),
828
+ plugin.category && /* @__PURE__ */ jsx5("span", { children: plugin.category }),
829
+ plugin.pinnedVersion && /* @__PURE__ */ jsx5("span", { children: t("plugins.manager.pinnedTo", {
830
+ version: plugin.pinnedVersion
831
+ }) }),
832
+ /* @__PURE__ */ jsx5(DownloadCount, { count: plugin.downloads })
833
+ ] })
834
+ ] }),
835
+ /* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 flex-col gap-1.5", children: [
836
+ plugin.installed && plugin.updateAvailable && !plugin.pinnedVersion && /* @__PURE__ */ jsxs5(
837
+ Button,
838
+ {
839
+ variant: "outline",
840
+ size: "sm",
841
+ className: "gap-1.5 border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
842
+ disabled: busy || locked,
843
+ onClick: () => void manager.requestUpdate(plugin),
844
+ children: [
845
+ /* @__PURE__ */ jsx5(Download, { className: "size-3.5" }),
846
+ t("plugins.manager.updateTo", {
847
+ version: plugin.latestVersion
848
+ })
849
+ ]
850
+ }
851
+ ),
852
+ tryableBeta && /* @__PURE__ */ jsxs5(
853
+ Button,
854
+ {
855
+ variant: "outline",
856
+ size: "sm",
857
+ className: "gap-1.5",
858
+ disabled: busy || locked,
859
+ onClick: () => void manager.tryBeta(plugin),
860
+ children: [
861
+ /* @__PURE__ */ jsx5(FlaskConical, { className: "size-3.5" }),
862
+ t("plugins.manager.beta.try", { version: tryableBeta })
863
+ ]
864
+ }
865
+ ),
866
+ plugin.installed ? /* @__PURE__ */ jsxs5(Fragment3, { children: [
867
+ failed ? /* @__PURE__ */ jsxs5(
868
+ Button,
869
+ {
870
+ variant: "outline",
871
+ size: "sm",
872
+ className: "gap-1.5",
873
+ disabled: busy || locked,
874
+ onClick: () => void manager.retry(plugin),
875
+ children: [
876
+ /* @__PURE__ */ jsx5(RotateCcw, { className: "size-3.5" }),
877
+ t("plugins.manager.restart")
878
+ ]
879
+ }
880
+ ) : null,
881
+ /* @__PURE__ */ jsx5(
882
+ Button,
883
+ {
884
+ variant: "outline",
885
+ size: "sm",
886
+ disabled: busy || plugin.managedByServer,
887
+ onClick: () => void manager.toggle(plugin),
888
+ children: plugin.enabled ? t("plugins.manager.disable") : t("plugins.manager.enable")
889
+ }
890
+ ),
891
+ !plugin.defaultLook && /* @__PURE__ */ jsxs5(
892
+ Button,
893
+ {
894
+ variant: "destructive",
895
+ size: "sm",
896
+ className: "gap-1.5",
897
+ disabled: busy || locked,
898
+ onClick: async () => {
899
+ if (await manager.uninstall(plugin)) onUninstalled();
900
+ },
901
+ children: [
902
+ /* @__PURE__ */ jsx5(Trash, { className: "size-3.5" }),
903
+ t("plugins.manager.uninstall")
904
+ ]
905
+ }
906
+ )
907
+ ] }) : plugin.unsupported ? /* @__PURE__ */ jsx5(
908
+ "span",
909
+ {
910
+ className: "text-xs text-muted-foreground",
911
+ title: t("plugins.manager.unsupportedHint"),
912
+ children: plugin.needsTermix ? t("plugins.manager.needsTermix", {
913
+ range: plugin.needsTermix
914
+ }) : t("plugins.manager.unsupported")
915
+ }
916
+ ) : /* @__PURE__ */ jsxs5(
917
+ Button,
918
+ {
919
+ variant: "outline",
920
+ size: "sm",
921
+ className: "gap-1.5 border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
922
+ disabled: busy || locked || !plugin.latestVersion,
923
+ onClick: () => manager.requestInstall(plugin),
924
+ children: [
925
+ /* @__PURE__ */ jsx5(Download, { className: "size-3.5" }),
926
+ t("plugins.manager.install")
927
+ ]
928
+ }
929
+ )
930
+ ] })
931
+ ] }),
932
+ plugin.installed && failed && plugin.lastError && /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-2 border border-destructive/30 bg-destructive/5 px-3 py-2.5", children: [
933
+ /* @__PURE__ */ jsx5(TriangleAlert, { className: "mt-px size-3.5 shrink-0 text-destructive" }),
934
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
935
+ /* @__PURE__ */ jsx5("span", { className: "text-xs font-medium", children: plugin.status === "blocked" ? t("plugins.manager.status.blocked") : t("plugins.manager.status.failed") }),
936
+ /* @__PURE__ */ jsx5("span", { className: "break-words font-mono text-[11px] leading-snug text-muted-foreground", children: plugin.lastError })
937
+ ] })
938
+ ] }),
939
+ plugin.status === "server" && /* @__PURE__ */ jsxs5("div", { className: "flex items-start gap-2 border border-border bg-muted/30 px-3 py-2.5", children: [
940
+ /* @__PURE__ */ jsx5(Server, { className: "mt-px size-3.5 shrink-0 text-muted-foreground" }),
941
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.runsOnServerHint") })
942
+ ] }),
943
+ /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-1 gap-2 lg:grid-cols-3", children: [
944
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2 lg:col-span-2", children: [
945
+ /* @__PURE__ */ jsx5(PluginVideo, { videoId: plugin.videoId, name: plugin.name }),
946
+ plugin.features.length > 0 && /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.features"), icon: null, children: /* @__PURE__ */ jsx5("ul", { className: "grid grid-cols-1 gap-x-4 gap-y-1.5 px-4 py-3 sm:grid-cols-2", children: plugin.features.map((feature) => /* @__PURE__ */ jsxs5(
947
+ "li",
948
+ {
949
+ className: "flex items-start gap-2 text-xs leading-snug",
950
+ children: [
951
+ /* @__PURE__ */ jsx5(Check, { className: "mt-px size-3.5 shrink-0 text-accent-brand" }),
952
+ /* @__PURE__ */ jsx5("span", { children: feature })
953
+ ]
954
+ },
955
+ feature
956
+ )) }) }),
957
+ /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.whatItCanDo"), icon: null, children: /* @__PURE__ */ jsxs5("div", { className: "divide-y divide-border", children: [
958
+ orderByRisk(plugin.capabilities).map((capability) => /* @__PURE__ */ jsx5(
959
+ CapabilityRow,
960
+ {
961
+ capability,
962
+ showId: true,
963
+ className: "px-4 py-2.5"
964
+ },
965
+ capability
966
+ )),
967
+ plugin.capabilities.length === 0 && /* @__PURE__ */ jsx5("div", { className: "px-4 py-2.5 text-[11px] text-muted-foreground", children: t("plugins.manager.consent.none") })
968
+ ] }) }),
969
+ plugin.env.length > 0 && /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.envVars"), icon: null, children: /* @__PURE__ */ jsx5("div", { className: "divide-y divide-border", children: plugin.env.map((v) => /* @__PURE__ */ jsxs5(
970
+ "div",
971
+ {
972
+ className: "flex flex-col gap-0.5 px-4 py-2.5",
973
+ children: [
974
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap items-center gap-2", children: [
975
+ /* @__PURE__ */ jsx5("code", { className: "font-mono text-xs font-semibold", children: v.name }),
976
+ v.required && /* @__PURE__ */ jsx5("span", { className: "text-[9px] font-bold uppercase tracking-widest text-accent-brand", children: t("plugins.manager.envRequired") }),
977
+ v.default !== void 0 && /* @__PURE__ */ jsx5("span", { className: "font-mono text-[10px] text-muted-foreground", children: t("plugins.manager.envDefault", {
978
+ value: v.default
979
+ }) })
980
+ ] }),
981
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: v.description })
982
+ ]
983
+ },
984
+ v.name
985
+ )) }) }),
986
+ /* @__PURE__ */ jsx5(PluginReleaseNotes, { plugin }),
987
+ plugin.installed && /* @__PURE__ */ jsx5(
988
+ DataCard,
989
+ {
990
+ plugin,
991
+ disabled: busy || locked,
992
+ onDelete: () => manager.removeData(plugin)
993
+ }
994
+ )
995
+ ] }),
996
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2", children: [
997
+ contributions.length > 0 && /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.addsToTermix"), icon: null, children: /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2 px-4 py-3", children: [
998
+ /* @__PURE__ */ jsx5("div", { className: "flex flex-wrap gap-1", children: contributions.map((c) => /* @__PURE__ */ jsx5(
999
+ "span",
1000
+ {
1001
+ className: "border border-border px-1.5 py-0.5 text-[10px] text-muted-foreground",
1002
+ children: t(`plugins.manager.contributes.${c.kind}`, {
1003
+ count: c.count
1004
+ })
1005
+ },
1006
+ c.kind
1007
+ )) }),
1008
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground/70", children: t("plugins.manager.addsToTermixHint") })
1009
+ ] }) }),
1010
+ plugin.inRegistry && compatible.length > 0 && /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.version"), icon: null, children: /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-2 px-4 py-3", children: [
1011
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: plugin.installed ? t("plugins.manager.versionPickHintInstalled") : t("plugins.manager.versionPickHint") }),
1012
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center gap-2", children: [
1013
+ /* @__PURE__ */ jsxs5(Select, { value: picked, onValueChange: setPicked, children: [
1014
+ /* @__PURE__ */ jsx5(SelectTrigger, { size: "sm", className: "flex-1 text-xs", children: /* @__PURE__ */ jsx5(SelectValue, {}) }),
1015
+ /* @__PURE__ */ jsx5(SelectContent, { children: compatible.map((v) => /* @__PURE__ */ jsx5(
1016
+ SelectItem,
1017
+ {
1018
+ value: v.version,
1019
+ className: "text-xs",
1020
+ children: v.version === plugin.latestVersion ? t("plugins.manager.latestVersion", {
1021
+ version: v.version
1022
+ }) : v.prerelease ? t("plugins.manager.beta.versionLabel", {
1023
+ version: v.version
1024
+ }) : v.version
1025
+ },
1026
+ v.version
1027
+ )) })
1028
+ ] }),
1029
+ /* @__PURE__ */ jsx5(
1030
+ Button,
1031
+ {
1032
+ size: "sm",
1033
+ variant: "outline",
1034
+ className: "h-8",
1035
+ disabled: busy || locked || !picked || picked === plugin.version,
1036
+ onClick: () => plugin.installed ? void manager.requestUpdate(plugin, picked) : manager.requestInstall(plugin, picked),
1037
+ children: plugin.installed ? t("plugins.manager.switchVersion") : t("plugins.manager.install")
1038
+ }
1039
+ )
1040
+ ] })
1041
+ ] }) }),
1042
+ plugin.installed && /* @__PURE__ */ jsx5(SectionCard, { title: t("plugins.manager.updates"), icon: null, children: /* @__PURE__ */ jsxs5("div", { className: "divide-y divide-border", children: [
1043
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
1044
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
1045
+ /* @__PURE__ */ jsx5("span", { className: "text-xs font-medium", children: t("plugins.manager.autoUpdate") }),
1046
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.autoUpdateHint") })
1047
+ ] }),
1048
+ /* @__PURE__ */ jsx5(
1049
+ FakeSwitch,
1050
+ {
1051
+ checked: plugin.autoUpdate,
1052
+ disabled: busy || locked || !!plugin.pinnedVersion,
1053
+ onChange: (v) => void manager.setOptions(plugin, { autoUpdate: v })
1054
+ }
1055
+ )
1056
+ ] }),
1057
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
1058
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
1059
+ /* @__PURE__ */ jsxs5("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1060
+ /* @__PURE__ */ jsx5(FlaskConical, { className: "size-3 text-muted-foreground" }),
1061
+ t("plugins.manager.beta.channel")
1062
+ ] }),
1063
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: plugin.channel === "stable" && plugin.isBeta ? t("plugins.manager.beta.waitingForStable", {
1064
+ version: plugin.version
1065
+ }) : t("plugins.manager.beta.channelHint") })
1066
+ ] }),
1067
+ /* @__PURE__ */ jsx5(
1068
+ FakeSwitch,
1069
+ {
1070
+ checked: plugin.channel === "beta",
1071
+ disabled: busy || locked,
1072
+ onChange: (v) => void (v ? manager.tryBeta(plugin) : manager.leaveBeta(plugin))
1073
+ }
1074
+ )
1075
+ ] }),
1076
+ /* @__PURE__ */ jsxs5("div", { className: "flex items-center justify-between gap-3 px-4 py-3", children: [
1077
+ /* @__PURE__ */ jsxs5("div", { className: "flex min-w-0 flex-col gap-0.5", children: [
1078
+ /* @__PURE__ */ jsxs5("span", { className: "flex items-center gap-1.5 text-xs font-medium", children: [
1079
+ /* @__PURE__ */ jsx5(Pin, { className: "size-3 text-muted-foreground" }),
1080
+ t("plugins.manager.pin")
1081
+ ] }),
1082
+ /* @__PURE__ */ jsx5("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.pinHint", {
1083
+ version: plugin.version
1084
+ }) })
1085
+ ] }),
1086
+ /* @__PURE__ */ jsx5(
1087
+ FakeSwitch,
1088
+ {
1089
+ checked: !!plugin.pinnedVersion,
1090
+ disabled: busy || locked,
1091
+ onChange: (v) => void manager.setOptions(plugin, { pinned: v })
1092
+ }
1093
+ )
1094
+ ] })
1095
+ ] }) }),
1096
+ plugin.docs && /* @__PURE__ */ jsxs5(
1097
+ "a",
1098
+ {
1099
+ href: plugin.docs,
1100
+ target: "_blank",
1101
+ rel: "noreferrer",
1102
+ className: LINK_ROW,
1103
+ children: [
1104
+ /* @__PURE__ */ jsx5(BookOpen, { className: "size-3.5 shrink-0" }),
1105
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: t("plugins.manager.documentation") })
1106
+ ]
1107
+ }
1108
+ ),
1109
+ hasSettings && /* @__PURE__ */ jsxs5(
1110
+ "button",
1111
+ {
1112
+ type: "button",
1113
+ onClick: () => openPluginSettings(plugin.id),
1114
+ className: LINK_ROW,
1115
+ children: [
1116
+ /* @__PURE__ */ jsx5(Settings, { className: "size-3.5 shrink-0" }),
1117
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: t("plugins.manager.openSettings") })
1118
+ ]
1119
+ }
1120
+ ),
1121
+ issueUrl && /* @__PURE__ */ jsxs5(
1122
+ "a",
1123
+ {
1124
+ href: issueUrl,
1125
+ target: "_blank",
1126
+ rel: "noreferrer",
1127
+ className: LINK_ROW,
1128
+ children: [
1129
+ /* @__PURE__ */ jsx5(Bug, { className: "size-3.5 shrink-0" }),
1130
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: plugin.source === "official" || plugin.source === "bundled" || !plugin.author ? t("plugins.manager.reportIssue") : t("plugins.manager.reportTo", { author: plugin.author }) })
1131
+ ]
1132
+ }
1133
+ ),
1134
+ feedbackUrl && /* @__PURE__ */ jsxs5(
1135
+ "a",
1136
+ {
1137
+ href: feedbackUrl,
1138
+ target: "_blank",
1139
+ rel: "noreferrer",
1140
+ className: LINK_ROW,
1141
+ children: [
1142
+ /* @__PURE__ */ jsx5(FlaskConical, { className: "size-3.5 shrink-0" }),
1143
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: t("plugins.manager.beta.sendFeedback") })
1144
+ ]
1145
+ }
1146
+ ),
1147
+ featureUrl && /* @__PURE__ */ jsxs5(
1148
+ "a",
1149
+ {
1150
+ href: featureUrl,
1151
+ target: "_blank",
1152
+ rel: "noreferrer",
1153
+ className: LINK_ROW,
1154
+ children: [
1155
+ /* @__PURE__ */ jsx5(Lightbulb, { className: "size-3.5 shrink-0" }),
1156
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: t("plugins.manager.requestFeature") })
1157
+ ]
1158
+ }
1159
+ ),
1160
+ plugin.repository && /* @__PURE__ */ jsxs5(
1161
+ "a",
1162
+ {
1163
+ href: plugin.repository,
1164
+ target: "_blank",
1165
+ rel: "noreferrer",
1166
+ className: LINK_ROW,
1167
+ children: [
1168
+ /* @__PURE__ */ jsx5(ExternalLink, { className: "size-3.5 shrink-0" }),
1169
+ /* @__PURE__ */ jsx5("span", { className: "truncate", children: plugin.repository.replace(/^https?:\/\//, "") })
1170
+ ]
1171
+ }
1172
+ )
1173
+ ] })
1174
+ ] })
1175
+ ]
1176
+ }
1177
+ );
1178
+ }
1179
+ function DataCard({
1180
+ plugin,
1181
+ disabled,
1182
+ onDelete
1183
+ }) {
1184
+ const { t } = useTranslation5();
1185
+ const [data, setData] = useState3(null);
1186
+ const [error, setError] = useState3(false);
1187
+ const [version, setVersion] = useState3(0);
1188
+ useEffect3(() => {
1189
+ let cancelled = false;
1190
+ setError(false);
1191
+ getPluginData(plugin.id).then((summary) => {
1192
+ if (!cancelled) setData(summary);
1193
+ }).catch(() => {
1194
+ if (!cancelled) setError(true);
1195
+ });
1196
+ return () => {
1197
+ cancelled = true;
1198
+ };
1199
+ }, [plugin.id, plugin.version, version]);
1200
+ const settings = data ? data.settings.admin + data.settings.user + data.settings.host : 0;
1201
+ const rows = data?.tables.reduce((sum, table) => sum + (table.rows ?? 0), 0);
1202
+ return /* @__PURE__ */ jsx5(
1203
+ SectionCard,
1204
+ {
1205
+ title: t("plugins.manager.data.title"),
1206
+ icon: /* @__PURE__ */ jsx5(Database, { className: "size-3.5" }),
1207
+ action: /* @__PURE__ */ jsxs5(
1208
+ Button,
1209
+ {
1210
+ variant: "ghost",
1211
+ size: "xs",
1212
+ className: "gap-1 text-muted-foreground hover:text-destructive",
1213
+ disabled,
1214
+ onClick: async () => {
1215
+ if (await onDelete()) setVersion((v) => v + 1);
1216
+ },
1217
+ children: [
1218
+ /* @__PURE__ */ jsx5(Trash, { className: "size-3" }),
1219
+ t("plugins.manager.data.delete")
1220
+ ]
1221
+ }
1222
+ ),
1223
+ children: error ? /* @__PURE__ */ jsx5("div", { className: "px-4 py-3 text-[11px] text-muted-foreground", children: t("plugins.manager.data.loadFailed") }) : !data ? /* @__PURE__ */ jsx5("div", { className: "px-4 py-3 text-[11px] text-muted-foreground", children: t("plugins.manager.loading") }) : /* @__PURE__ */ jsxs5("div", { className: "-mx-3 -my-1 flex flex-col md:-mx-4", children: [
1224
+ /* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-2 gap-px bg-border sm:grid-cols-4", children: [
1225
+ /* @__PURE__ */ jsx5(
1226
+ Stat,
1227
+ {
1228
+ label: t("plugins.manager.data.tables"),
1229
+ value: `${data.tables.length}`,
1230
+ hint: t("plugins.manager.data.rows", { count: rows ?? 0 })
1231
+ }
1232
+ ),
1233
+ /* @__PURE__ */ jsx5(
1234
+ Stat,
1235
+ {
1236
+ label: t("plugins.manager.data.kv"),
1237
+ value: `${data.kvKeys}`
1238
+ }
1239
+ ),
1240
+ /* @__PURE__ */ jsx5(
1241
+ Stat,
1242
+ {
1243
+ label: t("plugins.manager.data.settings"),
1244
+ value: `${settings}`,
1245
+ hint: t("plugins.manager.data.secrets", {
1246
+ count: data.settings.secret
1247
+ })
1248
+ }
1249
+ ),
1250
+ /* @__PURE__ */ jsx5(
1251
+ Stat,
1252
+ {
1253
+ label: t("plugins.manager.data.files"),
1254
+ value: formatBytes(data.filesBytes)
1255
+ }
1256
+ )
1257
+ ] }),
1258
+ data.tables.length > 0 && /* @__PURE__ */ jsx5("div", { className: "divide-y divide-border border-t border-border", children: data.tables.map((table) => /* @__PURE__ */ jsxs5(
1259
+ "div",
1260
+ {
1261
+ className: "flex items-center gap-2 px-4 py-1.5",
1262
+ children: [
1263
+ /* @__PURE__ */ jsx5("span", { className: "min-w-0 flex-1 truncate font-mono text-[11px]", children: table.name }),
1264
+ /* @__PURE__ */ jsx5("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: table.rows === null ? "-" : t("plugins.manager.data.rows", { count: table.rows }) })
1265
+ ]
1266
+ },
1267
+ table.name
1268
+ )) }),
1269
+ /* @__PURE__ */ jsxs5("div", { className: "flex flex-wrap gap-x-4 gap-y-1 border-t border-border px-4 py-2 text-[11px] text-muted-foreground", children: [
1270
+ /* @__PURE__ */ jsx5("span", { children: t("plugins.manager.data.migrations", {
1271
+ count: data.migrations.length
1272
+ }) }),
1273
+ /* @__PURE__ */ jsx5("span", { children: t("plugins.manager.data.grants", { count: data.grants.length }) })
1274
+ ] })
1275
+ ] })
1276
+ }
1277
+ );
1278
+ }
1279
+ function Stat({
1280
+ label,
1281
+ value,
1282
+ hint
1283
+ }) {
1284
+ return /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-0.5 bg-card px-4 py-2.5", children: [
1285
+ /* @__PURE__ */ jsx5("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-muted-foreground", children: label }),
1286
+ /* @__PURE__ */ jsx5("span", { className: "text-sm font-semibold", children: value }),
1287
+ hint && /* @__PURE__ */ jsx5("span", { className: "text-[10px] text-muted-foreground", children: hint })
1288
+ ] });
1289
+ }
1290
+
1291
+ // src/ui/plugins/use-plugins-manager.ts
1292
+ init_define_import_meta_env();
1293
+ var import_semver = __toESM(require_semver(), 1);
1294
+ import { useCallback as useCallback2, useEffect as useEffect4, useMemo as useMemo2, useRef, useState as useState4 } from "react";
1295
+ import { useTranslation as useTranslation6 } from "react-i18next";
1296
+ import { toast as toast2 } from "sonner";
1297
+ function readError(error, fallback) {
1298
+ const data = error?.response?.data;
1299
+ if (data && typeof data === "object") {
1300
+ return {
1301
+ message: typeof data.error === "string" ? data.error : fallback,
1302
+ code: typeof data.code === "string" ? data.code : void 0,
1303
+ data
1304
+ };
1305
+ }
1306
+ return {
1307
+ message: error instanceof Error ? error.message : fallback,
1308
+ data: {}
1309
+ };
1310
+ }
1311
+ function usePluginsManager() {
1312
+ const { t } = useTranslation6();
1313
+ const confirm = useConfirm();
1314
+ const [installed, setInstalled] = useState4([]);
1315
+ const [registry, setRegistry] = useState4(null);
1316
+ const [registryError, setRegistryError] = useState4(null);
1317
+ const [loading, setLoading] = useState4(true);
1318
+ const [refreshing, setRefreshing] = useState4(false);
1319
+ const [busy, setBusy] = useState4(/* @__PURE__ */ new Set());
1320
+ const [consent, setConsent] = useState4(null);
1321
+ const [developer, setDeveloper] = useState4({
1322
+ enabled: false,
1323
+ signedOnly: false
1324
+ });
1325
+ const mounted = useRef(true);
1326
+ useEffect4(() => {
1327
+ mounted.current = true;
1328
+ return () => {
1329
+ mounted.current = false;
1330
+ };
1331
+ }, []);
1332
+ const load = useCallback2(async (refresh = false) => {
1333
+ if (refresh) setRefreshing(true);
1334
+ const [plugins2, listing, devMode] = await Promise.allSettled([
1335
+ getPlugins(),
1336
+ getPluginRegistry(refresh),
1337
+ getDeveloperMode()
1338
+ ]);
1339
+ if (!mounted.current) return;
1340
+ if (plugins2.status === "fulfilled") setInstalled(plugins2.value);
1341
+ if (devMode.status === "fulfilled") setDeveloper(devMode.value);
1342
+ if (listing.status === "fulfilled") {
1343
+ setRegistry(listing.value);
1344
+ setRegistryError(listing.value.registry.error);
1345
+ } else {
1346
+ setRegistryError(readError(listing.reason, "").message || "unreachable");
1347
+ }
1348
+ setLoading(false);
1349
+ setRefreshing(false);
1350
+ }, []);
1351
+ useEffect4(() => {
1352
+ void load();
1353
+ }, [load]);
1354
+ useEffect4(() => {
1355
+ const onChange = (event) => {
1356
+ const enabled = event.detail;
1357
+ if (typeof enabled !== "boolean") return;
1358
+ setDeveloper((current) => ({ ...current, enabled }));
1359
+ };
1360
+ window.addEventListener(DEVELOPER_MODE_CHANGED_EVENT, onChange);
1361
+ return () => window.removeEventListener(DEVELOPER_MODE_CHANGED_EVENT, onChange);
1362
+ }, []);
1363
+ const plugins = useMemo2(
1364
+ () => mergePlugins(installed, registry?.plugins ?? null).map((plugin) => ({
1365
+ ...plugin,
1366
+ name: pluginName(t, plugin),
1367
+ description: pluginDescription(t, plugin)
1368
+ })),
1369
+ [installed, registry, t]
1370
+ );
1371
+ const run = useCallback2(
1372
+ async (id, action) => {
1373
+ setBusy((current) => new Set(current).add(id));
1374
+ try {
1375
+ await action();
1376
+ } finally {
1377
+ if (mounted.current) {
1378
+ setBusy((current) => {
1379
+ const next = new Set(current);
1380
+ next.delete(id);
1381
+ return next;
1382
+ });
1383
+ }
1384
+ await load();
1385
+ }
1386
+ },
1387
+ [load]
1388
+ );
1389
+ const failToast = useCallback2(
1390
+ (error, key, name) => {
1391
+ const info = readError(error, t("plugins.manager.errors.generic"));
1392
+ if (info.code === "MANAGED_BY_SERVER") {
1393
+ toast2.error(t("plugins.manager.errors.managedByServer"));
1394
+ } else if (info.code === "DEFAULT_LOOK") {
1395
+ toast2.error(t("plugins.manager.errors.defaultLook", { name }));
1396
+ } else if (info.code === "HAS_DEPENDENTS") {
1397
+ toast2.error(
1398
+ t("plugins.manager.errors.hasDependents", {
1399
+ name,
1400
+ list: (info.data.dependents ?? []).join(", ")
1401
+ })
1402
+ );
1403
+ } else if (info.code === "MISSING_DEPENDENCIES") {
1404
+ toast2.error(
1405
+ t("plugins.manager.errors.missingDependencies", {
1406
+ name,
1407
+ list: (info.data.missing ?? []).join(", ")
1408
+ })
1409
+ );
1410
+ } else {
1411
+ toast2.error(t(key, { name, error: info.message }));
1412
+ }
1413
+ },
1414
+ [t]
1415
+ );
1416
+ const versionCapabilities = (plugin, version) => plugin.versions.find((v) => v.version === version)?.capabilities ?? [];
1417
+ const requestInstall = useCallback2(
1418
+ (plugin, version) => {
1419
+ const target = version ?? plugin.latestVersion;
1420
+ if (!target) return;
1421
+ setConsent({
1422
+ plugin,
1423
+ mode: "install",
1424
+ version: target,
1425
+ capabilities: versionCapabilities(plugin, target)
1426
+ });
1427
+ },
1428
+ []
1429
+ );
1430
+ const doInstall = useCallback2(
1431
+ (plugin, version, capabilities) => run(plugin.id, async () => {
1432
+ try {
1433
+ const result = await installPlugin(plugin.id, {
1434
+ version: version === plugin.latestVersion ? void 0 : version,
1435
+ capabilities,
1436
+ registryId: plugin.registryId ?? void 0
1437
+ });
1438
+ if (result.state === "failed" || result.state === "blocked") {
1439
+ toast2.error(
1440
+ t("plugins.manager.toast.installedNotRunning", {
1441
+ name: plugin.name
1442
+ })
1443
+ );
1444
+ } else {
1445
+ toast2.success(
1446
+ t("plugins.manager.toast.installed", { name: plugin.name })
1447
+ );
1448
+ }
1449
+ } catch (error) {
1450
+ const info = readError(error, "");
1451
+ if (info.code === "CONSENT_MISMATCH") {
1452
+ toast2.error(t("plugins.manager.errors.consentChanged"));
1453
+ setConsent({
1454
+ plugin,
1455
+ mode: "install",
1456
+ version,
1457
+ capabilities: info.data.capabilities ?? []
1458
+ });
1459
+ return;
1460
+ }
1461
+ failToast(error, "plugins.manager.errors.install", plugin.name);
1462
+ }
1463
+ }),
1464
+ [run, t, failToast]
1465
+ );
1466
+ const doUpdate = useCallback2(
1467
+ (plugin, version, accepted) => run(plugin.id, async () => {
1468
+ try {
1469
+ await updatePlugin(plugin.id, {
1470
+ version: version === plugin.latestVersion ? void 0 : version,
1471
+ acceptCapabilities: accepted !== null,
1472
+ capabilities: accepted ?? void 0
1473
+ });
1474
+ toast2.success(
1475
+ t("plugins.manager.toast.updated", {
1476
+ name: plugin.name,
1477
+ version
1478
+ })
1479
+ );
1480
+ } catch (error) {
1481
+ const info = readError(error, "");
1482
+ if (info.code === "CAPABILITIES_ADDED" || info.code === "CONSENT_MISMATCH") {
1483
+ if (info.code === "CONSENT_MISMATCH") {
1484
+ toast2.error(t("plugins.manager.errors.consentChanged"));
1485
+ }
1486
+ setConsent({
1487
+ plugin,
1488
+ mode: "update",
1489
+ version,
1490
+ capabilities: info.data.capabilities ?? []
1491
+ });
1492
+ return;
1493
+ }
1494
+ failToast(error, "plugins.manager.errors.update", plugin.name);
1495
+ }
1496
+ }),
1497
+ [run, t, failToast]
1498
+ );
1499
+ const requestUpdate = useCallback2(
1500
+ async (plugin, version) => {
1501
+ const target = version ?? plugin.latestVersion;
1502
+ if (!target || !plugin.version) return;
1503
+ if (import_semver.default.valid(target) && import_semver.default.valid(plugin.version) && import_semver.default.lt(target, plugin.version)) {
1504
+ const ok = await confirm({
1505
+ title: t("plugins.manager.confirm.downgradeTitle", {
1506
+ name: plugin.name,
1507
+ version: target
1508
+ }),
1509
+ description: t("plugins.manager.confirm.downgradeBody"),
1510
+ confirmLabel: t("plugins.manager.confirm.downgradeConfirm"),
1511
+ destructive: false
1512
+ });
1513
+ if (!ok) return;
1514
+ }
1515
+ const have = new Set(plugin.capabilities);
1516
+ const added = versionCapabilities(plugin, target).filter(
1517
+ (c) => !have.has(c)
1518
+ );
1519
+ if (added.length > 0) {
1520
+ setConsent({
1521
+ plugin,
1522
+ mode: "update",
1523
+ version: target,
1524
+ capabilities: added
1525
+ });
1526
+ return;
1527
+ }
1528
+ await doUpdate(plugin, target, null);
1529
+ },
1530
+ [confirm, t, doUpdate]
1531
+ );
1532
+ const doInstallUpload = useCallback2(
1533
+ (request) => run(request.plugin.id, async () => {
1534
+ try {
1535
+ const result = await installUploadedPlugin(
1536
+ request.uploadToken,
1537
+ request.capabilities
1538
+ );
1539
+ if (result.state === "failed" || result.state === "blocked") {
1540
+ toast2.error(
1541
+ t("plugins.manager.toast.installedNotRunning", {
1542
+ name: request.plugin.name
1543
+ })
1544
+ );
1545
+ } else {
1546
+ toast2.success(
1547
+ t("plugins.manager.toast.installed", {
1548
+ name: request.plugin.name
1549
+ })
1550
+ );
1551
+ }
1552
+ } catch (error) {
1553
+ failToast(
1554
+ error,
1555
+ "plugins.manager.errors.install",
1556
+ request.plugin.name
1557
+ );
1558
+ }
1559
+ }),
1560
+ [run, t, failToast]
1561
+ );
1562
+ const requestUpload = useCallback2(
1563
+ async (file) => {
1564
+ try {
1565
+ const preview = await uploadPlugin(file);
1566
+ setConsent({
1567
+ plugin: uploadEntry(preview),
1568
+ mode: "upload",
1569
+ version: preview.version,
1570
+ capabilities: preview.capabilities,
1571
+ uploadToken: preview.token
1572
+ });
1573
+ } catch (error) {
1574
+ failToast(error, "plugins.manager.errors.upload", file.name);
1575
+ }
1576
+ },
1577
+ [failToast]
1578
+ );
1579
+ const doEnable = useCallback2(
1580
+ async (request) => run(request.plugin.id, async () => {
1581
+ try {
1582
+ const result = await setPluginState(request.plugin.id, true, {
1583
+ version: request.version,
1584
+ capabilities: request.capabilities
1585
+ });
1586
+ if (result.state === "failed" || result.state === "blocked") {
1587
+ toast2.error(
1588
+ t("plugins.manager.toast.failedToStart", {
1589
+ name: request.plugin.name
1590
+ })
1591
+ );
1592
+ }
1593
+ } catch (error) {
1594
+ failToast(error, "plugins.manager.errors.state", request.plugin.name);
1595
+ }
1596
+ }),
1597
+ [run, t, failToast]
1598
+ );
1599
+ const confirmConsent = useCallback2(async () => {
1600
+ if (!consent) return;
1601
+ const request = consent;
1602
+ setConsent(null);
1603
+ if (request.mode === "enable") {
1604
+ await doEnable(request);
1605
+ } else if (request.mode === "upload" && request.uploadToken) {
1606
+ await doInstallUpload(request);
1607
+ } else if (request.mode === "install") {
1608
+ await doInstall(request.plugin, request.version, request.capabilities);
1609
+ } else {
1610
+ await doUpdate(request.plugin, request.version, request.capabilities);
1611
+ }
1612
+ }, [consent, doInstall, doUpdate, doInstallUpload, doEnable]);
1613
+ const nameOf = useCallback2(
1614
+ (id) => plugins.find((p) => p.id === id)?.name ?? id,
1615
+ [plugins]
1616
+ );
1617
+ const toggle = useCallback2(
1618
+ async (plugin) => {
1619
+ const enable = !plugin.enabled;
1620
+ try {
1621
+ const preview = await previewPluginState(plugin.id, enable);
1622
+ if (enable && preview.missing.length > 0) {
1623
+ toast2.error(
1624
+ t("plugins.manager.errors.missingDependencies", {
1625
+ name: plugin.name,
1626
+ list: preview.missing.join(", ")
1627
+ })
1628
+ );
1629
+ return;
1630
+ }
1631
+ const others = enable ? preview.enable : preview.disable;
1632
+ if (others.length > 0) {
1633
+ const ok = await confirm({
1634
+ title: enable ? t("plugins.manager.confirm.enableAlsoTitle", {
1635
+ name: plugin.name
1636
+ }) : t("plugins.manager.confirm.disableAlsoTitle", {
1637
+ name: plugin.name
1638
+ }),
1639
+ description: enable ? t("plugins.manager.confirm.enableAlsoBody", {
1640
+ list: others.map(nameOf).join(", ")
1641
+ }) : t("plugins.manager.confirm.disableAlsoBody", {
1642
+ list: others.map(nameOf).join(", ")
1643
+ }),
1644
+ confirmLabel: enable ? t("plugins.manager.enable") : t("plugins.manager.disable"),
1645
+ destructive: !enable
1646
+ });
1647
+ if (!ok) return;
1648
+ }
1649
+ } catch (error) {
1650
+ failToast(error, "plugins.manager.errors.state", plugin.name);
1651
+ return;
1652
+ }
1653
+ if (enable && registry?.managedByServer) {
1654
+ setConsent({
1655
+ plugin,
1656
+ mode: "enable",
1657
+ version: plugin.version,
1658
+ capabilities: plugin.capabilities
1659
+ });
1660
+ return;
1661
+ }
1662
+ await run(plugin.id, async () => {
1663
+ try {
1664
+ const result = await setPluginState(plugin.id, enable);
1665
+ if (result.state === "failed" || result.state === "blocked") {
1666
+ toast2.error(
1667
+ t("plugins.manager.toast.failedToStart", { name: plugin.name })
1668
+ );
1669
+ }
1670
+ } catch (error) {
1671
+ failToast(error, "plugins.manager.errors.state", plugin.name);
1672
+ }
1673
+ });
1674
+ },
1675
+ [confirm, t, run, failToast, nameOf, registry?.managedByServer]
1676
+ );
1677
+ const retry = useCallback2(
1678
+ (plugin) => run(plugin.id, async () => {
1679
+ try {
1680
+ await retryPlugin(plugin.id);
1681
+ } catch (error) {
1682
+ failToast(error, "plugins.manager.errors.retry", plugin.name);
1683
+ }
1684
+ }),
1685
+ [run, failToast]
1686
+ );
1687
+ const uninstall = useCallback2(
1688
+ async (plugin) => {
1689
+ const ok = await confirm({
1690
+ title: t("plugins.manager.confirm.uninstallTitle", {
1691
+ name: plugin.name
1692
+ }),
1693
+ description: t("plugins.manager.confirm.uninstallBody"),
1694
+ confirmLabel: t("plugins.manager.uninstall")
1695
+ });
1696
+ if (!ok) return false;
1697
+ let done = false;
1698
+ await run(plugin.id, async () => {
1699
+ try {
1700
+ await uninstallPlugin(plugin.id);
1701
+ done = true;
1702
+ toast2.success(
1703
+ t("plugins.manager.toast.uninstalled", { name: plugin.name })
1704
+ );
1705
+ } catch (error) {
1706
+ failToast(error, "plugins.manager.errors.uninstall", plugin.name);
1707
+ }
1708
+ });
1709
+ return done;
1710
+ },
1711
+ [confirm, t, run, failToast]
1712
+ );
1713
+ const removeData = useCallback2(
1714
+ async (plugin) => {
1715
+ const ok = await confirm({
1716
+ title: t("plugins.manager.confirm.deleteDataTitle", {
1717
+ name: plugin.name
1718
+ }),
1719
+ description: t("plugins.manager.confirm.deleteDataBody"),
1720
+ confirmLabel: t("plugins.manager.data.delete")
1721
+ });
1722
+ if (!ok) return false;
1723
+ let done = false;
1724
+ await run(plugin.id, async () => {
1725
+ try {
1726
+ await deletePluginData(plugin.id);
1727
+ done = true;
1728
+ toast2.success(
1729
+ t("plugins.manager.toast.dataDeleted", { name: plugin.name })
1730
+ );
1731
+ } catch (error) {
1732
+ failToast(error, "plugins.manager.errors.deleteData", plugin.name);
1733
+ }
1734
+ });
1735
+ return done;
1736
+ },
1737
+ [confirm, t, run, failToast]
1738
+ );
1739
+ const setOptions = useCallback2(
1740
+ (plugin, options) => run(plugin.id, async () => {
1741
+ try {
1742
+ await setPluginOptions(plugin.id, options);
1743
+ } catch (error) {
1744
+ failToast(error, "plugins.manager.errors.options", plugin.name);
1745
+ }
1746
+ }),
1747
+ [run, failToast]
1748
+ );
1749
+ const tryBeta = useCallback2(
1750
+ async (plugin) => {
1751
+ const version = plugin.latestBeta;
1752
+ const ok = await confirm({
1753
+ title: t("plugins.manager.beta.confirmTitle", { name: plugin.name }),
1754
+ description: t("plugins.manager.beta.confirmBody"),
1755
+ confirmLabel: version ? t("plugins.manager.beta.confirmInstall", { version }) : t("plugins.manager.beta.confirmSwitch"),
1756
+ destructive: false
1757
+ });
1758
+ if (!ok) return;
1759
+ await setOptions(plugin, { channel: "beta", pinned: false });
1760
+ if (version && version !== plugin.version) {
1761
+ await requestUpdate({ ...plugin, pinnedVersion: null }, version);
1762
+ }
1763
+ },
1764
+ [confirm, t, setOptions, requestUpdate]
1765
+ );
1766
+ const leaveBeta = useCallback2(
1767
+ async (plugin) => {
1768
+ await setOptions(plugin, { channel: "stable" });
1769
+ if (plugin.isBeta) {
1770
+ toast2.info(
1771
+ t("plugins.manager.beta.leftWhileOnBeta", {
1772
+ name: plugin.name,
1773
+ version: plugin.version
1774
+ })
1775
+ );
1776
+ }
1777
+ },
1778
+ [setOptions, t]
1779
+ );
1780
+ const setAllChannels = useCallback2(
1781
+ (channel) => run("*", async () => {
1782
+ try {
1783
+ await setAllPluginChannels(channel);
1784
+ } catch (error) {
1785
+ failToast(error, "plugins.manager.errors.options", "");
1786
+ }
1787
+ }),
1788
+ [run, failToast]
1789
+ );
1790
+ const updateAll = useCallback2(
1791
+ () => run("*", async () => {
1792
+ try {
1793
+ const result = await updateAllPlugins();
1794
+ if (result.updated.length > 0) {
1795
+ toast2.success(
1796
+ t("plugins.manager.toast.updatedMany", {
1797
+ count: result.updated.length
1798
+ })
1799
+ );
1800
+ }
1801
+ for (const failed of result.failed) {
1802
+ toast2.error(
1803
+ t("plugins.manager.errors.update", {
1804
+ name: nameOf(failed.id),
1805
+ error: failed.error
1806
+ })
1807
+ );
1808
+ }
1809
+ } catch (error) {
1810
+ failToast(error, "plugins.manager.errors.updateAll", "");
1811
+ }
1812
+ }),
1813
+ [run, t, failToast, nameOf]
1814
+ );
1815
+ return {
1816
+ plugins,
1817
+ loading,
1818
+ refreshing,
1819
+ registryError,
1820
+ managedByServer: registry?.managedByServer ?? false,
1821
+ lastCheckedAt: registry?.registry.lastCheckedAt ?? null,
1822
+ registries: registry?.registries ?? [],
1823
+ reload: () => load(),
1824
+ busy,
1825
+ consent,
1826
+ setConsent,
1827
+ developerMode: developer.enabled,
1828
+ signedOnly: developer.signedOnly,
1829
+ requestUpload,
1830
+ refresh: () => load(true),
1831
+ requestInstall,
1832
+ requestUpdate,
1833
+ confirmConsent,
1834
+ toggle,
1835
+ retry,
1836
+ uninstall,
1837
+ removeData,
1838
+ setOptions,
1839
+ tryBeta,
1840
+ leaveBeta,
1841
+ setAllChannels,
1842
+ updateAll
1843
+ };
1844
+ }
1845
+
1846
+ // src/ui/plugins/PluginsTab.tsx
1847
+ import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
1848
+ var GRID_SIZES = ["auto", "2", "3", "4", "5", "6"];
1849
+ function useStoredChoice(key, allowed, fallback) {
1850
+ const [value, setValue] = useState5(() => {
1851
+ try {
1852
+ const saved = localStorage.getItem(key);
1853
+ return allowed.includes(saved) ? saved : fallback;
1854
+ } catch {
1855
+ return fallback;
1856
+ }
1857
+ });
1858
+ useEffect5(() => {
1859
+ try {
1860
+ localStorage.setItem(key, value);
1861
+ } catch {
1862
+ }
1863
+ }, [key, value]);
1864
+ return [value, setValue];
1865
+ }
1866
+ function PluginsTab() {
1867
+ return /* @__PURE__ */ jsx6(SurfaceScope, { kind: "tab", className: "h-full min-h-0", children: /* @__PURE__ */ jsx6(PluginsTabBody, {}) });
1868
+ }
1869
+ function PluginsTabBody() {
1870
+ const { t } = useTranslation7();
1871
+ const manager = usePluginsManager();
1872
+ const [section, setSection] = useState5("installed");
1873
+ const [query, setQuery] = useState5("");
1874
+ const [detailId, setDetailId] = useState5(null);
1875
+ const [view, setView] = useStoredChoice(
1876
+ "pluginsTab.view",
1877
+ ["grid", "list"],
1878
+ "grid"
1879
+ );
1880
+ const [columns, setColumns] = useStoredChoice(
1881
+ "pluginsTab.columns",
1882
+ GRID_SIZES,
1883
+ "auto"
1884
+ );
1885
+ const layout = { view, columns };
1886
+ const layoutControls = /* @__PURE__ */ jsx6(LayoutControls, { layout, onView: setView, onColumns: setColumns });
1887
+ const installed = manager.plugins.filter((p) => p.installed);
1888
+ const available = manager.plugins.filter((p) => p.inRegistry);
1889
+ const updates = installed.filter((p) => p.updateAvailable);
1890
+ const running = installed.filter((p) => p.status === "running").length;
1891
+ const detail = manager.plugins.find((p) => p.id === detailId) ?? null;
1892
+ const tabs = [
1893
+ {
1894
+ id: "installed",
1895
+ label: t("plugins.manager.tabs.installed"),
1896
+ icon: /* @__PURE__ */ jsx6(Puzzle, { className: "size-3" }),
1897
+ count: installed.length
1898
+ },
1899
+ {
1900
+ id: "browse",
1901
+ label: t("plugins.manager.tabs.browse"),
1902
+ icon: /* @__PURE__ */ jsx6(Globe, { className: "size-3" })
1903
+ },
1904
+ {
1905
+ id: "updates",
1906
+ label: t("plugins.manager.tabs.updates"),
1907
+ icon: /* @__PURE__ */ jsx6(Download, { className: "size-3" }),
1908
+ count: updates.filter((p) => !p.pinnedVersion).length || void 0
1909
+ },
1910
+ {
1911
+ id: "registries",
1912
+ label: t("plugins.manager.tabs.registries"),
1913
+ icon: /* @__PURE__ */ jsx6(Library, { className: "size-3" })
1914
+ }
1915
+ ];
1916
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
1917
+ /* @__PURE__ */ jsx6(
1918
+ PanelShell,
1919
+ {
1920
+ icon: /* @__PURE__ */ jsx6(Puzzle, { className: "size-4" }),
1921
+ title: t("nav.plugins"),
1922
+ docs: docsUrl("plugins"),
1923
+ status: t("plugins.manager.status.summary", {
1924
+ installed: installed.length,
1925
+ running
1926
+ }),
1927
+ scroll: false,
1928
+ actions: /* @__PURE__ */ jsx6(
1929
+ Button,
1930
+ {
1931
+ variant: "ghost",
1932
+ size: "icon-sm",
1933
+ title: t("plugins.manager.checkRegistry"),
1934
+ "aria-label": t("plugins.manager.checkRegistry"),
1935
+ disabled: manager.refreshing,
1936
+ onClick: () => void manager.refresh(),
1937
+ className: "text-muted-foreground",
1938
+ children: /* @__PURE__ */ jsx6(
1939
+ RefreshCw,
1940
+ {
1941
+ className: cn("size-3.5", manager.refreshing && "animate-spin")
1942
+ }
1943
+ )
1944
+ }
1945
+ ),
1946
+ tabs: /* @__PURE__ */ jsx6(
1947
+ TabStrip,
1948
+ {
1949
+ tabs,
1950
+ activeTab: section,
1951
+ onTabChange: (id) => setSection(id)
1952
+ }
1953
+ ),
1954
+ children: /* @__PURE__ */ jsxs6("div", { className: "flex min-h-0 flex-1 flex-col", children: [
1955
+ manager.managedByServer && /* @__PURE__ */ jsx6(Notice, { children: t("plugins.manager.managedByServer") }),
1956
+ manager.registryError && /* @__PURE__ */ jsx6(Notice, { children: t("plugins.manager.registryUnreachable", {
1957
+ error: manager.registryError
1958
+ }) }),
1959
+ manager.loading ? /* @__PURE__ */ jsx6("div", { className: PANEL.body, children: /* @__PURE__ */ jsx6(EmptyState, { icon: Puzzle, title: t("plugins.manager.loading") }) }) : section === "installed" ? /* @__PURE__ */ jsx6(
1960
+ InstalledSection,
1961
+ {
1962
+ plugins: installed,
1963
+ manager,
1964
+ query,
1965
+ onQuery: setQuery,
1966
+ onOpen: setDetailId,
1967
+ layout,
1968
+ layoutControls
1969
+ }
1970
+ ) : section === "browse" ? /* @__PURE__ */ jsx6(
1971
+ BrowseSection,
1972
+ {
1973
+ plugins: available,
1974
+ manager,
1975
+ query,
1976
+ onQuery: setQuery,
1977
+ onOpen: setDetailId,
1978
+ layout,
1979
+ layoutControls
1980
+ }
1981
+ ) : /* @__PURE__ */ jsx6("div", { className: "min-h-0 flex-1 overflow-y-auto thin-scrollbar", children: section === "updates" ? /* @__PURE__ */ jsx6(
1982
+ UpdatesSection,
1983
+ {
1984
+ plugins: updates,
1985
+ manager,
1986
+ onOpen: setDetailId
1987
+ }
1988
+ ) : /* @__PURE__ */ jsx6(RegistriesSection, { manager }) })
1989
+ ] })
1990
+ }
1991
+ ),
1992
+ /* @__PURE__ */ jsx6(
1993
+ InlineView,
1994
+ {
1995
+ open: !!detail,
1996
+ onOpenChange: (open) => !open && setDetailId(null),
1997
+ title: detail?.name ?? "",
1998
+ icon: /* @__PURE__ */ jsx6(Puzzle, { className: "size-4" }),
1999
+ width: "wide",
2000
+ bare: true,
2001
+ children: detail && /* @__PURE__ */ jsx6(
2002
+ PluginDetail,
2003
+ {
2004
+ plugin: detail,
2005
+ manager,
2006
+ onUninstalled: () => setDetailId(null)
2007
+ }
2008
+ )
2009
+ }
2010
+ ),
2011
+ /* @__PURE__ */ jsx6(
2012
+ PluginConsentPrompt,
2013
+ {
2014
+ request: manager.consent,
2015
+ onCancel: () => manager.setConsent(null),
2016
+ onConfirm: () => void manager.confirmConsent()
2017
+ }
2018
+ )
2019
+ ] });
2020
+ }
2021
+ function Notice({ children }) {
2022
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-start gap-2 border-b border-warning/30 bg-warning/5 px-3 py-2", children: [
2023
+ /* @__PURE__ */ jsx6(TriangleAlert, { className: "mt-px size-3.5 shrink-0 text-warning" }),
2024
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] leading-snug text-muted-foreground", children })
2025
+ ] });
2026
+ }
2027
+ function SortSelect({
2028
+ value,
2029
+ onChange
2030
+ }) {
2031
+ const { t } = useTranslation7();
2032
+ const keys = ["popular", "name", "updated", "category"];
2033
+ return /* @__PURE__ */ jsxs6(Select, { value, onValueChange: (v) => onChange(v), children: [
2034
+ /* @__PURE__ */ jsxs6(SelectTrigger, { size: "sm", className: "w-auto gap-1.5 text-xs", children: [
2035
+ /* @__PURE__ */ jsx6(ArrowDownWideNarrow, { className: "size-3.5 text-muted-foreground" }),
2036
+ /* @__PURE__ */ jsx6(SelectValue, {})
2037
+ ] }),
2038
+ /* @__PURE__ */ jsx6(SelectContent, { children: keys.map((key) => /* @__PURE__ */ jsx6(SelectItem, { value: key, className: "text-xs", children: t(`plugins.manager.sort.${key}`) }, key)) })
2039
+ ] });
2040
+ }
2041
+ function Toolbar({
2042
+ query,
2043
+ onQuery,
2044
+ placeholder,
2045
+ children
2046
+ }) {
2047
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-2", children: [
2048
+ /* @__PURE__ */ jsxs6("div", { className: "relative min-w-40 max-w-md flex-1", children: [
2049
+ /* @__PURE__ */ jsx6(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground" }),
2050
+ /* @__PURE__ */ jsx6(
2051
+ Input,
2052
+ {
2053
+ value: query,
2054
+ onChange: (e) => onQuery(e.target.value),
2055
+ placeholder,
2056
+ className: "h-8 pl-8 text-xs"
2057
+ }
2058
+ )
2059
+ ] }),
2060
+ children
2061
+ ] });
2062
+ }
2063
+ function Group({
2064
+ title,
2065
+ count,
2066
+ children
2067
+ }) {
2068
+ return /* @__PURE__ */ jsxs6("section", { className: "flex flex-col gap-2.5", children: [
2069
+ /* @__PURE__ */ jsx6(GroupHeading, { title, count }),
2070
+ children
2071
+ ] });
2072
+ }
2073
+ function LayoutControls({
2074
+ layout,
2075
+ onView,
2076
+ onColumns
2077
+ }) {
2078
+ const { t } = useTranslation7();
2079
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
2080
+ layout.view === "grid" && /* @__PURE__ */ jsxs6(
2081
+ Select,
2082
+ {
2083
+ value: layout.columns,
2084
+ onValueChange: (v) => onColumns(v),
2085
+ children: [
2086
+ /* @__PURE__ */ jsxs6(
2087
+ SelectTrigger,
2088
+ {
2089
+ size: "sm",
2090
+ className: "w-auto gap-1.5 text-xs",
2091
+ title: t("plugins.manager.columns.title"),
2092
+ "aria-label": t("plugins.manager.columns.title"),
2093
+ children: [
2094
+ /* @__PURE__ */ jsx6(Columns3, { className: "size-3.5 text-muted-foreground" }),
2095
+ /* @__PURE__ */ jsx6(SelectValue, {})
2096
+ ]
2097
+ }
2098
+ ),
2099
+ /* @__PURE__ */ jsx6(SelectContent, { children: GRID_SIZES.map((size) => /* @__PURE__ */ jsx6(SelectItem, { value: size, className: "text-xs", children: size === "auto" ? t("plugins.manager.columns.auto") : t("plugins.manager.columns.count", { count: Number(size) }) }, size)) })
2100
+ ]
2101
+ }
2102
+ ),
2103
+ /* @__PURE__ */ jsx6(ViewToggle, { view: layout.view, onView })
2104
+ ] });
2105
+ }
2106
+ function gridStyle(columns) {
2107
+ if (columns === "auto") {
2108
+ return {
2109
+ gridTemplateColumns: "repeat(auto-fill, minmax(min(100%, 400px), 1fr))"
2110
+ };
2111
+ }
2112
+ const n = Number(columns);
2113
+ const gaps = (n - 1) * 12;
2114
+ return {
2115
+ gridTemplateColumns: `repeat(auto-fill, minmax(max(220px, calc((100% - ${gaps}px) / ${n} - 0.5px)), 1fr))`
2116
+ };
2117
+ }
2118
+ function PluginCollection({
2119
+ plugins,
2120
+ layout,
2121
+ renderCard,
2122
+ listColumns,
2123
+ onOpen
2124
+ }) {
2125
+ if (layout.view === "list") {
2126
+ return /* @__PURE__ */ jsx6(
2127
+ DataView,
2128
+ {
2129
+ items: plugins,
2130
+ view: "list",
2131
+ getKey: (p) => p.id,
2132
+ renderCard,
2133
+ columns: {},
2134
+ listColumns,
2135
+ onRowClick: (p) => onOpen(p.id),
2136
+ empty: null
2137
+ }
2138
+ );
2139
+ }
2140
+ return /* @__PURE__ */ jsx6("div", { className: "grid gap-3", style: gridStyle(layout.columns), children: plugins.map((p) => /* @__PURE__ */ jsx6("div", { className: "flex flex-col *:flex-1", children: renderCard(p) }, p.id)) });
2141
+ }
2142
+ function NameCell({ plugin, muted }) {
2143
+ return /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 items-center gap-2", children: [
2144
+ /* @__PURE__ */ jsx6(PluginIconBox, { name: plugin.icon, size: "sm", muted }),
2145
+ /* @__PURE__ */ jsx6(
2146
+ "span",
2147
+ {
2148
+ className: cn(
2149
+ "truncate font-semibold",
2150
+ muted && "text-muted-foreground"
2151
+ ),
2152
+ children: plugin.name
2153
+ }
2154
+ ),
2155
+ plugin.unverified && /* @__PURE__ */ jsx6(UnverifiedBadge, {}),
2156
+ /* @__PURE__ */ jsx6(SourceBadge, { source: plugin.source, registryName: plugin.registryName }),
2157
+ plugin.isBeta && /* @__PURE__ */ jsx6(BetaBadge, {})
2158
+ ] });
2159
+ }
2160
+ function DescriptionCell({ plugin }) {
2161
+ return /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", title: plugin.description, children: plugin.description });
2162
+ }
2163
+ function stop(e) {
2164
+ e.stopPropagation();
2165
+ }
2166
+ var NO_FACETS = { kind: null, category: null, registry: null };
2167
+ function applyFacets(rows, facets, skip) {
2168
+ return rows.filter(
2169
+ (p) => (skip === "kind" || !facets.kind || p.kinds.includes(facets.kind)) && (skip === "category" || !facets.category || p.category === facets.category) && (skip === "registry" || !facets.registry || p.registryId === facets.registry)
2170
+ );
2171
+ }
2172
+ function activeFacets(facets) {
2173
+ return [facets.kind, facets.category, facets.registry].filter(Boolean).length;
2174
+ }
2175
+ var PAGE_SIZES = ["24", "48", "96"];
2176
+ function usePage(signature) {
2177
+ const [state, setState] = useState5({ signature, page: 1 });
2178
+ const page = state.signature === signature ? state.page : 1;
2179
+ return [page, (next) => setState({ signature, page: next })];
2180
+ }
2181
+ function FacetGroup({
2182
+ title,
2183
+ allLabel,
2184
+ options,
2185
+ value,
2186
+ onChange
2187
+ }) {
2188
+ if (options.length === 0) return null;
2189
+ const row = (key, label, count) => {
2190
+ const active = value === key;
2191
+ return /* @__PURE__ */ jsxs6(
2192
+ "button",
2193
+ {
2194
+ type: "button",
2195
+ "aria-pressed": active,
2196
+ onClick: () => onChange(key),
2197
+ className: cn(
2198
+ "flex w-full items-center gap-2 border-l-2 px-2.5 py-1 text-left text-xs transition-colors",
2199
+ active ? "border-accent-brand bg-accent-brand/10 text-accent-brand" : "border-transparent text-muted-foreground hover:bg-muted/50 hover:text-foreground",
2200
+ count === 0 && !active && "opacity-50"
2201
+ ),
2202
+ children: [
2203
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate", children: label }),
2204
+ count !== void 0 && /* @__PURE__ */ jsx6("span", { className: "shrink-0 text-[10px] tabular-nums opacity-70", children: count })
2205
+ ]
2206
+ },
2207
+ key ?? "__all"
2208
+ );
2209
+ };
2210
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-0.5", children: [
2211
+ /* @__PURE__ */ jsx6("span", { className: "px-2.5 pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70", children: title }),
2212
+ row(null, allLabel),
2213
+ options.map((o) => row(o.value, o.label, o.count))
2214
+ ] });
2215
+ }
2216
+ function FilterRail({
2217
+ rows,
2218
+ facets,
2219
+ onFacets,
2220
+ registries,
2221
+ children
2222
+ }) {
2223
+ const { t } = useTranslation7();
2224
+ const byKind = applyFacets(rows, facets, "kind");
2225
+ const kinds = kindsOf(rows).map(({ kind }) => ({
2226
+ value: kind,
2227
+ label: t(`plugins.manager.kinds.${kind}`),
2228
+ count: byKind.filter((p) => p.kinds.includes(kind)).length
2229
+ }));
2230
+ const byCategory = applyFacets(rows, facets, "category");
2231
+ const categories = categoriesOf(rows).map((c) => ({
2232
+ value: c,
2233
+ label: c,
2234
+ count: byCategory.filter((p) => p.category === c).length
2235
+ }));
2236
+ const byRegistry = applyFacets(rows, facets, "registry");
2237
+ const registryOptions = registries && registries.length > 1 ? registries.map((r) => ({
2238
+ value: r.id,
2239
+ label: r.name,
2240
+ count: byRegistry.filter((p) => p.registryId === r.id).length
2241
+ })) : [];
2242
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-col gap-4 py-3", children: [
2243
+ /* @__PURE__ */ jsx6(
2244
+ FacetGroup,
2245
+ {
2246
+ title: t("plugins.manager.filters.type"),
2247
+ allLabel: t("plugins.manager.filters.allTypes"),
2248
+ options: kinds,
2249
+ value: facets.kind,
2250
+ onChange: (kind) => onFacets({ ...facets, kind })
2251
+ }
2252
+ ),
2253
+ /* @__PURE__ */ jsx6(
2254
+ FacetGroup,
2255
+ {
2256
+ title: t("plugins.manager.filters.category"),
2257
+ allLabel: t("plugins.manager.filters.allCategories"),
2258
+ options: categories,
2259
+ value: facets.category,
2260
+ onChange: (category) => onFacets({ ...facets, category })
2261
+ }
2262
+ ),
2263
+ /* @__PURE__ */ jsx6(
2264
+ FacetGroup,
2265
+ {
2266
+ title: t("plugins.manager.registries.filter"),
2267
+ allLabel: t("plugins.manager.registries.allRegistries"),
2268
+ options: registryOptions,
2269
+ value: facets.registry,
2270
+ onChange: (registry) => onFacets({ ...facets, registry })
2271
+ }
2272
+ ),
2273
+ children && /* @__PURE__ */ jsx6("div", { className: "flex flex-col gap-2.5 border-t border-border px-2.5 pt-3", children })
2274
+ ] });
2275
+ }
2276
+ function RailToggle({
2277
+ label,
2278
+ hint,
2279
+ checked,
2280
+ onChange
2281
+ }) {
2282
+ return /* @__PURE__ */ jsxs6("label", { className: "flex items-center justify-between gap-2", title: hint, children: [
2283
+ /* @__PURE__ */ jsx6("span", { className: "text-xs text-muted-foreground", children: label }),
2284
+ /* @__PURE__ */ jsx6(FakeSwitch, { checked, onChange })
2285
+ ] });
2286
+ }
2287
+ function ActiveFilters({
2288
+ facets,
2289
+ onFacets,
2290
+ registries,
2291
+ shown
2292
+ }) {
2293
+ const { t } = useTranslation7();
2294
+ const chips = [];
2295
+ if (facets.kind) {
2296
+ chips.push({
2297
+ key: "kind",
2298
+ label: t(`plugins.manager.kinds.${facets.kind}`)
2299
+ });
2300
+ }
2301
+ if (facets.category) chips.push({ key: "category", label: facets.category });
2302
+ if (facets.registry) {
2303
+ chips.push({
2304
+ key: "registry",
2305
+ label: registries?.find((r) => r.id === facets.registry)?.name ?? facets.registry
2306
+ });
2307
+ }
2308
+ return /* @__PURE__ */ jsxs6("div", { className: "flex min-h-6 flex-wrap items-center gap-1.5", children: [
2309
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: t("plugins.manager.filters.results", { count: shown }) }),
2310
+ chips.map((chip) => /* @__PURE__ */ jsxs6(
2311
+ "button",
2312
+ {
2313
+ type: "button",
2314
+ onClick: () => onFacets({ ...facets, [chip.key]: null }),
2315
+ className: "flex items-center gap-1 border border-accent-brand/40 bg-accent-brand/10 px-1.5 py-0.5 text-[11px] text-accent-brand hover:bg-accent-brand/20",
2316
+ "aria-label": t("plugins.manager.filters.remove", {
2317
+ name: chip.label
2318
+ }),
2319
+ children: [
2320
+ chip.label,
2321
+ /* @__PURE__ */ jsx6(X, { className: "size-3" })
2322
+ ]
2323
+ },
2324
+ chip.key
2325
+ )),
2326
+ chips.length > 1 && /* @__PURE__ */ jsx6(
2327
+ "button",
2328
+ {
2329
+ type: "button",
2330
+ onClick: () => onFacets(NO_FACETS),
2331
+ className: "text-[11px] text-muted-foreground hover:text-foreground",
2332
+ children: t("plugins.manager.filters.clear")
2333
+ }
2334
+ )
2335
+ ] });
2336
+ }
2337
+ function Pager({
2338
+ page,
2339
+ pages,
2340
+ total,
2341
+ size,
2342
+ onPage,
2343
+ onSize
2344
+ }) {
2345
+ const { t } = useTranslation7();
2346
+ const per = Number(size);
2347
+ if (total <= Number(PAGE_SIZES[0])) return null;
2348
+ return /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-2 border-t border-border pt-2.5", children: [
2349
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] tabular-nums text-muted-foreground", children: t("plugins.manager.pager.range", {
2350
+ from: (page - 1) * per + 1,
2351
+ to: Math.min(page * per, total),
2352
+ total
2353
+ }) }),
2354
+ /* @__PURE__ */ jsxs6("div", { className: "ml-auto flex items-center gap-1.5", children: [
2355
+ /* @__PURE__ */ jsxs6(Select, { value: size, onValueChange: (v) => onSize(v), children: [
2356
+ /* @__PURE__ */ jsx6(
2357
+ SelectTrigger,
2358
+ {
2359
+ size: "sm",
2360
+ className: "w-auto text-xs",
2361
+ "aria-label": t("plugins.manager.pager.perPageLabel"),
2362
+ children: /* @__PURE__ */ jsx6(SelectValue, {})
2363
+ }
2364
+ ),
2365
+ /* @__PURE__ */ jsx6(SelectContent, { children: PAGE_SIZES.map((n) => /* @__PURE__ */ jsx6(SelectItem, { value: n, className: "text-xs", children: t("plugins.manager.pager.perPage", { count: Number(n) }) }, n)) })
2366
+ ] }),
2367
+ /* @__PURE__ */ jsx6(
2368
+ Button,
2369
+ {
2370
+ variant: "outline",
2371
+ size: "icon-sm",
2372
+ disabled: page <= 1,
2373
+ onClick: () => onPage(page - 1),
2374
+ title: t("plugins.manager.pager.previous"),
2375
+ "aria-label": t("plugins.manager.pager.previous"),
2376
+ children: /* @__PURE__ */ jsx6(ChevronLeft, { className: "size-3.5" })
2377
+ }
2378
+ ),
2379
+ /* @__PURE__ */ jsx6("span", { className: "px-1 text-[11px] tabular-nums text-muted-foreground", children: t("plugins.manager.pager.page", { page, pages }) }),
2380
+ /* @__PURE__ */ jsx6(
2381
+ Button,
2382
+ {
2383
+ variant: "outline",
2384
+ size: "icon-sm",
2385
+ disabled: page >= pages,
2386
+ onClick: () => onPage(page + 1),
2387
+ title: t("plugins.manager.pager.next"),
2388
+ "aria-label": t("plugins.manager.pager.next"),
2389
+ children: /* @__PURE__ */ jsx6(ChevronRight, { className: "size-3.5" })
2390
+ }
2391
+ )
2392
+ ] })
2393
+ ] });
2394
+ }
2395
+ function FilteredLayout({
2396
+ rail,
2397
+ activeCount,
2398
+ scrollRef,
2399
+ children
2400
+ }) {
2401
+ const { t } = useTranslation7();
2402
+ const [open, setOpen] = useState5(false);
2403
+ const button = /* @__PURE__ */ jsxs6(
2404
+ Button,
2405
+ {
2406
+ variant: "outline",
2407
+ className: cn(
2408
+ "gap-1.5 @3xl:hidden",
2409
+ activeCount > 0 && "border-accent-brand/40 text-accent-brand"
2410
+ ),
2411
+ "aria-expanded": open,
2412
+ onClick: () => setOpen((v) => !v),
2413
+ children: [
2414
+ /* @__PURE__ */ jsx6(ListFilter, { className: "size-3.5" }),
2415
+ activeCount > 0 ? t("plugins.manager.filters.buttonCount", { count: activeCount }) : t("plugins.manager.filters.button")
2416
+ ]
2417
+ }
2418
+ );
2419
+ return /* @__PURE__ */ jsxs6("div", { className: "@container flex min-h-0 flex-1", children: [
2420
+ /* @__PURE__ */ jsx6("aside", { className: "hidden w-52 shrink-0 overflow-y-auto border-r border-border thin-scrollbar @3xl:block", children: rail }),
2421
+ /* @__PURE__ */ jsxs6(
2422
+ "div",
2423
+ {
2424
+ ref: scrollRef,
2425
+ className: "min-h-0 flex-1 overflow-y-auto thin-scrollbar",
2426
+ children: [
2427
+ open && /* @__PURE__ */ jsx6("div", { className: "border-b border-border @3xl:hidden", children: rail }),
2428
+ children(button)
2429
+ ]
2430
+ }
2431
+ )
2432
+ ] });
2433
+ }
2434
+ function InstalledSection({
2435
+ plugins,
2436
+ manager,
2437
+ query,
2438
+ onQuery,
2439
+ onOpen,
2440
+ layout,
2441
+ layoutControls
2442
+ }) {
2443
+ const { t } = useTranslation7();
2444
+ const [sort, setSort] = useState5("name");
2445
+ const [facets, setFacets] = useState5(NO_FACETS);
2446
+ const searched = plugins.filter((p) => matchesQuery(p, query));
2447
+ const rows = sortPlugins(applyFacets(searched, facets), sort);
2448
+ const running = rows.filter((p) => p.status === "running");
2449
+ const onServer = rows.filter((p) => p.status === "server");
2450
+ const stopped = rows.filter(
2451
+ (p) => p.status !== "running" && p.status !== "server"
2452
+ );
2453
+ const pending = plugins.filter((p) => p.updateAvailable && !p.pinnedVersion);
2454
+ const listColumns = [
2455
+ {
2456
+ key: "name",
2457
+ header: t("plugins.manager.table.name"),
2458
+ width: "minmax(0,1.3fr)",
2459
+ cell: (p) => /* @__PURE__ */ jsx6(NameCell, { plugin: p, muted: p.status !== "running" })
2460
+ },
2461
+ {
2462
+ key: "description",
2463
+ header: t("plugins.manager.table.description"),
2464
+ width: "minmax(0,2fr)",
2465
+ hideBelow: "md",
2466
+ cell: (p) => /* @__PURE__ */ jsx6(DescriptionCell, { plugin: p })
2467
+ },
2468
+ {
2469
+ key: "version",
2470
+ header: t("plugins.manager.table.version"),
2471
+ width: "80px",
2472
+ cell: (p) => /* @__PURE__ */ jsxs6("span", { className: "tabular-nums text-muted-foreground", children: [
2473
+ "v",
2474
+ p.version
2475
+ ] })
2476
+ },
2477
+ {
2478
+ key: "status",
2479
+ header: t("plugins.manager.table.status"),
2480
+ width: "140px",
2481
+ hideBelow: "lg",
2482
+ cell: (p) => /* @__PURE__ */ jsx6(StatusCell, { plugin: p })
2483
+ },
2484
+ {
2485
+ key: "actions",
2486
+ header: "",
2487
+ width: "150px",
2488
+ align: "end",
2489
+ cell: (p) => /* @__PURE__ */ jsx6(InstalledRowActions, { plugin: p, manager })
2490
+ }
2491
+ ];
2492
+ const collection = (items) => /* @__PURE__ */ jsx6(
2493
+ PluginCollection,
2494
+ {
2495
+ plugins: items,
2496
+ layout,
2497
+ renderCard: (p) => /* @__PURE__ */ jsx6(
2498
+ InstalledCard,
2499
+ {
2500
+ plugin: p,
2501
+ manager,
2502
+ onOpen: () => onOpen(p.id)
2503
+ }
2504
+ ),
2505
+ listColumns,
2506
+ onOpen
2507
+ }
2508
+ );
2509
+ return /* @__PURE__ */ jsx6(
2510
+ FilteredLayout,
2511
+ {
2512
+ activeCount: activeFacets(facets),
2513
+ rail: /* @__PURE__ */ jsx6(FilterRail, { rows: searched, facets, onFacets: setFacets }),
2514
+ children: (filtersButton) => /* @__PURE__ */ jsxs6("div", { className: `flex flex-col ${PANEL.gap} ${PANEL.body}`, children: [
2515
+ /* @__PURE__ */ jsxs6(
2516
+ Toolbar,
2517
+ {
2518
+ query,
2519
+ onQuery,
2520
+ placeholder: t("plugins.manager.searchInstalled"),
2521
+ children: [
2522
+ filtersButton,
2523
+ /* @__PURE__ */ jsxs6("div", { className: "ml-auto flex items-center gap-2", children: [
2524
+ /* @__PURE__ */ jsx6(SortSelect, { value: sort, onChange: setSort }),
2525
+ layoutControls,
2526
+ manager.developerMode && !manager.signedOnly && /* @__PURE__ */ jsx6(UploadButton, { manager }),
2527
+ /* @__PURE__ */ jsxs6(
2528
+ Button,
2529
+ {
2530
+ variant: "outline",
2531
+ className: "gap-1.5",
2532
+ disabled: pending.length === 0 || manager.busy.size > 0 || manager.managedByServer,
2533
+ onClick: () => void manager.updateAll(),
2534
+ children: [
2535
+ /* @__PURE__ */ jsx6(Download, { className: "size-3.5" }),
2536
+ t("plugins.manager.updateAll")
2537
+ ]
2538
+ }
2539
+ )
2540
+ ] })
2541
+ ]
2542
+ }
2543
+ ),
2544
+ activeFacets(facets) > 0 && /* @__PURE__ */ jsx6(
2545
+ ActiveFilters,
2546
+ {
2547
+ facets,
2548
+ onFacets: setFacets,
2549
+ shown: rows.length
2550
+ }
2551
+ ),
2552
+ rows.length === 0 ? /* @__PURE__ */ jsx6(
2553
+ EmptyState,
2554
+ {
2555
+ icon: Puzzle,
2556
+ title: plugins.length === 0 ? t("plugins.manager.noneInstalled") : t("plugins.manager.nothingMatches")
2557
+ }
2558
+ ) : /* @__PURE__ */ jsxs6(Fragment4, { children: [
2559
+ running.length > 0 && /* @__PURE__ */ jsx6(
2560
+ Group,
2561
+ {
2562
+ title: t("plugins.manager.groups.running"),
2563
+ count: running.length,
2564
+ children: collection(running)
2565
+ }
2566
+ ),
2567
+ onServer.length > 0 && /* @__PURE__ */ jsx6(
2568
+ Group,
2569
+ {
2570
+ title: t("plugins.manager.groups.onServer"),
2571
+ count: onServer.length,
2572
+ children: collection(onServer)
2573
+ }
2574
+ ),
2575
+ stopped.length > 0 && /* @__PURE__ */ jsx6(
2576
+ Group,
2577
+ {
2578
+ title: t("plugins.manager.groups.notRunning"),
2579
+ count: stopped.length,
2580
+ children: collection(stopped)
2581
+ }
2582
+ )
2583
+ ] })
2584
+ ] })
2585
+ }
2586
+ );
2587
+ }
2588
+ function UploadButton({ manager }) {
2589
+ const { t } = useTranslation7();
2590
+ const input = useRef2(null);
2591
+ return /* @__PURE__ */ jsxs6(Fragment4, { children: [
2592
+ /* @__PURE__ */ jsx6(
2593
+ "input",
2594
+ {
2595
+ ref: input,
2596
+ type: "file",
2597
+ accept: ".tmxplug",
2598
+ className: "hidden",
2599
+ onChange: (e) => {
2600
+ const file = e.target.files?.[0];
2601
+ e.target.value = "";
2602
+ if (file) void manager.requestUpload(file);
2603
+ }
2604
+ }
2605
+ ),
2606
+ /* @__PURE__ */ jsxs6(
2607
+ Button,
2608
+ {
2609
+ variant: "outline",
2610
+ className: "gap-1.5",
2611
+ disabled: manager.busy.size > 0 || manager.managedByServer,
2612
+ onClick: () => input.current?.click(),
2613
+ children: [
2614
+ /* @__PURE__ */ jsx6(Upload, { className: "size-3.5" }),
2615
+ t("plugins.manager.developer.installFromFile")
2616
+ ]
2617
+ }
2618
+ )
2619
+ ] });
2620
+ }
2621
+ function CardHead({
2622
+ plugin,
2623
+ muted,
2624
+ facts,
2625
+ onOpen
2626
+ }) {
2627
+ return /* @__PURE__ */ jsxs6(
2628
+ "button",
2629
+ {
2630
+ type: "button",
2631
+ onClick: onOpen,
2632
+ className: "group/open flex flex-1 items-start gap-3 p-3 text-left",
2633
+ children: [
2634
+ /* @__PURE__ */ jsx6(PluginIconBox, { name: plugin.icon, muted }),
2635
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
2636
+ /* @__PURE__ */ jsx6(
2637
+ "span",
2638
+ {
2639
+ className: cn(
2640
+ "truncate text-sm font-semibold transition-colors group-hover/open:text-accent-brand",
2641
+ muted && "text-muted-foreground"
2642
+ ),
2643
+ children: plugin.name
2644
+ }
2645
+ ),
2646
+ /* @__PURE__ */ jsx6("span", { className: "line-clamp-2 text-[11px] leading-snug text-muted-foreground", children: plugin.description }),
2647
+ /* @__PURE__ */ jsx6(Facts, { className: "text-[10px] text-muted-foreground/70", children: facts })
2648
+ ] })
2649
+ ]
2650
+ }
2651
+ );
2652
+ }
2653
+ function InstalledCard({
2654
+ plugin,
2655
+ manager,
2656
+ onOpen
2657
+ }) {
2658
+ const { t } = useTranslation7();
2659
+ const running = plugin.status === "running";
2660
+ const failed = plugin.status === "failed" || plugin.status === "blocked";
2661
+ const busy = manager.busy.has(plugin.id) || manager.busy.has("*");
2662
+ const locked = manager.managedByServer;
2663
+ const adds = describeContributions(plugin.contributes).slice(0, 3).map((c) => t(`plugins.manager.contributes.${c.kind}`, { count: c.count }));
2664
+ return /* @__PURE__ */ jsxs6(Card, { className: "gap-0 overflow-hidden py-0", children: [
2665
+ /* @__PURE__ */ jsx6(
2666
+ CardHead,
2667
+ {
2668
+ plugin,
2669
+ muted: !running,
2670
+ onOpen,
2671
+ facts: /* @__PURE__ */ jsxs6(Fragment4, { children: [
2672
+ plugin.unverified && /* @__PURE__ */ jsx6(UnverifiedBadge, {}),
2673
+ /* @__PURE__ */ jsx6(
2674
+ SourceBadge,
2675
+ {
2676
+ source: plugin.source,
2677
+ registryName: plugin.registryName
2678
+ }
2679
+ ),
2680
+ plugin.isBeta && /* @__PURE__ */ jsx6(BetaBadge, {}),
2681
+ plugin.author && /* @__PURE__ */ jsx6("span", { children: plugin.author }),
2682
+ /* @__PURE__ */ jsxs6("span", { children: [
2683
+ "v",
2684
+ plugin.version
2685
+ ] }),
2686
+ adds.length > 0 && /* @__PURE__ */ jsx6("span", { children: adds.join(", ") })
2687
+ ] })
2688
+ }
2689
+ ),
2690
+ failed ? /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border bg-destructive/5 px-3 py-2", children: [
2691
+ /* @__PURE__ */ jsx6(TriangleAlert, { className: "size-3.5 shrink-0 text-destructive" }),
2692
+ /* @__PURE__ */ jsx6(
2693
+ "span",
2694
+ {
2695
+ className: "flex-1 truncate text-[11px] text-muted-foreground",
2696
+ title: plugin.lastError ?? void 0,
2697
+ children: plugin.status === "blocked" ? t("plugins.manager.status.blocked") : t("plugins.manager.status.failed")
2698
+ }
2699
+ )
2700
+ ] }) : !running ? /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2", children: [
2701
+ /* @__PURE__ */ jsx6("span", { className: "size-1.5 shrink-0 bg-muted-foreground/40" }),
2702
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: plugin.status === "server" ? t("plugins.manager.status.server") : t("plugins.manager.status.disabled") })
2703
+ ] }) : /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2", children: [
2704
+ /* @__PURE__ */ jsx6("span", { className: "size-1.5 shrink-0 bg-green-500" }),
2705
+ /* @__PURE__ */ jsx6("span", { className: "text-[11px] text-muted-foreground", children: plugin.pinnedVersion ? t("plugins.manager.pinnedTo", {
2706
+ version: plugin.pinnedVersion
2707
+ }) : t("plugins.manager.status.running") })
2708
+ ] }),
2709
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 border-t border-border px-3 py-2", children: [
2710
+ plugin.updateAvailable && !plugin.pinnedVersion && !failed && /* @__PURE__ */ jsxs6(
2711
+ Button,
2712
+ {
2713
+ variant: "outline",
2714
+ size: "xs",
2715
+ className: "gap-1 border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
2716
+ disabled: busy || locked,
2717
+ onClick: () => void manager.requestUpdate(plugin),
2718
+ children: [
2719
+ /* @__PURE__ */ jsx6(Download, { className: "size-3" }),
2720
+ plugin.addedCapabilities.length > 0 ? t("plugins.manager.reviewUpdate") : t("plugins.manager.updateTo", {
2721
+ version: plugin.latestVersion
2722
+ })
2723
+ ]
2724
+ }
2725
+ ),
2726
+ failed ? /* @__PURE__ */ jsxs6(
2727
+ Button,
2728
+ {
2729
+ variant: "outline",
2730
+ size: "xs",
2731
+ className: "gap-1",
2732
+ disabled: busy || locked,
2733
+ onClick: () => void manager.retry(plugin),
2734
+ children: [
2735
+ /* @__PURE__ */ jsx6(RotateCcw, { className: "size-3" }),
2736
+ t("plugins.manager.restart")
2737
+ ]
2738
+ }
2739
+ ) : /* @__PURE__ */ jsx6(
2740
+ Button,
2741
+ {
2742
+ variant: "outline",
2743
+ size: "xs",
2744
+ disabled: busy || plugin.managedByServer,
2745
+ onClick: () => void manager.toggle(plugin),
2746
+ children: plugin.enabled ? t("plugins.manager.disable") : t("plugins.manager.enable")
2747
+ }
2748
+ ),
2749
+ /* @__PURE__ */ jsx6(
2750
+ DocsLink,
2751
+ {
2752
+ href: plugin.docs,
2753
+ variant: "icon",
2754
+ className: "ml-auto size-6"
2755
+ }
2756
+ ),
2757
+ !plugin.defaultLook && /* @__PURE__ */ jsx6(
2758
+ Button,
2759
+ {
2760
+ variant: "ghost",
2761
+ size: "icon-xs",
2762
+ disabled: busy || locked,
2763
+ title: t("plugins.manager.uninstall"),
2764
+ "aria-label": t("plugins.manager.uninstall"),
2765
+ className: cn(
2766
+ "text-muted-foreground hover:text-destructive",
2767
+ !plugin.docs && "ml-auto"
2768
+ ),
2769
+ onClick: () => void manager.uninstall(plugin),
2770
+ children: /* @__PURE__ */ jsx6(Trash, { className: "size-3" })
2771
+ }
2772
+ )
2773
+ ] })
2774
+ ] });
2775
+ }
2776
+ function StatusCell({ plugin }) {
2777
+ const { t } = useTranslation7();
2778
+ const running = plugin.status === "running";
2779
+ const failed = plugin.status === "failed" || plugin.status === "blocked";
2780
+ const label = plugin.status === "blocked" ? t("plugins.manager.status.blocked") : failed ? t("plugins.manager.status.failed") : 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");
2781
+ return /* @__PURE__ */ jsxs6(
2782
+ "span",
2783
+ {
2784
+ className: "flex min-w-0 items-center gap-1.5 text-muted-foreground",
2785
+ title: failed ? plugin.lastError ?? void 0 : void 0,
2786
+ children: [
2787
+ failed ? /* @__PURE__ */ jsx6(TriangleAlert, { className: "size-3 shrink-0 text-destructive" }) : /* @__PURE__ */ jsx6(
2788
+ "span",
2789
+ {
2790
+ className: cn(
2791
+ "size-1.5 shrink-0",
2792
+ running ? "bg-green-500" : "bg-muted-foreground/40"
2793
+ )
2794
+ }
2795
+ ),
2796
+ /* @__PURE__ */ jsx6("span", { className: "truncate", children: label })
2797
+ ]
2798
+ }
2799
+ );
2800
+ }
2801
+ function InstalledRowActions({
2802
+ plugin,
2803
+ manager
2804
+ }) {
2805
+ const { t } = useTranslation7();
2806
+ const failed = plugin.status === "failed" || plugin.status === "blocked";
2807
+ const busy = manager.busy.has(plugin.id) || manager.busy.has("*");
2808
+ const locked = manager.managedByServer;
2809
+ const updateLabel = plugin.addedCapabilities.length > 0 ? t("plugins.manager.reviewUpdate") : t("plugins.manager.updateTo", { version: plugin.latestVersion });
2810
+ return /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-1", onClick: stop, children: [
2811
+ plugin.updateAvailable && !plugin.pinnedVersion && !failed && /* @__PURE__ */ jsx6(
2812
+ Button,
2813
+ {
2814
+ variant: "ghost",
2815
+ size: "icon-xs",
2816
+ title: updateLabel,
2817
+ "aria-label": updateLabel,
2818
+ className: "text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand",
2819
+ disabled: busy || locked,
2820
+ onClick: () => void manager.requestUpdate(plugin),
2821
+ children: /* @__PURE__ */ jsx6(Download, { className: "size-3" })
2822
+ }
2823
+ ),
2824
+ failed ? /* @__PURE__ */ jsxs6(
2825
+ Button,
2826
+ {
2827
+ variant: "outline",
2828
+ size: "xs",
2829
+ className: "gap-1",
2830
+ disabled: busy || locked,
2831
+ onClick: () => void manager.retry(plugin),
2832
+ children: [
2833
+ /* @__PURE__ */ jsx6(RotateCcw, { className: "size-3" }),
2834
+ t("plugins.manager.restart")
2835
+ ]
2836
+ }
2837
+ ) : /* @__PURE__ */ jsx6(
2838
+ Button,
2839
+ {
2840
+ variant: "outline",
2841
+ size: "xs",
2842
+ disabled: busy || plugin.managedByServer,
2843
+ onClick: () => void manager.toggle(plugin),
2844
+ children: plugin.enabled ? t("plugins.manager.disable") : t("plugins.manager.enable")
2845
+ }
2846
+ ),
2847
+ /* @__PURE__ */ jsx6(DocsLink, { href: plugin.docs, variant: "icon", className: "size-6" }),
2848
+ !plugin.defaultLook && /* @__PURE__ */ jsx6(
2849
+ Button,
2850
+ {
2851
+ variant: "ghost",
2852
+ size: "icon-xs",
2853
+ disabled: busy || locked,
2854
+ title: t("plugins.manager.uninstall"),
2855
+ "aria-label": t("plugins.manager.uninstall"),
2856
+ className: "text-muted-foreground hover:text-destructive",
2857
+ onClick: () => void manager.uninstall(plugin),
2858
+ children: /* @__PURE__ */ jsx6(Trash, { className: "size-3" })
2859
+ }
2860
+ )
2861
+ ] });
2862
+ }
2863
+ function BrowseSection({
2864
+ plugins,
2865
+ manager,
2866
+ query,
2867
+ onQuery,
2868
+ onOpen,
2869
+ layout,
2870
+ layoutControls
2871
+ }) {
2872
+ const { t } = useTranslation7();
2873
+ const [facets, setFacets] = useState5(NO_FACETS);
2874
+ const [sort, setSort] = useState5("popular");
2875
+ const [installedChoice, setInstalledChoice] = useStoredChoice(
2876
+ "pluginsTab.browseInstalled",
2877
+ ["hide", "show"],
2878
+ "hide"
2879
+ );
2880
+ const [unsupportedChoice, setUnsupportedChoice] = useStoredChoice(
2881
+ "pluginsTab.browseUnsupported",
2882
+ ["hide", "show"],
2883
+ "hide"
2884
+ );
2885
+ const [pageSize, setPageSize] = useStoredChoice(
2886
+ "pluginsTab.pageSize",
2887
+ PAGE_SIZES,
2888
+ "24"
2889
+ );
2890
+ const scroller = useRef2(null);
2891
+ const showInstalled = installedChoice === "show";
2892
+ const showUnsupported = unsupportedChoice === "show";
2893
+ const registries = manager.registries;
2894
+ const unsupportedCount = plugins.filter(
2895
+ (p) => p.unsupported && !p.installed
2896
+ ).length;
2897
+ const searched = plugins.filter(
2898
+ (p) => (showInstalled || !p.installed) && (showUnsupported || !p.unsupported || p.installed) && matchesQuery(p, query)
2899
+ );
2900
+ const rows = sortPlugins(applyFacets(searched, facets), sort);
2901
+ const [page, setPage] = usePage(
2902
+ JSON.stringify([query, facets, sort, showInstalled, showUnsupported])
2903
+ );
2904
+ const paged = pageOf(rows, page, Number(pageSize));
2905
+ const goTo = (next) => {
2906
+ setPage(next);
2907
+ if (scroller.current) scroller.current.scrollTop = 0;
2908
+ };
2909
+ const allInstalledHidden = !showInstalled && plugins.length > 0 && plugins.every((p) => p.installed);
2910
+ const listColumns = [
2911
+ {
2912
+ key: "name",
2913
+ header: t("plugins.manager.table.name"),
2914
+ width: "minmax(0,1.3fr)",
2915
+ cell: (p) => /* @__PURE__ */ jsx6(NameCell, { plugin: p, muted: p.unsupported })
2916
+ },
2917
+ {
2918
+ key: "description",
2919
+ header: t("plugins.manager.table.description"),
2920
+ width: "minmax(0,2fr)",
2921
+ hideBelow: "md",
2922
+ cell: (p) => /* @__PURE__ */ jsx6(DescriptionCell, { plugin: p })
2923
+ },
2924
+ {
2925
+ key: "category",
2926
+ header: t("plugins.manager.table.category"),
2927
+ width: "120px",
2928
+ hideBelow: "lg",
2929
+ cell: (p) => /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", children: p.category ?? "" })
2930
+ },
2931
+ {
2932
+ key: "downloads",
2933
+ header: t("plugins.manager.table.downloads"),
2934
+ width: "110px",
2935
+ hideBelow: "lg",
2936
+ cell: (p) => /* @__PURE__ */ jsx6("span", { className: "text-muted-foreground", children: /* @__PURE__ */ jsx6(DownloadCount, { count: p.downloads }) })
2937
+ },
2938
+ {
2939
+ key: "version",
2940
+ header: t("plugins.manager.table.version"),
2941
+ width: "80px",
2942
+ cell: (p) => /* @__PURE__ */ jsx6("span", { className: "tabular-nums text-muted-foreground", children: p.latestVersion ? `v${p.latestVersion}` : "" })
2943
+ },
2944
+ {
2945
+ key: "actions",
2946
+ header: "",
2947
+ width: "150px",
2948
+ align: "end",
2949
+ cell: (p) => /* @__PURE__ */ jsx6(BrowseAction, { plugin: p, manager })
2950
+ }
2951
+ ];
2952
+ return /* @__PURE__ */ jsx6(
2953
+ FilteredLayout,
2954
+ {
2955
+ activeCount: activeFacets(facets),
2956
+ scrollRef: scroller,
2957
+ rail: /* @__PURE__ */ jsxs6(
2958
+ FilterRail,
2959
+ {
2960
+ rows: searched,
2961
+ facets,
2962
+ onFacets: setFacets,
2963
+ registries,
2964
+ children: [
2965
+ /* @__PURE__ */ jsx6(
2966
+ RailToggle,
2967
+ {
2968
+ label: t("plugins.manager.showInstalled"),
2969
+ checked: showInstalled,
2970
+ onChange: (v) => setInstalledChoice(v ? "show" : "hide")
2971
+ }
2972
+ ),
2973
+ unsupportedCount > 0 && /* @__PURE__ */ jsx6(
2974
+ RailToggle,
2975
+ {
2976
+ label: t("plugins.manager.showUnsupported", {
2977
+ count: unsupportedCount
2978
+ }),
2979
+ hint: t("plugins.manager.showUnsupportedHint"),
2980
+ checked: showUnsupported,
2981
+ onChange: (v) => setUnsupportedChoice(v ? "show" : "hide")
2982
+ }
2983
+ )
2984
+ ]
2985
+ }
2986
+ ),
2987
+ children: (filtersButton) => /* @__PURE__ */ jsxs6("div", { className: `flex flex-col ${PANEL.gap} ${PANEL.body}`, children: [
2988
+ /* @__PURE__ */ jsxs6(
2989
+ Toolbar,
2990
+ {
2991
+ query,
2992
+ onQuery,
2993
+ placeholder: t("plugins.manager.searchPlugins"),
2994
+ children: [
2995
+ filtersButton,
2996
+ /* @__PURE__ */ jsxs6("div", { className: "ml-auto flex items-center gap-2", children: [
2997
+ /* @__PURE__ */ jsx6(SortSelect, { value: sort, onChange: setSort }),
2998
+ layoutControls
2999
+ ] })
3000
+ ]
3001
+ }
3002
+ ),
3003
+ /* @__PURE__ */ jsx6(
3004
+ ActiveFilters,
3005
+ {
3006
+ facets,
3007
+ onFacets: setFacets,
3008
+ registries,
3009
+ shown: rows.length
3010
+ }
3011
+ ),
3012
+ rows.length === 0 ? allInstalledHidden ? /* @__PURE__ */ jsx6(
3013
+ EmptyState,
3014
+ {
3015
+ icon: Puzzle,
3016
+ title: t("plugins.manager.allInstalled"),
3017
+ hint: t("plugins.manager.allInstalledHint")
3018
+ }
3019
+ ) : /* @__PURE__ */ jsx6(
3020
+ EmptyState,
3021
+ {
3022
+ icon: Puzzle,
3023
+ title: t("plugins.manager.nothingHere"),
3024
+ hint: plugins.length === 0 ? void 0 : t("plugins.manager.nothingHereHint")
3025
+ }
3026
+ ) : /* @__PURE__ */ jsx6(
3027
+ PluginCollection,
3028
+ {
3029
+ plugins: paged.items,
3030
+ layout,
3031
+ renderCard: (p) => /* @__PURE__ */ jsx6(
3032
+ BrowseCard,
3033
+ {
3034
+ plugin: p,
3035
+ manager,
3036
+ onOpen: () => onOpen(p.id)
3037
+ }
3038
+ ),
3039
+ listColumns,
3040
+ onOpen
3041
+ }
3042
+ ),
3043
+ /* @__PURE__ */ jsx6(
3044
+ Pager,
3045
+ {
3046
+ page: paged.page,
3047
+ pages: paged.pages,
3048
+ total: rows.length,
3049
+ size: pageSize,
3050
+ onPage: goTo,
3051
+ onSize: (next) => {
3052
+ setPageSize(next);
3053
+ goTo(1);
3054
+ }
3055
+ }
3056
+ ),
3057
+ /* @__PURE__ */ jsxs6("p", { className: "text-[11px] text-muted-foreground", children: [
3058
+ t("plugins.manager.registries.browseHint"),
3059
+ " ",
3060
+ /* @__PURE__ */ jsx6(DocsLink, { core: "communityRegistry", className: "text-[11px]", children: t("plugins.manager.communitySubmit") })
3061
+ ] })
3062
+ ] })
3063
+ }
3064
+ );
3065
+ }
3066
+ function BrowseCard({
3067
+ plugin,
3068
+ manager,
3069
+ onOpen
3070
+ }) {
3071
+ const { t } = useTranslation7();
3072
+ const severe = orderByRisk(plugin.capabilities).filter(isSevere);
3073
+ return /* @__PURE__ */ jsxs6(Card, { className: "gap-0 overflow-hidden py-0", children: [
3074
+ /* @__PURE__ */ jsx6(
3075
+ CardHead,
3076
+ {
3077
+ plugin,
3078
+ muted: plugin.unsupported,
3079
+ onOpen,
3080
+ facts: /* @__PURE__ */ jsxs6(Fragment4, { children: [
3081
+ /* @__PURE__ */ jsx6(
3082
+ SourceBadge,
3083
+ {
3084
+ source: plugin.source,
3085
+ registryName: plugin.registryName
3086
+ }
3087
+ ),
3088
+ plugin.author && /* @__PURE__ */ jsx6("span", { children: plugin.author }),
3089
+ plugin.latestVersion && /* @__PURE__ */ jsxs6("span", { children: [
3090
+ "v",
3091
+ plugin.latestVersion
3092
+ ] }),
3093
+ plugin.category && /* @__PURE__ */ jsx6("span", { children: plugin.category }),
3094
+ /* @__PURE__ */ jsx6(DownloadCount, { count: plugin.downloads })
3095
+ ] })
3096
+ }
3097
+ ),
3098
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-2 border-t border-border px-3 py-2", children: [
3099
+ /* @__PURE__ */ jsx6("span", { className: "min-w-0 flex-1 truncate text-[10px] text-muted-foreground", children: plugin.unsupported && !plugin.installed ? "" : severe.length > 0 ? t(`plugins.capabilities.${severe[0]}.title`, {
3100
+ defaultValue: severe[0],
3101
+ nsSeparator: false
3102
+ }) : t("plugins.manager.noServerAccess") }),
3103
+ /* @__PURE__ */ jsx6(BrowseAction, { plugin, manager })
3104
+ ] })
3105
+ ] });
3106
+ }
3107
+ function BrowseAction({
3108
+ plugin,
3109
+ manager
3110
+ }) {
3111
+ const { t } = useTranslation7();
3112
+ const busy = manager.busy.has(plugin.id) || manager.busy.has("*");
3113
+ return /* @__PURE__ */ jsxs6(
3114
+ "div",
3115
+ {
3116
+ className: "flex shrink-0 items-center justify-end gap-2",
3117
+ onClick: stop,
3118
+ children: [
3119
+ /* @__PURE__ */ jsx6(DocsLink, { href: plugin.docs, variant: "icon", className: "size-6" }),
3120
+ plugin.installed ? /* @__PURE__ */ jsxs6("span", { className: "flex shrink-0 items-center gap-1 text-[11px] font-medium text-accent-brand", children: [
3121
+ /* @__PURE__ */ jsx6(Check, { className: "size-3" }),
3122
+ t("plugins.manager.installedBadge")
3123
+ ] }) : plugin.unsupported ? /* @__PURE__ */ jsx6(
3124
+ "span",
3125
+ {
3126
+ className: "shrink-0 text-[11px] text-muted-foreground",
3127
+ title: t("plugins.manager.unsupportedHint"),
3128
+ children: plugin.needsTermix ? t("plugins.manager.needsTermix", { range: plugin.needsTermix }) : t("plugins.manager.unsupported")
3129
+ }
3130
+ ) : /* @__PURE__ */ jsx6(
3131
+ Button,
3132
+ {
3133
+ variant: "outline",
3134
+ size: "xs",
3135
+ disabled: busy || manager.managedByServer || !plugin.latestVersion,
3136
+ onClick: () => manager.requestInstall(plugin),
3137
+ className: "border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
3138
+ children: t("plugins.manager.install")
3139
+ }
3140
+ )
3141
+ ]
3142
+ }
3143
+ );
3144
+ }
3145
+ function UpdatesSection({
3146
+ plugins,
3147
+ manager,
3148
+ onOpen
3149
+ }) {
3150
+ const { t } = useTranslation7();
3151
+ const pinned = plugins.filter((p) => p.pinnedVersion);
3152
+ const open = plugins.filter((p) => !p.pinnedVersion);
3153
+ const gated = open.filter((p) => p.addedCapabilities.length > 0);
3154
+ const plain = open.filter((p) => p.addedCapabilities.length === 0);
3155
+ if (plugins.length === 0) {
3156
+ return /* @__PURE__ */ jsx6("div", { className: PANEL.body, children: /* @__PURE__ */ jsx6(
3157
+ EmptyState,
3158
+ {
3159
+ icon: Check,
3160
+ title: t("plugins.manager.upToDate"),
3161
+ hint: manager.lastCheckedAt ? t("plugins.manager.lastChecked", {
3162
+ time: new Date(manager.lastCheckedAt).toLocaleString()
3163
+ }) : void 0
3164
+ }
3165
+ ) });
3166
+ }
3167
+ return /* @__PURE__ */ jsxs6("div", { className: `flex flex-col ${PANEL.gap} ${PANEL.body}`, children: [
3168
+ plain.length > 0 && /* @__PURE__ */ jsxs6(Card, { className: "flex-row items-center gap-3 px-3 py-2.5", children: [
3169
+ /* @__PURE__ */ jsxs6("span", { className: "flex-1 text-xs text-muted-foreground", children: [
3170
+ t("plugins.manager.updatesReady", { count: plain.length }),
3171
+ gated.length > 0 && ` ${t("plugins.manager.updatesNeedReview", { count: gated.length })}`
3172
+ ] }),
3173
+ /* @__PURE__ */ jsxs6(
3174
+ Button,
3175
+ {
3176
+ variant: "outline",
3177
+ size: "sm",
3178
+ className: "gap-1.5 border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
3179
+ disabled: manager.busy.size > 0 || manager.managedByServer,
3180
+ onClick: () => void manager.updateAll(),
3181
+ children: [
3182
+ /* @__PURE__ */ jsx6(Download, { className: "size-3.5" }),
3183
+ t("plugins.manager.updateAll")
3184
+ ]
3185
+ }
3186
+ )
3187
+ ] }),
3188
+ /* @__PURE__ */ jsx6("div", { className: "flex flex-col gap-3", children: [...gated, ...plain, ...pinned].map((p) => /* @__PURE__ */ jsx6(
3189
+ UpdateRow,
3190
+ {
3191
+ plugin: p,
3192
+ manager,
3193
+ onOpen: () => onOpen(p.id)
3194
+ },
3195
+ p.id
3196
+ )) })
3197
+ ] });
3198
+ }
3199
+ function UpdateRow({
3200
+ plugin,
3201
+ manager,
3202
+ onOpen
3203
+ }) {
3204
+ const { t } = useTranslation7();
3205
+ const gated = plugin.addedCapabilities.length > 0;
3206
+ const busy = manager.busy.has(plugin.id) || manager.busy.has("*");
3207
+ const notes = plugin.versions.find((v) => v.version === plugin.latestVersion);
3208
+ return /* @__PURE__ */ jsx6(Card, { className: "gap-0 overflow-hidden py-0", children: /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-start gap-3 p-3", children: [
3209
+ /* @__PURE__ */ jsx6(PluginIconBox, { name: plugin.icon }),
3210
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [
3211
+ /* @__PURE__ */ jsxs6("div", { className: "flex flex-wrap items-center gap-1.5", children: [
3212
+ /* @__PURE__ */ jsx6(
3213
+ "button",
3214
+ {
3215
+ type: "button",
3216
+ onClick: onOpen,
3217
+ className: "truncate text-sm font-semibold hover:text-accent-brand",
3218
+ children: plugin.name
3219
+ }
3220
+ ),
3221
+ /* @__PURE__ */ jsx6("span", { className: "shrink-0 text-[11px] text-muted-foreground", children: t("plugins.manager.versionChange", {
3222
+ from: plugin.version,
3223
+ to: plugin.latestVersion
3224
+ }) })
3225
+ ] }),
3226
+ plugin.pinnedVersion ? /* @__PURE__ */ jsx6("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.pinnedTo", { version: plugin.pinnedVersion }) }) : gated ? /* @__PURE__ */ jsx6("span", { className: "text-[11px] leading-snug text-muted-foreground", children: t("plugins.manager.updateAsksMore") }) : null,
3227
+ notes?.releaseNotesUrl && /* @__PURE__ */ jsx6(
3228
+ "a",
3229
+ {
3230
+ href: notes.releaseNotesUrl,
3231
+ target: "_blank",
3232
+ rel: "noreferrer",
3233
+ className: "w-fit text-[11px] text-muted-foreground hover:text-accent-brand",
3234
+ children: t("plugins.manager.releaseNotes")
3235
+ }
3236
+ )
3237
+ ] }),
3238
+ /* @__PURE__ */ jsxs6("div", { className: "flex shrink-0 items-center gap-3", children: [
3239
+ /* @__PURE__ */ jsxs6("label", { className: "flex items-center gap-1.5", children: [
3240
+ /* @__PURE__ */ jsx6("span", { className: "text-[10px] text-muted-foreground", children: t("plugins.manager.auto") }),
3241
+ /* @__PURE__ */ jsx6(
3242
+ FakeSwitch,
3243
+ {
3244
+ checked: plugin.autoUpdate,
3245
+ disabled: busy || manager.managedByServer || !!plugin.pinnedVersion,
3246
+ onChange: (v) => void manager.setOptions(plugin, { autoUpdate: v })
3247
+ }
3248
+ )
3249
+ ] }),
3250
+ !plugin.pinnedVersion && /* @__PURE__ */ jsx6(
3251
+ Button,
3252
+ {
3253
+ variant: "outline",
3254
+ size: "xs",
3255
+ disabled: busy || manager.managedByServer,
3256
+ onClick: () => void manager.requestUpdate(plugin),
3257
+ className: "border-accent-brand/40 text-accent-brand hover:bg-accent-brand/10 hover:text-accent-brand dark:border-accent-brand/40 dark:bg-transparent dark:hover:bg-accent-brand/10",
3258
+ children: gated ? t("plugins.manager.review") : t("plugins.manager.update")
3259
+ }
3260
+ )
3261
+ ] })
3262
+ ] }) });
3263
+ }
3264
+ export {
3265
+ PluginsTab
3266
+ };