@peers-app/peers-ui 0.26.3 → 0.26.5

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 (64) 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/tabs-layout/network-status-indicator.d.ts +4 -1
  37. package/dist/tabs-layout/network-status-indicator.js +12 -6
  38. package/dist/tabs-layout/network-status-indicator.test.js +1 -1
  39. package/dist/ui-router/ui-loader.js +10 -37
  40. package/package.json +3 -3
  41. package/src/components/group-switcher.tsx +12 -4
  42. package/src/components/markdown-with-mentions.tsx +7 -2
  43. package/src/components/save-button.tsx +24 -3
  44. package/src/globals.tsx +32 -11
  45. package/src/host-shell/host-shell-consumer.test.ts +32 -0
  46. package/src/host-shell/host-shell-consumer.ts +53 -0
  47. package/src/mention-configs.ts +2 -2
  48. package/src/screens/groups/group-list.tsx +6 -4
  49. package/src/screens/packages/package-details.tsx +31 -8
  50. package/src/screens/packages/package-helpers.test.ts +57 -0
  51. package/src/screens/packages/package-helpers.ts +13 -55
  52. package/src/screens/packages/package-info.tsx +3 -3
  53. package/src/screens/packages/package-list.tsx +51 -16
  54. package/src/screens/packages/package-new-local.tsx +8 -13
  55. package/src/screens/packages/package-runtime-status.test.tsx +58 -0
  56. package/src/screens/packages/package-runtime-status.tsx +68 -0
  57. package/src/screens/packages/package-versions.tsx +9 -72
  58. package/src/screens/packages/packages-ipc.test.ts +47 -0
  59. package/src/screens/packages/packages-ipc.ts +125 -0
  60. package/src/screens/tools/tool-catalog-ipc.test.ts +7 -1
  61. package/src/screens/variables/variable-details.tsx +11 -6
  62. package/src/tabs-layout/network-status-indicator.test.tsx +1 -1
  63. package/src/tabs-layout/network-status-indicator.tsx +24 -11
  64. package/src/ui-router/ui-loader.tsx +12 -51
