@termix-ssh/plugin-sdk 1.0.20 → 1.0.22

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 (58) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/PLUGIN-GUIDE.md +66 -4
  3. package/cli/commands/build.mjs +55 -36
  4. package/cli/commands/validate.mjs +4 -0
  5. package/dist/backend.d.ts +32 -7
  6. package/dist/frontend.d.ts +7 -0
  7. package/dist/host/{PluginsTab-ZSMTJ5C5.js → PluginsTab-CM2B33BD.js} +26 -16
  8. package/dist/host/chunk-7J4QNGFM.js +345 -0
  9. package/dist/host/{chunk-J3ZQPBDK.js → chunk-HQ6NNFSX.js} +115 -1575
  10. package/dist/host/chunk-I5F2PSMR.js +1526 -0
  11. package/dist/host/{chunk-2MU6WNSX.js → chunk-JRV2HIDK.js} +1 -1
  12. package/dist/host/{chunk-7LEJ3HYF.js → chunk-P26QKKU5.js} +41 -10
  13. package/dist/host/{chunk-CITUENSU.js → chunk-ROB2SH5Q.js} +77 -49
  14. package/dist/host/chunk-TK3Y437H.js +53 -0
  15. package/dist/host/{chunk-EYONBBRJ.js → chunk-VKNGNEF3.js} +34650 -34968
  16. package/dist/host/core-classes.json +1 -1
  17. package/dist/host/frame-host.css +2 -0
  18. package/dist/host/frame-host.js +242 -0
  19. package/dist/host/{linked-server-OUF3POT7.js → linked-server-J5G457OA.js} +1 -1
  20. package/dist/host/{rbac-api-LZVENREG.js → rbac-api-PY22W5UY.js} +5 -1
  21. package/dist/host/testing-host.js +9 -6
  22. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +2 -0
  23. package/dist/host/types/src/types/index.d.ts +8 -0
  24. package/dist/host/types/src/ui/api/look-api.d.ts +2 -0
  25. package/dist/host/types/src/ui/api/plugins-api.d.ts +3 -2
  26. package/dist/host/types/src/ui/api/rbac-api.d.ts +19 -1
  27. package/dist/host/types/src/ui/components/data-view.d.ts +1 -1
  28. package/dist/host/types/src/ui/lib/app-return.d.ts +13 -0
  29. package/dist/host/types/src/ui/main-axios.d.ts +1 -1
  30. package/dist/host/types/src/ui/plugin-host/loader.d.ts +6 -0
  31. package/dist/host/types/src/ui/plugin-host/sdk-ui.d.ts +1 -1
  32. package/dist/host/types/src/ui/plugins/plugin-model.d.ts +1 -1
  33. package/dist/host/types/src/ui/plugins/use-plugins-manager.d.ts +1 -1
  34. package/dist/host/types/src/ui/shell/tab-registry.d.ts +2 -0
  35. package/dist/host/ui.js +418 -388
  36. package/dist/host/{use-permissions-BYCDS5PD.js → use-permissions-EWRI3WLT.js} +1 -1
  37. package/dist/host-commands.d.ts +9 -6
  38. package/dist/host-commands.js +38 -11
  39. package/dist/host-scripts/docker.d.ts +137 -0
  40. package/dist/host-scripts/docker.js +345 -0
  41. package/dist/host-scripts/index.d.ts +15 -0
  42. package/dist/host-scripts/index.js +15 -0
  43. package/dist/host-scripts/journal.d.ts +45 -0
  44. package/dist/host-scripts/journal.js +194 -0
  45. package/dist/host-scripts/metrics.d.ts +134 -0
  46. package/dist/host-scripts/metrics.js +320 -0
  47. package/dist/host-scripts/openrc.d.ts +6 -0
  48. package/dist/host-scripts/openrc.js +90 -0
  49. package/dist/host-scripts/shell.d.ts +43 -0
  50. package/dist/host-scripts/shell.js +95 -0
  51. package/dist/host-scripts/systemd.d.ts +73 -0
  52. package/dist/host-scripts/systemd.js +320 -0
  53. package/dist/host-scripts/text.d.ts +23 -0
  54. package/dist/host-scripts/text.js +111 -0
  55. package/dist/manifest.d.ts +28 -1
  56. package/dist/manifest.js +108 -1
  57. package/package.json +9 -1
  58. package/schema/manifest.schema.json +42 -0
