@termix-ssh/plugin-sdk 1.0.21 → 1.0.23

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 (45) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/PLUGIN-GUIDE.md +130 -3
  3. package/cli/commands/build.mjs +55 -36
  4. package/cli/commands/validate.mjs +4 -0
  5. package/dist/backend.d.ts +139 -7
  6. package/dist/capabilities.js +3 -0
  7. package/dist/frontend.d.ts +7 -0
  8. package/dist/host/{PluginsTab-HQGSDSHS.js → PluginsTab-MNQU5OJ3.js} +22 -14
  9. package/dist/host/{chunk-JFQGI4OD.js → chunk-6WLECSNB.js} +71 -12
  10. package/dist/host/chunk-BP42N374.js +53 -0
  11. package/dist/host/{chunk-WWFRBU2E.js → chunk-C52NRTA6.js} +82 -51
  12. package/dist/host/{chunk-GTHINW3L.js → chunk-DN6YKMFJ.js} +34650 -34968
  13. package/dist/host/chunk-GBBKHJRQ.js +1526 -0
  14. package/dist/host/{chunk-FGX27MAA.js → chunk-GRH7GSKG.js} +1 -1
  15. package/dist/host/chunk-IMPLAAUM.js +345 -0
  16. package/dist/host/{chunk-A5CULDWV.js → chunk-X65V7ZRW.js} +121 -1574
  17. package/dist/host/core-classes.json +1 -1
  18. package/dist/host/frame-host.css +2 -0
  19. package/dist/host/frame-host.js +242 -0
  20. package/dist/host/{linked-server-UDSFY3N7.js → linked-server-YCAJ2NX3.js} +1 -1
  21. package/dist/host/{rbac-api-AAAPMF52.js → rbac-api-A64TXDL5.js} +5 -1
  22. package/dist/host/testing-host.js +9 -6
  23. package/dist/host/types/src/types/host-sidebar-preferences.d.ts +2 -0
  24. package/dist/host/types/src/types/index.d.ts +8 -0
  25. package/dist/host/types/src/ui/api/plugins-api.d.ts +2 -1
  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/db-health-monitor.d.ts +5 -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/use-plugins-manager.d.ts +1 -1
  33. package/dist/host/types/src/ui/shell/tab-registry.d.ts +2 -0
  34. package/dist/host/ui.js +357 -370
  35. package/dist/host/{use-permissions-VAND5GEG.js → use-permissions-FLQAN6I7.js} +1 -1
  36. package/dist/host-commands.d.ts +7 -0
  37. package/dist/host-commands.js +35 -5
  38. package/dist/manifest.d.ts +33 -1
  39. package/dist/manifest.js +114 -1
  40. package/dist/operations.d.ts +222 -0
  41. package/dist/operations.js +650 -0
  42. package/dist/testing.d.ts +9 -1
  43. package/dist/testing.js +45 -0
  44. package/package.json +9 -1
  45. package/schema/manifest.schema.json +67 -0