@@ -0,0 +1,125 @@
1
+ import {
2
+ getDefaultDataContext,
3
+ getPackagesContractDefinition,
4
+ hostContractConsumer,
5
+ type IHostContractConsumer,
6
+ type IPackageObservedRuntime,
7
+ type IPackagesConsumer,
8
+ type PackageRuntimeStates,
9
+ } from "@peers-app/peers-sdk";
10
+ import { useEffect, useState } from "react";
11
+ import { useObservable } from "../../hooks";
12
+ import { useHostContract } from "../../hooks/use-host-contract";
13
+
14
+ const handles = new Map<string, IHostContractConsumer<IPackagesConsumer>>();
15
+
16
+ /**
17
+ * Shared System Packages handle for one data context; stable for `useHostContract`.
18
+ *
19
+ * @param dataContextId - Personal context (user id) or group id.
20
+ */
21
+ export function packagesContractFor(
22
+ dataContextId: string,
23
+ ): IHostContractConsumer<IPackagesConsumer> {
24
+ let handle = handles.get(dataContextId);
25
+ if (!handle) {
26
+ handle = hostContractConsumer<IPackagesConsumer>(getPackagesContractDefinition(), {
27
+ dataContextId: async () => dataContextId,
28
+ });
29
+ handles.set(dataContextId, handle);
30
+ }
31
+ return handle;
32
+ }
33
+
34
+ /**
35
+ * Resolve the System Packages consumer for a data context.
36
+ *
37
+ * @param dataContextId - Context to bind; defaults to the active group, the same context
38
+ * `Packages()` reads.
39
+ */
40
+ export async function getPackagesConsumer(dataContextId?: string): Promise<IPackagesConsumer> {
41
+ const id = dataContextId ?? (await getDefaultDataContext()).dataContextId;
42
+ return packagesContractFor(id).get();
43
+ }
44
+
45
+ /**
46
+ * System Packages tools bound to a data context.
47
+ *
48
+ * @param dataContextId - Context to bind; defaults to the active group.
49
+ */
50
+ export async function packagesTools(dataContextId?: string): Promise<IPackagesConsumer["tools"]> {
51
+ return (await getPackagesConsumer(dataContextId)).tools;
52
+ }
53
+
54
+ /** A handle that never connects, used until the data context id is known. */
55
+ const pendingHandle: IHostContractConsumer<IPackagesConsumer> = {
56
+ get: () => new Promise<IPackagesConsumer>(() => undefined),
57
+ dispose: () => undefined,
58
+ };
59
+
60
+ /**
61
+ * Live runtime state of a context's packages on this device.
62
+ *
63
+ * @param dataContextId - Context to watch; defaults to the active group.
64
+ * @returns States keyed by package id, or `undefined` until the host answers.
65
+ */
66
+ export function usePackageRuntimeStates(dataContextId?: string): PackageRuntimeStates | undefined {
67
+ const [handle, setHandle] = useState<IHostContractConsumer<IPackagesConsumer>>();
68
+ useEffect(() => {
69
+ let cancelled = false;
70
+ (dataContextId
71
+ ? Promise.resolve(dataContextId)
72
+ : getDefaultDataContext().then((dc) => dc.dataContextId)
73
+ ).then(
74
+ (id) => {
75
+ if (!cancelled) setHandle(packagesContractFor(id));
76
+ },
77
+ () => undefined,
78
+ );
79
+ return () => {
80
+ cancelled = true;
81
+ };
82
+ }, [dataContextId]);
83
+ const { consumer } = useHostContract(handle ?? pendingHandle);
84
+ const [states] = useObservable<PackageRuntimeStates | undefined>(
85
+ consumer?.observables.runtimeStates,
86
+ );
87
+ return states;
88
+ }
89
+
90
+ /** Display label and Bootstrap badge color for an observed package status. */
91
+ export interface IPackageStatusBadge {
92
+ /** Short status label. */
93
+ label: string;
94
+ /** Bootstrap text-bg color suffix (`success`, `danger`, ...). */
95
+ color: "success" | "danger" | "warning" | "secondary";
96
+ }
97
+
98
+ /**
99
+ * Describe a package's observed runtime status for a badge.
100
+ *
101
+ * @param runtime - Observed runtime state, or `undefined` before the host reports one.
102
+ * @returns The badge, or `undefined` when there is nothing to show.
103
+ */
104
+ export function packageStatusBadge(
105
+ runtime: Pick<IPackageObservedRuntime, "status" | "running" | "worker"> | undefined,
106
+ ): IPackageStatusBadge | undefined {
107
+ if (!runtime) return undefined;
108
+ if (runtime.worker?.lastWorkerError) return { label: "Worker failed", color: "danger" };
109
+ switch (runtime.status) {
110
+ case "running":
111
+ return { label: "Running", color: "success" };
112
+ case "failed":
113
+ return runtime.running
114
+ ? { label: "Update failed", color: "warning" }
115
+ : { label: "Failed", color: "danger" };
116
+ case "unavailable":
117
+ return { label: "Waiting for files", color: "warning" };
118
+ case "unsupported":
119
+ return { label: "Unsupported here", color: "secondary" };
120
+ case "disabled":
121
+ return { label: "Disabled", color: "secondary" };
122
+ default:
123
+ return undefined;
124
+ }
125
+ }
@@ -1,6 +1,8 @@
1
1
  const list = jest.fn();
2
2
  const get = jest.fn();
3
3
  const rows = jest.fn();
4
+ const mainContentPath = jest.fn();
5
+ jest.mock("../../globals", () => ({ mainContentPath }));
4
6
  let transport = true;
