@datalayer/core 1.2.28 → 1.2.29

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 (38) hide show
  1. package/lib/api/mcp/observability.d.ts +1 -1
  2. package/lib/api/mcp/observability.js +2 -0
  3. package/lib/api/mcp/operations.d.ts +28 -10
  4. package/lib/api/mcp/tasks.d.ts +9 -1
  5. package/lib/api/otel/firewall.d.ts +74 -0
  6. package/lib/api/otel/firewall.js +42 -0
  7. package/lib/api/otel/index.d.ts +1 -0
  8. package/lib/api/otel/index.js +1 -0
  9. package/lib/cli-login.js +2 -2
  10. package/lib/components/anonymous/AnonymousKeyExpired.js +15 -3
  11. package/lib/components/principal/PrincipalSwitcherMenu.js +4 -4
  12. package/lib/components/sharing/ShareAccessComponent.js +33 -8
  13. package/lib/components/toast/ToastPanel.js +18 -7
  14. package/lib/components/toast/toastQueue.d.ts +2 -0
  15. package/lib/hooks/index.d.ts +1 -0
  16. package/lib/hooks/index.js +1 -0
  17. package/lib/hooks/useCache.d.ts +4 -2
  18. package/lib/hooks/useCache.js +13 -4
  19. package/lib/hooks/useKeptScroll.d.ts +6 -0
  20. package/lib/hooks/useKeptScroll.js +54 -0
  21. package/lib/hooks/useMcp.d.ts +2 -2
  22. package/lib/hooks/useMcp.js +5 -2
  23. package/lib/hooks/useNavigate.d.ts +16 -2
  24. package/lib/hooks/useNavigate.js +65 -5
  25. package/lib/models/Usage.d.ts +13 -0
  26. package/lib/models/Usage.js +5 -0
  27. package/lib/otel/client/OtelClient.d.ts +56 -0
  28. package/lib/otel/client/OtelClient.js +25 -0
  29. package/lib/otel/client/index.d.ts +1 -1
  30. package/lib/otel/index.d.ts +1 -1
  31. package/lib/otel/views/OtelLive.js +1 -1
  32. package/lib/state/substates/LayoutState.js +24 -3
  33. package/lib/state/substates/NavigationState.d.ts +11 -3
  34. package/lib/state/substates/NavigationState.js +48 -5
  35. package/lib/views/mcp/McpObservability.d.ts +22 -3
  36. package/lib/views/mcp/McpObservability.js +65 -17
  37. package/lib/views/mcp/RunDetail.js +2 -2
  38. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { type McpTaskAnswered, type McpActivity, type McpActivityFilters, type McpAuditFilters, type McpBindingListFilters, type McpGatewayVersion, type McpMetricsFilters, type McpMetricsSnapshot, type McpOrganizationOverview, type McpOrganizationUsage, type McpOrganizationOverviewFilters, type McpPolicyFilters, type McpRunLogs, type McpRunTrace, type McpTaskEvent, type McpTaskListFilters, type McpWorkerList, type McpWorkflowsHealth } from '../api/mcp';
1
+ import { type McpTaskAnswered, type McpTaskCancelled, type McpActivity, type McpActivityFilters, type McpAuditFilters, type McpBindingListFilters, type McpGatewayVersion, type McpMetricsFilters, type McpMetricsSnapshot, type McpOrganizationOverview, type McpOrganizationUsage, type McpOrganizationOverviewFilters, type McpPolicyFilters, type McpRunLogs, type McpRunTrace, type McpTaskEvent, type McpTaskListFilters, type McpWorkerList, type McpWorkflowsHealth } from '../api/mcp';
2
2
  import { type ConnectedAgent } from '../api/iam/connectedAgents';
3
3
  import { type OrganizationTeam } from '../api/iam/organizationTeams';
4
4
  import { setAuditSettings, type McpAuditSettings } from '../api/iam/mcpAuditSettings';
@@ -28,7 +28,7 @@ export declare const useTaskEvents: (taskUid?: string, options?: {
28
28
  enabled?: boolean;
29
29
  onEvent?: (event: McpTaskEvent) => void;
30
30
  }) => void;
