@datalayer/core 1.2.12 → 1.2.13

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.
package/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## Overview
20
20
 
21
- Datalayer Core is the foundational package that powers the [Datalayer AI Platform](https://datalayer.app/). It provides a TypesScript and Python packages as a Command Line Interface (CLI) for AI engineers, data scientists, and researchers to seamlessly integrate scalable compute runtimes into their workflows.
21
+ Datalayer Core is the foundational package that powers the [Datalayer AI Platform](https://datalayer.ai/). It provides a TypesScript and Python packages as a Command Line Interface (CLI) for AI engineers, data scientists, and researchers to seamlessly integrate scalable compute runtimes into their workflows.
22
22
 
23
23
  This package serves as the base foundation used by many other Datalayer packages, containing core application classes, configuration, and unified APIs for authentication, runtime management, and code execution in cloud-based environments.
24
24
 
@@ -219,7 +219,7 @@ This project is licensed under the [BSD 3-Clause License](https://github.com/dat
219
219
 
220
220
  - **Documentation**: [Datalayer Platform Documentation](https://datalayer.ai/docs/)
221
221
  - **Issues**: [GitHub Issues](https://github.com/datalayer/core/issues)
222
- - **Community**: [Datalayer Platform](https://datalayer.app/)
222
+ - **Community**: [Datalayer Platform](https://datalayer.ai/)
223
223
 
224
224
  ---
225
225
 
@@ -229,5 +229,5 @@ This project is licensed under the [BSD 3-Clause License](https://github.com/dat
229
229
 
230
230
  <p align="center">
231
231
  <strong>🚀 AI Agents for Data Analysis</strong><br></br>
232
- <a href="https://datalayer.app/">Get started with Datalayer today!</a>
232
+ <a href="https://datalayer.ai/">Get started with Datalayer today!</a>
233
233
  </p>
@@ -498,6 +498,29 @@ export declare const useCache: ({ loginRoute }?: CacheProps) => {
498
498
  returnOrigin?: string;
499
499
  }, unknown>;
500
500
  useDisconnectBluesky: () => import("@tanstack/react-query").UseMutationResult<any, Error, void, unknown>;
501
+ useXConnection: (enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<{
502
+ configured: boolean;
503
+ connected: boolean;
504
+ account?: {
505
+ id: string;
506
+ username: string;
507
+ name: string;
508
+ profile_image_url: string;
509
+ url: string;
510
+ } | null;
511
+ }, Error>;
512
+ useConnectX: () => import("@tanstack/react-query").UseMutationResult<{
513
+ authorization_url: string;
514
+ }, Error, string | undefined, unknown>;
515
+ useDisconnectX: () => import("@tanstack/react-query").UseMutationResult<any, Error, void, unknown>;
516
+ usePublishXShare: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
517
+ itemId: string;
518
+ text: string;
519
+ idempotencyKey: string;
520
+ imageDataUrl?: string;
521
+ }, unknown>;
522
+ useUnpublishXShare: () => import("@tanstack/react-query").UseMutationResult<any, Error, string, unknown>;
523
+ useXShares: (itemId: string, enabled?: boolean) => import("@tanstack/react-query").UseQueryResult<any, Error>;
501
524
  usePublishLinkedInShare: () => import("@tanstack/react-query").UseMutationResult<any, Error, {
502
525
  itemId: string;
503
526
  commentary: string;
@@ -58,6 +58,19 @@ const DEFAULT_QUERY_OPTIONS = {
58
58
  // Ensure queries prioritize cache over network when data is fresh
59
59
  networkMode: 'online',
60
60
  };
61
+ /**
62
+ * A list of what a space holds — all of it, or the items of one type. Every
63
+ * such list takes these options (`spaceListsRefetchOnMount.unit.test.ts`
64
+ * holds them to it). Refetched every time a page mounts it: an item
65
+ * created elsewhere — a cell has no create mutation here to clear this cache —
66
+ * would otherwise stay missing from the list for the five minutes the default
67
+ * options keep it fresh, and coming back to the list is exactly when somebody
68
+ * looks for it.
69
+ */
70
+ const LIST_QUERY_OPTIONS = {
71
+ ...DEFAULT_QUERY_OPTIONS,
72
+ refetchOnMount: 'always',
73
+ };
61
74
  /**
62
75
  * Refresh what a visitor sees, not just what the editor is looking at.
63
76
  *
@@ -863,6 +876,32 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
863
876
  owner: toItemOwner(raw),
864
877
  };
865
878
  };
879
+ /**
880
+ * A published agentic UI application: what it answers, and its definition
881
+ * as the library hands it over — `model_s`, the string the Studio reads
882
+ * the definition from, left unparsed because the Studio owns its shape.
883
+ */
884
+ const toApp = (raw) => {
885
+ return {
886
+ id: raw.uid,
887
+ type: 'app',
888
+ name: raw.name_t,
889
+ description: raw.description_t,
890
+ tags: Array.isArray(raw.tags_ss) ? raw.tags_ss : [],
891
+ public: raw.is_public_b ?? false,
892
+ model_s: raw.model_s,
893
+ creationDate: raw.creation_ts_dt
894
+ ? new Date(raw.creation_ts_dt)
895
+ : undefined,
896
+ lastUpdateDate: raw.last_update_ts_dt
897
+ ? new Date(raw.last_update_ts_dt)
898
+ : undefined,
899
+ lastPublicationDate: raw.published_ts_dt
900
+ ? new Date(raw.published_ts_dt)
901
+ : undefined,
902
+ owner: toItemOwner(raw),
903
+ };
904
+ };
866
905
  /** An evaluator of the platform's catalogue (B5-01), published as an agent is. */
867
906
  const toEvaluator = (raw) => {
868
907
  return {
@@ -1375,6 +1414,10 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
1375
1414
  return toInvestigation(item);
1376
1415
  case 'evaluator':
1377
1416
  return toEvaluator(item);
1417
+ // Without this a published app answered `{}`, as a deck once did: a
1418
+ // card with no type and no name.
1419
+ case 'app':
1420
+ return toApp(item);
1378
1421
  case 'sitecard':
1379
1422
  return toSiteCard(item);
1380
1423
  default:
@@ -2345,7 +2388,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2345
2388
  }
2346
2389
  return [];
2347
2390
  },
2348
- ...DEFAULT_QUERY_OPTIONS,
2391
+ ...LIST_QUERY_OPTIONS,
2349
2392
  enabled: !!spaceId,
2350
2393
  });
2351
2394
  };
@@ -2477,7 +2520,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2477
2520
  }
2478
2521
  return [];
2479
2522
  },
2480
- ...DEFAULT_QUERY_OPTIONS,
2523
+ ...LIST_QUERY_OPTIONS,
2481
2524
  enabled: !!spaceId,
2482
2525
  });
2483
2526
  };
@@ -2652,7 +2695,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
2652
2695
  }