@@ -29,12 +29,19 @@ import {
29
29
  sortPlugins,
30
30
  uploadEntry,
31
31
  youtubeEmbedUrl
32
- } from "./chunk-WWFRBU2E.js";
32
+ } from "./chunk-C52NRTA6.js";
33
+ import {
34
+ Button,
35
+ FakeSwitch,
36
+ Input,
37
+ PluginIcon,
38
+ SectionCard,
39
+ hasSettingsPage
40
+ } from "./chunk-IMPLAAUM.js";
33
41
  import {
34
42
  ArrowDownWideNarrow,
35
43
  BookOpen,
36
44
  Bug,
37
- Button,
38
45
  Check,
39
46
  ChevronLeft,
40
47
  ChevronRight,
@@ -44,13 +51,11 @@ import {
44
51
  Download,
45
52
  ExternalLink,
46
53
  Facts,
47
- FakeSwitch,
48
54
  FlaskConical,
49
55
  GRID_SIZES,
50
56
  Globe,
51
57
  GroupHeading,
52
58
  InlineView,
53
- Input,
54
59
  Library,
55
60
  Lightbulb,
56
61
  ListFilter,
@@ -58,12 +63,10 @@ import {
58
63
  PanePrompt,
59
64
  PanelShell,
60
65
  Pin,
61
- PluginIcon,
62
66
  Puzzle,
63
67
  RefreshCw,
64
68
  RotateCcw,
65
69
  Search,
66
- SectionCard,
67
70
  Select,
68
71
  SelectContent,
69
72
  SelectItem,
@@ -88,7 +91,6 @@ import {
88
91
  getPluginRegistry,
89
92
  getPlugins,
90
93
  getRegistries,
91
- hasSettingsPage,
92
94
  installPlugin,
93
95
  installUploadedPlugin,
94
96
  openPluginSettings,
@@ -107,8 +109,8 @@ import {
107
109
  updatePlugin,
108
110
  uploadPlugin,
109
111
  useConfirm
110
- } from "./chunk-GTHINW3L.js";
111
- import "./chunk-JFQGI4OD.js";
112
+ } from "./chunk-DN6YKMFJ.js";
113
+ import "./chunk-6WLECSNB.js";
112
114
  import {
113
115
  __toESM,
114
116
  define_import_meta_env_default,
@@ -1560,9 +1562,12 @@ function usePluginsManager() {
1560
1562
  [run, t, failToast]
1561
1563
  );
1562
1564
  const requestUpload = useCallback2(
1563
- async (file) => {
1565
+ async (file, signatureFile) => {
1564
1566
  try {
1565
- const preview = await uploadPlugin(file);
1567
+ const preview = await uploadPlugin(
1568
+ file,
1569
+ signatureFile ? await signatureFile.text() : void 0
1570
+ );
1566
1571
  setConsent({
1567
1572
  plugin: uploadEntry(preview),
1568
1573
  mode: "upload",
@@ -2576,12 +2581,15 @@ function UploadButton({ manager }) {
2576
2581
  {
2577
2582
  ref: input,
2578
2583
  type: "file",
2579
- accept: ".tmxplug",
2584
+ accept: ".tmxplug,.sig",
2585
+ multiple: true,
2580
2586
  className: "hidden",
2581
2587
  onChange: (e) => {
2582
- const file = e.target.files?.[0];
2588
+ const files = [...e.target.files ?? []];
2583
2589
  e.target.value = "";
2584
- if (file) void manager.requestUpload(file);
2590
+ const file = files.find((f) => f.name.endsWith(".tmxplug"));
2591
+ const signature = files.find((f) => f.name.endsWith(".sig"));
2592
+ if (file) void manager.requestUpload(file, signature);
2585
2593
  }
2586
2594
  }
2587
2595
  ),
@@ -4428,7 +4428,7 @@ var FrontendLogger = class {
4428
4428
  this.serviceName = serviceName;
4429
4429
  this.serviceIcon = serviceIcon;
4430
4430
  this.serviceColor = serviceColor;
4431
- this.isDevelopment = true;
4431
+ this.isDevelopment = false;
4432
4432
  }
4433
4433
  getTimeStamp() {
4434
4434
  const now = /* @__PURE__ */ new Date();
@@ -4691,6 +4691,8 @@ var DatabaseHealthMonitor = class _DatabaseHealthMonitor {
4691
4691
  static instance;
4692
4692
  listeners = /* @__PURE__ */ new Map();
4693
4693
  degradedActive = false;
4694
+ probe = null;
4695
+ probing = null;
4694
4696
  constructor() {
4695
4697
  }
4696
4698
  static getInstance() {
@@ -4729,10 +4731,28 @@ var DatabaseHealthMonitor = class _DatabaseHealthMonitor {
4729
4731
  const errorCode = errorLike.response?.data?.code || errorLike.code;
4730
4732
  const lowerMessage = errorMessage.toLowerCase();
4731
4733
  const isDatabaseError = lowerMessage.includes("database") || lowerMessage.includes("sqlite") || lowerMessage.includes("drizzle") || errorCode === "DATABASE_ERROR" || errorCode === "DB_CONNECTION_FAILED";
4732
- const isBackendUnreachable = errorCode === "ERR_NETWORK" || errorCode === "ECONNREFUSED" || errorCode === "ECONNABORTED" || errorCode === "ECONNRESET" || errorCode === "ETIMEDOUT" || errorCode === "ERR_CANCELED" || lowerMessage.includes("network error") && errorLike.response === void 0 || lowerMessage.includes("request aborted") || lowerMessage.includes("timeout");
4733
- if (!(isDatabaseError || isBackendUnreachable)) {
4734
- return;
4734
+ if (errorCode === "ERR_CANCELED" || lowerMessage === "canceled") return;
4735
+ const isBackendUnreachable = errorCode === "ERR_NETWORK" || errorCode === "ECONNREFUSED" || errorCode === "ECONNRESET" || lowerMessage.includes("network error") && errorLike.response === void 0;
4736
+ const isTimeout = errorCode === "ECONNABORTED" || errorCode === "ETIMEDOUT" || lowerMessage.includes("request aborted") || lowerMessage.includes("timeout");
4737
+ if (isDatabaseError || isBackendUnreachable) {
4738
+ this.markDegraded(errorMessage, errorCode);
4739
+ } else if (isTimeout) {
4740
+ if (!this.probe) {
4741
+ this.markDegraded(errorMessage, errorCode);
4742
+ return;
4743
+ }
4744
+ this.probing ??= this.probe().catch(() => false).then((reachable) => {
4745
+ if (!reachable) this.markDegraded(errorMessage, errorCode);
4746
+ }).finally(() => {
4747
+ this.probing = null;
4748
+ });
4735
4749
  }
4750
+ }
4751
+ /** Sets the quick check used after a request times out. */
4752
+ setReachabilityProbe(probe) {
4753
+ this.probe = probe;
4754
+ }
4755
+ markDegraded(errorMessage, errorCode) {
4736
4756
  if (!this.degradedActive) {
4737
4757
  this.degradedActive = true;
4738
4758
  this.emit("database-connection-degraded", {
@@ -5205,6 +5225,7 @@ function defaultHostSidebarPreferences() {
5205
5225
  statusColorScheme: "accent",
5206
5226
  openOnDoubleClick: false,
5207
5227
  showFolderPaths: true,
5228
+ stripedRows: true,
5208
5229
  hostClickBehavior: "newTab",
5209
5230
  closeOnOpen: false,
5210
5231
  barActions: {},
@@ -5273,6 +5294,7 @@ function sanitizeHostSidebarPreferences(input) {
5273
5294
  ) ? displayObj.statusColorScheme : defaults2.display.statusColorScheme,
5274
5295
  openOnDoubleClick: typeof displayObj.openOnDoubleClick === "boolean" ? displayObj.openOnDoubleClick : defaults2.display.openOnDoubleClick,
5275
5296
  showFolderPaths: typeof displayObj.showFolderPaths === "boolean" ? displayObj.showFolderPaths : defaults2.display.showFolderPaths,
5297
+ stripedRows: typeof displayObj.stripedRows === "boolean" ? displayObj.stripedRows : defaults2.display.stripedRows,
5276
5298
  hostClickBehavior: HOST_CLICK_BEHAVIORS.includes(
5277
5299
  displayObj.hostClickBehavior
5278
5300
  ) ? displayObj.hostClickBehavior : defaults2.display.hostClickBehavior,
@@ -6318,7 +6340,7 @@ async function updateRole(roleId, roleData) {
6318
6340
  try {
6319
6341
  const response = await rbacApi.put(`/rbac/roles/${roleId}`, roleData);
6320
6342
  if (roleData.permissions) {
6321
- const { notifyPermissionsChanged } = await import("./use-permissions-VAND5GEG.js");
6343
+ const { notifyPermissionsChanged } = await import("./use-permissions-FLQAN6I7.js");
6322
6344
  notifyPermissionsChanged();
6323
6345
  }
6324
6346
  return response.data;
@@ -6404,9 +6426,11 @@ async function getFolderAccess(folder) {
6404
6426
  throw handleApiError(error, "get folder access");
6405
6427
  }
6406
6428
  }
6407
- async function revokeFolderAccess(ruleId) {
6429
+ async function revokeFolderAccess(ruleId, options = {}) {
6408
6430
  try {
6409
- await rbacApi.delete(`/rbac/folder/access/${ruleId}`);
6431
+ await rbacApi.delete(`/rbac/folder/access/${ruleId}`, {
6432
+ params: options.fromHosts ? { fromHosts: "true" } : void 0
6433
+ });
6410
6434
  } catch (error) {
6411
6435
  throw handleApiError(error, "revoke folder access");
6412
6436
  }
@@ -6479,6 +6503,29 @@ async function getHostAuthOverride(hostId, protocol, sharedCopySyncId) {
6479
6503
  throw handleApiError(error, "fetch host authentication override");
6480
6504
  }
6481
6505
  }
6506
+ async function getFolderAuthOverride(folder, protocol, sharedCopySyncId) {
6507
+ try {
6508
+ const target = await getSharedCopyTarget(0, sharedCopySyncId);
6509
+ const response = await target.api.get(`/rbac/folder-auth/${protocol}`, {
6510
+ params: { folder }
6511
+ });
6512
+ return response.data;
6513
+ } catch (error) {
6514
+ throw handleApiError(error, "fetch folder authentication override");
6515
+ }
6516
+ }
6517
+ async function setFolderAuthOverride(folder, protocol, credentialId, sharedCopySyncId) {
6518
+ try {
6519
+ const target = await getSharedCopyTarget(0, sharedCopySyncId);
6520
+ const response = await target.api.put(`/rbac/folder-auth/${protocol}`, {
6521
+ folder,
6522
+ credentialId
6523
+ });
6524
+ return response.data;
6525
+ } catch (error) {
6526
+ throw handleApiError(error, "update folder authentication override");
6527
+ }
6528
+ }
6482
6529
  async function setHostAuthOverride(hostId, protocol, credentialId, sharedCopySyncId) {
6483
6530
  try {
6484
6531
  const target = await getSharedCopyTarget(hostId, sharedCopySyncId);
@@ -6705,7 +6752,7 @@ function createApiInstance(baseURL, serviceName = "API") {
6705
6752
  operation: "request_start"
6706
6753
  };
6707
6754
  const logger = getLoggerForService(serviceName);
6708
- const isDevMode = true;
6755
+ const isDevMode = false;
6709
6756
  if (isDevMode) {
6710
6757
  logger.requestStart(method, fullUrl, context);
6711
6758
  }
@@ -6778,7 +6825,7 @@ function createApiInstance(baseURL, serviceName = "API") {
6778
6825
  operation: "request_success"
6779
6826
  };
6780
6827
  const logger = getLoggerForService(serviceName);
6781
- if (true) {
6828
+ if (false) {
6782
6829
  logger.requestSuccess(
6783
6830
  method,
6784
6831
  fullUrl,
@@ -6816,7 +6863,7 @@ function createApiInstance(baseURL, serviceName = "API") {
6816
6863
  };
6817
6864
  const logger = getLoggerForService(serviceName);
6818
6865
  const isSilentRetry = !!error.config?.__silentRetry;
6819
- if (!isSilentRetry) {
6866
+ if (false) {
6820
6867
  if (status === 401) {
6821
6868
  logger.authError(method, fullUrl, context);
6822
6869
  } else if (status === 0 || !status) {
@@ -6870,7 +6917,7 @@ function isDev() {
6870
6917
  if (isElectron()) {
6871
6918
  return false;
6872
6919
  }
6873
- return window.location.port === "3000" || window.location.port === "5173" || window.location.port === "" || window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1";
6920
+ return false;
6874
6921
  }
6875
6922
  var apiHost = define_import_meta_env_default.VITE_API_HOST || (typeof window !== "undefined" ? window.location.hostname : "localhost");
6876
6923
  function getBackendUrl(path) {
@@ -6900,7 +6947,7 @@ function createRemoteOriginApiInstance(path) {
6900
6947
  });
6901
6948
  instance.interceptors.request.use(
6902
6949
  async (config) => {
6903
- const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-UDSFY3N7.js");
6950
+ const { getLinkedSession: getLinkedSession2 } = await import("./linked-server-YCAJ2NX3.js");
6904
6951
  const linked = await getLinkedSession2();
6905
6952
  config.baseURL = linked ? `${linked.serverUrl}${path}` : "http://no-server-configured";
6906
6953
  if (config.headers.set) {
@@ -6927,6 +6974,16 @@ function initializeApiInstances() {
6927
6974
  hostApi = createApiInstance(getApiUrl("/host", 30001), "HOST");
6928
6975
  sshHostApi = hostApi;
6929
6976
  authApi = createApiInstance(getApiUrl("", 30001), "AUTH");
6977
+ const healthApi = authApi;
6978
+ dbHealthMonitor.setReachabilityProbe(
6979
+ () => healthApi.get("/health", {
6980
+ timeout: 5e3,
6981
+ __silentRetry: true
6982
+ }).then(
6983
+ () => true,
6984
+ () => false
6985
+ )
6986
+ );
6930
6987
  rbacApi = createApiInstance(getApiUrl("", 30001), "RBAC");
6931
6988
  }
6932
6989
  var hostApi;
@@ -7455,6 +7512,8 @@ export {
7455
7512
  getSharedHosts,
7456
7513
  revokeHostAccess,
7457
7514
  getHostAuthOverride,
7515
+ getFolderAuthOverride,
7516
+ setFolderAuthOverride,
7458
7517
  setHostAuthOverride,
7459
7518
  shareCredential,
7460
7519
  getCredentialAccess,
@@ -0,0 +1,53 @@
1
+ const __termixNoGlob = () => ({});
2
+ import {
3
+ useTheme
4
+ } from "./chunk-X65V7ZRW.js";
5
+ import {
6
+ init_define_import_meta_env
7
+ } from "./chunk-YGYBM7SQ.js";
8
+
9
+ // src/ui/components/sonner.tsx
10
+ init_define_import_meta_env();
11
+ import { Toaster as Sonner } from "sonner";
12
+ import { jsx } from "react/jsx-runtime";
13
+ var Toaster = ({ ...props }) => {
14
+ const { theme = "system" } = useTheme();
15
+ return /* @__PURE__ */ jsx(
16
+ Sonner,
17
+ {
18
+ theme,
19
+ className: "toaster group",
20
+ toastOptions: {
21
+ classNames: {
22
+ toast: "group toast group-[.toaster]:bg-card group-[.toaster]:text-card-foreground group-[.toaster]:border-border group-[.toaster]:rounded-none! group-[.toaster]:shadow-lg group-[.toaster]:font-mono",
23
+ description: "group-[.toast]:text-muted-foreground",
24
+ actionButton: "group-[.toast]:bg-accent-brand group-[.toast]:text-white group-[.toast]:font-semibold",
25
+ cancelButton: "group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
26
+ success: "group-[.toast]:!text-accent-brand group-[.toast]:!border-accent-brand/30",
27
+ info: "group-[.toast]:!text-accent-brand group-[.toast]:!border-accent-brand/30",
28
+ error: "group-[.toast]:!text-destructive group-[.toast]:!border-destructive/30"
29
+ }
30
+ },
31
+ style: {
32
+ "--radius": "0px",
33
+ "--normal-bg": "var(--card)",
34
+ "--normal-border": "var(--border)",
35
+ "--normal-text": "var(--card-foreground)",
36
+ "--success-bg": "var(--card)",
37
+ "--success-border": "var(--border)",
38
+ "--success-text": "var(--color-accent-brand)",
39
+ "--info-bg": "var(--card)",
40
+ "--info-border": "var(--border)",
41
+ "--info-text": "var(--color-accent-brand)",
42
+ "--error-bg": "var(--card)",
43
+ "--error-border": "var(--border)",
44
+ "--error-text": "var(--destructive)"
45
+ },
46
+ ...props
47
+ }
48
+ );
49
+ };
50
+
51
+ export {
52
+ Toaster
53
+ };
@@ -1,6 +1,8 @@
1
1
  const __termixNoGlob = () => ({});
2
2
  import {
3
- Button,
3
+ Button
4
+ } from "./chunk-IMPLAAUM.js";
5
+ import {
4
6
  ChevronRight,
5
7
  PANEL,
6
8
  Plus,
@@ -8,10 +10,10 @@ import {
8
10
  cn,
9
11
  gridSizeStyle,
10
12
  useAreaPreferences
11
- } from "./chunk-GTHINW3L.js";
13
+ } from "./chunk-DN6YKMFJ.js";
12
14
  import {
13
15
  isElectron
14
- } from "./chunk-JFQGI4OD.js";
16
+ } from "./chunk-6WLECSNB.js";
15
17
  import {
16
18
  __commonJS,
17
19
  __toESM,
@@ -2181,6 +2183,13 @@ function EmptyState({
2181
2183
  // src/ui/components/data-view.tsx
2182
2184
  init_define_import_meta_env();
2183
2185
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
2186
+ function listColumnTemplates(columns) {
2187
+ return {
2188
+ base: columns.filter((column) => column.hideBelow === void 0).map((column) => column.width).join(" ") || "none",
2189
+ md: columns.filter((column) => column.hideBelow !== "lg").map((column) => column.width).join(" ") || "none",
2190
+ lg: columns.map((column) => column.width).join(" ") || "none"
2191
+ };
2192
+ }
2184
2193
  var COLS = {
2185
2194
  1: "grid-cols-1",
2186
2195
  2: "grid-cols-2",
@@ -2205,6 +2214,7 @@ var XL_COLS = {
2205
2214
  3: "xl:grid-cols-3",
2206
2215
  4: "xl:grid-cols-4"
2207
2216
  };
2217
+ var LIST_GRID_CLASS = "grid grid-cols-[var(--data-view-cols)] @min-[48rem]:grid-cols-[var(--data-view-cols-md)] @min-[64rem]:grid-cols-[var(--data-view-cols-lg)] gap-x-2 px-3";
2208
2218
  function gridClass(columns) {
2209
2219
  return cn(
2210
2220
  COLS[columns.base ?? 1],
@@ -2213,6 +2223,11 @@ function gridClass(columns) {
2213
2223
  columns.xl && XL_COLS[columns.xl]
2214
2224
  );
2215
2225
  }
2226
+ function columnVisibility(hideBelow) {
2227
+ if (hideBelow === "md") return "hidden @min-[48rem]:block";
2228
+ if (hideBelow === "lg") return "hidden @min-[64rem]:block";
2229
+ return "";
2230
+ }
2216
2231
  function DataView({
2217
2232
  items,
2218
2233
  view,
@@ -2246,60 +2261,72 @@ function DataView({
2246
2261
  }
2247
2262
  );
2248
2263
  }
2249
- const template = listColumns.map((c) => c.width).join(" ");
2250
- const rowHeight = density === "compact" ? "h-7" : "h-9";
2251
- return /* @__PURE__ */ jsxs2("div", { className: cn("border border-border bg-card", className), role: "table", children: [
2252
- /* @__PURE__ */ jsx3(
2253
- "div",
2254
- {
2255
- className: "sticky top-0 z-10 grid items-center border-b border-border bg-card px-3 text-[10px] font-semibold uppercase tracking-widest text-muted-foreground",
2256
- style: { gridTemplateColumns: template },
2257
- children: listColumns.map((column) => /* @__PURE__ */ jsx3(
2258
- "span",
2264
+ const templates = listColumnTemplates(listColumns);
2265
+ const rowHeight = density === "compact" ? "min-h-7" : "min-h-9";
2266
+ return /* @__PURE__ */ jsxs2(
2267
+ "div",
2268
+ {
2269
+ className: cn("@container border border-border bg-card", className),
2270
+ style: {
2271
+ "--data-view-cols": templates.base,
2272
+ "--data-view-cols-md": templates.md,
2273
+ "--data-view-cols-lg": templates.lg
2274
+ },
2275
+ role: "table",
2276
+ children: [
2277
+ /* @__PURE__ */ jsx3(
2278
+ "div",
2259
2279
  {
2260
2280
  className: cn(
2261
- "truncate py-2",
2262
- column.align === "end" && "text-right",
2263
- column.hideBelow === "md" && "hidden md:block",
2264
- column.hideBelow === "lg" && "hidden lg:block"
2281
+ LIST_GRID_CLASS,
2282
+ "sticky top-0 z-10 items-center border-b border-border bg-card text-[10px] font-semibold uppercase tracking-widest text-muted-foreground"
2265
2283
  ),
2266
- children: column.header
2267
- },
2268
- column.key
2269
- ))
2270
- }
2271
- ),
2272
- /* @__PURE__ */ jsx3("div", { children: items.map((item) => {
2273
- const active = isRowActive?.(item) ?? false;
2274
- return /* @__PURE__ */ jsx3(
2275
- "div",
2276
- {
2277
- onClick: onRowClick ? () => onRowClick(item) : void 0,
2278
- className: cn(
2279
- "grid items-center gap-x-2 border-b border-border/50 px-3 text-xs transition-colors last:border-0",
2280
- rowHeight,
2281
- onRowClick && "cursor-pointer",
2282
- active ? "bg-accent-brand/10" : "hover:bg-muted/40"
2283
- ),
2284
- style: { gridTemplateColumns: template },
2285
- children: listColumns.map((column) => /* @__PURE__ */ jsx3(
2284
+ children: listColumns.map((column) => /* @__PURE__ */ jsx3(
2285
+ "span",
2286
+ {
2287
+ className: cn(
2288
+ "truncate py-2",
2289
+ column.align === "end" && "text-right",
2290
+ columnVisibility(column.hideBelow)
2291
+ ),
2292
+ children: column.header
2293
+ },
2294
+ column.key
2295
+ ))
2296
+ }
2297
+ ),
2298
+ /* @__PURE__ */ jsx3("div", { children: items.map((item) => {
2299
+ const active = isRowActive?.(item) ?? false;
2300
+ return /* @__PURE__ */ jsx3(
2286
2301
  "div",
2287
2302
  {
2303
+ onClick: onRowClick ? () => onRowClick(item) : void 0,
2288
2304
  className: cn(
2289
- "min-w-0 truncate",
2290
- column.align === "end" && "text-right",
2291
- column.hideBelow === "md" && "hidden md:block",
2292
- column.hideBelow === "lg" && "hidden lg:block"
2305
+ LIST_GRID_CLASS,
2306
+ "items-center border-b border-border/50 text-xs transition-colors last:border-0",
2307
+ rowHeight,
2308
+ onRowClick && "cursor-pointer",
2309
+ active ? "bg-accent-brand/10" : "hover:bg-muted/40"
2293
2310
  ),
2294
- children: column.cell(item)
2311
+ children: listColumns.map((column) => /* @__PURE__ */ jsx3(
2312
+ "div",
2313
+ {
2314
+ className: cn(
2315
+ "min-w-0 truncate",
2316
+ column.align === "end" && "text-right",
2317
+ columnVisibility(column.hideBelow)
2318
+ ),
2319
+ children: column.cell(item)
2320
+ },
2321
+ column.key
2322
+ ))
2295
2323
  },
2296
- column.key
2297
- ))
2298
- },
2299
- getKey(item)
2300
- );
2301
- }) })
2302
- ] });
2324
+ getKey(item)
2325
+ );
2326
+ }) })
2327
+ ]
2328
+ }
2329
+ );
2303
2330
  }
2304
2331
 
2305
2332
  // src/ui/lib/issue-url.ts
@@ -2402,6 +2429,8 @@ var CAPABILITY_CATALOG = [
2402
2429
  entry("ssh:transport", "high"),
2403
2430
  entry("device:serial", "high"),
2404
2431
  entry("notify:hub", "high"),
2432
+ entry("operations:invoke", "high"),
2433
+ entry("operations:approve", "high"),
2405
2434
  entry("ui:layout", "high"),
2406
2435
  entry("hosts:write", "medium"),
2407
2436
  entry("hosts:export", "high"),
@@ -2417,6 +2446,7 @@ var CAPABILITY_CATALOG = [
2417
2446
  entry("users:read", "medium"),
2418
2447
  entry("events:core", "medium"),
2419
2448
  entry("notify:send", "medium"),
2449
+ entry("operations:provide", "medium"),
2420
2450
  entry("audit:read", "medium"),
2421
2451
  entry("desktop:window", "medium"),
2422
2452
  entry("desktop:local", "high"),
@@ -2513,8 +2543,9 @@ function mergePlugins(installed, registry) {
2513
2543
  latestBeta: entry2?.latestBeta ?? null,
2514
2544
  channel: summary.channel ?? entry2?.channel ?? "stable",
2515
2545
  isBeta: isPrerelease(summary.version),
2516
- // The server keeps its own copy up to date.
2517
- updateAvailable: summary.runsOnServer ? false : entry2?.updateAvailable ?? false,
2546
+ // The server keeps its own copy up to date, and a plugin that is off
2547
+ // should not ask for attention.
2548
+ updateAvailable: summary.runsOnServer || !summary.enabled ? false : entry2?.updateAvailable ?? false,
2518
2549
  addedCapabilities: entry2?.addedCapabilities ?? [],
2519
2550
  versions: entry2?.versions ?? [],
2520
2551
  capabilities: summary.capabilities ?? [],