31
- export declare const useCancelTask: () => import("@tanstack/react-query").UseMutationResult<McpTask, Error, string, unknown>;
31
+ export declare const useCancelTask: () => import("@tanstack/react-query").UseMutationResult<McpTaskCancelled, Error, string, unknown>;
32
32
  export declare const useAnswerTask: () => import("@tanstack/react-query").UseMutationResult<McpTaskAnswered, Error, {
33
33
  taskUid: string;
34
34
  input: Record<string, unknown>;
@@ -124,8 +124,11 @@ export const useCancelTask = () => {
124
124
  const mcpUrl = useMcpServerUrl();
125
125
  return useMutation({
126
126
  mutationFn: taskUid => cancelTask(token ?? '', taskUid, mcpUrl),
127
- onSuccess: task => {
128
- queryClient.setQueryData(queryKeys.mcp.task(task.uid), task);
127
+ // The gateway answers `{uid, cancelled, status}`, not the task: read the
128
+ // task again rather than caching that reply in its place (the run page
129
+ // lost its tool and output on a cancel, as it did on an answer).
130
+ onSuccess: (_cancelled, taskUid) => {
131
+ queryClient.invalidateQueries({ queryKey: queryKeys.mcp.task(taskUid) });
129
132
  queryClient.invalidateQueries({ queryKey: queryKeys.mcp.tasks() });
130
133
  queryClient.invalidateQueries({ queryKey: queryKeys.mcp.activity() });
131
134
  },
@@ -5,8 +5,15 @@ export interface NavigateOptions {
5
5
  state?: unknown;
6
6
  /** Open the destination in a new tab and leave this one where it is. */
7
7
  newTab?: boolean;
8
- /** Scroll to the top on arrival; on by default. */
9
- scroll?: boolean;
8
+ /**
9
+ * Where the reader lands: the top of the page when unsaid or `true`; an
10
+ * element, by its id (with or without its `#`), scrolled into view once
11
+ * the page has it; `false` to leave the scroll where it is. The page and
12
+ * every scroll panel marked `data-scroll-root` are scrolled, so a layout
13
+ * that scrolls inside its own panel lands at the top too (the user,
14
+ * 6 November 2026: the Trust page opened scrolled down).
15
+ */
16
+ scroll?: boolean | string;
10
17
  /**
11
18
  * Close the layout's side portals on the way; on by default. Only the
12
19
  * history API host has them.
@@ -40,5 +47,12 @@ export type Navigate = (to: NavigateTarget, optionsOrEvent?: NavigateOptions | N
40
47
  export declare function isNewTabClick(event: NavigateEvent): boolean;
41
48
  /** Open `to` in a new tab, without handing it this window. */
42
49
  export declare function openInNewTab(to: string): void;
50
+ /**
51
+ * Scroll on arrival: to the top (`undefined` or `true`), to an element by
52
+ * its id (a string, `#` or not), or nowhere (`false`). Done now and again
53
+ * on the next frames, since the page navigated to renders after the call
54
+ * and a layout's panel keeps its scroll across pages.
55
+ */
56
+ export declare function scrollOnArrival(target: boolean | string | undefined): void;
43
57
  export declare const useNavigate: () => Navigate;
44
58
  export default useNavigate;
@@ -62,6 +62,68 @@ function isLinkTo(event, to) {
62
62
  return false;
63
63
  }
64
64
  }
65
+ /**
66
+ * The scroll panels a layout marks, beside the page itself: `data-scroll-root`,
67
+ * or the `dla-scroll-root` class for a component that passes a class name but
68
+ * no data attribute (Primer's `PageLayout.Content`, whose own element is the
69
+ * one that scrolls; 9 November 2026, a card on /agentspecs/identities opened
70
+ * its page scrolled down).
71
+ */
72
+ const scrollRoots = () => Array.from(document.querySelectorAll('[data-scroll-root], .dla-scroll-root'));
73
+ const nextFrame = (run) => {
74
+ if (typeof requestAnimationFrame === 'function') {
75
+ requestAnimationFrame(run);
76
+ }
77
+ else {
78
+ setTimeout(run, 16);
79
+ }
80
+ };
81
+ /** How many frames an element is waited for: a page loaded lazily takes a few. */
82
+ const ARRIVAL_FRAMES = 60;
83
+ /**
84
+ * Scroll on arrival: to the top (`undefined` or `true`), to an element by
85
+ * its id (a string, `#` or not), or nowhere (`false`). Done now and again
86
+ * on the next frames, since the page navigated to renders after the call
87
+ * and a layout's panel keeps its scroll across pages.
88
+ */
89
+ export function scrollOnArrival(target) {
90
+ if (target === false || typeof document === 'undefined') {
91
+ return;
92
+ }
93
+ const id = typeof target === 'string' ? target.replace(/^#/, '') : '';
94
+ if (!id) {
95
+ const toTop = () => {
96
+ window.scrollTo(0, 0);
97
+ document.body.scrollTop = 0;
98
+ document.documentElement.scrollTop = 0;
99
+ for (const root of scrollRoots()) {
100
+ root.scrollTop = 0;
101
+ }
102
+ };
103
+ toTop();
104
+ let frames = 0;
105
+ const again = () => {
106
+ toTop();
107
+ if (++frames < 2) {
108
+ nextFrame(again);
109
+ }
110
+ };
111
+ nextFrame(again);
112
+ return;
113
+ }
114
+ let tries = 0;
115
+ const attempt = () => {
116
+ const element = document.getElementById(id);
117
+ if (element) {
118
+ element.scrollIntoView({ block: 'start' });
119
+ return;
120
+ }
121
+ if (++tries < ARRIVAL_FRAMES) {
122
+ nextFrame(attempt);
123
+ }
124
+ };
125
+ attempt();
126
+ }
65
127
  const isEvent = (value) => !!value && typeof value.preventDefault === 'function';
66
128
  function parseCall(optionsOrEvent, third, fourth) {
67
129
  if (isEvent(optionsOrEvent)) {
@@ -129,10 +191,7 @@ export const useNavigate = () => {
129
191
  return;
130
192
  }
131
193
  event?.preventDefault();
132
- if (options.scroll !== false) {
133
- window.scrollTo(0, 0);
134
- document.body.scrollTop = 0;
135
- }
194
+ scrollOnArrival(options.scroll);
136
195
  // Where the reader is now, for the shell they are in. A new tab and a
137
196
  // step back through history are both already past by here. Only for
138
197
  // somebody signed in: the shells are theirs, and a page a visitor read
@@ -152,7 +211,8 @@ export const useNavigate = () => {
152
211
  nativeNavigate(to, {
153
212
  replace: options.replace,
154
213
  state: options.state,
155
- scroll: options.scroll,
214
+ // Scrolled above, after the page has rendered.
215
+ scroll: false,
156
216
  });
157
217
  }, [rrNavigate, nativeNavigate, layoutStore]);
158
218
  };
@@ -8,6 +8,15 @@ interface IResources {
8
8
  memory: string;
9
9
  'nvidia.com/gpu'?: string;
10
10
  }
11
+ /**
12
+ * What ran and hosted the agent a usage record is for (`runtime_engine`):
13
+ * a Datalayer runtime, kagent, or AWS AgentCore in an organization's AWS
14
+ * account. IAM records it from the launch; a record from before, or of a
15
+ * call with no agent runtime behind it, has none.
16
+ */
17
+ export type UsageRuntimeEngine = 'datalayer' | 'kagent' | 'agentcore';
18
+ /** The engine a raw record says, or `undefined` when it says none (or one not known). */
19
+ export declare const usageRuntimeEngineOf: (value: unknown) => UsageRuntimeEngine | undefined;
11
20
  export interface IRawUsage {
12
21
  account_uid: string;
13
22
  resource_uid: string;
@@ -26,6 +35,7 @@ export interface IRawUsage {
26
35
  usage_period_label?: string;
27
36
  usage_period_start?: string;
28
37
  usage_period_end?: string;
38
+ runtime_engine?: UsageRuntimeEngine | null;
29
39
  metadata: Map<string, string>;
30
40
  }
31
41
  export declare const asUsage: (u: any) => {
@@ -46,6 +56,7 @@ export declare const asUsage: (u: any) => {
46
56
  usagePeriodLabel: any;
47
57
  usagePeriodStart: Date | undefined;
48
58
  usagePeriodEnd: Date | undefined;
59
+ runtimeEngine: UsageRuntimeEngine | undefined;
49
60
  metadata: Map<string, string>;
50
61
  };
51
62
  export interface IUsage {
@@ -66,6 +77,8 @@ export interface IUsage {
66
77
  usagePeriodLabel?: string;
67
78
  usagePeriodStart?: Date;
68
79
  usagePeriodEnd?: Date;
80
+ /** What ran and hosted its agent; absent on a record that says none. */
81
+ runtimeEngine?: UsageRuntimeEngine;
69
82
  metadata: Map<string, string>;
70
83
  }
71
84
  export {};
@@ -2,6 +2,10 @@
2
2
  * Copyright (c) 2023-2025 Datalayer, Inc.
3
3
  * Distributed under the terms of the Modified BSD License.
4
4
  */
5
+ /** The engine a raw record says, or `undefined` when it says none (or one not known). */
6
+ export const usageRuntimeEngineOf = (value) => value === 'datalayer' || value === 'kagent' || value === 'agentcore'
7
+ ? value
8
+ : undefined;
5
9
  export const asUsage = (u) => {
6
10
  const usagePeriodKey = u.usage_period_key;
7
11
  const usagePeriodLabel = u.usage_period_label;
@@ -25,6 +29,7 @@ export const asUsage = (u) => {
25
29
  usagePeriodLabel,
26
30
  usagePeriodStart: usagePeriodStart ? new Date(usagePeriodStart) : undefined,
27
31
  usagePeriodEnd: usagePeriodEnd ? new Date(usagePeriodEnd) : undefined,
32
+ runtimeEngine: usageRuntimeEngineOf(u.runtime_engine),
28
33
  metadata: new Map(Object.entries(u.metadata ?? {})),
29
34
  };
30
35
  };
@@ -1,5 +1,53 @@
1
1
  import type { OtelSpan, OtelLog, OtelMetric, OtelQueryResult } from '../types';
2
2
  import type { OtelSystemData, OtelSystemProcess, OtelSystemDisk, OtelSystemTable } from '../hooks';
3
+ export interface FetchTokenUsageOptions {
4
+ /** The window, in hours back from now (24 by default). */
5
+ hours?: number;
6
+ /** Only these services (runtimes); every one by default. */
7
+ serviceNames?: string[];
8
+ accountUid?: string;
9
+ }
10
+ /** What `GET /api/otel/v1/metrics/tokens` answers. */
11
+ export interface OtelTokenUsage {
12
+ total: number;
13
+ prompt: number;
14
+ completion: number;
15
+ by_service: Array<{
16
+ service_name: string;
17
+ total: number;
18
+ prompt: number;
19
+ completion: number;
20
+ }>;
21
+ window?: {
22
+ start: number;
23
+ end: number;
24
+ hours: number;
25
+ };
26
+ /**
27
+ * When telemetry last reached the store, whatever the account: `at` in
28
+ * unix nanoseconds, from rows ingested since the service started
29
+ * (`ingest`) or from its newest stored file (`files`); `null` with the
30
+ * `reason` when nothing was ever stored or the time could not be read.
31
+ * Absent from a service that does not say it.
32
+ */
33
+ last_received?: OtelLastReceived;
34
+ /**
35
+ * The same for the asked account, known only from what arrived since the
36
+ * service started (`since`, unix nanoseconds): `at` is `null`, with a
37
+ * `reason`, until rows of the account arrive.
38
+ */
39
+ account_last_received?: OtelAccountLastReceived;
40
+ }
41
+ export interface OtelLastReceived {
42
+ at: number | null;
43
+ source: 'ingest' | 'files' | null;
44
+ reason: string | null;
45
+ }
46
+ export interface OtelAccountLastReceived {
47
+ at: number | null;
48
+ since: number;
49
+ reason: string | null;
50
+ }
3
51
  export interface OtelClientOptions {
4
52
  /**
5
53
  * Base URL of the OTEL service.
@@ -65,6 +113,14 @@ export declare class OtelClient {
65
113
  data: OtelMetric[];
66
114
  count: number;
67
115
  }>;
116
+ /**
117
+ * The tokens agents counted over the last `hours` (24 by default): total,
118
+ * prompt and completion, overall and per service, in one request — the
119
+ * service reads each counter's increase over the window in one scan. The
120
+ * answer also says when telemetry last reached the store (`last_received`),
121
+ * so a total of 0 can be told from a store that stopped receiving.
122
+ */
123
+ fetchTokenUsage(options?: FetchTokenUsageOptions): Promise<OtelTokenUsage>;
68
124
  /** Sum all values for one metric name, with optional service filter. */
69
125
  fetchMetricTotal(metricName: string, options?: FetchMetricTotalOptions): Promise<number>;
70
126
  /** List known service names observed in traces. */
@@ -177,6 +177,31 @@ export class OtelClient {
177
177
  : [];
178
178
  return { data: normalized, count: normalized.length };
179
179
  }
180
+ /**
181
+ * The tokens agents counted over the last `hours` (24 by default): total,
182
+ * prompt and completion, overall and per service, in one request — the
183
+ * service reads each counter's increase over the window in one scan. The
184
+ * answer also says when telemetry last reached the store (`last_received`),
185
+ * so a total of 0 can be told from a store that stopped receiving.
186
+ */
187
+ async fetchTokenUsage(options = {}) {
188
+ const params = new URLSearchParams({ hours: String(options.hours ?? 24) });
189
+ for (const name of options.serviceNames ?? []) {
190
+ params.append('service_name', name);
191
+ }
192
+ if (options.accountUid)
193
+ params.set('account_uid', options.accountUid);
194
+ const resp = await otelFetch(`${this.baseUrl}/api/otel/v1/metrics/tokens?${params}`, this.token, this.userUid);
195
+ return {
196
+ total: Number(resp.total ?? 0),
197
+ prompt: Number(resp.prompt ?? 0),
198
+ completion: Number(resp.completion ?? 0),
199
+ by_service: Array.isArray(resp.by_service) ? resp.by_service : [],
200
+ window: resp.window,
201
+ last_received: resp.last_received,
202
+ account_last_received: resp.account_last_received,
203
+ };
204
+ }
180
205
  /** Sum all values for one metric name, with optional service filter. */
181
206
  async fetchMetricTotal(metricName, options = {}) {
182
207
  const { serviceName, limit = 500, fallbackWithoutService = true, accountUid, } = options;
@@ -1,2 +1,2 @@
1
1
  export { OtelClient, createOtelClient } from './OtelClient';
2
- export type { OtelClientOptions, FetchTracesOptions, FetchLogsOptions, FetchMetricsOptions, FetchMetricTotalOptions, OtelSystemData, OtelSystemProcess, OtelSystemDisk, OtelSystemTable, } from './OtelClient';
2
+ export type { OtelClientOptions, FetchTracesOptions, FetchLogsOptions, FetchMetricsOptions, FetchMetricTotalOptions, FetchTokenUsageOptions, OtelTokenUsage, OtelLastReceived, OtelAccountLastReceived, OtelSystemData, OtelSystemProcess, OtelSystemDisk, OtelSystemTable, } from './OtelClient';
@@ -20,7 +20,7 @@ export type { ResolvedOtelAuth } from './auth';
20
20
  export { useOtelTraces, useOtelTrace, useOtelLogs, useOtelMetrics, useOtelServices, useOtelStats, useOtelQuery, useOtelSystem, useOtelWebSocket, setOtelOnUnauthorized, } from './hooks';
21
21
  export type { OtelWsMessage, OtelWsCallbacks, OtelSystemData, OtelSystemProcess, OtelSystemDisk, OtelSystemTable, } from './hooks';
22
22
  export { OtelClient, createOtelClient } from './client';
23
- export type { OtelClientOptions, FetchTracesOptions, FetchLogsOptions, FetchMetricsOptions, FetchMetricTotalOptions, } from './client';
23
+ export type { OtelClientOptions, FetchTracesOptions, FetchLogsOptions, FetchMetricsOptions, FetchMetricTotalOptions, FetchTokenUsageOptions, OtelTokenUsage, OtelLastReceived, OtelAccountLastReceived, } from './client';
24
24
  export { OtelLive } from './views';
25
25
  export { OtelTracesList } from './views';
26
26
  export { OtelSpanDetail } from './views';
@@ -292,7 +292,7 @@ export const OtelLive = ({ baseUrl = getOtelConsumeUrl(), wsBaseUrl, token, auto
292
292
  ? tracesLoading
293
293
  : signal === 'logs'
294
294
  ? logsLoading
295
- : metricsLoading }) }), _jsx(Box, { pr: 2, flexShrink: 0, children: _jsx(Label, { variant: wsConnected ? 'success' : 'secondary', size: "small", children: wsConnected ? '● Live' : '○ Polling' }) })] }), timelineBounds && rangeStart && rangeEnd && (_jsx(Box, { px: 3, pt: 2, pb: 1, borderBottom: "1px solid", borderColor: "border.default", bg: "canvas.subtle", flexShrink: 0, position: "relative", zIndex: 0, children: _jsx(OtelTimelineRangeSlider, { timelineStart: timelineBounds.start, timelineEnd: timelineBounds.end, selectedStart: rangeStart, selectedEnd: rangeEnd, onRangeChange: handleRangeChange, histogram: histogram, height: 48, tickCount: 6 }) })), _jsxs(Box, { display: "flex", flex: 1, minHeight: 0, overflow: "hidden", children: [_jsxs(Box, { display: "flex", flexDirection: "column", flex: hasDetail ? '0 0 55%' : '1 1 100%', minHeight: 0, overflow: "hidden", children: [signal === 'traces' && (_jsx(OtelTracesList, { spans: filteredTraces, loading: tracesLoading, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), signal === 'logs' && (_jsx(OtelLogsList, { logs: filteredLogs, loading: logsLoading, selectedLogIndex: selectedLogIdx, onSelectLog: handleLogSelect })), signal === 'metrics' && (_jsx(OtelMetricsList, { metrics: filteredMetrics, loading: metricsLoading }))] }), hasDetail && (_jsxs(Box, { flex: "0 0 45%", minHeight: 0, overflow: "auto", borderLeft: "1px solid", borderColor: "border.default", children: [signal === 'traces' && selectedSpan && (_jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans ?? undefined, onClose: handleCloseDetail })), signal === 'logs' && selectedLogIdx !== null && logs && (_jsxs(Box, { p: 3, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", mb: 3, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 2 }, children: "Log Detail" }), _jsx(Button, { size: "small", variant: "invisible", onClick: handleCloseDetail, children: "\u2715" })] }), _jsx(Box, { as: "pre", m: 0, fontSize: 1, fontFamily: "mono", whiteSpace: "pre-wrap", wordBreak: "break-word", children: JSON.stringify(logs[selectedLogIdx], null, 2) })] }))] }))] }), signal === 'traces' && selectedSpan && spanTree.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { display: "flex", gap: 1, px: 3, py: 1, bg: "canvas.subtle", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(Button, { size: "small", variant: bottomPane === 'timeline' ? 'primary' : 'invisible', leadingVisual: ClockIcon, onClick: () => toggleBottomPane('timeline'), children: "Timeline" }), _jsx(Button, { size: "small", variant: bottomPane === 'tree' ? 'primary' : 'invisible', leadingVisual: GitBranchIcon, onClick: () => toggleBottomPane('tree'), children: "Span Tree" })] }), bottomPane && (_jsxs(Box, { height: 260, overflow: "auto", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, children: [bottomPane === 'timeline' && (_jsx(OtelTimeline, { spans: traceSpans ?? [], selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), bottomPane === 'tree' && (_jsx(OtelSpanTree, { spans: spanTree, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect, defaultExpandDepth: 3 }))] }))] }))] }));
295
+ : metricsLoading }) }), wsConnected ? null : (_jsx(Box, { pr: 2, flexShrink: 0, children: _jsx(Label, { variant: "secondary", size: "small", children: "\u25CB Polling" }) }))] }), timelineBounds && rangeStart && rangeEnd && (_jsx(Box, { px: 3, pt: 2, pb: 1, borderBottom: "1px solid", borderColor: "border.default", bg: "canvas.subtle", flexShrink: 0, position: "relative", zIndex: 0, children: _jsx(OtelTimelineRangeSlider, { timelineStart: timelineBounds.start, timelineEnd: timelineBounds.end, selectedStart: rangeStart, selectedEnd: rangeEnd, onRangeChange: handleRangeChange, histogram: histogram, height: 48, tickCount: 6 }) })), _jsxs(Box, { display: "flex", flex: 1, minHeight: 0, overflow: "hidden", children: [_jsxs(Box, { display: "flex", flexDirection: "column", flex: hasDetail ? '0 0 55%' : '1 1 100%', minHeight: 0, overflow: "hidden", children: [signal === 'traces' && (_jsx(OtelTracesList, { spans: filteredTraces, loading: tracesLoading, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), signal === 'logs' && (_jsx(OtelLogsList, { logs: filteredLogs, loading: logsLoading, selectedLogIndex: selectedLogIdx, onSelectLog: handleLogSelect })), signal === 'metrics' && (_jsx(OtelMetricsList, { metrics: filteredMetrics, loading: metricsLoading }))] }), hasDetail && (_jsxs(Box, { flex: "0 0 45%", minHeight: 0, overflow: "auto", borderLeft: "1px solid", borderColor: "border.default", children: [signal === 'traces' && selectedSpan && (_jsx(OtelSpanDetail, { span: selectedSpan, traceSpans: traceSpans ?? undefined, onClose: handleCloseDetail })), signal === 'logs' && selectedLogIdx !== null && logs && (_jsxs(Box, { p: 3, children: [_jsxs(Box, { display: "flex", justifyContent: "space-between", mb: 3, children: [_jsx(Text, { sx: { fontWeight: 'bold', fontSize: 2 }, children: "Log Detail" }), _jsx(Button, { size: "small", variant: "invisible", onClick: handleCloseDetail, children: "\u2715" })] }), _jsx(Box, { as: "pre", m: 0, fontSize: 1, fontFamily: "mono", whiteSpace: "pre-wrap", wordBreak: "break-word", children: JSON.stringify(logs[selectedLogIdx], null, 2) })] }))] }))] }), signal === 'traces' && selectedSpan && spanTree.length > 0 && (_jsxs(_Fragment, { children: [_jsxs(Box, { display: "flex", gap: 1, px: 3, py: 1, bg: "canvas.subtle", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, children: [_jsx(Button, { size: "small", variant: bottomPane === 'timeline' ? 'primary' : 'invisible', leadingVisual: ClockIcon, onClick: () => toggleBottomPane('timeline'), children: "Timeline" }), _jsx(Button, { size: "small", variant: bottomPane === 'tree' ? 'primary' : 'invisible', leadingVisual: GitBranchIcon, onClick: () => toggleBottomPane('tree'), children: "Span Tree" })] }), bottomPane && (_jsxs(Box, { height: 260, overflow: "auto", borderTop: "1px solid", borderColor: "border.default", flexShrink: 0, children: [bottomPane === 'timeline' && (_jsx(OtelTimeline, { spans: traceSpans ?? [], selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect })), bottomPane === 'tree' && (_jsx(OtelSpanTree, { spans: spanTree, selectedSpanId: selectedSpan?.span_id, onSelectSpan: handleSpanSelect, defaultExpandDepth: 3 }))] }))] }))] }));
296
296
  };
297
297
  // ── Client-side filter helpers ──────────────────────────────────────
298
298
  function filterSpans(spans, q) {
@@ -5,6 +5,23 @@
5
5
  import { createStore } from 'zustand/vanilla';
6
6
  import { useStore } from 'zustand';
7
7
  import { registerSessionState } from '../sessionEnd.js';
8
+ /**
9
+ * Whether an update is about the entity the layout holds. An update naming
10
+ * another space or organization (by `uid`, else `id`) replaces the one held:
11
+ * merged over it, the fields the new one does not carry (an organization's
12
+ * handle, a team) stayed from the one left. An update naming none, or one
13
+ * naming the same, is merged onto what is held.
14
+ */
15
+ const entityUid = (entity) => {
16
+ const record = entity;
17
+ const uid = record?.uid ?? record?.id;
18
+ return typeof uid === 'string' && uid.trim() ? uid.trim() : undefined;
19
+ };
20
+ const sameEntity = (held, update) => {
21
+ const heldUid = entityUid(held);
22
+ const updateUid = entityUid(update);
23
+ return !heldUid || !updateUid || heldUid === updateUid;
24
+ };
8
25
  /**
9
26
  * Name of the cookie holding the space last selected by the user.
10
27
  *
@@ -25,7 +42,9 @@ const readSpaceContextCookie = () => {
25
42
  }
26
43
  try {
27
44
  const parsed = JSON.parse(decodeURIComponent(match[1]));
28
- if (!parsed || typeof parsed !== 'object' || typeof parsed.id !== 'string') {
45
+ if (!parsed ||
46
+ typeof parsed !== 'object' ||
47
+ typeof parsed.id !== 'string') {
29
48
  return undefined;
30
49
  }
31
50
  return {
@@ -102,7 +121,9 @@ export const layoutStore = createStore((set, get) => ({
102
121
  return {
103
122
  organization: organization
104
123
  ? {
105
- ...state.organization,
124
+ ...(sameEntity(state.organization, organization)
125
+ ? state.organization
126
+ : undefined),
106
127
  ...organization,
107
128
  }
108
129
  : undefined,
@@ -121,7 +142,7 @@ export const layoutStore = createStore((set, get) => ({
121
142
  updateLayoutSpace: (space) => set((state) => {
122
143
  const next = space
123
144
  ? {
124
- ...state.space,
145
+ ...(sameEntity(state.space, space) ? state.space : undefined),
125
146
  ...space,
126
147
  }
127
148
  : undefined;
@@ -15,7 +15,7 @@
15
15
  * `datalayer-research` organization, at `/research` (the public papers, read
16
16
  * signed in, at `/research/papers/…` beside them).
17
17
  */
18
- export type NavigationView = 'studio' | 'home' | 'agentify' | 'research' | 'admin' | 'apps';
18
+ export type NavigationView = 'studio' | 'home' | 'spaces' | 'agentify' | 'research' | 'admin' | 'apps';
19
19
  /** The shells, and where each one opens when it has not been visited. */
20
20
  export declare const NAVIGATION_VIEW_HOMES: Record<NavigationView, string>;
21
21
  /** Well-known view identifiers for {@link NavigationState.tabsByView}. */
@@ -77,13 +77,21 @@ export declare function isRememberableRoute(route: string): boolean;
77
77
  */
78
78
  export declare function isHostedAppRoute(route: string): boolean;
79
79
  /**
80
- * Whether a page belongs to no shell: the settings, the documentation and
81
- * the decks.
80
+ * Whether a page belongs to no shell: the settings, the documentation, the
81
+ * presentations and the agentspecs catalogue (since 8 November 2026: reached
82
+ * from an agent's picker on /agents/new, its env var's page swapped the
83
+ * Sessions sidebar for the Studio's).
82
84
  *
83
85
  * They are read from whichever shell the reader is in — its sidebar stays
84
86
  * beside them and the header's selector does not move — so they are never a
85
87
  * shell's own page: opening one does not change the shell, and no shell
86
88
  * returns to one.
89
+ *
90
+ * The presentations are `/decks/<collection>/<slug>` and their list,
91
+ * `/presentations`. Bare `/decks` is not one of them: it is the selected
92
+ * space's decks table, a page of the Spaces shell (the user, 12 November
93
+ * 2026). Neutral, it took the current shell whenever one was given, and the
94
+ * Sessions sidebar stayed beside it.
87
95
  */
88
96
  export declare function isShellNeutralRoute(route: string): boolean;
89
97
  /**
@@ -38,6 +38,9 @@ export const NAVIGATION_VIEW_HOMES = {
38
38
  // of its own to be sent to. It was `/power` until 2026-10-09, when the
39
39
  // shell took the name it is called by everywhere else.
40
40
  home: '/platform',
41
+ // The spaces and what they hold: the Workspace of the Sessions shell,
42
+ // a shell of its own since 9 November 2026.
43
+ spaces: '/spaces',
41
44
  agentify: '/agentify',
42
45
  // The research areas and their spaces.
43
46
  research: '/research',
@@ -60,6 +63,7 @@ const FORMER_TABS_KEY = 'datalayer_view_tabs';
60
63
  const COOKIE_MAX_AGE_DAYS = 365;
61
64
  const isView = (value) => value === 'studio' ||
62
65
  value === 'home' ||
66
+ value === 'spaces' ||
63
67
  value === 'agentify' ||
64
68
  value === 'research' ||
65
69
  value === 'admin' ||
@@ -243,17 +247,26 @@ export function isHostedAppRoute(route) {
243
247
  (segments.length === 4 && segments[2] === 'preview')));
244
248
  }
245
249
  /**
246
- * Whether a page belongs to no shell: the settings, the documentation and
247
- * the decks.
250
+ * Whether a page belongs to no shell: the settings, the documentation, the
251
+ * presentations and the agentspecs catalogue (since 8 November 2026: reached
252
+ * from an agent's picker on /agents/new, its env var's page swapped the
253
+ * Sessions sidebar for the Studio's).
248
254
  *
249
255
  * They are read from whichever shell the reader is in — its sidebar stays
250
256
  * beside them and the header's selector does not move — so they are never a
251
257
  * shell's own page: opening one does not change the shell, and no shell
252
258
  * returns to one.
259
+ *
260
+ * The presentations are `/decks/<collection>/<slug>` and their list,
261
+ * `/presentations`. Bare `/decks` is not one of them: it is the selected
262
+ * space's decks table, a page of the Spaces shell (the user, 12 November
263
+ * 2026). Neutral, it took the current shell whenever one was given, and the
264
+ * Sessions sidebar stayed beside it.
253
265
  */
254
266
  export function isShellNeutralRoute(route) {
255
267
  const path = pathOf(route);
256
- return ['/settings', '/docs', '/decks'].some(prefix => path === prefix || path.startsWith(`${prefix}/`));
268
+ return (path.startsWith('/decks/') ||
269
+ ['/settings', '/docs', '/presentations', '/agentspecs'].some(prefix => path === prefix || path.startsWith(`${prefix}/`)));
257
270
  }
258
271
  /**
259
272
  * The shell a route belongs to, read off its first segment.
@@ -275,8 +288,8 @@ export function viewForRoute(route, current) {
275
288
  return current;
276
289
  }
277
290
  // The root is the Studio: what a signed-in reader opens on. The settings
278
- // are read from it too, with its sidebar beside them — and so is the
279
- // agentspecs catalogue, the specs an app is composed from.
291
+ // and the agentspecs catalogue are read from it too, with its sidebar
292
+ // beside them, when no shell is current.
280
293
  if (path === '/' ||
281
294
  path === '/studio' ||
282
295
  path.startsWith('/studio/') ||
@@ -286,6 +299,10 @@ export function viewForRoute(route, current) {
286
299
  path.startsWith('/agentspecs/')) {
287
300
  return 'studio';
288
301
  }
302
+ if (SPACES_SHELL_ROUTES.some(prefix => path === prefix || path.startsWith(`${prefix}/`)) ||
303
+ SPACES_SHELL_EXACT_ROUTES.includes(path)) {
304
+ return 'spaces';
305
+ }
289
306
  if (path === '/agentify' || path.startsWith('/agentify/')) {
290
307
  return 'agentify';
291
308
  }
@@ -315,6 +332,32 @@ export function viewForRoute(route, current) {
315
332
  }
316
333
  return 'home';
317
334
  }
335
+ /**
336
+ * The Spaces shell's pages: the spaces, then the work they hold — what was
337
+ * the Workspace of the Sessions sidebar until 9 November 2026, when it became
338
+ * a shell of its own.
339
+ */
340
+ const SPACES_SHELL_ROUTES = [
341
+ '/spaces',
342
+ '/items',
343
+ // The Library, public and private, beside the Spaces sidebar (the user,
344
+ // 9 November 2026).
345
+ '/library',
346
+ '/shared',
347
+ '/notebooks',
348
+ '/documents',
349
+ '/cells',
350
+ // The selected space's spreadsheets (the user, 12 November 2026).
351
+ '/spreadsheets',
352
+ '/datasets',
353
+ '/contents',
354
+ ];
355
+ /**
356
+ * The selected space's decks, `/decks` itself only (the user, 12 November
357
+ * 2026): `/decks/<collection>/<slug>` are presentations, drawn by their own
358
+ * page, not the Spaces shell's.
359
+ */
360
+ const SPACES_SHELL_EXACT_ROUTES = ['/decks'];
318
361
  /**
319
362
  * `/<account>/<space>/<kind>[/…]` for the kinds a space shows beside the
320
363
  * Agentify sidebar — decks, spreadsheets (Cells), courses and what a course
@@ -24,6 +24,8 @@ import type { JSX } from 'react';
24
24
  import type { McpWorkflowsHealth } from '../../api/mcp/operations';
25
25
  import { type McpErrorStateFn, type McpRoutes } from './types';
26
26
  export type McpObservabilityPane = 'runs' | 'live' | 'metrics';
27
+ /** Every pane, in the order the switcher shows them. */
28
+ export declare const ALL_PANES: readonly McpObservabilityPane[];
27
29
  export interface McpObservabilityProps {
28
30
  /** The application's words for a failed request. */
29
31
  errorState: McpErrorStateFn;
@@ -32,6 +34,13 @@ export interface McpObservabilityProps {
32
34
  /** Which pane is open; the address owns it. */
33
35
  pane: McpObservabilityPane;
34
36
  onPaneChange: (pane: McpObservabilityPane) => void;
37
+ /**
38
+ * The panes offered, in this order; all three when unsaid. The Live pane
39
+ * reads the platform's own engine and is a platform administrator's
40
+ * (the user, 6 November 2026): an application shows an account its runs
41
+ * and metrics, and its admin page the live engine.
42
+ */
43
+ panes?: readonly McpObservabilityPane[];
35
44
  /** The run to open, when the address names one. */
36
45
  taskUid?: string;
37
46
  /** The trace to open — what a synchronous call leaves behind. */
@@ -67,13 +76,23 @@ export declare const stageOf: (name: string) => string;
67
76
  * second is usually a problem with the network between them, and sending
68
77
  * somebody to the wrong one costs an hour.
69
78
  *
70
- * A healthy engine with a backlog is still healthy, and says so with the
71
- * backlog beside it: a queue is a queue, not a fault.
79
+ * An engine taking work is healthy; a queue it was told to run that nobody
80
+ * serves is drawn as a warning beside it — work sent there is accepted and
81
+ * never done. The engine reports no backlog of its own (DBOS's pending and
82
+ * enqueued are both `running`), so what is running is counted among the
83
+ * newest runs it lists, and said as such.
72
84
  */
73
85
  export declare const engineLook: (health: McpWorkflowsHealth | undefined, failed: boolean) => {
74
86
  label: string;
75
87
  variant: "success" | "attention" | "danger" | "secondary";
76
88
  note: string;
77
89
  };
78
- export declare const McpObservability: ({ errorState, routes, pane, onPaneChange, taskUid, traceId, onSelect, agent, org, showTitle, }: McpObservabilityProps) => JSX.Element;
90
+ /** Each queue the engine polls, with how many of its newest runs are running there and the oldest of them. */
91
+ export declare const engineQueues: (health: McpWorkflowsHealth | undefined, now?: number) => {
92
+ name: string;
93
+ running: number;
94
+ oldestSeconds?: number;
95
+ served: boolean;
96
+ }[];
97
+ export declare const McpObservability: ({ errorState, routes, pane, onPaneChange, panes, taskUid, traceId, onSelect, agent, org, showTitle, }: McpObservabilityProps) => JSX.Element;
79
98
  export default McpObservability;