@@ -0,0 +1,1526 @@
1
+ const __termixNoGlob = () => ({});
2
+ import {
3
+ arrangeCompanions,
4
+ companionApplies,
5
+ companionKey,
6
+ getPanel,
7
+ getSettingsComponent,
8
+ hostToSSHHost,
9
+ invokeAction,
10
+ patchCompanionState,
11
+ resolveCompanionLayout,
12
+ useActionSlot,
13
+ useCompanionDefs,
14
+ useCompanionLayoutDefs,
15
+ useCompanionState,
16
+ useIsMobile,
17
+ useSettingsComponents
18
+ } from "./chunk-HQ6NNFSX.js";
19
+ import {
20
+ usePermissions
21
+ } from "./chunk-JRV2HIDK.js";
22
+ import {
23
+ Button,
24
+ DefaultsOnly,
25
+ FakeSwitch,
26
+ HostDefaultBadge,
27
+ Input,
28
+ PluginIcon,
29
+ SectionCard,
30
+ SettingRow,
31
+ hasVisibleFields,
32
+ isFieldShown
33
+ } from "./chunk-7J4QNGFM.js";
34
+ import {
35
+ ArrowDownToLine,
36
+ ArrowLeftToLine,
37
+ ArrowRightToLine,
38
+ Check,
39
+ Checkbox,
40
+ CheckboxIndicator,
41
+ CircleSlash,
42
+ DocsLink,
43
+ Eye,
44
+ EyeOff,
45
+ LayoutPanelLeft,
46
+ OPEN_PLUGINS_EVENT,
47
+ PLUGINS_CHANGED_EVENT,
48
+ PanelRight,
49
+ Puzzle,
50
+ Select,
51
+ SelectContent,
52
+ SelectItem,
53
+ SelectTrigger,
54
+ SelectValue,
55
+ Server,
56
+ Settings,
57
+ X,
58
+ cn,
59
+ getPlugins,
60
+ isRedactedSecret,
61
+ pluginKey,
62
+ pluginName,
63
+ usePluginScope
64
+ } from "./chunk-VKNGNEF3.js";
65
+ import {
66
+ getTabType,
67
+ unregisteredViewStatus,
68
+ usePluginStore
69
+ } from "./chunk-P26QKKU5.js";
70
+ import {
71
+ init_define_import_meta_env
72
+ } from "./chunk-YGYBM7SQ.js";
73
+
74
+ // src/ui/components/checkbox.tsx
75
+ init_define_import_meta_env();
76
+ import { jsx } from "react/jsx-runtime";
77
+ function Checkbox2({
78
+ className,
79
+ ...props
80
+ }) {
81
+ return /* @__PURE__ */ jsx(
82
+ Checkbox,
83
+ {
84
+ "data-slot": "checkbox",
85
+ className: cn(
86
+ "peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-none border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
87
+ className
88
+ ),
89
+ ...props,
90
+ children: /* @__PURE__ */ jsx(
91
+ CheckboxIndicator,
92
+ {
93
+ "data-slot": "checkbox-indicator",
94
+ className: "flex items-center justify-center text-current transition-none",
95
+ children: /* @__PURE__ */ jsx(Check, { className: "size-3.5" })
96
+ }
97
+ )
98
+ }
99
+ );
100
+ }
101
+
102
+ // src/ui/components/password-input.tsx
103
+ init_define_import_meta_env();
104
+ import * as React from "react";
105
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
106
+ var PasswordInput = React.forwardRef(({ className, ...props }, ref) => {
107
+ const [showPassword, setShowPassword] = React.useState(false);
108
+ return /* @__PURE__ */ jsxs("div", { className: "relative w-full", children: [
109
+ /* @__PURE__ */ jsx2(
110
+ Input,
111
+ {
112
+ ref,
113
+ type: showPassword ? "text" : "password",
114
+ className: cn("h-11 text-base pr-12", className),
115
+ ...props
116
+ }
117
+ ),
118
+ /* @__PURE__ */ jsx2(
119
+ "button",
120
+ {
121
+ type: "button",
122
+ onClick: () => setShowPassword((prev) => !prev),
123
+ className: "absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition",
124
+ "aria-label": showPassword ? "Hide password" : "Show password",
125
+ children: showPassword ? /* @__PURE__ */ jsx2(EyeOff, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx2(Eye, { className: "h-5 w-5" })
126
+ }
127
+ )
128
+ ] });
129
+ });
130
+ PasswordInput.displayName = "PasswordInput";
131
+
132
+ // src/ui/components/textarea.tsx
133
+ init_define_import_meta_env();
134
+ import * as React2 from "react";
135
+ import { jsx as jsx3 } from "react/jsx-runtime";
136
+ var Textarea = React2.forwardRef(
137
+ ({ className, ...props }, ref) => {
138
+ return /* @__PURE__ */ jsx3(
139
+ "textarea",
140
+ {
141
+ className: cn(
142
+ "flex min-h-[80px] w-full rounded-none border border-input bg-transparent dark:bg-input/30 px-3 py-2 text-sm shadow-xs transition-[color,box-shadow] duration-200 outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 disabled:pointer-events-none",
143
+ className
144
+ ),
145
+ ref,
146
+ ...props
147
+ }
148
+ );
149
+ }
150
+ );
151
+ Textarea.displayName = "Textarea";
152
+
153
+ // src/ui/hooks/use-installed-plugins.ts
154
+ init_define_import_meta_env();
155
+ import { useEffect, useState as useState2 } from "react";
156
+ function useInstalledPlugins() {
157
+ const [plugins, setPlugins] = useState2([]);
158
+ useEffect(() => {
159
+ let cancelled = false;
160
+ let request = 0;
161
+ const load = () => {
162
+ const current = ++request;
163
+ void getPlugins().then((loaded) => {
164
+ if (!cancelled && current === request) setPlugins(loaded);
165
+ }).catch(() => {
166
+ });
167
+ };
168
+ load();
169
+ window.addEventListener(PLUGINS_CHANGED_EVENT, load);
170
+ return () => {
171
+ cancelled = true;
172
+ window.removeEventListener(PLUGINS_CHANGED_EVENT, load);
173
+ };
174
+ }, []);
175
+ return plugins;
176
+ }
177
+
178
+ // src/ui/settings/SettingsFields.tsx
179
+ init_define_import_meta_env();
180
+ import { useTranslation } from "react-i18next";
181
+ import { Fragment, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
182
+ function SettingsFieldRow({
183
+ pluginId,
184
+ field,
185
+ values,
186
+ setValue,
187
+ running,
188
+ error,
189
+ disabled = false,
190
+ defaultKey
191
+ }) {
192
+ const { t } = useTranslation();
193
+ useSettingsComponents();
194
+ const label = (key) => key ? t(pluginKey(pluginId, key)) : "";
195
+ if (field.type === "custom") {
196
+ const Component = getSettingsComponent(pluginId, field.component);
197
+ if (!Component) return null;
198
+ return /* @__PURE__ */ jsx4(
199
+ Component,
200
+ {
201
+ pluginId,
202
+ values,
203
+ setValue,
204
+ running
205
+ }
206
+ );
207
+ }
208
+ const value = values[field.key];
209
+ const readOnly = disabled || !running;
210
+ const description = field.descriptionKey ? /* @__PURE__ */ jsxs2(Fragment, { children: [
211
+ label(field.descriptionKey),
212
+ error && /* @__PURE__ */ jsx4("span", { className: "block text-destructive mt-0.5", children: error })
213
+ ] }) : error ? /* @__PURE__ */ jsx4("span", { className: "text-destructive", children: error }) : void 0;
214
+ const control = /* @__PURE__ */ jsx4(
215
+ FieldControl,
216
+ {
217
+ pluginId,
218
+ field,
219
+ value,
220
+ setValue,
221
+ readOnly
222
+ }
223
+ );
224
+ if (field.type === "boolean") {
225
+ return /* @__PURE__ */ jsx4(
226
+ SettingRow,
227
+ {
228
+ label: label(field.labelKey),
229
+ description,
230
+ badge: error ? t("common.error") : void 0,
231
+ defaultKey,
232
+ children: control
233
+ }
234
+ );
235
+ }
236
+ return /* @__PURE__ */ jsx4(DefaultsOnly, { settingKey: defaultKey ?? "", children: /* @__PURE__ */ jsxs2("div", { className: "flex flex-col gap-1.5 py-3 border-b border-border last:border-0 min-w-0", children: [
237
+ /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-1.5 flex-wrap", children: [
238
+ /* @__PURE__ */ jsx4("span", { className: "text-sm font-medium leading-snug", children: label(field.labelKey) }),
239
+ error && /* @__PURE__ */ jsx4("span", { className: "text-[10px] font-bold text-warning border border-warning/40 px-1 shrink-0", children: t("common.error") }),
240
+ defaultKey && /* @__PURE__ */ jsx4(HostDefaultBadge, { settingKey: defaultKey })
241
+ ] }),
242
+ description && /* @__PURE__ */ jsx4("span", { className: "text-xs text-muted-foreground leading-snug", children: description }),
243
+ /* @__PURE__ */ jsx4("div", { className: "min-w-0", children: control })
244
+ ] }) });
245
+ }
246
+ function FieldControl({
247
+ pluginId,
248
+ field,
249
+ value,
250
+ setValue,
251
+ readOnly
252
+ }) {
253
+ const { t } = useTranslation();
254
+ const label = (key) => key ? t(pluginKey(pluginId, key)) : "";
255
+ const placeholder = field.placeholderKey ? label(field.placeholderKey) : void 0;
256
+ switch (field.type) {
257
+ case "boolean":
258
+ return /* @__PURE__ */ jsx4(
259
+ FakeSwitch,
260
+ {
261
+ checked: value === true,
262
+ disabled: readOnly,
263
+ onChange: (next) => setValue(field.key, next)
264
+ }
265
+ );
266
+ case "number":
267
+ return /* @__PURE__ */ jsx4(
268
+ Input,
269
+ {
270
+ type: "number",
271
+ className: "h-8 w-full max-w-40 text-sm",
272
+ disabled: readOnly,
273
+ min: field.min,
274
+ max: field.max,
275
+ placeholder,
276
+ value: value === void 0 || value === null ? "" : String(value),
277
+ onChange: (event) => setValue(
278
+ field.key,
279
+ event.target.value === "" ? null : Number(event.target.value)
280
+ )
281
+ }
282
+ );
283
+ case "select":
284
+ return /* @__PURE__ */ jsxs2(
285
+ Select,
286
+ {
287
+ disabled: readOnly,
288
+ value: typeof value === "string" ? value : "",
289
+ onValueChange: (next) => setValue(field.key, next),
290
+ children: [
291
+ /* @__PURE__ */ jsx4(SelectTrigger, { className: "h-8 w-full min-w-0 text-sm", children: /* @__PURE__ */ jsx4(SelectValue, { placeholder }) }),
292
+ /* @__PURE__ */ jsx4(SelectContent, { children: (field.options ?? []).map((option) => /* @__PURE__ */ jsx4(SelectItem, { value: option.value, children: label(option.labelKey) }, option.value)) })
293
+ ]
294
+ }
295
+ );
296
+ case "multiselect": {
297
+ const selected = Array.isArray(value) ? value : [];
298
+ return /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-1.5", children: (field.options ?? []).map((option) => /* @__PURE__ */ jsxs2(
299
+ "label",
300
+ {
301
+ className: "flex items-center gap-2 text-xs text-muted-foreground",
302
+ children: [
303
+ /* @__PURE__ */ jsx4(
304
+ Checkbox2,
305
+ {
306
+ disabled: readOnly,
307
+ checked: selected.includes(option.value),
308
+ onCheckedChange: (checked) => setValue(
309
+ field.key,
310
+ checked ? [...selected, option.value] : selected.filter((entry) => entry !== option.value)
311
+ )
312
+ }
313
+ ),
314
+ /* @__PURE__ */ jsx4("span", { children: label(option.labelKey) })
315
+ ]
316
+ },
317
+ option.value
318
+ )) });
319
+ }
320
+ case "secret":
321
+ return /* @__PURE__ */ jsx4(
322
+ SecretControl,
323
+ {
324
+ field,
325
+ value,
326
+ setValue,
327
+ readOnly,
328
+ placeholder
329
+ }
330
+ );
331
+ case "textarea":
332
+ case "json":
333
+ return /* @__PURE__ */ jsx4(
334
+ Textarea,
335
+ {
336
+ className: "w-full text-sm font-mono min-h-16",
337
+ disabled: readOnly,
338
+ placeholder,
339
+ value: typeof value === "string" ? value : value === void 0 || value === null ? "" : JSON.stringify(value, null, 2),
340
+ onChange: (event) => setValue(field.key, event.target.value)
341
+ }
342
+ );
343
+ default:
344
+ return /* @__PURE__ */ jsx4(
345
+ Input,
346
+ {
347
+ className: "h-8 w-full text-sm",
348
+ disabled: readOnly,
349
+ placeholder,
350
+ value: typeof value === "string" ? value : "",
351
+ onChange: (event) => setValue(field.key, event.target.value)
352
+ }
353
+ );
354
+ }
355
+ }
356
+ function SecretControl({
357
+ field,
358
+ value,
359
+ setValue,
360
+ readOnly,
361
+ placeholder
362
+ }) {
363
+ const { t } = useTranslation();
364
+ const redacted = isRedactedSecret(value);
365
+ const stored = redacted && value.set;
366
+ if (redacted) {
367
+ return /* @__PURE__ */ jsxs2("div", { className: "flex flex-wrap items-center gap-2", children: [
368
+ /* @__PURE__ */ jsx4("span", { className: "text-xs text-muted-foreground", children: stored ? t("settings.secretSet") : t("settings.secretNotSet") }),
369
+ /* @__PURE__ */ jsx4(
370
+ Button,
371
+ {
372
+ variant: "outline",
373
+ size: "sm",
374
+ className: "h-7 text-xs",
375
+ disabled: readOnly,
376
+ onClick: () => setValue(field.key, ""),
377
+ children: stored ? t("settings.secretReplace") : t("common.edit")
378
+ }
379
+ )
380
+ ] });
381
+ }
382
+ return /* @__PURE__ */ jsxs2("div", { className: "flex items-center gap-2 min-w-0", children: [
383
+ /* @__PURE__ */ jsx4("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx4(
384
+ PasswordInput,
385
+ {
386
+ className: "h-8 w-full text-sm",
387
+ disabled: readOnly,
388
+ placeholder,
389
+ value: typeof value === "string" ? value : "",
390
+ onChange: (event) => setValue(field.key, event.target.value)
391
+ }
392
+ ) }),
393
+ /* @__PURE__ */ jsx4(
394
+ Button,
395
+ {
396
+ variant: "ghost",
397
+ size: "sm",
398
+ className: "h-7 text-xs",
399
+ disabled: readOnly,
400
+ onClick: () => setValue(field.key, { set: stored }),
401
+ children: t("common.cancel")
402
+ }
403
+ )
404
+ ] });
405
+ }
406
+
407
+ // src/ui/settings/HostPluginSections.tsx
408
+ init_define_import_meta_env();
409
+ import { useMemo } from "react";
410
+ import { useTranslation as useTranslation2 } from "react-i18next";
411
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
412
+ function usePluginHostSections() {
413
+ const plugins = useInstalledPlugins();
414
+ return useMemo(
415
+ () => plugins.filter((plugin) => {
416
+ if (!plugin.enabled) return false;
417
+ const host = plugin.contributes?.settings?.host;
418
+ return !!host && (hasVisibleFields(host.fields) || !!host.enableKey);
419
+ }),
420
+ [plugins]
421
+ );
422
+ }
423
+ function hostFieldDefaults(plugin) {
424
+ const host = plugin.contributes?.settings?.host;
425
+ if (!host) return {};
426
+ const defaults = {};
427
+ if (host.enableKey) defaults[host.enableKey] = host.enableDefault ?? false;
428
+ for (const field of host.fields) {
429
+ if (field.default !== void 0) defaults[field.key] = field.default;
430
+ }
431
+ return defaults;
432
+ }
433
+ function HostPluginSections({
434
+ plugins,
435
+ values,
436
+ setValue
437
+ }) {
438
+ if (plugins.length === 0) return null;
439
+ return /* @__PURE__ */ jsx5("div", { className: "flex flex-col gap-2", children: plugins.map((plugin) => /* @__PURE__ */ jsx5(
440
+ HostPluginSection,
441
+ {
442
+ plugin,
443
+ values: values[plugin.id] ?? {},
444
+ setValue: (key, value) => setValue(plugin.id, key, value)
445
+ },
446
+ plugin.id
447
+ )) });
448
+ }
449
+ function HostFeatureFields({
450
+ form,
451
+ updateForm
452
+ }) {
453
+ const pluginId = usePluginScope();
454
+ const plugin = usePluginHostSections().find((p) => p.id === pluginId);
455
+ if (!plugin) return null;
456
+ const values = form?.pluginSettings ?? {};
457
+ return /* @__PURE__ */ jsx5(
458
+ HostPluginSection,
459
+ {
460
+ plugin,
461
+ values: values[plugin.id] ?? {},
462
+ setValue: (key, value) => updateForm((current) => {
463
+ const all = current.pluginSettings ?? {};
464
+ return {
465
+ ...current,
466
+ pluginSettings: {
467
+ ...all,
468
+ [plugin.id]: { ...all[plugin.id] ?? {}, [key]: value }
469
+ }
470
+ };
471
+ })
472
+ }
473
+ );
474
+ }
475
+ function HostPluginSection({
476
+ plugin,
477
+ values,
478
+ setValue
479
+ }) {
480
+ const { t } = useTranslation2();
481
+ const host = plugin.contributes?.settings?.host;
482
+ if (!host) return null;
483
+ const shown = { ...hostFieldDefaults(plugin), ...values };
484
+ const running = plugin.enabled && plugin.state !== "failed";
485
+ const enableKey = host.enableKey;
486
+ const enabled = enableKey ? shown[enableKey] === true : true;
487
+ return /* @__PURE__ */ jsxs3(
488
+ SectionCard,
489
+ {
490
+ title: pluginName(t, plugin),
491
+ icon: /* @__PURE__ */ jsx5(PluginIcon, { name: plugin.icon, className: "size-3.5" }),
492
+ action: /* @__PURE__ */ jsx5(DocsLink, { href: plugin.docs }),
493
+ children: [
494
+ !running && /* @__PURE__ */ jsx5("div", { className: "my-3 border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning", children: t("settings.featureUnavailable", {
495
+ name: pluginName(t, plugin)
496
+ }) }),
497
+ enableKey && /* @__PURE__ */ jsx5(
498
+ SettingsFieldRow,
499
+ {
500
+ pluginId: plugin.id,
501
+ field: {
502
+ key: enableKey,
503
+ type: "boolean",
504
+ labelKey: host.enableLabelKey,
505
+ descriptionKey: host.enableDescriptionKey
506
+ },
507
+ values: shown,
508
+ setValue,
509
+ running,
510
+ defaultKey: `${plugin.id}.${enableKey}`
511
+ }
512
+ ),
513
+ enabled && host.fields.filter((field) => isFieldShown(field, shown)).map((field) => /* @__PURE__ */ jsx5(
514
+ SettingsFieldRow,
515
+ {
516
+ pluginId: plugin.id,
517
+ field,
518
+ values: shown,
519
+ setValue,
520
+ running,
521
+ defaultKey: `${plugin.id}.${field.key}`
522
+ },
523
+ field.key
524
+ ))
525
+ ]
526
+ }
527
+ );
528
+ }
529
+
530
+ // src/ui/shell/ActionSlot.tsx
531
+ init_define_import_meta_env();
532
+ import React3 from "react";
533
+ import { useTranslation as useTranslation3 } from "react-i18next";
534
+ import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
535
+ function ActionSlot({
536
+ slotId,
537
+ context,
538
+ className,
539
+ enabled = true,
540
+ renderItem,
541
+ hideLabels,
542
+ when
543
+ }) {
544
+ const { t } = useTranslation3();
545
+ const contributions = useActionSlot(slotId, when).filter(
546
+ (contribution) => contribution.kind !== "component"
547
+ );
548
+ if (!enabled || contributions.length === 0) return null;
549
+ return /* @__PURE__ */ jsx6(Fragment2, { children: contributions.map((contribution) => {
550
+ const invoke = () => {
551
+ void invokeAction(contribution.actionId, ...context?.() ?? []);
552
+ };
553
+ if (renderItem) {
554
+ return /* @__PURE__ */ jsx6(React3.Fragment, { children: renderItem(contribution, invoke) }, contribution.actionId);
555
+ }
556
+ const Icon = contribution.icon;
557
+ const label = t(contribution.titleKey);
558
+ return /* @__PURE__ */ jsxs4(
559
+ "button",
560
+ {
561
+ type: "button",
562
+ className,
563
+ "aria-label": label,
564
+ title: label,
565
+ onClick: invoke,
566
+ children: [
567
+ Icon && /* @__PURE__ */ jsx6(Icon, { className: "size-4 shrink-0" }),
568
+ !hideLabels && label
569
+ ]
570
+ },
571
+ contribution.actionId
572
+ );
573
+ }) });
574
+ }
575
+ function ComponentSlot({
576
+ slotId,
577
+ props,
578
+ when
579
+ }) {
580
+ const contributions = useActionSlot(slotId, when).filter(
581
+ (contribution) => contribution.kind === "component" && contribution.component
582
+ );
583
+ if (contributions.length === 0) return null;
584
+ return /* @__PURE__ */ jsx6(Fragment2, { children: contributions.map((contribution) => {
585
+ const Contributed = contribution.component;
586
+ return /* @__PURE__ */ jsx6(Contributed, { ...props ?? {} }, contribution.actionId);
587
+ }) });
588
+ }
589
+
590
+ // src/ui/shell/tab-handles.ts
591
+ init_define_import_meta_env();
592
+ import { useSyncExternalStore } from "react";
593
+ var version = 0;
594
+ var listeners = /* @__PURE__ */ new Set();
595
+ var callbacks = /* @__PURE__ */ new WeakMap();
596
+ function subscribe(listener) {
597
+ listeners.add(listener);
598
+ return () => listeners.delete(listener);
599
+ }
600
+ function trackTabHandle(ref) {
601
+ if (!ref) return void 0;
602
+ let callback = callbacks.get(ref);
603
+ if (!callback) {
604
+ callback = (value) => {
605
+ if (ref.current === value) return;
606
+ ref.current = value;
607
+ version += 1;
608
+ for (const listener of listeners) listener();
609
+ };
610
+ callbacks.set(ref, callback);
611
+ }
612
+ return callback;
613
+ }
614
+ function useTabHandlesVersion() {
615
+ return useSyncExternalStore(
616
+ subscribe,
617
+ () => version,
618
+ () => version
619
+ );
620
+ }
621
+
622
+ // src/ui/plugin-host/PluginViewPlaceholder.tsx
623
+ init_define_import_meta_env();
624
+ import { useTranslation as useTranslation4 } from "react-i18next";
625
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
626
+ function PluginViewPlaceholder({
627
+ kind,
628
+ viewId,
629
+ compact = false
630
+ }) {
631
+ const { t } = useTranslation4();
632
+ usePluginStore();
633
+ const { has } = usePermissions();
634
+ const { status, owner } = unregisteredViewStatus(kind, viewId);
635
+ const name = owner ? pluginName(t, owner.summary) : viewId;
636
+ if (status === "loading") {
637
+ return /* @__PURE__ */ jsx7(
638
+ "div",
639
+ {
640
+ className: "flex h-full w-full items-center justify-center",
641
+ "aria-label": t("plugins.runtime.loading", { name }),
642
+ children: /* @__PURE__ */ jsx7("div", { className: "size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" })
643
+ }
644
+ );
645
+ }
646
+ const title = status === "failed" ? t("plugins.runtime.failed", { name }) : owner ? t("plugins.runtime.needsPlugin", { name }) : t("plugins.runtime.needsUnknownPlugin");
647
+ const hint = status === "failed" ? t("plugins.runtime.failedHint") : status === "disabled" ? t("plugins.runtime.disabledHint") : t("plugins.runtime.missingHint");
648
+ return /* @__PURE__ */ jsxs5(
649
+ "div",
650
+ {
651
+ "data-testid": "plugin-view-placeholder",
652
+ "data-status": status,
653
+ className: `flex h-full w-full flex-col items-center justify-center gap-2 text-center ${compact ? "p-3" : "p-6"}`,
654
+ children: [
655
+ /* @__PURE__ */ jsx7(CircleSlash, { className: "size-6 text-muted-foreground/40" }),
656
+ /* @__PURE__ */ jsx7("span", { className: "text-sm font-semibold text-muted-foreground", children: title }),
657
+ /* @__PURE__ */ jsx7("span", { className: "text-xs text-muted-foreground/70", children: hint }),
658
+ !compact && status !== "failed" && has("admin.plugins.manage") && /* @__PURE__ */ jsx7(
659
+ Button,
660
+ {
661
+ variant: "outline",
662
+ size: "sm",
663
+ className: "mt-1",
664
+ onClick: () => window.dispatchEvent(new CustomEvent(OPEN_PLUGINS_EVENT)),
665
+ children: t("plugins.manager.browsePlugins")
666
+ }
667
+ )
668
+ ]
669
+ }
670
+ );
671
+ }
672
+
673
+ // src/ui/lib/adaptive-resource-budget.ts
674
+ init_define_import_meta_env();
675
+
676
+ // src/ui/lib/local-adaptive-engine.ts
677
+ init_define_import_meta_env();
678
+ var LOCAL_ADAPTIVE_ENGINE_KEY = "termix.local.adaptive-engine.v1";
679
+ var VERSION = 1;
680
+ var HALF_LIFE_MS = 30 * 24 * 60 * 60 * 1e3;
681
+ var MAX_SCOPES = 128;
682
+ var MAX_ACTIONS_PER_SCOPE = 12;
683
+ var DEFAULT_MIN_EVIDENCE = 2.5;
684
+ var DEFAULT_MIN_SHARE = 0.6;
685
+ var DEFAULT_MIN_MARGIN = 1;
686
+ var EWMA_ALPHA = 0.25;
687
+ function emptyStore() {
688
+ return { version: VERSION, scopes: {} };
689
+ }
690
+ function finiteNonNegative(value) {
691
+ return Number.isFinite(value) && Number(value) >= 0 ? Number(value) : 0;
692
+ }
693
+ function sanitizeAction(value) {
694
+ if (!value || typeof value !== "object") return null;
695
+ const stat = value;
696
+ if (!Number.isFinite(stat.weight) || Number(stat.weight) < 0 || !Number.isFinite(stat.updatedAt)) {
697
+ return null;
698
+ }
699
+ const latencyEwmaMs = finiteNonNegative(stat.latencyEwmaMs);
700
+ return {
701
+ weight: Number(stat.weight),
702
+ updatedAt: Number(stat.updatedAt),
703
+ observations: finiteNonNegative(stat.observations),
704
+ successes: finiteNonNegative(stat.successes),
705
+ cancellations: finiteNonNegative(stat.cancellations),
706
+ fallbacks: finiteNonNegative(stat.fallbacks),
707
+ ...latencyEwmaMs > 0 ? { latencyEwmaMs } : {}
708
+ };
709
+ }
710
+ function sanitizeStore(value) {
711
+ if (!value || typeof value !== "object") return emptyStore();
712
+ const candidate = value;
713
+ if (candidate.version !== VERSION || !candidate.scopes || typeof candidate.scopes !== "object" || Array.isArray(candidate.scopes)) {
714
+ return emptyStore();
715
+ }
716
+ const scopes2 = {};
717
+ for (const [scope, rawScope] of Object.entries(candidate.scopes)) {
718
+ if (!rawScope || typeof rawScope !== "object") continue;
719
+ const value2 = rawScope;
720
+ if (!Number.isFinite(value2.updatedAt) || !value2.actions || typeof value2.actions !== "object" || Array.isArray(value2.actions)) {
721
+ continue;
722
+ }
723
+ const actions = {};
724
+ for (const [action, rawStat] of Object.entries(value2.actions)) {
725
+ const stat = sanitizeAction(rawStat);
726
+ if (stat) actions[action] = stat;
727
+ }
728
+ scopes2[scope] = { actions, updatedAt: Number(value2.updatedAt) };
729
+ }
730
+ return { version: VERSION, scopes: scopes2 };
731
+ }
732
+ function readStore() {
733
+ if (typeof localStorage === "undefined") return emptyStore();
734
+ try {
735
+ return sanitizeStore(
736
+ JSON.parse(localStorage.getItem(LOCAL_ADAPTIVE_ENGINE_KEY) ?? "null")
737
+ );
738
+ } catch {
739
+ return emptyStore();
740
+ }
741
+ }
742
+ function writeStore(store) {
743
+ if (typeof localStorage === "undefined") return;
744
+ try {
745
+ localStorage.setItem(LOCAL_ADAPTIVE_ENGINE_KEY, JSON.stringify(store));
746
+ } catch {
747
+ }
748
+ }
749
+ function decayedWeight(stat, now) {
750
+ const age = Math.max(0, now - stat.updatedAt);
751
+ return stat.weight * 2 ** (-age / HALF_LIFE_MS);
752
+ }
753
+ function trimStore(store, now) {
754
+ for (const scope of Object.values(store.scopes)) {
755
+ scope.actions = Object.fromEntries(
756
+ Object.entries(scope.actions).sort(
757
+ ([, a], [, b]) => decayedWeight(b, now) - decayedWeight(a, now) || b.updatedAt - a.updatedAt
758
+ ).slice(0, MAX_ACTIONS_PER_SCOPE)
759
+ );
760
+ }
761
+ store.scopes = Object.fromEntries(
762
+ Object.entries(store.scopes).sort(([, a], [, b]) => b.updatedAt - a.updatedAt).slice(0, MAX_SCOPES)
763
+ );
764
+ }
765
+ function updateEwma(previous, value) {
766
+ return previous === void 0 ? value : previous + EWMA_ALPHA * (value - previous);
767
+ }
768
+ function getActionStats(store, scope, action, now) {
769
+ const scopeStats = store.scopes[scope] ?? { actions: {}, updatedAt: now };
770
+ return [
771
+ scopeStats,
772
+ scopeStats.actions[action] ?? {
773
+ weight: 0,
774
+ updatedAt: now,
775
+ observations: 0,
776
+ successes: 0,
777
+ cancellations: 0,
778
+ fallbacks: 0
779
+ }
780
+ ];
781
+ }
782
+ function saveActionStats(store, scope, action, scopeStats, actionStats, now) {
783
+ scopeStats.actions[action] = actionStats;
784
+ scopeStats.updatedAt = now;
785
+ store.scopes[scope] = scopeStats;
786
+ trimStore(store, now);
787
+ writeStore(store);
788
+ }
789
+ function recordLocalAdaptiveChoice(scope, action, now = Date.now()) {
790
+ if (!scope || !action) return;
791
+ const store = readStore();
792
+ const [scopeStats, previous] = getActionStats(store, scope, action, now);
793
+ saveActionStats(
794
+ store,
795
+ scope,
796
+ action,
797
+ scopeStats,
798
+ {
799
+ ...previous,
800
+ weight: decayedWeight(previous, now) + 1,
801
+ updatedAt: now
802
+ },
803
+ now
804
+ );
805
+ }
806
+ function recordLocalAdaptiveOutcome(scope, action, outcome, now = Date.now()) {
807
+ if (!scope || !action || !Object.values(outcome).some((v) => v !== void 0)) {
808
+ return;
809
+ }
810
+ const store = readStore();
811
+ const [scopeStats, previous] = getActionStats(store, scope, action, now);
812
+ const latency = finiteNonNegative(outcome.latencyMs);
813
+ saveActionStats(
814
+ store,
815
+ scope,
816
+ action,
817
+ scopeStats,
818
+ {
819
+ ...previous,
820
+ updatedAt: now,
821
+ observations: previous.observations + 1,
822
+ successes: previous.successes + (outcome.success === true ? 1 : 0),
823
+ cancellations: previous.cancellations + (outcome.cancelled === true ? 1 : 0),
824
+ fallbacks: previous.fallbacks + (outcome.fallback === true ? 1 : 0),
825
+ ...latency > 0 ? { latencyEwmaMs: updateEwma(previous.latencyEwmaMs, latency) } : {}
826
+ },
827
+ now
828
+ );
829
+ }
830
+ function chooseLocalAdaptiveAction({
831
+ scope,
832
+ candidates,
833
+ fallback,
834
+ now = Date.now(),
835
+ ttlMs = 5 * 60 * 1e3,
836
+ minEvidence = DEFAULT_MIN_EVIDENCE,
837
+ minShare = DEFAULT_MIN_SHARE,
838
+ minMargin = DEFAULT_MIN_MARGIN
839
+ }) {
840
+ const expiresAt = now + Math.max(0, ttlMs);
841
+ const stats = readStore().scopes[scope]?.actions;
842
+ if (!stats || candidates.length === 0) {
843
+ return { action: fallback, confidence: 0, reason: "fallback", expiresAt };
844
+ }
845
+ const ranked = candidates.map((action) => ({
846
+ action,
847
+ weight: stats[action] ? decayedWeight(stats[action], now) : 0
848
+ })).sort((a, b) => b.weight - a.weight);
849
+ const total = ranked.reduce((sum, item) => sum + item.weight, 0);
850
+ const [best, second] = ranked;
851
+ if (!best || total <= 0) {
852
+ return { action: fallback, confidence: 0, reason: "fallback", expiresAt };
853
+ }
854
+ const share = best.weight / total;
855
+ const margin = best.weight - (second?.weight ?? 0);
856
+ const confidence = Math.min(
857
+ 1,
858
+ best.weight / Math.max(minEvidence, Number.EPSILON),
859
+ share / Math.max(minShare, Number.EPSILON),
860
+ margin / Math.max(minMargin, Number.EPSILON)
861
+ );
862
+ const learned = best.weight >= minEvidence && share >= minShare && margin >= minMargin;
863
+ return {
864
+ action: learned ? best.action : fallback,
865
+ confidence,
866
+ reason: learned ? "learned" : "insufficient-evidence",
867
+ expiresAt
868
+ };
869
+ }
870
+ function getLocalAdaptiveStats(scope) {
871
+ return readStore().scopes[scope]?.actions ?? {};
872
+ }
873
+ function clearLocalAdaptiveEngine() {
874
+ if (typeof localStorage === "undefined") return;
875
+ try {
876
+ localStorage.removeItem(LOCAL_ADAPTIVE_ENGINE_KEY);
877
+ } catch {
878
+ }
879
+ }
880
+
881
+ // src/ui/lib/adaptive-resource-budget.ts
882
+ var scopes = {
883
+ module: "resource-budget:module",
884
+ network: "resource-budget:network"
885
+ };
886
+ var usefulnessScopes = {
887
+ module: "resource-usefulness:module",
888
+ network: "resource-usefulness:network"
889
+ };
890
+ var PRELOAD_USE_WINDOW_MS = 1e4;
891
+ var pendingPreloads = /* @__PURE__ */ new Map();
892
+ var activeTasks = {
893
+ module: 0,
894
+ network: 0
895
+ };
896
+ var budgets = {
897
+ paused: {
898
+ tier: "paused",
899
+ allowModulePreload: false,
900
+ allowNetworkPrefetch: false,
901
+ maxPrefetchBytes: 0,
902
+ maxConcurrentModulePreloads: 0,
903
+ maxConcurrentNetworkPrefetches: 0,
904
+ reason: "hidden"
905
+ },
906
+ constrained: {
907
+ tier: "constrained",
908
+ allowModulePreload: false,
909
+ allowNetworkPrefetch: false,
910
+ maxPrefetchBytes: 0,
911
+ maxConcurrentModulePreloads: 0,
912
+ maxConcurrentNetworkPrefetches: 0,
913
+ reason: "environment"
914
+ },
915
+ balanced: {
916
+ tier: "balanced",
917
+ allowModulePreload: true,
918
+ allowNetworkPrefetch: true,
919
+ maxPrefetchBytes: 128 * 1024,
920
+ maxConcurrentModulePreloads: 1,
921
+ maxConcurrentNetworkPrefetches: 1,
922
+ reason: "environment"
923
+ },
924
+ eager: {
925
+ tier: "eager",
926
+ allowModulePreload: true,
927
+ allowNetworkPrefetch: true,
928
+ maxPrefetchBytes: 512 * 1024,
929
+ maxConcurrentModulePreloads: 2,
930
+ maxConcurrentNetworkPrefetches: 2,
931
+ reason: "default"
932
+ }
933
+ };
934
+ function withReason(tier, reason) {
935
+ return { ...budgets[tier], reason };
936
+ }
937
+ function feedbackIsPoor(feedback) {
938
+ if (!feedback || feedback.observations < 4) return false;
939
+ const failureRate = 1 - feedback.successes / feedback.observations;
940
+ const abandoned = feedback.cancellations + feedback.fallbacks;
941
+ return failureRate > 0.25 || abandoned / feedback.observations > 0.25 || (feedback.latencyEwmaMs ?? 0) > 2e3 || (feedback.usefulnessObservations ?? 0) >= 4 && (feedback.usefulPreloads ?? 0) / (feedback.usefulnessObservations ?? 1) < 0.25;
942
+ }
943
+ function preloadKey(kind, action) {
944
+ return `${kind}:${action}`;
945
+ }
946
+ function flushExpiredPreloads(now = Date.now()) {
947
+ for (const [key, preload] of pendingPreloads) {
948
+ if (preload.expiresAt > now) continue;
949
+ recordLocalAdaptiveOutcome(usefulnessScopes[preload.kind], preload.action, {
950
+ success: false
951
+ });
952
+ pendingPreloads.delete(key);
953
+ }
954
+ }
955
+ function markAdaptiveResourceUsed(kind, action, now = Date.now()) {
956
+ flushExpiredPreloads(now);
957
+ const key = preloadKey(kind, action);
958
+ if (!pendingPreloads.has(key)) return;
959
+ pendingPreloads.delete(key);
960
+ recordLocalAdaptiveOutcome(
961
+ usefulnessScopes[kind],
962
+ action,
963
+ { success: true },
964
+ now
965
+ );
966
+ }
967
+ function computeAdaptiveResourceBudget(environment, feedback) {
968
+ if (!environment.visible) return withReason("paused", "hidden");
969
+ if (environment.saveData || ["slow-2g", "2g", "3g"].includes(environment.effectiveType ?? "")) {
970
+ return withReason("constrained", "environment");
971
+ }
972
+ const limitedDevice = (environment.deviceMemoryGb ?? 8) <= 4 || (environment.hardwareConcurrency ?? 8) <= 4;
973
+ const baseline = limitedDevice ? "balanced" : "eager";
974
+ if (!feedbackIsPoor(feedback)) {
975
+ return withReason(baseline, limitedDevice ? "environment" : "default");
976
+ }
977
+ return withReason(
978
+ baseline === "eager" ? "balanced" : "constrained",
979
+ "feedback"
980
+ );
981
+ }
982
+ function readEnvironment() {
983
+ const navigatorLike = typeof navigator === "undefined" ? void 0 : navigator;
984
+ return {
985
+ visible: typeof document === "undefined" || document.visibilityState !== "hidden",
986
+ saveData: navigatorLike?.connection?.saveData,
987
+ effectiveType: navigatorLike?.connection?.effectiveType,
988
+ deviceMemoryGb: navigatorLike?.deviceMemory,
989
+ hardwareConcurrency: navigatorLike?.hardwareConcurrency
990
+ };
991
+ }
992
+ function readFeedback(kind) {
993
+ flushExpiredPreloads();
994
+ const stats = Object.values(getLocalAdaptiveStats(scopes[kind]));
995
+ const usefulness = Object.values(
996
+ getLocalAdaptiveStats(usefulnessScopes[kind])
997
+ );
998
+ const observations = stats.reduce((sum, stat) => sum + stat.observations, 0);
999
+ const latencySamples = stats.filter(
1000
+ (stat) => stat.latencyEwmaMs !== void 0 && stat.observations > 0
1001
+ );
1002
+ const latencyWeight = latencySamples.reduce(
1003
+ (sum, stat) => sum + stat.observations,
1004
+ 0
1005
+ );
1006
+ return {
1007
+ observations,
1008
+ successes: stats.reduce((sum, stat) => sum + stat.successes, 0),
1009
+ cancellations: stats.reduce((sum, stat) => sum + stat.cancellations, 0),
1010
+ fallbacks: stats.reduce((sum, stat) => sum + stat.fallbacks, 0),
1011
+ usefulnessObservations: usefulness.reduce(
1012
+ (sum, stat) => sum + stat.observations,
1013
+ 0
1014
+ ),
1015
+ usefulPreloads: usefulness.reduce((sum, stat) => sum + stat.successes, 0),
1016
+ ...latencyWeight > 0 ? {
1017
+ latencyEwmaMs: latencySamples.reduce(
1018
+ (sum, stat) => sum + (stat.latencyEwmaMs ?? 0) * stat.observations,
1019
+ 0
1020
+ ) / latencyWeight
1021
+ } : {}
1022
+ };
1023
+ }
1024
+ function getAdaptiveResourceBudget(kind) {
1025
+ return computeAdaptiveResourceBudget(readEnvironment(), readFeedback(kind));
1026
+ }
1027
+ function runAdaptiveBackgroundTask(kind, action, task, options = {}) {
1028
+ const budget = getAdaptiveResourceBudget(kind);
1029
+ const allowed = kind === "module" ? budget.allowModulePreload : budget.allowNetworkPrefetch && (options.estimatedBytes === void 0 || options.estimatedBytes <= budget.maxPrefetchBytes);
1030
+ const concurrency = kind === "module" ? budget.maxConcurrentModulePreloads : budget.maxConcurrentNetworkPrefetches;
1031
+ if (!allowed || activeTasks[kind] >= concurrency) return false;
1032
+ activeTasks[kind] += 1;
1033
+ const key = preloadKey(kind, action);
1034
+ pendingPreloads.set(key, {
1035
+ kind,
1036
+ action,
1037
+ expiresAt: Date.now() + PRELOAD_USE_WINDOW_MS
1038
+ });
1039
+ const startedAt = performance.now();
1040
+ void Promise.resolve().then(task).then(
1041
+ () => {
1042
+ activeTasks[kind] -= 1;
1043
+ recordLocalAdaptiveOutcome(scopes[kind], action, {
1044
+ success: true,
1045
+ latencyMs: performance.now() - startedAt
1046
+ });
1047
+ },
1048
+ () => {
1049
+ activeTasks[kind] -= 1;
1050
+ pendingPreloads.delete(key);
1051
+ recordLocalAdaptiveOutcome(scopes[kind], action, {
1052
+ success: false,
1053
+ latencyMs: performance.now() - startedAt
1054
+ });
1055
+ }
1056
+ );
1057
+ return true;
1058
+ }
1059
+
1060
+ // src/ui/shell/tabUtils.tsx
1061
+ init_define_import_meta_env();
1062
+ import { lazy, memo as memo2, Suspense } from "react";
1063
+ import { useTranslation as useTranslation6 } from "react-i18next";
1064
+
1065
+ // src/ui/shell/CompanionFrame.tsx
1066
+ init_define_import_meta_env();
1067
+ import {
1068
+ memo,
1069
+ useCallback,
1070
+ useEffect as useEffect2,
1071
+ useMemo as useMemo2,
1072
+ useRef,
1073
+ useState as useState3
1074
+ } from "react";
1075
+ import { useTranslation as useTranslation5 } from "react-i18next";
1076
+ import { Fragment as Fragment3, jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
1077
+ var DEFAULT_SIDE_SIZE = 360;
1078
+ var DEFAULT_BOTTOM_SIZE = 280;
1079
+ var MIN_SIZE = 200;
1080
+ function DefaultsReader({
1081
+ def,
1082
+ onValue
1083
+ }) {
1084
+ const Reader = def.Defaults;
1085
+ const report = useCallback(
1086
+ (value) => onValue(def.id, value),
1087
+ [def.id, onValue]
1088
+ );
1089
+ return Reader ? /* @__PURE__ */ jsx8(Reader, { onValue: report }) : null;
1090
+ }
1091
+ function sideIcon(side) {
1092
+ if (side === "left") return ArrowLeftToLine;
1093
+ if (side === "bottom") return ArrowDownToLine;
1094
+ return ArrowRightToLine;
1095
+ }
1096
+ var CompanionFrame = memo(function CompanionFrame2({
1097
+ tab,
1098
+ isVisible,
1099
+ children
1100
+ }) {
1101
+ const defs = useCompanionDefs();
1102
+ const layoutDefs = useCompanionLayoutDefs();
1103
+ const applicable = useMemo2(
1104
+ () => arrangeCompanions(
1105
+ defs.filter((def) => companionApplies(def, tab)),
1106
+ resolveCompanionLayout(tab.type, tab.host).order
1107
+ ),
1108
+ // tab.host and tab.type decide it
1109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1110
+ [defs, layoutDefs, tab.type, tab.host]
1111
+ );
1112
+ if (applicable.length === 0) return /* @__PURE__ */ jsx8(Fragment3, { children });
1113
+ return /* @__PURE__ */ jsx8(CompanionLayout, { tab, defs: applicable, isVisible, children });
1114
+ });
1115
+ function CompanionLayout({
1116
+ tab,
1117
+ defs,
1118
+ isVisible,
1119
+ children
1120
+ }) {
1121
+ const { t } = useTranslation5();
1122
+ const isMobile = useIsMobile();
1123
+ const key = companionKey(tab);
1124
+ const state = useCompanionState(key);
1125
+ const [defaults, setDefaults] = useState3(
1126
+ {}
1127
+ );
1128
+ const [dragging, setDragging] = useState3(false);
1129
+ const [mounted, setMounted] = useState3(/* @__PURE__ */ new Set());
1130
+ const rootRef = useRef(null);
1131
+ const onDefaults = useCallback((id, value) => {
1132
+ setDefaults((prev) => {
1133
+ const old = prev[id];
1134
+ if (old && old.open === value.open && old.side === value.side && old.size === value.size && old.edgeButton === value.edgeButton) {
1135
+ return prev;
1136
+ }
1137
+ return { ...prev, [id]: value };
1138
+ });
1139
+ }, []);
1140
+ useEffect2(() => {
1141
+ if (state.seen || !isVisible) return;
1142
+ const ready = defs.every(
1143
+ (def) => !def.Defaults || defaults[def.id] !== void 0
1144
+ );
1145
+ if (!ready) return;
1146
+ const auto = defs.find((def) => defaults[def.id]?.open);
1147
+ patchCompanionState(key, { seen: true, openId: auto?.id ?? null });
1148
+ }, [state.seen, isVisible, defs, defaults, key]);
1149
+ const active = defs.find((def) => def.id === state.openId) ?? null;
1150
+ useEffect2(() => {
1151
+ if (!active) return;
1152
+ setMounted(
1153
+ (prev) => prev.has(active.id) ? prev : new Set(prev).add(active.id)
1154
+ );
1155
+ }, [active]);
1156
+ const activeDefaults = active ? defaults[active.id] : void 0;
1157
+ const layout = useMemo2(
1158
+ () => resolveCompanionLayout(tab.type, tab.host),
1159
+ [tab.type, tab.host]
1160
+ );
1161
+ const side = isMobile ? "bottom" : state.side ?? layout.side ?? activeDefaults?.side ?? "right";
1162
+ const baseSize = state.size ?? activeDefaults?.size ?? (side === "bottom" ? DEFAULT_BOTTOM_SIZE : DEFAULT_SIDE_SIZE);
1163
+ const setSide = useCallback(
1164
+ (next) => patchCompanionState(key, { side: next }),
1165
+ [key]
1166
+ );
1167
+ const close = useCallback(
1168
+ () => patchCompanionState(key, { openId: null }),
1169
+ [key]
1170
+ );
1171
+ const startResize = (event) => {
1172
+ event.preventDefault();
1173
+ const root = rootRef.current;
1174
+ if (!root) return;
1175
+ const rect = root.getBoundingClientRect();
1176
+ const total = side === "bottom" ? rect.height : rect.width;
1177
+ const max = Math.max(MIN_SIZE, total - MIN_SIZE);
1178
+ setDragging(true);
1179
+ const move = (e) => {
1180
+ const raw = side === "right" ? rect.right - e.clientX : side === "left" ? e.clientX - rect.left : rect.bottom - e.clientY;
1181
+ patchCompanionState(key, {
1182
+ size: Math.round(Math.min(max, Math.max(MIN_SIZE, raw)))
1183
+ });
1184
+ };
1185
+ const up = () => {
1186
+ setDragging(false);
1187
+ window.removeEventListener("mousemove", move);
1188
+ window.removeEventListener("mouseup", up);
1189
+ };
1190
+ window.addEventListener("mousemove", move);
1191
+ window.addEventListener("mouseup", up);
1192
+ };
1193
+ const getHandle = useCallback(
1194
+ () => tab.terminalRef?.current ?? null,
1195
+ [tab.terminalRef]
1196
+ );
1197
+ const sshHost = useMemo2(
1198
+ () => tab.host ? hostToSSHHost(tab.host) : void 0,
1199
+ [tab.host]
1200
+ );
1201
+ const showEdge = defs.filter((def) => defaults[def.id]?.edgeButton !== false);
1202
+ const vertical = side === "bottom";
1203
+ const clamped = vertical ? Math.min(baseSize, 2e3) : baseSize;
1204
+ const paneStyle = isMobile ? { height: "55%" } : vertical ? { height: clamped } : { width: clamped };
1205
+ const SideIcon = sideIcon(side);
1206
+ return /* @__PURE__ */ jsxs6("div", { className: "flex h-full w-full min-h-0 min-w-0", children: [
1207
+ defs.map(
1208
+ (def) => def.Defaults ? /* @__PURE__ */ jsx8(DefaultsReader, { def, onValue: onDefaults }, def.id) : null
1209
+ ),
1210
+ /* @__PURE__ */ jsxs6(
1211
+ "div",
1212
+ {
1213
+ ref: rootRef,
1214
+ className: cn(
1215
+ "relative flex flex-1 min-h-0 min-w-0",
1216
+ vertical ? "flex-col" : side === "left" ? "flex-row-reverse" : "flex-row"
1217
+ ),
1218
+ children: [
1219
+ /* @__PURE__ */ jsx8("div", { className: "relative flex-1 min-h-0 min-w-0", children }),
1220
+ defs.map((def) => {
1221
+ if (!mounted.has(def.id)) return null;
1222
+ const open = active?.id === def.id;
1223
+ const Component = def.component;
1224
+ const Icon = def.icon;
1225
+ return /* @__PURE__ */ jsxs6(
1226
+ "div",
1227
+ {
1228
+ className: cn(
1229
+ "relative flex-shrink-0 flex-col min-h-0 min-w-0 bg-background border-border",
1230
+ open ? "flex" : "hidden",
1231
+ vertical ? "border-t" : side === "left" ? "border-r" : "border-l",
1232
+ isMobile && "absolute inset-x-0 bottom-0 z-20 shadow-lg"
1233
+ ),
1234
+ style: paneStyle,
1235
+ children: [
1236
+ !isMobile && /* @__PURE__ */ jsx8(
1237
+ "div",
1238
+ {
1239
+ role: "separator",
1240
+ "aria-label": t("companion.resize"),
1241
+ onMouseDown: startResize,
1242
+ className: cn(
1243
+ "absolute z-10",
1244
+ vertical ? "inset-x-0 -top-1 h-2 cursor-row-resize" : side === "left" ? "inset-y-0 -right-1 w-2 cursor-col-resize" : "inset-y-0 -left-1 w-2 cursor-col-resize",
1245
+ dragging && "bg-primary/30"
1246
+ )
1247
+ }
1248
+ ),
1249
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center gap-1.5 h-8 px-2 border-b border-border flex-shrink-0", children: [
1250
+ Icon && /* @__PURE__ */ jsx8(Icon, { className: "size-3.5 text-muted-foreground" }),
1251
+ /* @__PURE__ */ jsx8("span", { className: "text-xs font-medium truncate flex-1", children: def.titleKey ? t(def.titleKey) : def.id }),
1252
+ !isMobile && /* @__PURE__ */ jsx8(
1253
+ "button",
1254
+ {
1255
+ type: "button",
1256
+ className: "p-1 text-muted-foreground hover:text-foreground",
1257
+ title: t("companion.moveSide"),
1258
+ onClick: () => setSide(
1259
+ side === "right" ? "bottom" : side === "bottom" ? "left" : "right"
1260
+ ),
1261
+ children: /* @__PURE__ */ jsx8(SideIcon, { className: "size-3.5" })
1262
+ }
1263
+ ),
1264
+ /* @__PURE__ */ jsx8(
1265
+ "button",
1266
+ {
1267
+ type: "button",
1268
+ className: "p-1 text-muted-foreground hover:text-foreground",
1269
+ title: t("companion.close"),
1270
+ onClick: close,
1271
+ children: /* @__PURE__ */ jsx8(X, { className: "size-3.5" })
1272
+ }
1273
+ )
1274
+ ] }),
1275
+ /* @__PURE__ */ jsx8("div", { className: "flex-1 min-h-0 min-w-0 overflow-hidden", children: /* @__PURE__ */ jsx8(
1276
+ Component,
1277
+ {
1278
+ tab,
1279
+ host: tab.host,
1280
+ sshHost,
1281
+ getHandle,
1282
+ side,
1283
+ isVisible: isVisible && open,
1284
+ close,
1285
+ setSide
1286
+ }
1287
+ ) })
1288
+ ]
1289
+ },
1290
+ def.id
1291
+ );
1292
+ })
1293
+ ]
1294
+ }
1295
+ ),
1296
+ showEdge.length > 0 && !isMobile && /* @__PURE__ */ jsx8("div", { className: "flex flex-col flex-shrink-0 w-6 items-center gap-1 pt-1 border-l border-border bg-background", children: showEdge.map((def) => {
1297
+ const Icon = def.icon ?? PanelRight;
1298
+ const open = active?.id === def.id;
1299
+ return /* @__PURE__ */ jsx8(
1300
+ "button",
1301
+ {
1302
+ type: "button",
1303
+ title: def.titleKey ? t(def.titleKey) : def.id,
1304
+ "aria-pressed": open,
1305
+ onClick: () => patchCompanionState(key, {
1306
+ seen: true,
1307
+ openId: open ? null : def.id
1308
+ }),
1309
+ className: cn(
1310
+ "p-1 text-muted-foreground hover:text-foreground",
1311
+ open && "text-foreground bg-accent"
1312
+ ),
1313
+ children: /* @__PURE__ */ jsx8(Icon, { className: "size-3.5" })
1314
+ },
1315
+ def.id
1316
+ );
1317
+ }) })
1318
+ ] });
1319
+ }
1320
+
1321
+ // src/ui/shell/tabUtils.tsx
1322
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
1323
+ var PluginsTab = lazy(
1324
+ () => import("./PluginsTab-CM2B33BD.js").then((m) => ({ default: m.PluginsTab }))
1325
+ );
1326
+ function preloadTabSurface(type) {
1327
+ const loader = getTabType(type)?.preload;
1328
+ if (loader) runAdaptiveBackgroundTask("module", `tab:${type}`, loader);
1329
+ }
1330
+ function markTabSurfaceUsed(type) {
1331
+ markAdaptiveResourceUsed("module", `tab:${type}`);
1332
+ }
1333
+ function EmptyState({
1334
+ icon: Icon,
1335
+ messageKey
1336
+ }) {
1337
+ const { t } = useTranslation6();
1338
+ return /* @__PURE__ */ jsxs7("div", { className: "flex flex-col items-center justify-center flex-1 gap-3 p-6 text-center", children: [
1339
+ /* @__PURE__ */ jsx9("div", { className: "size-10 rounded-full bg-muted/40 flex items-center justify-center", children: /* @__PURE__ */ jsx9(Icon, { className: "size-5 text-muted-foreground/30" }) }),
1340
+ /* @__PURE__ */ jsx9("span", { className: "text-sm font-semibold text-muted-foreground/60", children: t(messageKey) })
1341
+ ] });
1342
+ }
1343
+ function TabChunkFallback() {
1344
+ return /* @__PURE__ */ jsx9("div", { className: "flex h-full w-full items-center justify-center bg-background", children: /* @__PURE__ */ jsx9("div", { className: "size-5 rounded-full border-2 border-muted-foreground/30 border-t-muted-foreground/70 animate-spin" }) });
1345
+ }
1346
+ function withTabSuspense(node) {
1347
+ return /* @__PURE__ */ jsx9(Suspense, { fallback: /* @__PURE__ */ jsx9(TabChunkFallback, {}), children: node });
1348
+ }
1349
+ function PanelTabFrame({ children }) {
1350
+ return /* @__PURE__ */ jsx9("div", { className: "flex h-full w-full justify-center overflow-y-auto bg-background", children: /* @__PURE__ */ jsx9("div", { className: "flex flex-col flex-1 min-h-0 w-full max-w-5xl", children }) });
1351
+ }
1352
+ function tabIcon(type) {
1353
+ switch (type) {
1354
+ case "user-profile":
1355
+ case "admin-settings":
1356
+ return /* @__PURE__ */ jsx9(Settings, { className: "size-3.5" });
1357
+ case "split-screen":
1358
+ return /* @__PURE__ */ jsx9(LayoutPanelLeft, { className: "size-3.5" });
1359
+ case "plugins":
1360
+ return /* @__PURE__ */ jsx9(Puzzle, { className: "size-3.5" });
1361
+ default: {
1362
+ const Icon = getTabType(type)?.icon;
1363
+ return Icon ? /* @__PURE__ */ jsx9(Icon, { className: "size-3.5" }) : null;
1364
+ }
1365
+ }
1366
+ }
1367
+ var RegisteredTab = memo2(function RegisteredTab2({
1368
+ def,
1369
+ tab,
1370
+ isVisible,
1371
+ isFocusedPane,
1372
+ inSplit,
1373
+ shell
1374
+ }) {
1375
+ const { host, label } = tab;
1376
+ if (def.requiresHost && !host) {
1377
+ return /* @__PURE__ */ jsx9(
1378
+ EmptyState,
1379
+ {
1380
+ icon: Server,
1381
+ messageKey: def.noHostMessageKey ?? "hosts.noHostSelected"
1382
+ }
1383
+ );
1384
+ }
1385
+ const Component = def.component;
1386
+ const content = /* @__PURE__ */ jsx9(
1387
+ Component,
1388
+ {
1389
+ tab,
1390
+ host,
1391
+ sshHost: host ? hostToSSHHost(host) : void 0,
1392
+ label,
1393
+ isVisible,
1394
+ isFocusedPane,
1395
+ inSplit,
1396
+ handleRef: trackTabHandle(
1397
+ tab.terminalRef
1398
+ ),
1399
+ shell
1400
+ }
1401
+ );
1402
+ return withTabSuspense(
1403
+ /* @__PURE__ */ jsx9(CompanionFrame, { tab, isVisible, children: def.panelFrame ? /* @__PURE__ */ jsx9(PanelTabFrame, { children: content }) : content })
1404
+ );
1405
+ });
1406
+ var PanelAsTab = memo2(function PanelAsTab2({
1407
+ panel,
1408
+ isVisible,
1409
+ targetTab,
1410
+ shell
1411
+ }) {
1412
+ const Panel = panel.component;
1413
+ return withTabSuspense(
1414
+ /* @__PURE__ */ jsx9(PanelTabFrame, { children: /* @__PURE__ */ jsx9(
1415
+ Panel,
1416
+ {
1417
+ targetTab,
1418
+ active: isVisible,
1419
+ shell,
1420
+ setEditing: noop,
1421
+ placement: "tab"
1422
+ }
1423
+ ) })
1424
+ );
1425
+ });
1426
+ function noop() {
1427
+ }
1428
+ function renderTabContent(tab, context) {
1429
+ const {
1430
+ shell,
1431
+ isVisible = true,
1432
+ isFocusedPane = true,
1433
+ inSplit = false
1434
+ } = context;
1435
+ switch (tab.type) {
1436
+ case "split-screen":
1437
+ return null;
1438
+ case "plugins":
1439
+ return withTabSuspense(/* @__PURE__ */ jsx9(PluginsTab, {}));
1440
+ case "user-profile":
1441
+ case "admin-settings":
1442
+ return null;
1443
+ default: {
1444
+ const def = getTabType(tab.type);
1445
+ if (!def && tab.type === "dashboard") return null;
1446
+ if (!def) {
1447
+ const panel = getPanel(tab.type);
1448
+ if (panel) {
1449
+ return /* @__PURE__ */ jsx9(
1450
+ PanelAsTab,
1451
+ {
1452
+ panel,
1453
+ isVisible,
1454
+ targetTab: context.panelTargetTab,
1455
+ shell
1456
+ }
1457
+ );
1458
+ }
1459
+ return /* @__PURE__ */ jsx9(PluginViewPlaceholder, { kind: "tab", viewId: tab.type });
1460
+ }
1461
+ return /* @__PURE__ */ jsx9(
1462
+ RegisteredTab,
1463
+ {
1464
+ def,
1465
+ tab,
1466
+ isVisible,
1467
+ isFocusedPane,
1468
+ inSplit,
1469
+ shell
1470
+ }
1471
+ );
1472
+ }
1473
+ }
1474
+ }
1475
+
1476
+ // src/ui/lib/local-adaptive-preferences.ts
1477
+ init_define_import_meta_env();
1478
+ var LOCAL_ADAPTIVE_PREFERENCES_KEY = LOCAL_ADAPTIVE_ENGINE_KEY;
1479
+ function recordLocalPreference(scope, action, now = Date.now()) {
1480
+ recordLocalAdaptiveChoice(scope, action, now);
1481
+ }
1482
+ function getLocalPreference(scope, candidates, fallback, now = Date.now()) {
1483
+ return chooseLocalAdaptiveAction({
1484
+ scope,
1485
+ candidates,
1486
+ fallback,
1487
+ now
1488
+ }).action;
1489
+ }
1490
+ function clearLocalAdaptivePreferences() {
1491
+ clearLocalAdaptiveEngine();
1492
+ }
1493
+ var hostActionScope = (hostId) => `host-action:${hostId}`;
1494
+ function recordHostActionPreference(hostId, action) {
1495
+ recordLocalPreference(hostActionScope(hostId), action);
1496
+ }
1497
+ function getPreferredHostAction(hostId, candidates, fallback) {
1498
+ return getLocalPreference(hostActionScope(hostId), candidates, fallback);
1499
+ }
1500
+
1501
+ export {
1502
+ Checkbox2 as Checkbox,
1503
+ PasswordInput,
1504
+ Textarea,
1505
+ useInstalledPlugins,
1506
+ SettingsFieldRow,
1507
+ usePluginHostSections,
1508
+ HostPluginSections,
1509
+ HostFeatureFields,
1510
+ ActionSlot,
1511
+ ComponentSlot,
1512
+ trackTabHandle,
1513
+ useTabHandlesVersion,
1514
+ PluginViewPlaceholder,
1515
+ markAdaptiveResourceUsed,
1516
+ getAdaptiveResourceBudget,
1517
+ runAdaptiveBackgroundTask,
1518
+ preloadTabSurface,
1519
+ markTabSurfaceUsed,
1520
+ tabIcon,
1521
+ renderTabContent,
1522
+ LOCAL_ADAPTIVE_PREFERENCES_KEY,
1523
+ clearLocalAdaptivePreferences,
1524
+ recordHostActionPreference,
1525
+ getPreferredHostAction
1526
+ };