@peers-app/peers-ui 0.26.4 → 0.26.6

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 (59) hide show
  1. package/dist/components/group-switcher.d.ts +6 -0
  2. package/dist/components/group-switcher.js +8 -1
  3. package/dist/components/markdown-with-mentions.d.ts +4 -0
  4. package/dist/components/markdown-with-mentions.js +6 -1
  5. package/dist/components/save-button.d.ts +15 -0
  6. package/dist/components/save-button.js +14 -3
  7. package/dist/globals.js +34 -8
  8. package/dist/host-shell/host-shell-consumer.d.ts +21 -0
  9. package/dist/host-shell/host-shell-consumer.js +50 -0
  10. package/dist/host-shell/host-shell-consumer.test.d.ts +1 -0
  11. package/dist/host-shell/host-shell-consumer.test.js +28 -0
  12. package/dist/mention-configs.js +2 -1
  13. package/dist/screens/groups/group-list.js +4 -1
  14. package/dist/screens/packages/package-details.d.ts +1 -0
  15. package/dist/screens/packages/package-details.js +27 -8
  16. package/dist/screens/packages/package-helpers.d.ts +3 -23
  17. package/dist/screens/packages/package-helpers.js +12 -46
  18. package/dist/screens/packages/package-helpers.test.d.ts +1 -0
  19. package/dist/screens/packages/package-helpers.test.js +53 -0
  20. package/dist/screens/packages/package-info.js +3 -2
  21. package/dist/screens/packages/package-list.d.ts +4 -2
  22. package/dist/screens/packages/package-list.js +50 -10
  23. package/dist/screens/packages/package-new-local.js +7 -4
  24. package/dist/screens/packages/package-runtime-status.d.ts +12 -0
  25. package/dist/screens/packages/package-runtime-status.js +33 -0
  26. package/dist/screens/packages/package-runtime-status.test.d.ts +1 -0
  27. package/dist/screens/packages/package-runtime-status.test.js +50 -0
  28. package/dist/screens/packages/package-versions.js +8 -55
  29. package/dist/screens/packages/packages-ipc.d.ts +41 -0
  30. package/dist/screens/packages/packages-ipc.js +102 -0
  31. package/dist/screens/packages/packages-ipc.test.d.ts +1 -0
  32. package/dist/screens/packages/packages-ipc.test.js +43 -0
  33. package/dist/screens/tools/tool-catalog-ipc.test.js +6 -0
  34. package/dist/screens/variables/variable-details.d.ts +1 -0
  35. package/dist/screens/variables/variable-details.js +11 -5
  36. package/dist/ui-router/ui-loader.js +10 -37
  37. package/package.json +3 -3
  38. package/src/components/group-switcher.tsx +12 -4
  39. package/src/components/markdown-with-mentions.tsx +7 -2
  40. package/src/components/save-button.tsx +24 -3
  41. package/src/globals.tsx +32 -11
  42. package/src/host-shell/host-shell-consumer.test.ts +32 -0
  43. package/src/host-shell/host-shell-consumer.ts +53 -0
  44. package/src/mention-configs.ts +2 -2
  45. package/src/screens/groups/group-list.tsx +6 -4
  46. package/src/screens/packages/package-details.tsx +31 -8
  47. package/src/screens/packages/package-helpers.test.ts +57 -0
  48. package/src/screens/packages/package-helpers.ts +13 -55
  49. package/src/screens/packages/package-info.tsx +3 -3
  50. package/src/screens/packages/package-list.tsx +51 -16
  51. package/src/screens/packages/package-new-local.tsx +8 -13
  52. package/src/screens/packages/package-runtime-status.test.tsx +58 -0
  53. package/src/screens/packages/package-runtime-status.tsx +68 -0
  54. package/src/screens/packages/package-versions.tsx +9 -72
  55. package/src/screens/packages/packages-ipc.test.ts +47 -0
  56. package/src/screens/packages/packages-ipc.ts +125 -0
  57. package/src/screens/tools/tool-catalog-ipc.test.ts +7 -1
  58. package/src/screens/variables/variable-details.tsx +11 -6
  59. package/src/ui-router/ui-loader.tsx +12 -51
@@ -10,9 +10,12 @@ const loading_indicator_1 = require("../../components/loading-indicator");
10
10
  const tooltip_1 = require("../../components/tooltip");
11
11
  const globals_1 = require("../../globals");
12
12
  const hooks_1 = require("../../hooks");
13
+ const host_shell_consumer_1 = require("../../host-shell/host-shell-consumer");
13
14
  const ui_loader_1 = require("../../ui-router/ui-loader");
14
15
  const local_folder_grants_panel_1 = require("./local-folder-grants-panel");
15
16
  const package_helpers_1 = require("./package-helpers");
