@datalayer/core 1.2.19 → 1.2.20

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 (144) hide show
  1. package/lib/components/avatars/UserAvatar.d.ts +10 -11
  2. package/lib/components/avatars/UserAvatar.js +23 -28
  3. package/lib/components/avatars/UserProfileAvatar.js +1 -1
  4. package/lib/components/avatars/avatarChoice.d.ts +42 -0
  5. package/lib/components/avatars/avatarChoice.js +65 -0
  6. package/lib/components/avatars/index.d.ts +1 -0
  7. package/lib/components/avatars/index.js +1 -0
  8. package/lib/components/collaboration/LiveEditorCollaborators.d.ts +3 -4
  9. package/lib/components/collaboration/LiveEditorCollaborators.js +7 -5
  10. package/lib/components/emoji/faceEmojis.js +48 -0
  11. package/lib/components/emoji/fluent/loaders.js +46 -0
  12. package/lib/components/emoji/fluent/svg/1f30a.d.ts +2 -0
  13. package/lib/components/emoji/fluent/svg/1f30a.js +5 -0
  14. package/lib/components/emoji/fluent/svg/1f310.d.ts +2 -0
  15. package/lib/components/emoji/fluent/svg/1f310.js +5 -0
  16. package/lib/components/emoji/fluent/svg/1f324.d.ts +2 -0
  17. package/lib/components/emoji/fluent/svg/1f324.js +5 -0
  18. package/lib/components/emoji/fluent/svg/1f326.d.ts +2 -0
  19. package/lib/components/emoji/fluent/svg/1f326.js +5 -0
  20. package/lib/components/emoji/fluent/svg/1f327.d.ts +2 -0
  21. package/lib/components/emoji/fluent/svg/1f327.js +5 -0
  22. package/lib/components/emoji/fluent/svg/1f33e.d.ts +2 -0
  23. package/lib/components/emoji/fluent/svg/1f33e.js +5 -0
  24. package/lib/components/emoji/fluent/svg/1f372.d.ts +2 -0
  25. package/lib/components/emoji/fluent/svg/1f372.js +5 -0
  26. package/lib/components/emoji/fluent/svg/1f373.d.ts +2 -0
  27. package/lib/components/emoji/fluent/svg/1f373.js +5 -0
  28. package/lib/components/emoji/fluent/svg/1f39b.d.ts +2 -0
  29. package/lib/components/emoji/fluent/svg/1f39b.js +5 -0
  30. package/lib/components/emoji/fluent/svg/1f3ab.d.ts +2 -0
  31. package/lib/components/emoji/fluent/svg/1f3ab.js +5 -0
  32. package/lib/components/emoji/fluent/svg/1f41b.d.ts +2 -0
  33. package/lib/components/emoji/fluent/svg/1f41b.js +5 -0
  34. package/lib/components/emoji/fluent/svg/1f441.d.ts +2 -0
  35. package/lib/components/emoji/fluent/svg/1f441.js +5 -0
  36. package/lib/components/emoji/fluent/svg/1f465.d.ts +2 -0
  37. package/lib/components/emoji/fluent/svg/1f465.js +5 -0
  38. package/lib/components/emoji/fluent/svg/1f4b9.d.ts +2 -0
  39. package/lib/components/emoji/fluent/svg/1f4b9.js +5 -0
  40. package/lib/components/emoji/fluent/svg/1f4be.d.ts +2 -0
  41. package/lib/components/emoji/fluent/svg/1f4be.js +5 -0
  42. package/lib/components/emoji/fluent/svg/1f4c1.d.ts +2 -0
  43. package/lib/components/emoji/fluent/svg/1f4c1.js +5 -0
  44. package/lib/components/emoji/fluent/svg/1f4c2.d.ts +2 -0
  45. package/lib/components/emoji/fluent/svg/1f4c2.js +5 -0
  46. package/lib/components/emoji/fluent/svg/1f4c4.d.ts +2 -0
  47. package/lib/components/emoji/fluent/svg/1f4c4.js +5 -0
  48. package/lib/components/emoji/fluent/svg/1f4d3.d.ts +2 -0
  49. package/lib/components/emoji/fluent/svg/1f4d3.js +5 -0
  50. package/lib/components/emoji/fluent/svg/1f4d6.d.ts +2 -0
  51. package/lib/components/emoji/fluent/svg/1f4d6.js +5 -0
  52. package/lib/components/emoji/fluent/svg/1f4dc.d.ts +2 -0
  53. package/lib/components/emoji/fluent/svg/1f4dc.js +5 -0
  54. package/lib/components/emoji/fluent/svg/1f4e1.d.ts +2 -0
  55. package/lib/components/emoji/fluent/svg/1f4e1.js +5 -0
  56. package/lib/components/emoji/fluent/svg/1f4e2.d.ts +2 -0
  57. package/lib/components/emoji/fluent/svg/1f4e2.js +5 -0
  58. package/lib/components/emoji/fluent/svg/1f4e7.d.ts +2 -0
  59. package/lib/components/emoji/fluent/svg/1f4e7.js +5 -0
  60. package/lib/components/emoji/fluent/svg/1f500.d.ts +2 -0
  61. package/lib/components/emoji/fluent/svg/1f500.js +5 -0
  62. package/lib/components/emoji/fluent/svg/1f501.d.ts +2 -0
  63. package/lib/components/emoji/fluent/svg/1f501.js +5 -0
  64. package/lib/components/emoji/fluent/svg/1f504.d.ts +2 -0
  65. package/lib/components/emoji/fluent/svg/1f504.js +5 -0
  66. package/lib/components/emoji/fluent/svg/1f52d.d.ts +2 -0
  67. package/lib/components/emoji/fluent/svg/1f52d.js +5 -0
  68. package/lib/components/emoji/fluent/svg/1f552.d.ts +2 -0
  69. package/lib/components/emoji/fluent/svg/1f552.js +5 -0
  70. package/lib/components/emoji/fluent/svg/1f58b.d.ts +2 -0
  71. package/lib/components/emoji/fluent/svg/1f58b.js +5 -0
  72. package/lib/components/emoji/fluent/svg/1f5b2.d.ts +2 -0
  73. package/lib/components/emoji/fluent/svg/1f5b2.js +5 -0
  74. package/lib/components/emoji/fluent/svg/1f5c3.d.ts +2 -0
  75. package/lib/components/emoji/fluent/svg/1f5c3.js +5 -0
  76. package/lib/components/emoji/fluent/svg/1f5d1.d.ts +2 -0
  77. package/lib/components/emoji/fluent/svg/1f5d1.js +5 -0
  78. package/lib/components/emoji/fluent/svg/1f5dc.d.ts +2 -0
  79. package/lib/components/emoji/fluent/svg/1f5dc.js +5 -0
  80. package/lib/components/emoji/fluent/svg/1f604.d.ts +2 -0
  81. package/lib/components/emoji/fluent/svg/1f604.js +5 -0
  82. package/lib/components/emoji/fluent/svg/1f917.d.ts +2 -0
  83. package/lib/components/emoji/fluent/svg/1f917.js +5 -0
  84. package/lib/components/emoji/fluent/svg/1f9d1-200d-2696.d.ts +2 -0
  85. package/lib/components/emoji/fluent/svg/1f9d1-200d-2696.js +5 -0
  86. package/lib/components/emoji/fluent/svg/1fa79.d.ts +2 -0
  87. package/lib/components/emoji/fluent/svg/1fa79.js +5 -0
  88. package/lib/components/emoji/fluent/svg/1fa9d.d.ts +2 -0
  89. package/lib/components/emoji/fluent/svg/1fa9d.js +5 -0
  90. package/lib/components/emoji/fluent/svg/1fa9f.d.ts +2 -0
  91. package/lib/components/emoji/fluent/svg/1fa9f.js +5 -0
  92. package/lib/components/emoji/fluent/svg/23f3.d.ts +2 -0
  93. package/lib/components/emoji/fluent/svg/23f3.js +5 -0
  94. package/lib/components/emoji/fluent/svg/2611.d.ts +2 -0
  95. package/lib/components/emoji/fluent/svg/2611.js +5 -0
  96. package/lib/components/emoji/fluent/svg/2702.d.ts +2 -0
  97. package/lib/components/emoji/fluent/svg/2702.js +5 -0
  98. package/lib/components/emoji/fluent/svg/270f.d.ts +2 -0
  99. package/lib/components/emoji/fluent/svg/270f.js +5 -0
  100. package/lib/components/emoji/fluent/svg/2728.d.ts +2 -0
  101. package/lib/components/emoji/fluent/svg/2728.js +5 -0
  102. package/lib/components/emoji/fluent/svg/2b1c.d.ts +2 -0
  103. package/lib/components/emoji/fluent/svg/2b1c.js +5 -0
  104. package/lib/components/principal/Principal.js +1 -1
  105. package/lib/components/principal/PrincipalAppearance.d.ts +24 -1
  106. package/lib/components/principal/PrincipalAppearance.js +105 -34
  107. package/lib/components/principal/PrincipalAvatar.d.ts +5 -7
  108. package/lib/components/principal/PrincipalAvatar.js +12 -18
  109. package/lib/components/principal/PrincipalDetailsOverlay.d.ts +4 -7
  110. package/lib/components/principal/PrincipalDetailsOverlay.js +2 -2
  111. package/lib/components/principal/PrincipalSwitcherMenu.js +1 -1
  112. package/lib/hooks/unlinkIAMProvider.d.ts +22 -0
  113. package/lib/hooks/unlinkIAMProvider.js +24 -0
  114. package/lib/hooks/useCache.d.ts +8 -1
  115. package/lib/hooks/useCache.js +35 -4
  116. package/lib/hooks/useNavigate.js +8 -2
  117. package/lib/models/IAMProviderLinked.d.ts +9 -0
  118. package/lib/models/IAMProviderLinked.js +34 -5
  119. package/lib/models/McpTask.d.ts +18 -1
  120. package/lib/models/McpTask.js +15 -0
  121. package/lib/models/Profile.d.ts +2 -2
  122. package/lib/models/User.js +5 -12
  123. package/lib/models/UserOnboarding.d.ts +9 -0
  124. package/lib/models/UserOnboarding.js +33 -0
  125. package/lib/portable/index.d.ts +3 -2
  126. package/lib/portable/index.js +4 -2
  127. package/lib/portable/states.d.ts +39 -0
  128. package/lib/portable/states.js +71 -0
  129. package/lib/state/substates/NavigationState.d.ts +5 -3
  130. package/lib/state/substates/NavigationState.js +12 -3
  131. package/lib/views/mcp/AlertDestinations.js +2 -2
  132. package/lib/views/mcp/ApprovalQueue.js +2 -2
  133. package/lib/views/mcp/EnterpriseConsole.js +2 -2
  134. package/lib/views/mcp/McpDashboard.js +2 -2
  135. package/lib/views/mcp/McpHome.js +4 -4
  136. package/lib/views/mcp/McpObservability.js +5 -5
  137. package/lib/views/mcp/NotebookRuns.js +2 -2
  138. package/lib/views/mcp/OrganizationUsage.js +2 -2
  139. package/lib/views/mcp/Policies.js +2 -2
  140. package/lib/views/mcp/RunDetail.d.ts +1 -1
  141. package/lib/views/mcp/RunDetail.js +4 -1
  142. package/lib/views/mcp/ScimProvisioning.js +2 -2
  143. package/lib/views/mcp/ToolAccess.js +2 -2
  144. package/package.json +1 -1
