@termix-ssh/plugin-sdk 1.0.11 → 1.0.12

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