17
+ const package_runtime_status_1 = require("./package-runtime-status");
18
+ const packages_ipc_1 = require("./packages-ipc");
16
19
  function tagBadgeClass(tag) {
17
20
  if (tag === "dev")
18
21
  return "text-bg-danger";
@@ -31,6 +34,18 @@ const PackageRow = ({ pkg, onUpdated }) => {
31
34
  const [updating, setUpdating] = (0, react_1.useState)(false);
32
35
  const [devicePrefs] = (0, hooks_1.useObservable)((0, peers_sdk_1.packagePrefsVar)(pkg.packageId));
33
36
  const effective = (0, peers_sdk_1.getEffectivePackagePrefs)(pkg, devicePrefs);
37
+ // A dev reload rewrites the selected version row in place (same id, new version), so the
38
+ // ids below do not change; recompute when this package's version rows change.
39
+ const [versionsRevision, setVersionsRevision] = (0, react_1.useState)(0);
40
+ (0, react_1.useEffect)(() => {
41
+ const sub = (0, peers_sdk_1.PackageVersions)().dataChanged.subscribe((evt) => {
42
+ if (evt.dataObject?.packageId === pkg.packageId)
43
+ setVersionsRevision((value) => value + 1);
44
+ });
45
+ return () => {
46
+ sub.unsubscribe();
47
+ };
48
+ }, [pkg.packageId]);
34
49
  const status = (0, hooks_1.usePromise)(async () => {
35
50
  if (!effective.activePackageVersionId)
36
51
  return null;
@@ -65,6 +80,7 @@ const PackageRow = ({ pkg, onUpdated }) => {
65
80
  pkg.packageId,
66
81
  effective.followTags,
67
82
  pkg.activePackageVersionId,
83
+ versionsRevision,
68
84
  ]);
69
85
  const isOnDev = status?.activePv.versionTag === "dev";
70
86
  const hasGroupRelease = !!status?.groupReleasePv;
@@ -91,7 +107,10 @@ const PackageRow = ({ pkg, onUpdated }) => {
91
107
  async function handleUseRelease() {
92
108
  setUpdating(true);
93
109
  try {
94
- await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, { activePackageVersionId: undefined });
110
+ await (await (0, packages_ipc_1.packagesTools)()).activatePackageVersion({
111
+ packageId: pkg.packageId,
112
+ scope: "device",
113
+ });
95
114
  onUpdated();
96
115
  }
97
116
  catch (err) {
@@ -106,8 +125,10 @@ const PackageRow = ({ pkg, onUpdated }) => {
106
125
  return;
107
126
  setUpdating(true);
108
127
  try {
109
- await (0, peers_sdk_1.updatePackagePrefs)(pkg.packageId, {
110
- activePackageVersionId: status.latestDevPv.packageVersionId,
128
+ await (await (0, packages_ipc_1.packagesTools)()).activatePackageVersion({
129
+ packageId: pkg.packageId,
130
+ packageVersionId: status.latestDevPv.packageVersionId,
131
+ scope: "device",
111
132
  });
112
133
  onUpdated();
113
134
  }
@@ -118,10 +139,12 @@ const PackageRow = ({ pkg, onUpdated }) => {
118
139
  setUpdating(false);
119
140
  }
120
141
  }
121
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4 d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", style: { minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#packages/${pkg.packageId}`, children: pkg.name }), isDeviceOverridden && ((0, jsx_runtime_1.jsx)("span", { className: "text-warning ms-1", title: "This device is using a different version than the group", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-pc-display" }) })), status && ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", status.activePv.version] }), status.activePv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${tagBadgeClass(status.activePv.versionTag)}`, style: { fontSize: "0.65em" }, children: status.activePv.versionTag })), hasUpdate ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 ${updateBadgeClass(status.newerLevel)}`, style: { fontSize: "0.65em" }, children: ["v", status.newestPv?.version, " available"] })) : isOnDev && hasGroupRelease ? ((0, jsx_runtime_1.jsxs)("span", { className: "badge ms-1 text-bg-secondary", style: { fontSize: "0.65em" }, children: ["release: v", status.groupReleasePv?.version] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] })), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ["bottom", "top", "right", "left"] })] }), hasUpdate && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary ms-2 text-nowrap", disabled: updating, onClick: handleUpdate, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-up-circle me-1" })), "Update"] })), isOnDev && hasGroupRelease && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary ms-2 text-nowrap", disabled: updating, onClick: handleUseRelease, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" })), "Use Release"] })), hasDevVersion && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-danger ms-2 text-nowrap", disabled: updating, onClick: handleUseDev, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-tools me-1" })), "Use Dev"] }))] }));
142
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid pb-4 d-flex align-items-center", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex-grow-1", style: { minWidth: 0 }, children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-fill" }), "\u00A0\u00A0", (0, jsx_runtime_1.jsx)("a", { href: `#packages/${pkg.packageId}`, children: pkg.name }), (0, jsx_runtime_1.jsx)("span", { className: "ms-2", children: (0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeBadge, { packageId: pkg.packageId }) }), isDeviceOverridden && ((0, jsx_runtime_1.jsx)("span", { className: "text-warning ms-1", title: "This device is using a different version than the group", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-pc-display" }) })), status && ((0, jsx_runtime_1.jsxs)("span", { className: "ms-2", children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["v", status.activePv.version] }), status.activePv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ms-1 ${tagBadgeClass(status.activePv.versionTag)}`, style: { fontSize: "0.65em" }, children: status.activePv.versionTag })), hasUpdate ? ((0, jsx_runtime_1.jsxs)("span", { className: `badge ms-1 ${updateBadgeClass(status.newerLevel)}`, style: { fontSize: "0.65em" }, children: ["v", status.newestPv?.version, " available"] })) : isOnDev && hasGroupRelease ? ((0, jsx_runtime_1.jsxs)("span", { className: "badge ms-1 text-bg-secondary", style: { fontSize: "0.65em" }, children: ["release: v", status.groupReleasePv?.version] })) : ((0, jsx_runtime_1.jsx)("span", { className: "badge ms-1 text-bg-success", style: { fontSize: "0.65em" }, children: "up to date" }))] })), (0, jsx_runtime_1.jsx)(tooltip_1.Tooltip, { markdownContent: pkg.description, positions: ["bottom", "top", "right", "left"] })] }), hasUpdate && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary ms-2 text-nowrap", disabled: updating, onClick: handleUpdate, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-arrow-up-circle me-1" })), "Update"] })), isOnDev && hasGroupRelease && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary ms-2 text-nowrap", disabled: updating, onClick: handleUseRelease, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-arrow-in-right me-1" })), "Use Release"] })), hasDevVersion && ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-danger ms-2 text-nowrap", disabled: updating, onClick: handleUseDev, children: [updating ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-tools me-1" })), "Use Dev"] }))] }));
122
143
  };
123
- /** Searchable list of installed packages, plus create/import from the search box. */
124
- /** Packages screen: search, local folder grants, and the installed package list. */
144
+ /**
145
+ * Packages screen: search, create/import/install from the search box, local folder grants,
146
+ * and the installed package list.
147
+ */
125
148
  const PackageList = () => {
126
149
  const [searchTextObs] = (0, react_1.useState)(() => (0, peers_sdk_1.observable)(""));
127
150
  const [searchText] = (0, hooks_1.useObservable)(searchTextObs);
@@ -201,12 +224,12 @@ const PackageList = () => {
201
224
  const name = searchText.trim();
202
225
  if (!name)
203
226
  return;
204
- if (name.startsWith("http") || name.endsWith(".git")) {
227
+ if ((0, peers_sdk_1.isPackageRepositoryUrl)(name)) {
205
228
  if (!confirm(`Add remote package: ${name}`))
206
229
  return;
207
230
  try {
208
231
  addingPackage(true);
209
- const { packageId } = (0, peers_sdk_1.unwrapAddOrUpdatePackageResult)(await peers_sdk_1.rpcServerCalls.addOrUpdatePackage(name));
232
+ const { packageId } = await (await (0, packages_ipc_1.packagesTools)()).clonePackage({ url: name });
210
233
  const pkg = await (0, peers_sdk_1.Packages)().get(packageId);
211
234
  if (pkg) {
212
235
  (0, globals_1.mainContentPath)(`packages/${packageId}`);
@@ -222,11 +245,28 @@ const PackageList = () => {
222
245
  }
223
246
  return;
224
247
  }
248
+ // A package id re-enables that package, or copies it here from another of your contexts.
249
+ if ((0, peers_sdk_1.isid)(name)) {
250
+ if (!confirm(`Install package ${name} into this context`))
251
+ return;
252
+ try {
253
+ addingPackage(true);
254
+ const { packageId } = await (await (0, packages_ipc_1.packagesTools)()).installPackage({ packageId: name });
255
+ (0, globals_1.mainContentPath)(`packages/${packageId}`);
256
+ }
257
+ catch (err) {
258
+ confirm(`Error installing package: ${err instanceof Error ? err.message : String(err)}`);
259
+ }
260
+ finally {
261
+ addingPackage(false);
262
+ }
263
+ return;
264
+ }
225
265
  if (!confirm(`Create or import local package: ${searchText}`))
226
266
  return;
227
267
  try {
228
268
  addingPackage(true);
229
- const { packageId, warning } = (0, peers_sdk_1.unwrapAddOrUpdatePackageResult)(await peers_sdk_1.rpcServerCalls.addOrUpdatePackage(name));
269
+ const { packageId, warning } = await (await (0, packages_ipc_1.packagesTools)()).createPackage({ name });
230
270
  const pkg = await (0, peers_sdk_1.Packages)().get(packageId);
231
271
  if (pkg) {
232
272
  if (warning) {
@@ -245,7 +285,7 @@ const PackageList = () => {
245
285
  if (addingPackage()) {
246
286
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "container-fluid d-flex justify-content-center mt-5", children: [(0, jsx_runtime_1.jsx)("i", { className: "bi bi-box-seam" }), "\u00A0 Adding Package"] }), (0, jsx_runtime_1.jsx)("div", { className: "container-fluid d-flex justify-content-center mt-2", children: (0, jsx_runtime_1.jsx)("div", { className: "progress", role: "progressbar", "aria-label": "Animated striped example", "aria-valuenow": 100, "aria-valuemin": 0, "aria-valuemax": 100, children: (0, jsx_runtime_1.jsx)("div", { className: "progress-bar progress-bar-striped progress-bar-animated", style: { width: 150 } }) }) })] }));
247
287
  }
248
- return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => peers_sdk_1.rpcServerCalls.openPath(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)(local_folder_grants_panel_1.LocalFolderGrantsPanel, {}), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: `${searchText}_${refreshKey()}`, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
288
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "container-fluid", children: [(0, jsx_runtime_1.jsxs)("div", { className: "input-group mt-3 mb-3", children: [(0, jsx_runtime_1.jsx)(input_1.Input, { value: searchTextObs, className: "form-control", placeholder: "Search, add, or create package", autoFocus: !!(0, globals_1.isDesktop)(), onKeyUp: (evt) => searchSubmit(evt) }), (0, jsx_runtime_1.jsx)("button", { className: "btn btn-outline-secondary dropdown-toggle sm", type: "button", "data-bs-toggle": "dropdown", "aria-expanded": "false", children: (0, jsx_runtime_1.jsx)("i", { className: "bi bi-plus-lg" }) }), (0, jsx_runtime_1.jsxs)("ul", { className: "dropdown-menu dropdown-menu-end", children: [(0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", href: "#packages/newlocal", children: "New Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Local Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item disabled", href: "#", children: "Add Remote Package" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("hr", { className: "dropdown-divider" }) }), (0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsx)("a", { className: "dropdown-item", onClick: () => void (0, host_shell_consumer_1.openOnHost)(peers_sdk_1.packagesRootDir), style: { cursor: "pointer" }, children: "Open Packages Directory" }) })] })] }), (0, jsx_runtime_1.jsx)(local_folder_grants_panel_1.LocalFolderGrantsPanel, {}), (0, jsx_runtime_1.jsx)("div", { className: "peers-list-container", children: (0, jsx_runtime_1.jsx)(lazy_list_1.LazyList, { resetTrigger: `${searchText}_${refreshKey()}`, loadMore: loadMore, scrollThreshold: 0.6, renderItems: (packages) => {
249
289
  return packages.map((pkg) => ((0, jsx_runtime_1.jsx)(PackageRow, { pkg: pkg, onUpdated: () => refreshKey(refreshKey() + 1) }, pkg.packageId)));
250
290
  }, loadingIndicator: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 }, children: (0, jsx_runtime_1.jsx)(loading_indicator_1.LoadingIndicator, {}) }), endOfList: (0, jsx_runtime_1.jsx)("div", { className: "d-flex justify-content-center", style: { height: 200 } }) }) })] }));
251
291
  };
@@ -38,6 +38,8 @@ const jsx_runtime_1 = require("react/jsx-runtime");
38
38
  const peers_sdk_1 = require("@peers-app/peers-sdk");
39
39
  const react_1 = __importStar(require("react"));
40
40
  const globals_1 = require("../../globals");
41
+ const host_shell_consumer_1 = require("../../host-shell/host-shell-consumer");
42
+ const packages_ipc_1 = require("./packages-ipc");
41
43
  /** Form for scaffolding a new local package from the template into `~/peers/packages`. */
42
44
  const PackageNewLocal = () => {
43
45
  const defaultName = "New Package";
@@ -59,16 +61,17 @@ const PackageNewLocal = () => {
59
61
  };
60
62
  const createLocalPackage = async () => {
61
63
  try {
62
- const { packageId, warning } = (0, peers_sdk_1.unwrapAddOrUpdatePackageResult)(await peers_sdk_1.rpcServerCalls.addOrUpdatePackage(packageName, {
63
- packageLocation: packageLocation || packageLocationDefault,
64
- }));
64
+ const { packageId, warning } = await (await (0, packages_ipc_1.packagesTools)()).createPackage({
65
+ name: packageName,
66
+ location: packageLocation || packageLocationDefault,
67
+ });
65
68
  const pkg = await (0, peers_sdk_1.Packages)().get(packageId);
66
69
  if (pkg) {
67
70
  if (warning) {
68
71
  confirm(warning);
69
72
  }
70
73
  (0, globals_1.mainContentPath)(`packages/${packageId}`);
71
- peers_sdk_1.rpcServerCalls.openPackage(packageLocation || packageLocationDefault);
74
+ void (0, host_shell_consumer_1.openInEditorOnHost)(packageLocation || packageLocationDefault);
72
75
  }
73
76
  }
74
77
  catch (err) {
@@ -0,0 +1,12 @@
1
+ interface IPackageRuntimeProps {
2
+ /** Package to show. */
3
+ packageId: string;
4
+ }
5
+ /** Small badge with a package's runtime status on this device; renders nothing until known. */
6
+ export declare const PackageRuntimeBadge: ({ packageId }: IPackageRuntimeProps) => import("react/jsx-runtime").JSX.Element;
7
+ /**
8
+ * Runtime status on this device: badge, running version, and the last load or worker
9
+ * failure.
10
+ */
11
+ export declare const PackageRuntimeStatus: ({ packageId }: IPackageRuntimeProps) => import("react/jsx-runtime").JSX.Element | null;
12
+ export {};
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PackageRuntimeStatus = exports.PackageRuntimeBadge = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const packages_ipc_1 = require("./packages-ipc");
6
+ /** Small badge with a package's runtime status on this device; renders nothing until known. */
7
+ const PackageRuntimeBadge = ({ packageId }) => {
8
+ const states = (0, packages_ipc_1.usePackageRuntimeStates)();
9
+ return (0, jsx_runtime_1.jsx)(RuntimeBadge, { state: states?.[packageId] });
10
+ };
11
+ exports.PackageRuntimeBadge = PackageRuntimeBadge;
12
+ /**
13
+ * Runtime status on this device: badge, running version, and the last load or worker
14
+ * failure.
15
+ */
16
+ const PackageRuntimeStatus = ({ packageId }) => {
17
+ const state = (0, packages_ipc_1.usePackageRuntimeStates)()?.[packageId];
18
+ if (!state)
19
+ return null;
20
+ const running = state.running;
21
+ const workerError = state.worker?.lastWorkerError;
22
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "mb-3", "data-testid": "package-runtime-status", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center gap-2", children: [(0, jsx_runtime_1.jsx)(RuntimeBadge, { state: state }), running?.version ? ((0, jsx_runtime_1.jsxs)("span", { className: "text-muted small", children: ["Running v", running.version, running.versionTag ? ` (${running.versionTag})` : "", " on this device"] })) : state.reason ? ((0, jsx_runtime_1.jsx)("span", { className: "text-muted small", children: state.reason })) : null] }), state.status === "failed" && state.lastError ? ((0, jsx_runtime_1.jsx)(RuntimeError, { title: state.lastError.version
23
+ ? `Loading v${state.lastError.version} failed`
24
+ : "Loading this package failed", message: state.lastError.message })) : null, workerError ? ((0, jsx_runtime_1.jsx)(RuntimeError, { title: "The package worker failed", message: workerError.message })) : null] }));
25
+ };
26
+ exports.PackageRuntimeStatus = PackageRuntimeStatus;
27
+ const RuntimeBadge = ({ state }) => {
28
+ const badge = (0, packages_ipc_1.packageStatusBadge)(state);
29
+ if (!badge)
30
+ return null;
31
+ return ((0, jsx_runtime_1.jsx)("span", { className: `badge text-bg-${badge.color}`, title: state?.lastError?.message, children: badge.label }));
32
+ };
33
+ const RuntimeError = ({ title, message }) => ((0, jsx_runtime_1.jsxs)("div", { className: "alert alert-danger py-2 px-3 mt-2 mb-0 small", role: "alert", children: [(0, jsx_runtime_1.jsxs)("strong", { children: [title, ":"] }), " ", (0, jsx_runtime_1.jsx)("span", { className: "font-monospace", children: message })] }));
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const react_1 = require("@testing-library/react");
5
+ const package_runtime_status_1 = require("./package-runtime-status");
6
+ let mockStates;
7
+ jest.mock("./packages-ipc", () => ({
8
+ ...jest.requireActual("./packages-ipc"),
9
+ usePackageRuntimeStates: () => mockStates,
10
+ }));
11
+ const packageId = "pkg";
12
+ describe("PackageRuntimeStatus", () => {
13
+ it("renders nothing until the host reports state", () => {
14
+ mockStates = undefined;
15
+ expect((0, react_1.render)((0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeStatus, { packageId: packageId })).container).toBeEmptyDOMElement();
16
+ });
17
+ it("shows the running version and a failed replacement's error", () => {
18
+ mockStates = {
19
+ [packageId]: {
20
+ packageId,
21
+ status: "failed",
22
+ running: { kind: "isolated", version: "1.0.0", versionTag: "dev" },
23
+ lastError: { phase: "load", message: "manifest has unknown key", version: "1.0.1", at: 1 },
24
+ updatedAt: 1,
25
+ },
26
+ };
27
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeStatus, { packageId: packageId }));
28
+ expect(view.container).toHaveTextContent("Update failed");
29
+ expect(view.container).toHaveTextContent("Running v1.0.0 (dev) on this device");
30
+ expect(view.getByRole("alert")).toHaveTextContent("Loading v1.0.1 failed: manifest has unknown key");
31
+ });
32
+ it("shows a worker failure and a reason when nothing is running", () => {
33
+ mockStates = {
34
+ [packageId]: {
35
+ packageId,
36
+ status: "unavailable",
37
+ reason: "The selected version's bundle file is not on this device yet",
38
+ worker: { workerRunning: false, lastWorkerError: { message: "boom", at: 1 } },
39
+ updatedAt: 1,
40
+ },
41
+ };
42
+ const view = (0, react_1.render)((0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeStatus, { packageId: packageId }));
43
+ expect(view.container).toHaveTextContent("not on this device yet");
44
+ expect(view.getByRole("alert")).toHaveTextContent("The package worker failed: boom");
45
+ });
46
+ it("renders a running badge", () => {
47
+ mockStates = { [packageId]: { packageId, status: "running", updatedAt: 1 } };
48
+ expect((0, react_1.render)((0, jsx_runtime_1.jsx)(package_runtime_status_1.PackageRuntimeBadge, { packageId: packageId })).container).toHaveTextContent("Running");
49
+ });
50
+ });
@@ -6,6 +6,7 @@ const peers_sdk_1 = require("@peers-app/peers-sdk");
6
6
  const react_1 = require("react");
7
7
  const hooks_1 = require("../../hooks");
8
8
  const package_helpers_1 = require("./package-helpers");
9
+ const packages_ipc_1 = require("./packages-ipc");
9
10
  function formatDate(iso) {
10
11
  try {
11
12
  const d = new Date(iso);
@@ -107,50 +108,16 @@ const PackageVersionsList = (props) => {
107
108
  }
108
109
  async function promoteVersion(pv, newTag) {
109
110
  try {
110
- const me = await (0, peers_sdk_1.getMe)();
111
- const history = [...(pv.history ?? [])];
112
- history.push({
113
- action: `promoted:${newTag}`,
114
- by: me.userId,
115
- at: new Date().toISOString(),
116
- signature: "",
117
- });
118
- const pvHash = (0, peers_sdk_1.computePackageVersionHash)(pv.version, newTag, pv.packageBundleFileHash, pv.routesBundleFileHash, pv.uiBundleFileHash);
119
- // Auto-sign with package author key if available (server/PWA host — key never
120
- // exposed to UI). Soft-fail when the key is missing so group admins can still
121
- // promote, but never keep a signature that was computed for the old tag.
122
- const authorSig = await peers_sdk_1.rpcServerCalls.signPackageAuthorVersion({
123
- packageId: pv.packageId,
111
+ // The host re-tags, re-signs, records history, and activates the version when this
112
+ // device was running it and the group follows the new tag.
113
+ const result = await (await (0, packages_ipc_1.packagesTools)()).promotePackageVersion({
124
114
  packageVersionId: pv.packageVersionId,
125
- version: pv.version,
126
- versionTag: newTag,
127
- packageBundleFileHash: pv.packageBundleFileHash,
128
- routesBundleFileHash: pv.routesBundleFileHash,
129
- uiBundleFileHash: pv.uiBundleFileHash,
130
- appNavs: pv.appNavs,
131
- createdBy: pv.createdBy,
132
- createdAt: pv.createdAt,
133
- });
134
- const updated = {
135
- ...pv,
136
115
  versionTag: newTag,
137
- packageVersionHash: pvHash,
138
- history,
139
- packageAuthorSignature: authorSig,
140
- };
141
- await (0, peers_sdk_1.PackageVersions)().signAndSave(updated, { saveAsSnapshot: true });
142
- // Promoting the version this device is already running should advance the
143
- // group active version when the new tag matches group follow policy
144
- // (e.g. promote active → stable with default follow settings).
145
- const pkgJs = pkg.toJS();
146
- const activated = await (0, package_helpers_1.maybeActivateAfterPromote)(pkg.packageId, updated, {
147
- wasEffectiveActive: pv.packageVersionId === effectiveActiveVersionId,
148
- groupFollowTags: pkgJs.followVersionTags,
149
- groupPinned: pkgJs.versionFollowRange === "pinned",
150
116
  });
151
- if (activated) {
117
+ if (result.warning)
118
+ alert(result.warning);
119
+ if (result.activated)
152
120
  await pkg.load();
153
- }
154
121
  refreshKey(refreshKey() + 1);
155
122
  }
156
123
  catch (err) {
@@ -190,24 +157,10 @@ const PackageVersionsList = (props) => {
190
157
  return ((0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: [sorted.length, " version", sorted.length !== 1 ? "s" : ""] }), (0, jsx_runtime_1.jsx)("div", { className: "list-group mt-2", children: sorted.map((pv) => ((0, jsx_runtime_1.jsx)(VersionRow, { pv: pv, isActive: pv.packageVersionId === effectiveActiveVersionId, isPinned: isPinned, pinning: pinning, activating: activating, deleting: deleting, userMap: userMap, onActivate: activateVersion, onDelete: deleteVersion, onPin: pinVersion, onPromote: promoteVersion }, pv.packageVersionId))) })] }));
191
158
  };
192
159
  exports.PackageVersionsList = PackageVersionsList;
193
- /**
194
- * Valid promotion targets for each version tag. Promotion only moves forward:
195
- * dev -> beta -> stable. No demotions allowed.
196
- */
197
- function getPromoteTargets(currentTag) {
198
- switch (currentTag) {
199
- case "dev":
200
- return ["beta", "stable"];
201
- case "beta":
202
- return ["stable"];
203
- default:
204
- return [];
205
- }
206
- }
207
160
  function VersionRow(props) {
208
161
  const { pv, isActive, isPinned, pinning, activating, deleting, userMap } = props;
209
162
  const creatorName = userMap.get(pv.createdBy);
210
163
  const currentTag = pv.versionTag || "dev";
211
- const promoteTargets = getPromoteTargets(currentTag);
164
+ const promoteTargets = (0, peers_sdk_1.packageVersionPromotionTargets)(currentTag);
212
165
  return ((0, jsx_runtime_1.jsx)("div", { className: `list-group-item ${isActive ? "list-group-item-success" : ""}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center justify-content-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: "d-flex align-items-center flex-wrap", children: [(0, jsx_runtime_1.jsxs)("strong", { className: "me-2", children: ["v", pv.version] }), pv.versionTag && ((0, jsx_runtime_1.jsx)("span", { className: `badge ${tagBadgeClass(pv.versionTag)} me-2`, children: pv.versionTag })), (0, jsx_runtime_1.jsx)("code", { className: "text-muted small me-2", children: pv.packageVersionHash?.substring(0, 8) }), (0, jsx_runtime_1.jsx)("small", { className: "text-muted me-2", children: formatDate(pv.createdAt) }), creatorName && (0, jsx_runtime_1.jsxs)("small", { className: "text-muted", children: ["by ", creatorName] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "d-flex gap-1 align-items-center", children: [promoteTargets.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "dropdown", children: [(0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-info dropdown-toggle", "data-bs-toggle": "dropdown", "data-bs-popper-config": '{"strategy":"fixed"}', title: "Change version tag", children: "Promote" }), (0, jsx_runtime_1.jsx)("ul", { className: "dropdown-menu dropdown-menu-end", children: promoteTargets.map((tag) => ((0, jsx_runtime_1.jsx)("li", { children: (0, jsx_runtime_1.jsxs)("button", { className: "dropdown-item", onClick: () => props.onPromote(pv, tag), children: [currentTag, " \u2192 ", tag] }) }, tag))) })] })), isActive ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-secondary", disabled: isPinned || pinning, onClick: () => props.onPin(), title: isPinned ? "Already pinned" : "Pin to this version (disable auto-updates)", children: [pinning ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-pin-fill" })), isPinned ? " Pinned" : " Pin"] }), (0, jsx_runtime_1.jsx)("span", { className: "badge text-bg-info align-self-center", children: "active" })] })) : ((0, jsx_runtime_1.jsxs)("button", { className: "btn btn-sm btn-outline-primary", disabled: activating === pv.packageVersionId, onClick: () => props.onActivate(pv), children: [activating === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm me-1" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-check2-circle me-1" })), "Activate"] })), !isActive && ((0, jsx_runtime_1.jsx)("button", { className: "btn btn-sm btn-outline-danger", disabled: deleting === pv.packageVersionId, onClick: () => props.onDelete(pv), children: deleting === pv.packageVersionId ? ((0, jsx_runtime_1.jsx)("span", { className: "spinner-border spinner-border-sm" })) : ((0, jsx_runtime_1.jsx)("i", { className: "bi bi-trash" })) }))] })] }) }));
213
166
  }
@@ -0,0 +1,41 @@
1
+ import { type IHostContractConsumer, type IPackageObservedRuntime, type IPackagesConsumer, type PackageRuntimeStates } from "@peers-app/peers-sdk";
2
+ /**
3
+ * Shared System Packages handle for one data context; stable for `useHostContract`.
4
+ *
5
+ * @param dataContextId - Personal context (user id) or group id.
6
+ */
7
+ export declare function packagesContractFor(dataContextId: string): IHostContractConsumer<IPackagesConsumer>;
8
+ /**
9
+ * Resolve the System Packages consumer for a data context.
10
+ *
11
+ * @param dataContextId - Context to bind; defaults to the active group, the same context
12
+ * `Packages()` reads.
13
+ */
14
+ export declare function getPackagesConsumer(dataContextId?: string): Promise<IPackagesConsumer>;
15
+ /**
16
+ * System Packages tools bound to a data context.
17
+ *
18
+ * @param dataContextId - Context to bind; defaults to the active group.
19
+ */
20
+ export declare function packagesTools(dataContextId?: string): Promise<IPackagesConsumer["tools"]>;
21
+ /**
22
+ * Live runtime state of a context's packages on this device.
23
+ *
24
+ * @param dataContextId - Context to watch; defaults to the active group.
25
+ * @returns States keyed by package id, or `undefined` until the host answers.
26
+ */
27
+ export declare function usePackageRuntimeStates(dataContextId?: string): PackageRuntimeStates | undefined;
28
+ /** Display label and Bootstrap badge color for an observed package status. */
29
+ export interface IPackageStatusBadge {
30
+ /** Short status label. */
31
+ label: string;
32
+ /** Bootstrap text-bg color suffix (`success`, `danger`, ...). */
33
+ color: "success" | "danger" | "warning" | "secondary";
34
+ }
35
+ /**
36
+ * Describe a package's observed runtime status for a badge.
37
+ *
38
+ * @param runtime - Observed runtime state, or `undefined` before the host reports one.
39
+ * @returns The badge, or `undefined` when there is nothing to show.
40
+ */
41
+ export declare function packageStatusBadge(runtime: Pick<IPackageObservedRuntime, "status" | "running" | "worker"> | undefined): IPackageStatusBadge | undefined;
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.packagesContractFor = packagesContractFor;
4
+ exports.getPackagesConsumer = getPackagesConsumer;
5
+ exports.packagesTools = packagesTools;
6
+ exports.usePackageRuntimeStates = usePackageRuntimeStates;
7
+ exports.packageStatusBadge = packageStatusBadge;
8
+ const peers_sdk_1 = require("@peers-app/peers-sdk");
9
+ const react_1 = require("react");
10
+ const hooks_1 = require("../../hooks");
11
+ const use_host_contract_1 = require("../../hooks/use-host-contract");
12
+ const handles = new Map();
13
+ /**
14
+ * Shared System Packages handle for one data context; stable for `useHostContract`.
15
+ *
16
+ * @param dataContextId - Personal context (user id) or group id.
17
+ */
18
+ function packagesContractFor(dataContextId) {
19
+ let handle = handles.get(dataContextId);
20
+ if (!handle) {
21
+ handle = (0, peers_sdk_1.hostContractConsumer)((0, peers_sdk_1.getPackagesContractDefinition)(), {
22
+ dataContextId: async () => dataContextId,
23
+ });
24
+ handles.set(dataContextId, handle);
25
+ }
26
+ return handle;
27
+ }
28
+ /**
29
+ * Resolve the System Packages consumer for a data context.
30
+ *
31
+ * @param dataContextId - Context to bind; defaults to the active group, the same context
32
+ * `Packages()` reads.
33
+ */
34
+ async function getPackagesConsumer(dataContextId) {
35
+ const id = dataContextId ?? (await (0, peers_sdk_1.getDefaultDataContext)()).dataContextId;
36
+ return packagesContractFor(id).get();
37
+ }
38
+ /**
39
+ * System Packages tools bound to a data context.
40
+ *
41
+ * @param dataContextId - Context to bind; defaults to the active group.
42
+ */
43
+ async function packagesTools(dataContextId) {
44
+ return (await getPackagesConsumer(dataContextId)).tools;
45
+ }
46
+ /** A handle that never connects, used until the data context id is known. */
47
+ const pendingHandle = {
48
+ get: () => new Promise(() => undefined),
49
+ dispose: () => undefined,
50
+ };
51
+ /**
52
+ * Live runtime state of a context's packages on this device.
53
+ *
54
+ * @param dataContextId - Context to watch; defaults to the active group.
55
+ * @returns States keyed by package id, or `undefined` until the host answers.
56
+ */
57
+ function usePackageRuntimeStates(dataContextId) {
58
+ const [handle, setHandle] = (0, react_1.useState)();
59
+ (0, react_1.useEffect)(() => {
60
+ let cancelled = false;
61
+ (dataContextId
62
+ ? Promise.resolve(dataContextId)
63
+ : (0, peers_sdk_1.getDefaultDataContext)().then((dc) => dc.dataContextId)).then((id) => {
64
+ if (!cancelled)
65
+ setHandle(packagesContractFor(id));
66
+ }, () => undefined);
67
+ return () => {
68
+ cancelled = true;
69
+ };
70
+ }, [dataContextId]);
71
+ const { consumer } = (0, use_host_contract_1.useHostContract)(handle ?? pendingHandle);
72
+ const [states] = (0, hooks_1.useObservable)(consumer?.observables.runtimeStates);
73
+ return states;
74
+ }
75
+ /**
76
+ * Describe a package's observed runtime status for a badge.
77
+ *
78
+ * @param runtime - Observed runtime state, or `undefined` before the host reports one.
79
+ * @returns The badge, or `undefined` when there is nothing to show.
80
+ */
81
+ function packageStatusBadge(runtime) {
82
+ if (!runtime)
83
+ return undefined;
84
+ if (runtime.worker?.lastWorkerError)
85
+ return { label: "Worker failed", color: "danger" };
86
+ switch (runtime.status) {
87
+ case "running":
88
+ return { label: "Running", color: "success" };
89
+ case "failed":
90
+ return runtime.running
91
+ ? { label: "Update failed", color: "warning" }
92
+ : { label: "Failed", color: "danger" };
93
+ case "unavailable":
94
+ return { label: "Waiting for files", color: "warning" };
95
+ case "unsupported":
96
+ return { label: "Unsupported here", color: "secondary" };
97
+ case "disabled":
98
+ return { label: "Disabled", color: "secondary" };
99
+ default:
100
+ return undefined;
101
+ }
102
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const hostContractConsumer = jest.fn((_definition, options) => ({
4
+ get: async () => ({ tools: { boundTo: await options.dataContextId() } }),
5
+ dispose: jest.fn(),
6
+ }));
7
+ jest.mock("@peers-app/peers-sdk", () => ({
8
+ getDefaultDataContext: async () => ({ dataContextId: "active-group" }),
9
+ getPackagesContractDefinition: () => ({ name: "System Packages" }),
10
+ hostContractConsumer: (definition, options) => hostContractConsumer(definition, options),
11
+ subscribeHostContractTransport: () => () => undefined,
12
+ }));
13
+ const packages_ipc_1 = require("./packages-ipc");
14
+ test("caches one host consumer per data context and defaults to the active group", async () => {
15
+ expect((0, packages_ipc_1.packagesContractFor)("group-a")).toBe((0, packages_ipc_1.packagesContractFor)("group-a"));
16
+ expect((0, packages_ipc_1.packagesContractFor)("group-b")).not.toBe((0, packages_ipc_1.packagesContractFor)("group-a"));
17
+ expect(await (0, packages_ipc_1.packagesTools)("group-b")).toEqual({ boundTo: "group-b" });
18
+ expect(await (0, packages_ipc_1.packagesTools)()).toEqual({ boundTo: "active-group" });
19
+ expect(hostContractConsumer).toHaveBeenCalledTimes(3);
20
+ });
21
+ test.each([
22
+ [undefined, undefined],
23
+ [{ status: "running" }, { label: "Running", color: "success" }],
24
+ [{ status: "failed" }, { label: "Failed", color: "danger" }],
25
+ [
26
+ { status: "failed", running: { kind: "isolated", version: "1.0.0" } },
27
+ { label: "Update failed", color: "warning" },
28
+ ],
29
+ [
30
+ {
31
+ status: "running",
32
+ worker: { workerRunning: false, lastWorkerError: { message: "x", at: 1 } },
33
+ },
34
+ { label: "Worker failed", color: "danger" },
35
+ ],
36
+ [{ status: "unavailable" }, { label: "Waiting for files", color: "warning" }],
37
+ [{ status: "unsupported" }, { label: "Unsupported here", color: "secondary" }],
38
+ [{ status: "disabled" }, { label: "Disabled", color: "secondary" }],
39
+ [{ status: "inactive" }, undefined],
40
+ [{ status: "notLoaded" }, undefined],
41
+ ])("badge for %p", (runtime, expected) => {
42
+ expect((0, packages_ipc_1.packageStatusBadge)(runtime)).toEqual(expected);
43
+ });
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const list = jest.fn();
4
4
  const get = jest.fn();
5
5
  const rows = jest.fn();
6
+ const mainContentPath = jest.fn();
7
+ jest.mock("../../globals", () => ({ mainContentPath }));
6
8
  let transport = true;
7
9
  jest.mock("@peers-app/peers-sdk", () => ({
8
10
  getDefaultDataContext: async () => ({ dataContextId: "group" }),
@@ -32,6 +34,10 @@ test("tool mentions include only workflow-invocable catalog tools", async () =>
32
34
  name: "historical",
33
35
  });
34
36
  });
37
+ test("type mentions navigate directly through the UI route observable", () => {
38
+ mention_configs_1.valueTypeMentionConfig.onClick?.({ kind: "valueType", id: "type-id", name: "Type" });
39
+ expect(mainContentPath).toHaveBeenCalledWith("peer-types/type-id");
40
+ });
35
41
  test("browser fallback omits quarantined rows and preserves historical detail", async () => {
36
42
  transport = false;
37
43
  rows.mockResolvedValue([
@@ -1,5 +1,6 @@
1
1
  interface IProps {
2
2
  persistentVarId: string;
3
3
  }
4
+ /** Details screen for one persistent variable: name, value (copyable), and settings. */
4
5
  export declare const VariableDetails: (props: IProps) => import("react/jsx-runtime").JSX.Element;
5
6
  export {};