@@ -33,7 +33,7 @@
33
33
  * ```
34
34
  */
35
35
  import { QueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
36
- import { IAnyOrganization, IContact, IIAMToken, IOrganization, ISchool, ISecret, IStudent, ITeam, IUser, IUserOnboarding, IUserSettings } from '../models';
36
+ import { IAnyOrganization, IContact, IIAMProviderName, IIAMToken, IOrganization, ISchool, ISecret, IStudent, ITeam, IUser, IUserOnboarding, IUserSettings } from '../models';
37
37
  type IAnySpace = any;
38
38
  export type CacheProps = {
39
39
  loginRoute?: string;
@@ -486,6 +486,7 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
486
486
  useGetGitHubProfile: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
487
487
  useGetGoogleProfile: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
488
488
  useGetLinkedinProfile: () => import("@tanstack/react-query").UseMutationResult<any, Error, void, unknown>;
489
+ useUnlinkIAMProvider: () => import("@tanstack/react-query").UseMutationResult<void, Error, IIAMProviderName, unknown>;
489
490
  usePostLinkedinShare: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
490
491
  postText: string;
491
492
  }, unknown>;
@@ -1015,6 +1016,12 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
1015
1016
  useSecrets: (options?: {
1016
1017
  principalUid?: string;
1017
1018
  principalKind?: "personal" | "organization" | "team";
1019
+ } & {
1020
+ /**
1021
+ * `false` reads nothing: a page a visitor opens signed out, whose 401
1022
+ * would otherwise send them to the sign-in.
1023
+ */
1024
+ enabled?: boolean;
1018
1025
  }) => import("@tanstack/react-query").UseQueryResult<any, Error>;
1019
1026
  useCreateSecret: (options?: {
1020
1027
  principalUid?: string;
@@ -40,6 +40,7 @@ import { useQuery, useQueries, useMutation, useQueryClient, } from '@tanstack/re
40
40
  import { BOOTSTRAP_USER_ONBOARDING, asContact, asInvite, asOrganization, asSecret, asTeam, asToken, asUser, } from '../models/index.js';
41
41
  import { asSpace } from './cacheConverters.js';
42
42
  import { libraryAppFieldsOf } from './libraryApp.js';
43
+ import { unlinkIAMProvider } from './unlinkIAMProvider.js';
43
44
  import { useCoreStore, useIAMStore, profileStore } from '../state/index.js';
44
45
  import { asDisplayName, namesAsInitials, asArray } from '../utils/index.js';
45
46
  import { useDatalayer } from './useDatalayer.js';
@@ -2853,6 +2854,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2853
2854
  },
2854
2855
  ...DEFAULT_QUERY_OPTIONS,
2855
2856
  refetchOnMount: true, // Override to refetch when stale after invalidation
2857
+ enabled: options?.enabled ?? true,
2856
2858
  });
2857
2859
  };
2858
2860
  /**
@@ -5740,14 +5742,17 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
5740
5742
  });
5741
5743
  };
5742
5744
  /**
5743
- * Confirm password change with token
5745
+ * Confirm password change with the token the reset email carried. The
5746
+ * handle and token travel in the body, never in the URL: paths are written
5747
+ * to access logs.
5744
5748
  */
5745
5749
  const useConfirmPasswordWithToken = () => {
5746
5750
  return useMutation({
5747
5751
  mutationFn: async ({ userHandle, token, }) => {
5748
5752
  return requestDatalayer({
5749
- url: `${configuration.iamUrl}/api/iam/v1/password/confirm/users/${userHandle}/tokens/${token}`,
5750
- method: 'PUT',
5753
+ url: `${configuration.iamUrl}/api/iam/v1/password-reset/confirm`,
5754
+ method: 'POST',
5755
+ body: { handle: userHandle, token },
5751
5756
  });
5752
5757
  },
5753
5758
  });
@@ -7273,13 +7278,38 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7273
7278
  },
7274
7279
  });
7275
7280
  };
7276
- /** Get the server-connected LinkedIn profile. */
7281
+ /**
7282
+ * Unlink a provider (github, google, linkedin, x) from the signed-in
7283
+ * account; IAM deletes the link and the tokens it holds for it.
7284
+ *
7285
+ * IAM's refusal (409: the provider is the account's only way to sign in)
7286
+ * is the error's message, for the page to show: nothing is toasted. On
7287
+ * success the signed-in user is read again, so the settings pages and the
7288
+ * header's provider icons follow its linked providers.
7289
+ */
7290
+ const useUnlinkIAMProvider = () => {
7291
+ const iamStore = useIAMStore();
7292
+ return useMutation({
7293
+ mutationFn: async (provider) => unlinkIAMProvider(requestDatalayer, configuration.iamUrl, provider, async () => {
7294
+ queryClient.invalidateQueries({ queryKey: queryKeys.auth.me() });
7295
+ await iamStore.refreshUser();
7296
+ }),
7297
+ });
7298
+ };
7299
+ /**
7300
+ * Get the server-connected LinkedIn profile.
7301
+ *
7302
+ * IAM answers 404 when no LinkedIn posting account is connected: a state
7303
+ * the caller shows (a link button), never an error toast. The resolved
7304
+ * response then carries `success: false`, `status` and `message`.
7305
+ */
7277
7306
  const useGetLinkedinProfile = () => {
7278
7307
  return useMutation({
7279
7308
  mutationFn: async () => {
7280
7309
  return requestDatalayer({
7281
7310
  url: `${configuration.iamUrl}/api/iam/v1/social/linkedin/profile`,
7282
7311
  method: 'GET',
7312
+ notifyOnError: false,
7283
7313
  });
7284
7314
  },
7285
7315
  });
@@ -8426,6 +8456,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
8426
8456
  useGetGitHubProfile,
8427
8457
  useGetGoogleProfile,
8428
8458
  useGetLinkedinProfile,
8459
+ useUnlinkIAMProvider,
8429
8460
  usePostLinkedinShare,
8430
8461
  usePostLinkedinShareWithUpload,
8431
8462
  useGetBlueskyConnection,
@@ -25,6 +25,7 @@
25
25
  import { useCallback, useMemo } from 'react';
26
26
  import { useLayoutStore } from '../state/index.js';
27
27
  import { navigationStore } from '../state/substates/NavigationState.js';
28
+ import { iamStore } from '../state/substates/IAMState.js';
28
29
  import { createNativeNavigate } from '../navigation/adapters/native.js';
29
30
  // Import React Router hooks from our wrapper
30
31
  import { useNavigateRR } from '../navigation/adapters/react-router.js';
@@ -133,8 +134,13 @@ export const useNavigate = () => {
133
134
  document.body.scrollTop = 0;
134
135
  }
135
136
  // Where the reader is now, for the shell they are in. A new tab and a
136
- // step back through history are both already past by here.
137
- navigationStore.getState().rememberRoute(to);
137
+ // step back through history are both already past by here. Only for
138
+ // somebody signed in: the shells are theirs, and a page a visitor read
139
+ // signed out (`/features`) would otherwise come back as the Platform's
140
+ // last page after they sign in.
141
+ if (iamStore.getState().user) {
142
+ navigationStore.getState().rememberRoute(to);
143
+ }
138
144
  if (rrNavigate) {
139
145
  rrNavigate(to, { replace: options.replace, state: options.state });
140
146
  return;
@@ -1,6 +1,15 @@
1
1
  import { JSONObject } from '@lumino/coreutils';
2
2
  import { IIAMProviderName } from './IAMProvidersSpecs';
3
+ /**
4
+ * One `iam_providers` child of the user IAM returns (whoami, login): its
5
+ * Solr fields `iam_provider_name_s`, `linked_account_s`,
6
+ * `linked_account_url_s`, `linked_account_id_s`.
7
+ */
3
8
  export declare function asIAMProviderLinked(iamProvider: any): IIAMProviderLinked;
9
+ /** Whether the user has linked this provider in IAM. */
10
+ export declare function hasLinkedIAMProvider(user: {
11
+ iamProviders?: Array<IIAMProviderLinked>;
12
+ } | null | undefined, name: IIAMProviderName): boolean;
4
13
  export type IIAMProviderLinked = {
5
14
  iamProviderName: IIAMProviderName;
6
15
  linkedAccount: JSONObject;
@@ -2,12 +2,41 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /**
6
+ * The account a provider returned, as IAM keeps it in `linked_account_s`.
7
+ * GitHub, Google and LinkedIn keep a JSON object; Bluesky and X keep a plain
8
+ * handle, which is no object to read, so it reads as `{}`.
9
+ */
10
+ function linkedAccountOf(raw) {
11
+ if (typeof raw !== 'string' || raw.trim() === '') {
12
+ return {};
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(raw);
16
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
17
+ ? parsed
18
+ : {};
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ /**
25
+ * One `iam_providers` child of the user IAM returns (whoami, login): its
26
+ * Solr fields `iam_provider_name_s`, `linked_account_s`,
27
+ * `linked_account_url_s`, `linked_account_id_s`.
28
+ */
5
29
  export function asIAMProviderLinked(iamProvider) {
6
- const info = iamProvider.linkedAccount.replaceAll("'", '"');
7
30
  return {
8
- iamProviderName: iamProvider.iam_ProviderName,
9
- linkedAccount: JSON.parse(info === '' ? '{}' : info),
10
- linkedAccountUrl: iamProvider.linkedAccountUrl,
11
- linkedAccountId: iamProvider.linkedAccountId,
31
+ iamProviderName: String(iamProvider.iam_provider_name_s ?? '')
32
+ .trim()
33
+ .toLowerCase(),
34
+ linkedAccount: linkedAccountOf(iamProvider.linked_account_s),
35
+ linkedAccountUrl: iamProvider.linked_account_url_s ?? '',
36
+ linkedAccountId: iamProvider.linked_account_id_s ?? '',
12
37
  };
13
38
  }
39
+ /** Whether the user has linked this provider in IAM. */
40
+ export function hasLinkedIAMProvider(user, name) {
41
+ return (user?.iamProviders ?? []).some(p => p.iamProviderName === name);
42
+ }
@@ -58,7 +58,12 @@ export interface McpTask {
58
58
  pollIntervalMs?: number | null;
59
59
  /** The `CallToolResult` for small results; a resource URI otherwise. */
60
60
  result?: McpCallToolResult | string | null;
61
- error?: string | null;
61
+ /**
62
+ * Why it failed, as the server keeps it: an object — empty while nothing
63
+ * failed, else its `reason`, and where one applies the `code`, the `step`
64
+ * and what the resource was doing.
65
+ */
66
+ error?: McpTaskError | null;
62
67
  traceId?: string | null;
63
68
  workflowEngine?: McpWorkflowEngine | null;
64
69
  /** Always the task uid, so a retried start cannot create a second execution. */
@@ -92,3 +97,15 @@ export interface McpTaskEvent {
92
97
  /** The statuses from which nothing more happens. */
93
98
  export declare const MCP_TERMINAL_TASK_STATUSES: ReadonlySet<McpTaskStatus>;
94
99
  export declare const isMcpTaskTerminal: (status: McpTaskStatus) => boolean;
100
+ /** A task's failure, as the server keeps it (camel-cased like the rest). */
101
+ export type McpTaskError = {
102
+ reason?: string;
103
+ code?: string;
104
+ step?: string;
105
+ [key: string]: unknown;
106
+ };
107
+ /**
108
+ * Why a task failed, in a line: its reason, after its code where it has one;
109
+ * `null` when the server's object says nothing — an empty one is no failure.
110
+ */
111
+ export declare const taskFailureOf: (error: McpTaskError | null | undefined) => string | null;
@@ -5,3 +5,18 @@
5
5
  /** The statuses from which nothing more happens. */
6
6
  export const MCP_TERMINAL_TASK_STATUSES = new Set(['completed', 'failed', 'cancelled']);
7
7
  export const isMcpTaskTerminal = (status) => MCP_TERMINAL_TASK_STATUSES.has(status);
8
+ /**
9
+ * Why a task failed, in a line: its reason, after its code where it has one;
10
+ * `null` when the server's object says nothing — an empty one is no failure.
11
+ */
12
+ export const taskFailureOf = (error) => {
13
+ if (!error || Object.keys(error).length === 0) {
14
+ return null;
15
+ }
16
+ const reason = typeof error.reason === 'string' ? error.reason : null;
17
+ const code = typeof error.code === 'string' ? error.code : null;
18
+ if (reason) {
19
+ return code ? `${code}: ${reason}` : reason;
20
+ }
21
+ return code ?? JSON.stringify(error);
22
+ };
@@ -34,8 +34,8 @@ export interface Profile {
34
34
  avatar_url_s?: string;
35
35
  avatar_icon_s?: string;
36
36
  banner_s?: string;
37
- /** Onboarding state as JSON string */
38
- onboarding_s?: string;
37
+ /** Onboarding state: the child IAM keeps on the user (read by `userOnboardingOf`). */
38
+ onboarding?: Record<string, unknown>;
39
39
  /** New password request timestamp */
40
40
  new_password_request_ts_dt?: string | null;
41
41
  /** New password confirmation timestamp */
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { asDisplayName, namesAsInitials, toFriendlyName } from '../utils/index.js';
6
6
  import { asIAMProviderLinked } from './IAMProviderLinked.js';
7
- import { BOOTSTRAP_USER_ONBOARDING } from './UserOnboarding.js';
7
+ import { userOnboardingOf } from './UserOnboarding.js';
8
8
  import { UserSettings } from './UserSettings.js';
9
9
  import { asUserEvent } from './UserEvent.js';
10
10
  export const ANONYMOUS_USER = undefined;
@@ -76,20 +76,13 @@ export class User {
76
76
  this.credits = u.credits_i ? Number(u.credits_i) : 0;
77
77
  this.creditsCustomerId = u.stripe_customer_id_s;
78
78
  this.roles = u.roles_ss ?? [];
79
- let iamProviders = [];
80
- try {
81
- iamProviders = (u.iam_providers ?? []).map(iamProvider => asIAMProviderLinked(iamProvider));
82
- }
83
- catch (e) {
84
- // no-op for backwards compatibility.
85
- }
86
- this.iamProviders = iamProviders;
79
+ // IAM sends one linked provider as an object, several as a list.
80
+ const rawIAMProviders = u.iam_providers ?? [];
81
+ this.iamProviders = (Array.isArray(rawIAMProviders) ? rawIAMProviders : [rawIAMProviders]).map(asIAMProviderLinked);
87
82
  this.settings = new UserSettings(u.settings ?? {});
88
83
  this.unsubscribedFromOutbounds = u.unsubscribed_from_outbounds_b ?? false;
89
84
  this.mfaUrl = u.mfa_url_s;
90
- this.onboarding = u.onboarding_s
91
- ? JSON.parse(u.onboarding_s)
92
- : BOOTSTRAP_USER_ONBOARDING;
85
+ this.onboarding = userOnboardingOf(u.onboarding);
93
86
  this.linkedContactId = u.linked_contact_uid;
94
87
  this.subscription = u.subscription;
95
88
  let events = u.events ?? [];
@@ -12,3 +12,12 @@ export type IUserOnboarding = {
12
12
  position: IOnboardingPosition;
13
13
  tours: IOnboardingTours;
14
14
  };
15
+ /**
16
+ * The user's onboarding, as IAM keeps it: a child of the user whose fields
17
+ * say the guide's position (`position_s`), each client's progress
18
+ * (`client_<client>_i`) and each tour's status (`tour_<tour>_status_s`), over
19
+ * what nothing has said yet. Read from the old `onboarding_s` string, a
20
+ * status saved since was never seen: the welcome tour started again on every
21
+ * load and was saved again (13 October 2026).
22
+ */
23
+ export declare const userOnboardingOf: (child: unknown) => IUserOnboarding;
@@ -17,3 +17,36 @@ export const BOOTSTRAP_USER_ONBOARDING = {
17
17
  },
18
18
  },
19
19
  };
20
+ /**
21
+ * The user's onboarding, as IAM keeps it: a child of the user whose fields
22
+ * say the guide's position (`position_s`), each client's progress
23
+ * (`client_<client>_i`) and each tour's status (`tour_<tour>_status_s`), over
24
+ * what nothing has said yet. Read from the old `onboarding_s` string, a
25
+ * status saved since was never seen: the welcome tour started again on every
26
+ * load and was saved again (13 October 2026).
27
+ */
28
+ export const userOnboardingOf = (child) => {
29
+ const stored = (Array.isArray(child) ? child[0] : child);
30
+ const onboarding = {
31
+ clients: { ...BOOTSTRAP_USER_ONBOARDING.clients },
32
+ position: BOOTSTRAP_USER_ONBOARDING.position,
33
+ tours: { ...BOOTSTRAP_USER_ONBOARDING.tours },
34
+ };
35
+ if (!stored || typeof stored !== 'object') {
36
+ return onboarding;
37
+ }
38
+ if (typeof stored.position_s === 'string') {
39
+ onboarding.position = stored.position_s;
40
+ }
41
+ for (const [key, value] of Object.entries(stored)) {
42
+ const client = /^client_(.+)_i$/.exec(key)?.[1];
43
+ if (client && typeof value === 'number') {
44
+ onboarding.clients[client] = value;
45
+ }
46
+ const tour = /^tour_(.+)_status_s$/.exec(key)?.[1];
47
+ if (tour && typeof value === 'string') {
48
+ onboarding.tours[tour] = { status: value };
49
+ }
50
+ }
51
+ return onboarding;
52
+ };
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * What any JavaScript runtime can import from Core: no React, DOM, Jupyter or
3
- * Node built-in. Re-exports only; the mobile app reaches Core through this
4
- * entry alone.
3
+ * Node built-in. The mobile app reaches Core through this entry alone, and
4
+ * its modules import nothing but each other and Core's pure models.
5
5
  *
6
6
  * @module portable
7
7
  */
8
8
  export { personOf, type Person } from '../models/Person';
9
+ export { detailOf, isProblem, kindOfStatus, Problem, PROBLEM_WORDS, problemOf, retries, STATE_WORDS, type ProblemKind, } from './states';
@@ -4,9 +4,11 @@
4
4
  */
5
5
  /**
6
6
  * What any JavaScript runtime can import from Core: no React, DOM, Jupyter or
7
- * Node built-in. Re-exports only; the mobile app reaches Core through this
8
- * entry alone.
7
+ * Node built-in. The mobile app reaches Core through this entry alone, and
8
+ * its modules import nothing but each other and Core's pure models.
9
9
  *
10
10
  * @module portable
11
11
  */
12
12
  export { personOf } from '../models/Person.js';
13
+ // Loading, empty and problem states, in a person's words (MOBILE M-013b).
14
+ export { detailOf, isProblem, kindOfStatus, Problem, PROBLEM_WORDS, problemOf, retries, STATE_WORDS, } from './states.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Loading, empty and problem states as a person is told them (MOBILE M-013b):
3
+ * what went wrong in their words, the diagnostic detail kept behind
4
+ * *Details* (never a token, a prompt or tool arguments, never a query value),
5
+ * whether trying again can help, and the words around them. A renderer on
6
+ * any platform draws these; it does not word them itself.
7
+ *
8
+ * @module portable/states
9
+ */
10
+ /** What went wrong, as a person is told it. */
11
+ export type ProblemKind = 'unauthenticated' | 'forbidden' | 'not-found' | 'conflict' | 'rate-limited' | 'unavailable' | 'timeout' | 'offline' | 'unexpected';
12
+ export declare const PROBLEM_WORDS: Record<ProblemKind, string>;
13
+ /** The words around a state: loading, nothing yet, trying again, the detail. */
14
+ export declare const STATE_WORDS: {
15
+ readonly loading: "Loading";
16
+ readonly empty: "Nothing yet.";
17
+ readonly retry: "Try again";
18
+ readonly details: "Details";
19
+ readonly hideDetails: "Hide details";
20
+ readonly showDetailsLabel: "Show diagnostic details";
21
+ readonly hideDetailsLabel: "Hide diagnostic details";
22
+ };
23
+ /** A failed request: the person's words, and the detail behind *Details*. */
24
+ export declare class Problem extends Error {
25
+ readonly kind: ProblemKind;
26
+ readonly status?: number;
27
+ /** For *Details* only; never a token, a prompt or tool arguments. */
28
+ readonly detail: string;
29
+ constructor(kind: ProblemKind, detail: string, status?: number);
30
+ }
31
+ export declare const isProblem: (error: unknown) => error is Problem;
32
+ /** A thrown value as a Problem: anything unknown is *unexpected*. */
33
+ export declare const problemOf: (error: unknown) => Problem;
34
+ /** The kind of an HTTP status that failed. */
35
+ export declare function kindOfStatus(status: number): ProblemKind;
36
+ /** Whether trying again can help: not for what is gone or not theirs. */
37
+ export declare const retries: (problem: Problem) => boolean;
38
+ /** The detail a server gave, without query values that could carry a token. */
39
+ export declare function detailOf(method: string, url: string, status: number, body: unknown): string;
@@ -0,0 +1,71 @@
1
+ /*
2
+ * Copyright (c) 2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export const PROBLEM_WORDS = {
6
+ unauthenticated: 'Your session has ended. Sign in again.',
7
+ forbidden: 'You do not have access to this.',
8
+ 'not-found': 'This item is no longer available.',
9
+ conflict: 'This changed somewhere else. It has been refreshed.',
10
+ 'rate-limited': 'Too many requests. Try again in a moment.',
11
+ unavailable: 'Datalayer is not answering right now. Try again.',
12
+ timeout: 'This is taking too long. Try again.',
13
+ offline: 'You are offline. Connect to continue.',
14
+ unexpected: 'Something went wrong.',
15
+ };
16
+ /** The words around a state: loading, nothing yet, trying again, the detail. */
17
+ export const STATE_WORDS = {
18
+ loading: 'Loading',
19
+ empty: 'Nothing yet.',
20
+ retry: 'Try again',
21
+ details: 'Details',
22
+ hideDetails: 'Hide details',
23
+ showDetailsLabel: 'Show diagnostic details',
24
+ hideDetailsLabel: 'Hide diagnostic details',
25
+ };
26
+ /** A failed request: the person's words, and the detail behind *Details*. */
27
+ export class Problem extends Error {
28
+ kind;
29
+ status;
30
+ /** For *Details* only; never a token, a prompt or tool arguments. */
31
+ detail;
32
+ constructor(kind, detail, status) {
33
+ super(PROBLEM_WORDS[kind]);
34
+ this.name = 'Problem';
35
+ this.kind = kind;
36
+ this.detail = detail;
37
+ this.status = status;
38
+ }
39
+ }
40
+ export const isProblem = (error) => error instanceof Problem;
41
+ /** A thrown value as a Problem: anything unknown is *unexpected*. */
42
+ export const problemOf = (error) => isProblem(error)
43
+ ? error
44
+ : new Problem('unexpected', error instanceof Error ? error.message : String(error));
45
+ /** The kind of an HTTP status that failed. */
46
+ export function kindOfStatus(status) {
47
+ if (status === 401)
48
+ return 'unauthenticated';
49
+ if (status === 403)
50
+ return 'forbidden';
51
+ if (status === 404)
52
+ return 'not-found';
53
+ if (status === 409)
54
+ return 'conflict';
55
+ if (status === 429)
56
+ return 'rate-limited';
57
+ if (status === 502 || status === 503 || status === 504)
58
+ return 'unavailable';
59
+ return 'unexpected';
60
+ }
61
+ /** Whether trying again can help: not for what is gone or not theirs. */
62
+ export const retries = (problem) => problem.kind !== 'not-found' && problem.kind !== 'forbidden';
63
+ /** The detail a server gave, without query values that could carry a token. */
64
+ export function detailOf(method, url, status, body) {
65
+ const path = url.split('?')[0];
66
+ const said = body && typeof body === 'object'
67
+ ? (body.detail ??
68
+ body.message)
69
+ : undefined;
70
+ return `${method} ${path} → ${status}${typeof said === 'string' ? `: ${said}` : ''}`;
71
+ }
@@ -1,6 +1,5 @@
1
- /** The signed-in shell: the platform, the business cases, or the admin. */
2
1
  /**
3
- * The five shells of the signed-in application.
2
+ * The six shells of the signed-in application.
4
3
  *
5
4
  * `studio` is where an agentic UI application is composed, validated and
6
5
  * shipped — the default for somebody who has not chosen, and what the root
@@ -12,8 +11,11 @@
12
11
  * a sidebar of their own, every address under `/apps`, and the workspace at
13
12
  * `/apps` itself. It was `loop`, at `/loop`: a cookie still saying `loop`
14
13
  * names no shell and is ignored, the reader opening on the Studio.
14
+ * `research` is the research areas, each one a space of the
15
+ * `datalayer-research` organization, at `/research` (the public papers, read
16
+ * signed in, at `/research/papers/…` beside them).
15
17
  */
16
- export type NavigationView = 'studio' | 'home' | 'agentify' | 'admin' | 'apps';
18
+ export type NavigationView = 'studio' | 'home' | 'agentify' | 'research' | 'admin' | 'apps';
17
19
  /** The shells, and where each one opens when it has not been visited. */
18
20
  export declare const NAVIGATION_VIEW_HOMES: Record<NavigationView, string>;
19
21
  /** Well-known view identifiers for {@link NavigationState.tabsByView}. */
@@ -34,10 +34,13 @@ import { registerSessionState } from '../sessionEnd.js';
34
34
  /** The shells, and where each one opens when it has not been visited. */
35
35
  export const NAVIGATION_VIEW_HOMES = {
36
36
  studio: '/studio',
37
- // Not `/`: the root is the Studio's, so the Power shell has an address of
38
- // its own to be sent to.
39
- home: '/power',
37
+ // Not `/`: the root is the Studio's, so the Platform shell has an address
38
+ // of its own to be sent to. It was `/power` until 2026-10-09, when the
39
+ // shell took the name it is called by everywhere else.
40
+ home: '/platform',
40
41
  agentify: '/agentify',
42
+ // The research areas and their spaces.
43
+ research: '/research',
41
44
  admin: '/admin',
42
45
  // The applications: what a builder opens the Apps shell for.
43
46
  apps: '/apps/apps',
@@ -58,6 +61,7 @@ const COOKIE_MAX_AGE_DAYS = 365;
58
61
  const isView = (value) => value === 'studio' ||
59
62
  value === 'home' ||
60
63
  value === 'agentify' ||
64
+ value === 'research' ||
61
65
  value === 'admin' ||
62
66
  value === 'apps';
63
67
  function readCookie(name) {
@@ -244,6 +248,11 @@ export function viewForRoute(route, current) {
244
248
  if (path === '/agentify' || path.startsWith('/agentify/')) {
245
249
  return 'agentify';
246
250
  }
251
+ // The research areas — and the papers, read signed in under
252
+ // `/research/papers/…`, with the areas' sidebar beside them.
253
+ if (path === '/research' || path.startsWith('/research/')) {
254
+ return 'research';
255
+ }
247
256
  if (path === '/admin' || path.startsWith('/admin/')) {
248
257
  return 'admin';
249
258
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useMemo, useState } from 'react';
3
3
  import { Button, Flash, FormControl, Heading, Spinner, Text, TextInput, } from '@primer/react';
4
- import { Box } from '@datalayer/primer-addons';
4
+ import { Box, Card } from '@datalayer/primer-addons';
5
5
  import { McpErrorBlankslate } from '../../components/mcp/index.js';
6
6
  import { useAuditSettings, useSetAuditSettings } from '../../hooks/useMcp.js';
7
7
  import { useToast } from '../../hooks/index.js';
@@ -62,6 +62,6 @@ export const AlertDestinations = ({ errorState, orgUid, readOnly = false, }) =>
62
62
  if (settings.isPending && settings.data === undefined) {
63
63
  return (_jsx(Box, { display: "flex", justifyContent: "center", py: 4, children: _jsx(Spinner, {}) }));
64
64
  }
65
- return (_jsxs(Box, { display: "grid", gap: 3, maxWidth: "42rem", p: 3, border: "1px solid", borderColor: "border.default", borderRadius: 2, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 1 }, children: "Where alerts go" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Set once for the organization, not per rule \u2014 a URL repeated on twenty rules is nineteen places to forget when it rotates. Every firing also appears in the app, and that copy is sent first so it is not lost when one of these is down." })] }), refusal && (_jsx(Flash, { variant: "danger", children: _jsx(Text, { sx: { fontSize: 1 }, children: refusal }) })), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Email" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertEmails, onChange: event => set('alertEmails', event.target.value), placeholder: "ops@example.co, oncall@example.co" }), _jsx(FormControl.Caption, { children: "Comma-separated, up to twenty. An alert list is the people who want to know, not a mailing list \u2014 a firing that emails two hundred people is a firing that gets the rule switched off." })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Slack" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertSlackWebhookUrl, onChange: event => set('alertSlackWebhookUrl', event.target.value), placeholder: "https://hooks.slack.com/services/\u2026" }), _jsx(FormControl.Caption, { children: "A Slack incoming webhook, and only Slack\u2019s \u2014 anything else belongs in the field below. Through this one Slack gets a coloured attachment with the reading, the severity and the scope as separate fields; through the generic webhook it shows one grey line." })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Webhook" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertWebhookUrl, onChange: event => set('alertWebhookUrl', event.target.value), placeholder: "https://events.pagerduty.com/\u2026" }), _jsxs(FormControl.Caption, { children: ["Anything that takes a JSON POST: PagerDuty, a chat relay, your own endpoint. ", _jsx("strong", { children: "https only" }), " \u2014 an alert names your organization and what its agents did. It carries no credential."] })] }), !readOnly && (_jsxs(Box, { display: "flex", gap: 2, children: [_jsx(Button, { variant: "primary", onClick: apply, disabled: !changed || save.isPending, children: "Save destinations" }), changed && (_jsx(Button, { variant: "invisible", onClick: () => setDraft(stored), children: "Discard changes" }))] })), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: ["Each copy is attempted on its own: if the webhook is down you still get the email. A copy that fails is recorded and", ' ', _jsx("strong", { children: "not retried" }), " \u2014 retrying a notice against an endpoint that is down turns its outage into a loop against it, and the alert is already in the app."] })] }));
65
+ return (_jsxs(Card, { border: true, shadow: "none", display: "grid", gap: 3, maxWidth: "42rem", p: 3, children: [_jsxs(Box, { children: [_jsx(Heading, { as: "h3", sx: { fontSize: 2, mb: 1 }, children: "Where alerts go" }), _jsx(Text, { as: "p", sx: { color: 'fg.muted', fontSize: 1, m: 0 }, children: "Set once for the organization, not per rule \u2014 a URL repeated on twenty rules is nineteen places to forget when it rotates. Every firing also appears in the app, and that copy is sent first so it is not lost when one of these is down." })] }), refusal && (_jsx(Flash, { variant: "danger", children: _jsx(Text, { sx: { fontSize: 1 }, children: refusal }) })), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Email" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertEmails, onChange: event => set('alertEmails', event.target.value), placeholder: "ops@example.co, oncall@example.co" }), _jsx(FormControl.Caption, { children: "Comma-separated, up to twenty. An alert list is the people who want to know, not a mailing list \u2014 a firing that emails two hundred people is a firing that gets the rule switched off." })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Slack" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertSlackWebhookUrl, onChange: event => set('alertSlackWebhookUrl', event.target.value), placeholder: "https://hooks.slack.com/services/\u2026" }), _jsx(FormControl.Caption, { children: "A Slack incoming webhook, and only Slack\u2019s \u2014 anything else belongs in the field below. Through this one Slack gets a coloured attachment with the reading, the severity and the scope as separate fields; through the generic webhook it shows one grey line." })] }), _jsxs(FormControl, { children: [_jsx(FormControl.Label, { children: "Webhook" }), _jsx(TextInput, { block: true, disabled: readOnly, value: draft.alertWebhookUrl, onChange: event => set('alertWebhookUrl', event.target.value), placeholder: "https://events.pagerduty.com/\u2026" }), _jsxs(FormControl.Caption, { children: ["Anything that takes a JSON POST: PagerDuty, a chat relay, your own endpoint. ", _jsx("strong", { children: "https only" }), " \u2014 an alert names your organization and what its agents did. It carries no credential."] })] }), !readOnly && (_jsxs(Box, { display: "flex", gap: 2, children: [_jsx(Button, { variant: "primary", onClick: apply, disabled: !changed || save.isPending, children: "Save destinations" }), changed && (_jsx(Button, { variant: "invisible", onClick: () => setDraft(stored), children: "Discard changes" }))] })), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.subtle' }, children: ["Each copy is attempted on its own: if the webhook is down you still get the email. A copy that fails is recorded and", ' ', _jsx("strong", { children: "not retried" }), " \u2014 retrying a notice against an endpoint that is down turns its outage into a loop against it, and the alert is already in the app."] })] }));
66
66
  };
67
67
  export default AlertDestinations;
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { Button, Heading, Label, RelativeTime, SegmentedControl, Spinner, Text, Textarea, } from '@primer/react';
4
- import { Box } from '@datalayer/primer-addons';
4
+ import { Box, Card } from '@datalayer/primer-addons';
5
5
  import { McpErrorBlankslate } from '../../components/mcp/index.js';
6
6
  import { useDecideMcpApproval, useMcpApprovals } from '../../hooks/useContents.js';
7
7
  /** How each state of an approval is drawn. */
@@ -59,7 +59,7 @@ export const ApprovalQueue = ({ errorState, sourceUid, }) => {
59
59
  : `No ${APPROVAL_LOOK[status].label.toLowerCase()} approvals.` })) : (items.map(approval => {
60
60
  const look = approvalLook(approval);
61
61
  const decidable = isDecidable(approval);
62
- return (_jsxs(Box, { border: "1px solid", borderColor: decidable ? 'attention.emphasis' : 'border.default', borderRadius: 2, p: 3, display: "grid", gap: 2, minWidth: 0, children: [_jsxs(Box, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap", children: [_jsx(Label, { size: "small", variant: look.variant, children: look.label }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: approval.tool }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: approval.actorUid }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["asked ", _jsx(RelativeTime, { datetime: approval.createdAt })] }), decidable && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["expires ", _jsx(RelativeTime, { datetime: approval.expiresAt })] }))] }), _jsx(Box, { as: "pre", fontFamily: "mono", fontSize: 0, whiteSpace: "pre-wrap", overflowX: "auto", bg: "canvas.subtle", borderRadius: 2, p: 2, m: 0, children: JSON.stringify(approval.argumentsRedacted, null, 2) }), approval.destinationUri && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Writes to", ' ', _jsx(Text, { as: "span", sx: { fontFamily: 'mono' }, children: approval.destinationUri })] })), decidable ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": `Why, for ${approval.tool}`, rows: 2, placeholder: "Why (optional; kept with the decision)", value: noteFor[approval.uid] ?? '', onChange: event => setNoteFor(current => ({
62
+ return (_jsxs(Card, { border: true, shadow: "none", borderColor: decidable ? 'attention.emphasis' : 'border.default', p: 3, display: "grid", gap: 2, minWidth: 0, children: [_jsxs(Box, { display: "flex", gap: 2, alignItems: "center", flexWrap: "wrap", children: [_jsx(Label, { size: "small", variant: look.variant, children: look.label }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: approval.tool }), _jsx(Text, { sx: { fontSize: 0, color: 'fg.muted', fontFamily: 'mono' }, children: approval.actorUid }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["asked ", _jsx(RelativeTime, { datetime: approval.createdAt })] }), decidable && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["expires ", _jsx(RelativeTime, { datetime: approval.expiresAt })] }))] }), _jsx(Box, { as: "pre", fontFamily: "mono", fontSize: 0, whiteSpace: "pre-wrap", overflowX: "auto", bg: "canvas.subtle", borderRadius: 2, p: 2, m: 0, children: JSON.stringify(approval.argumentsRedacted, null, 2) }), approval.destinationUri && (_jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Writes to", ' ', _jsx(Text, { as: "span", sx: { fontFamily: 'mono' }, children: approval.destinationUri })] })), decidable ? (_jsxs(_Fragment, { children: [_jsx(Textarea, { "aria-label": `Why, for ${approval.tool}`, rows: 2, placeholder: "Why (optional; kept with the decision)", value: noteFor[approval.uid] ?? '', onChange: event => setNoteFor(current => ({
63
63
  ...current,
64
64
  [approval.uid]: event.target.value,
65
65
  })) }), _jsxs(Box, { display: "flex", gap: 2, children: [_jsx(Button, { variant: "primary", disabled: decide.isPending, onClick: () => decide.mutate({
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Button, Heading, Label, SegmentedControl, Spinner, Text, } from '@primer/react';
3
3
  import { Blankslate, DataTable, Table } from '@primer/react/experimental';
4
- import { Box } from '@datalayer/primer-addons';
4
+ import { Box, Card } from '@datalayer/primer-addons';
5
5
  import { AlertIcon, CheckCircleIcon, LockIcon, PlugIcon, } from '@primer/octicons-react';
6
6
  import { ClientBadge, McpErrorBlankslate, ScopeList, } from '../../components/mcp/index.js';
7
7
  import { useDisconnectAgent, useMcpActivity, useOrgMcpOverview, } from '../../hooks/useMcp.js';
@@ -90,7 +90,7 @@ export const pagesForRoles = (roles) => {
90
90
  }
91
91
  return [];
92
92
  };
93
- const Metric = ({ label, value, note, tone = 'neutral', }) => (_jsxs(Box, { p: 3, border: "1px solid", borderColor: "border.default", borderRadius: 2, minWidth: 0, children: [_jsx(Text, { sx: {
93
+ const Metric = ({ label, value, note, tone = 'neutral', }) => (_jsxs(Card, { border: true, shadow: "none", display: "block", p: 3, minWidth: 0, children: [_jsx(Text, { sx: {
94
94
  display: 'block',
95
95
  fontSize: 4,
96
96
  fontWeight: 'bold',