5
7
  jest.mock("@peers-app/peers-sdk", () => ({
6
8
  getDefaultDataContext: async () => ({ dataContextId: "group" }),
@@ -10,7 +12,7 @@ jest.mock("@peers-app/peers-sdk", () => ({
10
12
  Tools: () => ({ list: rows, get: async (toolId: string) => ({ toolId, name: "historical" }) }),
11
13
  }));
12
14
 
13
- import { toolMentionConfig } from "../../mention-configs";
15
+ import { toolMentionConfig, valueTypeMentionConfig } from "../../mention-configs";
14
16
  import { getToolDetails, listWorkflowMentionTools } from "./tool-catalog-ipc";
15
17
 
16
18
  beforeEach(() => {
@@ -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
+ 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([
@@ -7,7 +7,6 @@ import {
7
7
  type Observable,
8
8
  observable,
9
9
  PersistentVars,
10
- rpcServerCalls,
11
10
  } from "@peers-app/peers-sdk";
12
11
  import { useEffect, useState } from "react";
13
12
  import { Checkbox } from "../../components/checkbox";
@@ -17,12 +16,14 @@ import { MarkdownEditorInline } from "../../components/markdown-editor/editor-in
17
16
  import { SaveButton } from "../../components/save-button";
18
17
  import { ScreenTabBody, Tabs } from "../../components/tabs";
19
18
  import { useObservable, usePromise, useSubscription } from "../../hooks";
19
+ import { hostShellTools } from "../../host-shell/host-shell-consumer";
20
20
  import { updateActiveTabTitle } from "../../tabs-layout/tabs-state";
21
21
 
22
22
  interface IProps {
23
23
  persistentVarId: string;
24
24
  }
25
25
 
26
+ /** Details screen for one persistent variable: name, value (copyable), and settings. */
26
27
  export const VariableDetails = (props: IProps) => {
27
28
  const persistentVariable = usePromise(
28
29
  async () => {
@@ -242,9 +243,9 @@ const VariableValue = (props: { pvar: IDoc<IPersistentVar> }) => {
242
243
 
243
244
  /**
244
245
  * Button that copies a persistent variable's value to the clipboard. Decryption (for
245
- * secret values) and the clipboard write happen entirely on the server/main process via
246
- * {@link rpcServerCalls.copyPvarValueToClipboard}, so the plaintext is never exposed to
247
- * the UI. Shows transient success/error feedback on the icon.
246
+ * secret values) and the clipboard write happen entirely on the host, through System Host
247
+ * Shell `copyVariableToClipboard`, so the plaintext is never exposed to the UI. Shows
248
+ * transient success/error feedback on the icon.
248
249
  */
249
250
  const CopyValueButton = (props: { pvar: IDoc<IPersistentVar> }) => {
250
251
  const { pvar } = props;
@@ -254,8 +255,12 @@ const CopyValueButton = (props: { pvar: IDoc<IPersistentVar> }) => {
254
255
  try {
255
256
  const userContext = await getUserContext();
256
257
  const groupId = userContext.currentlyActiveGroupId();
257
- const result = await rpcServerCalls.copyPvarValueToClipboard(pvar.persistentVarId, groupId);
258
- setStatus(result.success ? "copied" : "error");
258
+ const tools = await hostShellTools();
259
+ const result = await tools.copyVariableToClipboard({
260
+ persistentVarId: pvar.persistentVarId,
261
+ groupId,
262
+ });
263
+ setStatus(result.copied ? "copied" : "error");
259
264
  } catch (err) {
260
265
  console.error("Failed to copy variable value", err);
261
266
  setStatus("error");
@@ -454,7 +454,7 @@ describe("NetworkStatusIndicator", () => {
454
454
  view.rerender(<NetworkStatusIndicator />);
455
455
  const badge = view.container.querySelector(".network-status-update-badge");
456
456
  expect(badge).toHaveClass("bi-arrow-up-circle-fill", "text-warning");
457
- expect(badge).toHaveStyle({ left: "-0.3rem", bottom: "-0.32rem" });
457
+ expect(badge).toHaveStyle({ left: "-0.46rem", bottom: "-0.08rem" });
458
458
 
459
459
  fireEvent.click(view.getByRole("button", { name: /Version 1\.1\.0 is ready to install/ }));
460
460
  const section = view.getByTestId("app-update-section");
@@ -57,7 +57,10 @@ interface INetworkStatusPresentation {
57
57
  * Green when > 0; red zero when connected devices exist but no groups are live.
58
58
  */
59
59
  connectedGroupsOverlay?: { count: number; colorClass: "text-success" | "text-danger" };
60
- /** Bottom-left yellow upgrade arrow when this device has an app update available. */
60
+ /**
61
+ * Yellow upgrade arrow on the lower-left corner of the status mark when this device
62
+ * has an app update available. Kept on the mark so it does not hang below the tab strip.
63
+ */
61
64
  updateAvailableOverlay?: boolean;
62
65
  }
63
66
 
@@ -679,16 +682,26 @@ export function NetworkStatusIndicator({ compact = false }: { compact?: boolean
679
682
  </span>
680
683
  )}
681
684
  {presentation.updateAvailableOverlay && (
682
- <i
683
- className={`network-status-update-badge bi ${APP_UPDATE_ICON_CLASS} text-warning position-absolute`}
684
- style={{
685
- fontSize: "0.5rem",
686
- lineHeight: 1,
687
- left: "-0.3rem",
688
- bottom: "-0.32rem",
689
- }}
690
- aria-hidden="true"
691
- />
685
+ <>
686
+ <i
687
+ className={`network-status-update-badge bi ${APP_UPDATE_ICON_CLASS} text-warning position-absolute`}
688
+ style={{
689
+ fontSize: "0.5rem",
690
+ lineHeight: 1,
691
+ width: "0.5rem",
692
+ height: "0.5rem",
693
+ left: "-0.46rem",
694
+ bottom: "-0.08rem",
695
+ }}
696
+ aria-hidden="true"
697
+ />
698
+ {/* Bootstrap Icons shift the glyph below the em box; keep it inside this badge. */}
699
+ <style>{`
700
+ .network-status-update-badge.bi::before {
701
+ vertical-align: 0;
702
+ }
703
+ `}</style>
704
+ </>
692
705
  )}
693
706
  </span>
694
707
  {!compact && (
@@ -256,10 +256,6 @@ const UILoader = (args: { peersUIId: string; props?: Record<string, unknown> })
256
256
 
257
257
  const uiLoadingPromises: Record<string, Promise<void>> = {};
258
258
 
259
- // Check if we're running in a React Native WebView (has injectUIBundle available)
260
- const isReactNativeWebView =
261
- typeof (window as Window & { ReactNativeWebView?: unknown }).ReactNativeWebView !== "undefined";
262
-
263
259
  async function resolveUiBundleFileId(pkg: IPackage): Promise<string | undefined> {
264
260
  let pvId = pkg.activePackageVersionId;
265
261
  try {
@@ -278,12 +274,6 @@ async function resolveUiBundleFileId(pkg: IPackage): Promise<string | undefined>
278
274
  }
279
275
  }
280
276
 
281
- type PeersUiBundleWindow = Window & {
282
- __peersUIs?: Record<string, IPeersUI>;
283
- __peersUIBundleLoaded?: (loadedFileId: string) => void;
284
- __peersUIBundleError?: (errorFileId: string, errorMsg: string) => void;
285
- };
286
-
287
277
  function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
288
278
  let importPromise: Promise<void> = uiLoadingPromises[pkg.packageId];
289
279
  if (!importPromise || forceRefresh) {
@@ -295,48 +285,19 @@ function loadUIBundle(pkg: IPackage, forceRefresh?: boolean) {
295
285
  return;
296
286
  }
297
287
 
298
- if (isReactNativeWebView && rpcServerCalls.injectUIBundle) {
299
- const _w = window as PeersUiBundleWindow;
300
- _w.__peersUIs = _w.__peersUIs || {};
301
-
302
- const loadPromise = new Promise<void>((resolveLoad, rejectLoad) => {
303
- _w.__peersUIBundleLoaded = (loadedFileId: string) => {
304
- if (loadedFileId === uiBundleFileId) {
305
- const store = _w.__peersUIs || {};
306
- Object.keys(store).forEach((peersUIId) => {
307
- peersUIs[peersUIId] = store[peersUIId];
308
- });
309
- resolveLoad();
310
- }
311
- };
312
-
313
- _w.__peersUIBundleError = (errorFileId: string, errorMsg: string) => {
314
- if (errorFileId === uiBundleFileId) {
315
- rejectLoad(new Error(errorMsg));
316
- }
317
- };
318
- });
319
-
320
- await rpcServerCalls.injectUIBundle(uiBundleFileId);
321
- await loadPromise;
322
- console.log(
323
- `finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms`,
324
- );
325
- } else {
326
- const bundleCode = await rpcServerCalls.getFileContents(uiBundleFileId);
327
- if (bundleCode) {
328
- const exportUIs = (peerUIs: IPeersPackageUIs) => {
329
- peerUIs?.uis?.forEach((ui) => {
330
- peersUIs[ui.peersUIId] = ui;
331
- });
332
- };
333
- const bundleFunction = new Function("exportUIs", bundleCode);
334
- await bundleFunction(exportUIs);
335
- }
336
- console.log(
337
- `finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms, ${bundleCode ? (bundleCode.length / 1000).toFixed(0) : "0"} KB`,
338
- );
288
+ const bundleCode = await rpcServerCalls.getFileContents(uiBundleFileId);
289
+ if (bundleCode) {
290
+ const exportUIs = (peerUIs: IPeersPackageUIs) => {
291
+ peerUIs?.uis?.forEach((ui) => {
292
+ peersUIs[ui.peersUIId] = ui;
293
+ });
294
+ };
295
+ const bundleFunction = new Function("exportUIs", bundleCode);
296
+ await bundleFunction(exportUIs);
339
297
  }
298
+ console.log(
299
+ `finished loading ui bundle for ${pkg.name}: ${(Date.now() - sTime).toFixed(0)}ms, ${bundleCode ? (bundleCode.length / 1000).toFixed(0) : "0"} KB`,
300
+ );
340
301
  })().catch((err: unknown) => {
341
302
  console.error(`Error while loading ui module for ${pkg.name}`, err);
342
303
  throw err;