2653
2696
  return [];
2654
2697
  },
2655
- ...DEFAULT_QUERY_OPTIONS,
2698
+ ...LIST_QUERY_OPTIONS,
2656
2699
  enabled: !!spaceId,
2657
2700
  });
2658
2701
  };
@@ -4160,7 +4203,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4160
4203
  return [];
4161
4204
  },
4162
4205
  enabled: !!spaceId,
4163
- ...DEFAULT_QUERY_OPTIONS,
4206
+ ...LIST_QUERY_OPTIONS,
4164
4207
  });
4165
4208
  };
4166
4209
  /**
@@ -4347,7 +4390,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4347
4390
  return [];
4348
4391
  },
4349
4392
  enabled: !!spaceId,
4350
- ...DEFAULT_QUERY_OPTIONS,
4393
+ ...LIST_QUERY_OPTIONS,
4351
4394
  });
4352
4395
  };
4353
4396
  /**
@@ -4438,7 +4481,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4438
4481
  return [];
4439
4482
  },
4440
4483
  enabled: !!spaceId,
4441
- ...DEFAULT_QUERY_OPTIONS,
4484
+ ...LIST_QUERY_OPTIONS,
4442
4485
  });
4443
4486
  };
4444
4487
  /**
@@ -4478,7 +4521,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4478
4521
  return [];
4479
4522
  },
4480
4523
  enabled: !!spaceId,
4481
- ...DEFAULT_QUERY_OPTIONS,
4524
+ ...LIST_QUERY_OPTIONS,
4482
4525
  });
4483
4526
  };
4484
4527
  /**
@@ -4547,7 +4590,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4547
4590
  return [];
4548
4591
  },
4549
4592
  enabled: !!spaceId,
4550
- ...DEFAULT_QUERY_OPTIONS,
4593
+ ...LIST_QUERY_OPTIONS,
4551
4594
  });
4552
4595
  };
4553
4596
  /**
@@ -4641,7 +4684,7 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
4641
4684
  return [];
4642
4685
  },
4643
4686
  enabled: !!spaceId,
4644
- ...DEFAULT_QUERY_OPTIONS,
4687
+ ...LIST_QUERY_OPTIONS,
4645
4688
  });
4646
4689
  };
4647
4690
  /**
@@ -6332,6 +6375,8 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6332
6375
  'evalset',
6333
6376
  'dataset',
6334
6377
  'agent',
6378
+ // An agentic UI application, published from the Studio.
6379
+ 'app',
6335
6380
  ];
6336
6381
  const librarySearchQueryString = ({ q, types, max = 20, offset = 0, sort = 'relevance', featured = false, owner, }) => {
6337
6382
  const normalizedTypes = Array.from(new Set((types || [])
@@ -6906,6 +6951,59 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
6906
6951
  enabled: Boolean(configuration.libraryUrl) && Boolean(itemId) && enabled,
6907
6952
  });
6908
6953
  };
6954
+ /**
6955
+ * Post a public artifact to X through the Library's saga: the author's
6956
+ * text, then the artifact's attributed share-card link (X draws the card),
6957
+ * and optionally one picked result as an image.
6958
+ */
6959
+ const usePublishXShare = () => {
6960
+ return useMutation({
6961
+ mutationFn: async ({ itemId, text, idempotencyKey, imageDataUrl, }) => {
6962
+ const resp = await requestDatalayer({
6963
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/shares/x`,
6964
+ method: 'POST',
6965
+ headers: { 'Idempotency-Key': idempotencyKey },
6966
+ body: { text, imageDataUrl },
6967
+ });
6968
+ if (!resp.success) {
6969
+ throw new Error(resp.message || 'Failed to post to X');
6970
+ }
6971
+ return resp.share;
6972
+ },
6973
+ });
6974
+ };
6975
+ const useUnpublishXShare = () => {
6976
+ return useMutation({
6977
+ mutationFn: async (idempotencyKey) => {
6978
+ const resp = await requestDatalayer({
6979
+ url: `${configuration.libraryUrl}/api/library/v1/shares/x/${encodeURIComponent(idempotencyKey)}`,
6980
+ method: 'DELETE',
6981
+ });
6982
+ if (!resp.success) {
6983
+ throw new Error(resp.message || 'Failed to remove the X post');
6984
+ }
6985
+ return resp.share;
6986
+ },
6987
+ });
6988
+ };
6989
+ const useXShares = (itemId, enabled = true) => {
6990
+ return useQuery({
6991
+ queryKey: ['library', 'social-shares', 'x', itemId],
6992
+ queryFn: async () => {
6993
+ const resp = await requestDatalayer({
6994
+ url: `${configuration.libraryUrl}/api/library/v1/items/${encodeURIComponent(itemId)}/shares/x`,
6995
+ method: 'GET',
6996
+ notifyOnError: false,
6997
+ });
6998
+ if (!resp.success) {
6999
+ throw new Error(resp.message || 'Failed to load X posts');
7000
+ }
7001
+ return Array.isArray(resp.shares) ? resp.shares : [];
7002
+ },
7003
+ ...DEFAULT_QUERY_OPTIONS,
7004
+ enabled: Boolean(configuration.libraryUrl) && Boolean(itemId) && enabled,
7005
+ });
7006
+ };
6909
7007
  /** Publish one selected public result through the durable Bluesky saga. */
6910
7008
  const usePublishBlueskyShare = () => {
6911
7009
  return useMutation({
@@ -7241,6 +7339,39 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
7241
7339
  method: 'DELETE',
7242
7340
  }),
7243
7341
  });
7342
+ /**
7343
+ * The X connection, as IAM reports it: whether X is configured on this
7344
+ * deployment, whether the caller connected it, and the account (no token).
7345
+ */
7346
+ const useXConnection = (enabled = true) => useQuery({
7347
+ queryKey: ['iam', 'social', 'x', 'status'],
7348
+ queryFn: async () => requestDatalayer({
7349
+ url: `${configuration.iamUrl}/api/iam/v1/social/x/status`,
7350
+ method: 'GET',
7351
+ notifyOnError: false,
7352
+ }),
7353
+ ...DEFAULT_QUERY_OPTIONS,
7354
+ enabled: Boolean(configuration.iamUrl) && enabled,
7355
+ });
7356
+ /**
7357
+ * Begin connecting X. Answers the start link to open at top level;
7358
+ * `returnOrigin` is where to come back to (an origin only: IAM builds the
7359
+ * path, so this cannot ask to be sent anywhere).
7360
+ */
7361
+ const useConnectX = () => useMutation({
7362
+ mutationFn: async (returnOrigin) => requestDatalayer({
7363
+ url: `${configuration.iamUrl}/api/iam/v1/social/x/connect`,
7364
+ method: 'POST',
7365
+ body: returnOrigin ? { return_origin: returnOrigin } : {},
7366
+ }),
7367
+ });
7368
+ const useDisconnectX = () => useMutation({
7369
+ mutationFn: async () => requestDatalayer({
7370
+ url: `${configuration.iamUrl}/api/iam/v1/social/x/connection`,
7371
+ method: 'DELETE',
7372
+ }),
7373
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ['iam', 'social', 'x'] }),
7374
+ });
7244
7375
  // ============================================================================
7245
7376
  // Waiting List & Growth
7246
7377
  // ============================================================================
@@ -8292,6 +8423,12 @@ export const useCache = ({ loginRoute = '/login' } = {}) => {
8292
8423
  useGetBlueskyConnection,
8293
8424
  useConnectBluesky,
8294
8425
  useDisconnectBluesky,
8426
+ useXConnection,
8427
+ useConnectX,
8428
+ useDisconnectX,
8429
+ usePublishXShare,
8430
+ useUnpublishXShare,
8431
+ useXShares,
8295
8432
  usePublishLinkedInShare,
8296
8433
  useUnpublishLinkedInShare,
8297
8434
  useLinkedInShares,
@@ -156,6 +156,9 @@ export const useContentSources = (filters = {}) => {
156
156
  queryFn: () => listSources(token ?? '', filters, contentsUrl),
157
157
  enabled: Boolean(token && contentsUrl),
158
158
  staleTime: 30_000,
159
+ // Coming back to a list is when somebody looks for what they just made:
160
+ // it is read again, whatever made it.
161
+ refetchOnMount: 'always',
159
162
  refetchOnWindowFocus: true,
160
163
  });
161
164
  };
@@ -1,3 +1,6 @@
1
+ import type { IIAMProviderName } from './IAMProvidersSpecs';
2
+ /** What an identity provider's account URN starts with, before its name. */
3
+ export declare const IAM_PROVIDER_URN_PREFIX = "urn:dla:iam:ext::";
1
4
  export declare class URN implements IURN {
2
5
  private _partition;
3
6
  private _service;
@@ -12,7 +15,41 @@ export declare class URN implements IURN {
12
15
  get account(): string;
13
16
  get type(): string;
14
17
  get uid(): string;
18
+ /** An identity provider's account (`urn:dla:iam:ext::github:…`). */
19
+ get isIAMProviderAccount(): boolean;
20
+ toString(): string;
15
21
  }
22
+ /** A URN, or `undefined` when the value is not one — never a throw. */
23
+ export declare function parseURN(value?: string | null): URN | undefined;
24
+ /**
25
+ * Where an account came from, read from its `origin`.
26
+ *
27
+ * - `datalayer`: made on Datalayer, with a password.
28
+ * - `iam-provider`: made by signing in with an identity provider, which it
29
+ * still signs in through (`urn:dla:iam:ext::<provider>:<account id>`).
30
+ * - `urn`: any other URN — an AWS Marketplace or enterprise origin.
31
+ * - `unknown`: nothing recorded, or a value that is none of the above.
32
+ */
33
+ export type IAccountOrigin = {
34
+ kind: 'datalayer';
35
+ } | {
36
+ kind: 'iam-provider';
37
+ provider: IIAMProviderName;
38
+ accountId: string;
39
+ urn: URN;
40
+ } | {
41
+ kind: 'urn';
42
+ urn: URN;
43
+ } | {
44
+ kind: 'unknown';
45
+ value: string;
46
+ };
47
+ export declare function accountOriginOf(origin?: string | null): IAccountOrigin;
48
+ /**
49
+ * Whether an account signs in through a provider: disconnecting that
50
+ * provider would leave it nothing to sign in with.
51
+ */
52
+ export declare function signsInWith(origin: string | null | undefined, provider: IIAMProviderName): boolean;
16
53
  /**
17
54
  Datalayer Uniform Resource Name (URN)
18
55
 
package/lib/models/URN.js CHANGED
@@ -2,6 +2,8 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /** What an identity provider's account URN starts with, before its name. */
6
+ export const IAM_PROVIDER_URN_PREFIX = 'urn:dla:iam:ext::';
5
7
  export class URN {
6
8
  _partition;
7
9
  _service;
@@ -17,7 +19,8 @@ export class URN {
17
19
  this._region = parts[3];
18
20
  this._account = parts[4];
19
21
  this._type = parts[5];
20
- this._uid = parts[6];
22
+ // The rest, colons and all: a provider's account id may carry them.
23
+ this._uid = parts.slice(6).join(':');
21
24
  }
22
25
  get partition() {
23
26
  return this._partition;
@@ -37,4 +40,59 @@ export class URN {
37
40
  get uid() {
38
41
  return this._uid;
39
42
  }
43
+ /** An identity provider's account (`urn:dla:iam:ext::github:…`). */
44
+ get isIAMProviderAccount() {
45
+ return this._service === 'iam' && this._region === 'ext' && !!this._type;
46
+ }
47
+ toString() {
48
+ return [
49
+ 'urn',
50
+ this._partition,
51
+ this._service,
52
+ this._region,
53
+ this._account,
54
+ this._type,
55
+ this._uid,
56
+ ].join(':');
57
+ }
58
+ }
59
+ /** A URN, or `undefined` when the value is not one — never a throw. */
60
+ export function parseURN(value) {
61
+ const text = (value ?? '').trim();
62
+ if (!text.toLowerCase().startsWith('urn:') || text.split(':').length < 7) {
63
+ return undefined;
64
+ }
65
+ return new URN(text);
66
+ }
67
+ const IAM_PROVIDER_NAMES = [
68
+ 'bluesky',
69
+ 'discord',
70
+ 'github',
71
+ 'google',
72
+ 'linkedin',
73
+ 'okta',
74
+ 'x',
75
+ ];
76
+ export function accountOriginOf(origin) {
77
+ const value = (origin ?? '').trim();
78
+ if (value === 'datalayer') {
79
+ return { kind: 'datalayer' };
80
+ }
81
+ const urn = parseURN(value);
82
+ if (!urn) {
83
+ return { kind: 'unknown', value };
84
+ }
85
+ const provider = urn.type.toLowerCase();
86
+ if (urn.isIAMProviderAccount && IAM_PROVIDER_NAMES.includes(provider)) {
87
+ return { kind: 'iam-provider', provider, accountId: urn.uid, urn };
88
+ }
89
+ return { kind: 'urn', urn };
90
+ }
91
+ /**
92
+ * Whether an account signs in through a provider: disconnecting that
93
+ * provider would leave it nothing to sign in with.
94
+ */
95
+ export function signsInWith(origin, provider) {
96
+ const parsed = accountOriginOf(origin);
97
+ return parsed.kind === 'iam-provider' && parsed.provider === provider;
40
98
  }
@@ -1,5 +1,15 @@
1
1
  /** The signed-in shell: the platform, the business cases, or the admin. */
2
- export type NavigationView = 'home' | 'agentify' | 'admin';
2
+ /**
3
+ * The four shells of the signed-in application.
4
+ *
5
+ * `studio` is where an agentic UI application is composed, validated and
6
+ * shipped — the default for somebody who has not chosen, and what the root
7
+ * shows. `home` is the whole platform, shown as *Power*, at `/power`: the key
8
+ * is kept so a cookie written before the Studio existed still names the
9
+ * shell it meant. `agentify` is the business cases; `admin` is the
10
+ * platform's administration.
11
+ */
12
+ export type NavigationView = 'studio' | 'home' | 'agentify' | 'admin';
3
13
  /** The shells, and where each one opens when it has not been visited. */
4
14
  export declare const NAVIGATION_VIEW_HOMES: Record<NavigationView, string>;
5
15
  /** Well-known view identifiers for {@link NavigationState.tabsByView}. */
@@ -33,7 +33,10 @@ import { createStore } from 'zustand/vanilla';
33
33
  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
- home: '/',
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
40
  agentify: '/agentify',
38
41
  admin: '/admin',
39
42
  };
@@ -50,7 +53,10 @@ const SHELL_COOKIE = 'datalayer-signed-in-view';
50
53
  const FORMER_TABS_KEY = 'datalayer_view_tabs';
51
54
  /** Days a remembered shell lives in the cookie. */
52
55
  const COOKIE_MAX_AGE_DAYS = 365;
53
- const isView = (value) => value === 'home' || value === 'agentify' || value === 'admin';
56
+ const isView = (value) => value === 'studio' ||
57
+ value === 'home' ||
58
+ value === 'agentify' ||
59
+ value === 'admin';
54
60
  function readCookie(name) {
55
61
  if (typeof document === 'undefined') {
56
62
  return undefined;
@@ -90,7 +96,8 @@ function restore() {
90
96
  const parsed = JSON.parse(stored);
91
97
  // The landing kept this under the same key through zustand's
92
98
  // `persist`, which wraps what it writes in `{ state, version }`.
93
- persisted = parsed?.state ?? parsed ?? {};
99
+ const kept = parsed?.state ?? parsed;
100
+ persisted = kept && typeof kept === 'object' ? kept : {};
94
101
  }
95
102
  catch {
96
103
  persisted = {};
@@ -112,8 +119,32 @@ function restore() {
112
119
  if (isView(shell)) {
113
120
  persisted.view = shell;
114
121
  }
122
+ persisted.lastRouteByView = restoredRoutes(persisted.lastRouteByView);
115
123
  return persisted;
116
124
  }
125
+ /**
126
+ * The shells' pages as they were written down, less the ones no shell
127
+ * returns to.
128
+ *
129
+ * What is in storage was written by whichever version ran last, under that
130
+ * version's rules. Before the Power shell had an address of its own, its
131
+ * page could be the root — which is now the Studio's, and would send Power
132
+ * to the Studio — and an earlier version filed pages under the shell the
133
+ * toggle named. Such a page is dropped here rather than carried, so the next
134
+ * write does not put it back.
135
+ */
136
+ function restoredRoutes(stored) {
137
+ const routes = {};
138
+ if (!stored || typeof stored !== 'object') {
139
+ return routes;
140
+ }
141
+ for (const [view, route] of Object.entries(stored)) {
142
+ if (isView(view) && isRouteOfView(route, view)) {
143
+ routes[view] = route;
144
+ }
145
+ }
146
+ return routes;
147
+ }
117
148
  function persist(state) {
118
149
  const written = {
119
150
  view: state.view,
@@ -162,6 +193,15 @@ export function isRememberableRoute(route) {
162
193
  */
163
194
  export function viewForRoute(route) {
164
195
  const path = route.split('?')[0];
196
+ // The root is the Studio: what a signed-in reader opens on. The settings
197
+ // are read from it too, with its sidebar beside them.
198
+ if (path === '/' ||
199
+ path === '/studio' ||
200
+ path.startsWith('/studio/') ||
201
+ path === '/settings' ||
202
+ path.startsWith('/settings/')) {
203
+ return 'studio';
204
+ }
165
205
  if (path === '/agentify' || path.startsWith('/agentify/')) {
166
206
  return 'agentify';
167
207
  }
@@ -170,9 +210,17 @@ export function viewForRoute(route) {
170
210
  }
171
211
  return 'home';
172
212
  }
213
+ /** Whether a page may be the one a shell returns to. */
214
+ function isRouteOfView(route, view) {
215
+ return (typeof route === 'string' &&
216
+ isRememberableRoute(route) &&
217
+ viewForRoute(route) === view);
218
+ }
173
219
  const initial = restore();
174
220
  export const navigationStore = createStore((set, get) => ({
175
- view: initial.view ?? 'home',
221
+ // The Studio, for a reader who never chose: a cookie from before it
222
+ // existed says `home`, and is honoured.
223
+ view: initial.view ?? 'studio',
176
224
  lastRouteByView: initial.lastRouteByView ?? {},
177
225
  tabsByView: initial.tabsByView ?? {},
178
226
  requestedRoute: undefined,
@@ -210,9 +258,10 @@ export const navigationStore = createStore((set, get) => ({
210
258
  // Only a page of that shell: an earlier version filed pages under the
211
259
  // shell the toggle named rather than the one the route belonged to, so a
212
260
  // reader can still have `/items` written down as Agentify's page. Going
213
- // "to Agentify" must never land on a Home page.
261
+ // "to Agentify" must never land on a Home page — nor Power on the root,
262
+ // which was its page before it had `/power` and is now the Studio's.
214
263
  const remembered = get().lastRouteByView[view];
215
- return remembered && viewForRoute(remembered) === view
264
+ return isRouteOfView(remembered, view)
216
265
  ? remembered
217
266
  : NAVIGATION_VIEW_HOMES[view];
218
267
  },
@@ -2,7 +2,7 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
- const EXT_URN_PREFIX = 'urn:dla:iam:ext::';
5
+ import { IAM_PROVIDER_URN_PREFIX as EXT_URN_PREFIX } from '../models/URN.js';
6
6
  export function isExternalUrnHandle(handle) {
7
7
  const normalized = (handle || '').trim().toLowerCase();
8
8
  return normalized.startsWith(EXT_URN_PREFIX);
@@ -3,7 +3,7 @@ import { useMemo, useRef, useState } from 'react';
3
3
  import { ActionList, ActionMenu, Button, Heading, Label, Link, RelativeTime, Select, Spinner, Text, } from '@primer/react';
4
4
  import { Blankslate, DataTable, Dialog, Table, } from '@primer/react/experimental';
5
5
  import { Box } from '@datalayer/primer-addons';
6
- import { ChecklistIcon, KebabHorizontalIcon, PlayIcon, PlugIcon, StackIcon, TelescopeIcon, } from '@primer/octicons-react';
6
+ import { ChecklistIcon, KebabHorizontalIcon, PlayIcon, PlugIcon, StackIcon, } from '@primer/octicons-react';
7
7
  import { ShareAccessDialog, sandboxSharingUrl } from '../../components/sharing/index.js';
8
8
  import { ClientBadge, McpErrorBlankslate } from '../../components/mcp/index.js';
9
9
  import { useDisconnectAgent, useMcpActivity, useTerminateBinding, } from '../../hooks/useMcp.js';
@@ -54,7 +54,7 @@ const boundTo = (client) => {
54
54
  return '';
55
55
  };
56
56
  const countLook = (tone) => tone === 'danger' ? 'danger.fg' : 'fg.default';
57
- const Count = ({ label, value, tone = 'neutral', note, }) => (_jsxs(Box, { sx: {
57
+ const Count = ({ label, value, tone = 'neutral', note, to, onOpen, }) => (_jsxs(Box, { sx: {
58
58
  p: 3,
59
59
  border: '1px solid',
60
60
  borderColor: 'border.default',
@@ -66,7 +66,14 @@ const Count = ({ label, value, tone = 'neutral', note, }) => (_jsxs(Box, { sx: {
66
66
  fontSize: 4,
67
67
  fontWeight: 'bold',
68
68
  color: countLook(tone),
69
- }, children: value }), _jsx(Text, { sx: { display: 'block', fontSize: 0, color: 'fg.muted' }, children: label }), note && (_jsx(Text, { sx: { display: 'block', fontSize: 0, color: 'fg.subtle', mt: 1 }, children: note }))] }));
69
+ }, children: value }), to ? (_jsx(Link, { href: to, onClick: (event) => {
70
+ // A modified click asks for a new tab; `href` is real, so let it.
71
+ if (event.metaKey || event.ctrlKey || event.shiftKey) {
72
+ return;
73
+ }
74
+ event.preventDefault();
75
+ onOpen?.(to);
76
+ }, sx: { display: 'block', fontSize: 0 }, children: label })) : (_jsx(Text, { sx: { display: 'block', fontSize: 0, color: 'fg.muted' }, children: label })), note && (_jsx(Text, { sx: { display: 'block', fontSize: 0, color: 'fg.subtle', mt: 1 }, children: note }))] }));
70
77
  /** A panel that has nothing to show, and the step that would fill it. */
71
78
  const PanelEmpty = ({ icon: Icon, heading, description, action, }) => (_jsxs(Blankslate, { border: true, spacious: true, children: [_jsx(Blankslate.Visual, { children: _jsx(Icon, { size: "medium" }) }), _jsx(Blankslate.Heading, { children: heading }), _jsx(Blankslate.Description, { children: _jsx(Text, { as: "p", sx: { textAlign: 'center', m: 0 }, children: description }) }), action && (_jsx(Button, { size: "small", onClick: action.onClick, children: action.label }))] }));
72
79
  export const McpDashboard = ({ errorState, routes, org, showTitle = true, section, }) => {
@@ -379,7 +386,10 @@ export const McpDashboard = ({ errorState, routes, org, showTitle = true, sectio
379
386
  'repeat(3, 1fr)',
380
387
  'repeat(6, 1fr)',
381
388
  ],
382
- }, children: [_jsx(Count, { label: "Clients connected", value: clients.length }), _jsx(Count, { label: "Sandboxes bound", value: sandboxes.filter(binding => (binding.state ?? 'active') === 'active').length }), _jsx(Count, { label: "Tasks running", value: data?.tasks.length ?? 0 }), _jsx(Count, { label: "Calls today", value: data?.today.calls ?? 0 }), _jsx(Count, { label: "Refusals today", value: data?.today.refusals ?? 0, tone: (data?.today.refusals ?? 0) > 0 ? 'danger' : 'neutral' }), _jsx(Count, { label: "Credits by agents", value: data?.today.credits ?? 0, note: "today" })] }))), draws('agents') &&
389
+ }, children: [_jsx(Count, { label: "Clients connected", value: clients.length, to: routes.agents, onOpen: navigate }), _jsx(Count, { label: "Sandboxes bound", to: routes.sandboxes, onOpen: navigate, value: sandboxes.filter(binding => (binding.state ?? 'active') === 'active').length }), _jsx(Count, { label: "Tasks running", value: data?.tasks.length ?? 0, to: routes.runs, onOpen: navigate }), _jsx(Count, { label: "Calls today", value: data?.today.calls ?? 0, to: routes.calls, onOpen: navigate }), _jsx(Count, { label: "Refusals today", value: data?.today.refusals ?? 0, to: routes.audit, onOpen: navigate, tone: (data?.today.refusals ?? 0) > 0 ? 'danger' : 'neutral' }), _jsx(Count, { label: "Credits by agents", value: data?.today.credits ?? 0, note: "today",
390
+ // Credits are summed from the day's calls, which the Calls tab
391
+ // lists.
392
+ to: routes.calls, onOpen: navigate })] }))), draws('agents') &&
383
393
  !loading &&
384
394
  (clients.length > 0 ? (_jsxs(Table.Container, { children: [_jsx(Table.Title, { as: "h3", id: "mcp-clients", children: "Connected clients" }), _jsxs(Table.Subtitle, { as: "p", id: "mcp-clients-subtitle", children: [plural(clients.length, 'client'), " with a live grant on your workspace."] }), _jsx(DataTable, { "aria-labelledby": "mcp-clients", "aria-describedby": "mcp-clients-subtitle", data: clients, columns: clientColumns })] })) : (_jsx(PanelEmpty, { icon: PlugIcon, heading: "No client connected", description: "Connect Claude Code, Codex, Cursor, VS Code or any MCP client to your workspace, and it appears here on its first call.", action: {
385
395
  label: 'Set up a client',
@@ -396,12 +406,7 @@ export const McpDashboard = ({ errorState, routes, org, showTitle = true, sectio
396
406
  }, children: "No call matches these filters." }))] })) : (_jsx(PanelEmpty, { icon: ChecklistIcon, heading: "No call yet", description: "Ask your agent to list your notebooks. The call, its decision and its outcome land here and in the audit log.", action: {
397
407
  label: 'Open the audit log',
398
408
  onClick: () => navigate(routes.audit),
399
- } }))), !loading && (_jsxs(Box, { sx: {
400
- display: 'flex',
401
- gap: 3,
402
- alignItems: 'center',
403
- flexWrap: 'wrap',
404
- }, children: [_jsx(Button, { size: "small", leadingVisual: TelescopeIcon, onClick: () => navigate(routes.observability), children: "Observability" }), _jsxs(Text, { sx: { fontSize: 0, color: 'fg.muted' }, children: ["Refreshed ", timeAgo(data?.at) || 'just now', ", and again every ten seconds."] })] })), disconnecting && (_jsx(Dialog, { title: "Disconnect this client?", onClose: () => setDisconnecting(null), returnFocusRef: returnFocusRef, footerButtons: [
409
+ } }))), disconnecting && (_jsx(Dialog, { title: "Disconnect this client?", onClose: () => setDisconnecting(null), returnFocusRef: returnFocusRef, footerButtons: [
405
410
  {
406
411
  buttonType: 'default',
407
412
  content: 'Keep it',
@@ -63,7 +63,7 @@ export const Copyable = ({ text }) => {
63
63
  } })] }));
64
64
  };
65
65
  /** One of the five summaries: a heading, what it holds, and the way in. */
66
- const Summary = ({ icon: Icon, title, children, action, }) => (_jsxs(Box, { sx: {
66
+ const Summary = ({ icon: Icon, title, children, action, to, onOpen, }) => (_jsxs(Box, { sx: {
67
67
  p: 3,
68
68
  border: '1px solid',
69
69
  borderColor: 'border.default',
@@ -72,7 +72,14 @@ const Summary = ({ icon: Icon, title, children, action, }) => (_jsxs(Box, { sx:
72
72
  gap: 2,
73
73
  alignContent: 'start',
74
74
  minWidth: 0,
75
- }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Icon, { size: 16 }), _jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: title })] }), _jsx(Box, { sx: { fontSize: 0, color: 'fg.muted', display: 'grid', gap: 1 }, children: children }), action && (_jsx(Box, { children: _jsx(Link, { as: "button", sx: { fontSize: 0, cursor: 'pointer' }, onClick: action.onClick, children: action.label }) }))] }));
75
+ }, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 2 }, children: [_jsx(Icon, { size: 16 }), to ? (_jsx(Link, { href: to, onClick: event => {
76
+ // A modified click asks for a new tab; `href` is real, so let it.
77
+ if (event.metaKey || event.ctrlKey || event.shiftKey) {
78
+ return;
79
+ }
80
+ event.preventDefault();
81
+ onOpen?.(to);
82
+ }, sx: { fontSize: 1, fontWeight: 'semibold' }, children: title })) : (_jsx(Text, { sx: { fontSize: 1, fontWeight: 'semibold' }, children: title }))] }), _jsx(Box, { sx: { fontSize: 0, color: 'fg.muted', display: 'grid', gap: 1 }, children: children }), action && (_jsx(Box, { children: _jsx(Link, { as: "button", sx: { fontSize: 0, cursor: 'pointer' }, onClick: action.onClick, children: action.label }) }))] }));
76
83
  export const McpHome = ({ routes, clientDocsBase = '/docs/mcp-clients', showTitle = true, showSummaries = true, showSetup = true, onOpenDocs, }) => {
77
84
  const navigate = useNavigate();
78
85
  const endpoint = useCoreStore(state => state.configuration.datalayerMcpServerUrl);
@@ -130,12 +137,12 @@ export const McpHome = ({ routes, clientDocsBase = '/docs/mcp-clients', showTitl
130
137
  display: 'grid',
131
138
  gap: 3,
132
139
  gridTemplateColumns: ['1fr', 'repeat(2, 1fr)', 'repeat(3, 1fr)'],
133
- }, children: [_jsx(Summary, { icon: PlugIcon, title: "Agents", action: {
140
+ }, children: [_jsx(Summary, { icon: PlugIcon, title: "Agents", to: routes.agents, onOpen: navigate, action: {
134
141
  label: 'Connected agents',
135
142
  onClick: () => navigate(routes.agents),
136
143
  }, children: connected.length === 0 ? (_jsx(Text, { children: "No agent has been authorized yet. Set up a client above and approve the scopes it asks for." })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [plural(connected.length, 'agent'), " authorized."] }), connected.slice(0, 3).map(agent => (_jsxs(Text, { children: [agent.clientName || agent.clientId, " \u2014", ' ', agent.lastUsedAt
137
144
  ? `used ${timeAgo(agent.lastUsedAt)}`
138
- : 'never used'] }, agent.uid)))] })) }), _jsx(Summary, { icon: PulseIcon, title: "Runs", action: { label: 'All runs', onClick: () => navigate(routes.runs) }, children: runs.length === 0 ? (_jsx(Text, { children: "Nothing has run yet. A tool call that outlives its request becomes a run you can watch and cancel." })) : (runs.map(task => (_jsxs(Text, { children: [task.tool, " \u2014 ", task.status, " ", timeAgo(task.lastUpdatedAt)] }, task.uid)))) }), _jsxs(Summary, { icon: TelescopeIcon, title: "Observability", action: {
145
+ : 'never used'] }, agent.uid)))] })) }), _jsx(Summary, { icon: PulseIcon, title: "Runs", to: routes.runs, onOpen: navigate, action: { label: 'All runs', onClick: () => navigate(routes.runs) }, children: runs.length === 0 ? (_jsx(Text, { children: "Nothing has run yet. A tool call that outlives its request becomes a run you can watch and cancel." })) : (runs.map(task => (_jsxs(Text, { children: [task.tool, " \u2014 ", task.status, " ", timeAgo(task.lastUpdatedAt)] }, task.uid)))) }), _jsxs(Summary, { icon: TelescopeIcon, title: "Observability", to: routes.observability, onOpen: navigate, action: {
139
146
  label: 'Runs and metrics',
140
147
  onClick: () => navigate(routes.observability),
141
148
  }, children: [_jsx(Text, { children: metrics.data?.slis.samples.calls
@@ -145,7 +152,7 @@ export const McpHome = ({ routes, clientDocsBase = '/docs/mcp-clients', showTitl
145
152
  : 'Latency not measured yet.' }), _jsx(Text, { children: metrics.data?.slis.availability === null ||
146
153
  metrics.data?.slis.availability === undefined
147
154
  ? 'Error rate not measured yet.'
148
- : `${((1 - metrics.data.slis.availability) * 100).toFixed(1)}% of calls failed.` })] }), _jsx(Summary, { icon: ToolsIcon, title: "Tools", children: _jsx(Text, { children: "The toolsets an agent may execute \u2014 the Contents sources you have enabled for it, their tools, their approval policy and their health \u2014 are managed here once toolset bindings ship." }) }), _jsx(Summary, { icon: ShareIcon, title: "Reach", children: _jsx(Text, { children: "Which notebooks and sources an agent can actually reach, as a list you can read at a glance, arrives with sharing to agents as principals." }) }), _jsx(Summary, { icon: KeyIcon, title: "Policies", action: {
155
+ : `${((1 - metrics.data.slis.availability) * 100).toFixed(1)}% of calls failed.` })] }), _jsx(Summary, { icon: ToolsIcon, title: "Tools", children: _jsx(Text, { children: "The toolsets an agent may execute \u2014 the Contents sources you have enabled for it, their tools, their approval policy and their health \u2014 are managed here once toolset bindings ship." }) }), _jsx(Summary, { icon: ShareIcon, title: "Reach", children: _jsx(Text, { children: "Which notebooks and sources an agent can actually reach, as a list you can read at a glance, arrives with sharing to agents as principals." }) }), _jsx(Summary, { icon: KeyIcon, title: "Policies", to: routes.policies, onOpen: navigate, action: {
149
156
  label: 'What applies',
150
157
  onClick: () => navigate(routes.policies),
151
158
  }, children: _jsx(Text, { children: "What your agents are allowed to do, and which layer decided each rule." }) })] }))] }));
@@ -63,4 +63,8 @@ export interface McpRoutes {
63
63
  agents: string;
64
64
  /** The effective policy. */
65
65
  policies: string;
66
+ /** The calls list, where an Overview count of calls or credits leads. */
67
+ calls?: string;
68
+ /** The bound code sandboxes, where the Overview's sandbox count leads. */
69
+ sandboxes?: string;
66
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalayer/core",
3
- "version": "1.2.12",
3
+ "version": "1.2.13",
4
4
  "type": "module",
5
5
  "workspaces": [
6
6
  ".",
@@ -109,11 +109,11 @@
109
109
  "rebuild:fresh": "npm run create:patches && npm install && npm run build && npm run clean:cache"
110
110
  },
111
111
  "dependencies": {
112
- "@datalayer/design": "^1.0.0",
113
- "@datalayer/icons-react": "^1.0.7",
114
- "@datalayer/jupyter-lexical": "^1.0.24",
115
- "@datalayer/jupyter-react": "^2.0.15",
116
- "@datalayer/primer-addons": "^1.0.15",
112
+ "@datalayer/design": "^1.0.3",
113
+ "@datalayer/icons-react": "^1.0.14",
114
+ "@datalayer/jupyter-lexical": "^2.0.18",
115
+ "@datalayer/jupyter-react": "^2.0.18",
116
+ "@datalayer/primer-addons": "^1.0.23",
117
117
  "@datalayer/primer-rjsf": "^1.0.3",
118
118
  "@fluentui/react": "^8.125.3",
119
119
  "@jupyter-widgets/base-manager": "^1.0.12",