@datalayer/agent-runtimes 1.0.25 → 1.0.28
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/lib/components/agents/index.d.ts +6 -0
- package/lib/components/{students → agents}/index.js +6 -1
- package/lib/components/{NotificationEventCard.js → cards/NotificationEventCard.js} +1 -1
- package/lib/components/{OutputCard.js → cards/OutputCard.js} +1 -1
- package/lib/components/cards/index.d.ts +9 -0
- package/lib/components/cards/index.js +13 -0
- package/lib/components/{runtimes/RuntimeCellVariables.d.ts → code-sandboxes/CodeSandboxCellVariables.d.ts} +3 -3
- package/lib/components/{runtimes/RuntimeCellVariables.js → code-sandboxes/CodeSandboxCellVariables.js} +2 -2
- package/lib/components/{runtimes/RuntimeCellVariablesDialog.d.ts → code-sandboxes/CodeSandboxCellVariablesDialog.d.ts} +4 -4
- package/lib/components/{runtimes/RuntimeCellVariablesDialog.js → code-sandboxes/CodeSandboxCellVariablesDialog.js} +4 -4
- package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +0 -2
- package/lib/components/code-sandboxes/CodeSandboxLauncher.js +98 -11
- package/lib/components/code-sandboxes/CodeSandboxNames.d.ts +47 -0
- package/lib/components/code-sandboxes/CodeSandboxNames.js +166 -0
- package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +30 -5
- package/lib/components/code-sandboxes/CodeSandboxPicker.js +239 -55
- package/lib/components/{runtimes/RuntimePickerCell.d.ts → code-sandboxes/CodeSandboxPickerCell.d.ts} +5 -5
- package/lib/components/{runtimes/RuntimePickerCell.js → code-sandboxes/CodeSandboxPickerCell.js} +6 -6
- package/lib/components/{runtimes/RuntimeReservationControl.d.ts → code-sandboxes/CodeSandboxReservationControl.d.ts} +3 -3
- package/lib/components/{runtimes/RuntimeReservationControl.js → code-sandboxes/CodeSandboxReservationControl.js} +5 -5
- package/lib/components/{runtimes/RuntimeTransfer.d.ts → code-sandboxes/CodeSandboxTransfer.d.ts} +7 -1
- package/lib/components/{runtimes/RuntimeUtils.d.ts → code-sandboxes/CodeSandboxUtils.d.ts} +3 -3
- package/lib/components/{runtimes/RuntimeUtils.js → code-sandboxes/CodeSandboxUtils.js} +15 -10
- package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +71 -0
- package/lib/components/{runtimes/RuntimeVariables.js → code-sandboxes/CodeSandboxVariables.js} +77 -2
- package/lib/components/code-sandboxes/NewCodeSandboxControls.d.ts +17 -0
- package/lib/components/code-sandboxes/NewCodeSandboxControls.js +5 -5
- package/lib/components/code-sandboxes/index.d.ts +20 -1
- package/lib/components/code-sandboxes/index.js +20 -1
- package/lib/components/index.d.ts +4 -6
- package/lib/components/index.js +4 -6
- package/lib/components/progress/CreditsIndicator.js +1 -1
- package/package.json +2 -2
- package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +0 -7
- package/lib/components/runtimes/RuntimeLauncherDialog.js +0 -11
- package/lib/components/runtimes/RuntimePickerBase.d.ts +0 -8
- package/lib/components/runtimes/RuntimePickerBase.js +0 -12
- package/lib/components/runtimes/RuntimePickerNotebook.d.ts +0 -42
- package/lib/components/runtimes/RuntimePickerNotebook.js +0 -197
- package/lib/components/runtimes/RuntimeSimplePicker.d.ts +0 -38
- package/lib/components/runtimes/RuntimeSimplePicker.js +0 -156
- package/lib/components/runtimes/RuntimeVariables.d.ts +0 -38
- package/lib/components/runtimes/index.d.ts +0 -12
- package/lib/components/runtimes/index.js +0 -17
- package/lib/components/students/StudentItemStatus.d.ts +0 -7
- package/lib/components/students/StudentItemStatus.js +0 -68
- package/lib/components/students/index.d.ts +0 -1
- package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
- /package/lib/components/{AgentSummary.d.ts → agents/AgentSummary.d.ts} +0 -0
- /package/lib/components/{AgentSummary.js → agents/AgentSummary.js} +0 -0
- /package/lib/components/{NotificationEventCard.d.ts → cards/NotificationEventCard.d.ts} +0 -0
- /package/lib/components/{OutputCard.d.ts → cards/OutputCard.d.ts} +0 -0
- /package/lib/components/{ToolApprovalCard.d.ts → cards/ToolApprovalCard.d.ts} +0 -0
- /package/lib/components/{ToolApprovalCard.js → cards/ToolApprovalCard.js} +0 -0
- /package/lib/components/{runtimes/RuntimeTransfer.js → code-sandboxes/CodeSandboxTransfer.js} +0 -0
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
4
4
|
* Distributed under the terms of the Modified BSD License.
|
|
5
5
|
*/
|
|
6
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
|
|
7
7
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
8
8
|
//import { kernelIcon } from '@jupyterlab/ui-components';
|
|
9
9
|
import { ActionList, ActionMenu, IconButton, Text, RadioGroup, Radio, FormControl, LabelGroup, Label, } from '@primer/react';
|
|
@@ -12,8 +12,12 @@ import { Box } from '@datalayer/primer-addons';
|
|
|
12
12
|
import { CpuIcon } from '@primer/octicons-react';
|
|
13
13
|
import { BrowserIcon, LaptopSimpleIcon } from '@datalayer/icons-react';
|
|
14
14
|
import { CreditsIndicator } from '../progress';
|
|
15
|
-
import { isRuntimeRemote } from '../../runtimes';
|
|
16
|
-
import {
|
|
15
|
+
import { isRuntimeRemote, } from '../../runtimes';
|
|
16
|
+
import { useIAMStore } from '../../state/substates';
|
|
17
|
+
import { getGroupedCodeSandboxDescs, } from './CodeSandboxUtils';
|
|
18
|
+
import { listCodeSandboxGivenNames, nextCodeSandboxGivenName, } from './CodeSandboxNames';
|
|
19
|
+
import { creditsLimitFor, NewCodeSandboxControls, useNewCodeSandboxAllowance, } from './NewCodeSandboxControls';
|
|
20
|
+
import { CodeSandboxVariables, useCodeSandboxVariablesTransfer, } from './CodeSandboxVariables';
|
|
17
21
|
/**
|
|
18
22
|
* Maximal runtime display name length after which it is trimmed.
|
|
19
23
|
*/
|
|
@@ -22,8 +26,23 @@ const RUNTIME_DISPLAY_NAME_MAX_LENGTH = 25;
|
|
|
22
26
|
* The picker of a code sandbox: what runs already, and what could run.
|
|
23
27
|
*/
|
|
24
28
|
export function CodeSandboxPicker(props) {
|
|
25
|
-
const { disabled, display, filterRuntime, multiServiceManager, postActions, preActions, preference,
|
|
26
|
-
|
|
29
|
+
const { disabled, display, filterRuntime, multiServiceManager, onTransferChange, postActions, preActions, preference, sessionContext, translator, variant, withNewSandboxControls, withVariablesTransfer, } = props;
|
|
30
|
+
/*
|
|
31
|
+
* The choice, held here when the host does not hold it.
|
|
32
|
+
*
|
|
33
|
+
* A cell keeps its sandbox in its metadata and passes both the value and
|
|
34
|
+
* the setter; a dialog has nowhere to keep it, so the picker does.
|
|
35
|
+
*/
|
|
36
|
+
const isControlled = props.setRuntimeDesc !== undefined;
|
|
37
|
+
const [ownRuntimeDesc, setOwnRuntimeDesc] = useState();
|
|
38
|
+
const runtimeDesc = isControlled ? props.runtimeDesc : ownRuntimeDesc;
|
|
39
|
+
const setRuntimeDesc = useCallback((desc) => {
|
|
40
|
+
if (!isControlled) {
|
|
41
|
+
setOwnRuntimeDesc(desc ? { ...desc } : undefined);
|
|
42
|
+
}
|
|
43
|
+
props.setRuntimeDesc?.(desc);
|
|
44
|
+
}, [isControlled, props.setRuntimeDesc]);
|
|
45
|
+
const [groupedRuntimeDescs, setGroupedRuntimeDescs] = useState(getGroupedCodeSandboxDescs(multiServiceManager, preference?.id, translator, filterRuntime, variant));
|
|
27
46
|
const trans = useMemo(() => (translator ?? nullTranslator).load('jupyterlab'), [translator]);
|
|
28
47
|
const [defaultSet, setDefaultSet] = useState(false);
|
|
29
48
|
const [remoteModelsReady, setRemoteModelsReady] = useState(!multiServiceManager.remote);
|
|
@@ -38,7 +57,7 @@ export function CodeSandboxPicker(props) {
|
|
|
38
57
|
if (disposed) {
|
|
39
58
|
return;
|
|
40
59
|
}
|
|
41
|
-
setGroupedRuntimeDescs(
|
|
60
|
+
setGroupedRuntimeDescs(getGroupedCodeSandboxDescs(multiServiceManager, preference?.id, translator, filterRuntime, variant));
|
|
42
61
|
};
|
|
43
62
|
const refreshRuntimeModels = multiServiceManager.remote?.runtimesManager.refreshRuntimeModels?.();
|
|
44
63
|
if (refreshRuntimeModels) {
|
|
@@ -80,6 +99,15 @@ export function CodeSandboxPicker(props) {
|
|
|
80
99
|
multiServiceManager.remote?.environments.changed.disconnect(updateGroupedRuntimeDescs);
|
|
81
100
|
};
|
|
82
101
|
}, [multiServiceManager, preference?.id, translator, filterRuntime, variant]);
|
|
102
|
+
/*
|
|
103
|
+
* The sandbox the session already runs on, shown as chosen.
|
|
104
|
+
*
|
|
105
|
+
* Preferably the very row of the list, so the choice is visibly one of
|
|
106
|
+
* them. A session whose sandbox is not listed — the list may leave the
|
|
107
|
+
* current one out, and the remote ones land late — is named from the
|
|
108
|
+
* specifications instead, so the picker still opens on what is running
|
|
109
|
+
* rather than on nothing.
|
|
110
|
+
*/
|
|
83
111
|
useEffect(() => {
|
|
84
112
|
if (defaultSet) {
|
|
85
113
|
return;
|
|
@@ -96,8 +124,24 @@ export function CodeSandboxPicker(props) {
|
|
|
96
124
|
}
|
|
97
125
|
});
|
|
98
126
|
});
|
|
99
|
-
if (!matched
|
|
100
|
-
|
|
127
|
+
if (!matched) {
|
|
128
|
+
if (!remoteModelsReady) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const kernelName = sessionContext.session?.kernel?.name;
|
|
132
|
+
const spec = kernelName
|
|
133
|
+
? sessionContext.specsManager.specs?.kernelspecs[kernelName]
|
|
134
|
+
: undefined;
|
|
135
|
+
if (spec) {
|
|
136
|
+
setRuntimeDesc({
|
|
137
|
+
name: spec.name,
|
|
138
|
+
kernelId,
|
|
139
|
+
language: spec.language,
|
|
140
|
+
displayName: sessionContext.kernelDisplayName,
|
|
141
|
+
location: sessionContext.location ??
|
|
142
|
+
'local',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
101
145
|
}
|
|
102
146
|
}
|
|
103
147
|
}
|
|
@@ -109,55 +153,195 @@ export function CodeSandboxPicker(props) {
|
|
|
109
153
|
sessionContext,
|
|
110
154
|
setRuntimeDesc,
|
|
111
155
|
]);
|
|
156
|
+
/*
|
|
157
|
+
* What a NEW sandbox costs, and the answer the host is given.
|
|
158
|
+
*
|
|
159
|
+
* Only asked when the host wants them: a cell picking among the sandboxes
|
|
160
|
+
* that already run has neither a reservation to make nor an answer to
|
|
161
|
+
* shape. The arithmetic is the launcher's, from its own module.
|
|
162
|
+
*/
|
|
163
|
+
const { refreshCredits } = useIAMStore();
|
|
164
|
+
const [timeLimit, setTimeLimit] = useState(10);
|
|
165
|
+
/*
|
|
166
|
+
* The name a new sandbox is given.
|
|
167
|
+
*
|
|
168
|
+
* Offered from what was picked — the environment or the specification —
|
|
169
|
+
* numbered against the sandboxes that already carry that name, and written
|
|
170
|
+
* into the description so whoever creates the sandbox creates it named:
|
|
171
|
+
* `given_name` for a runtime of the platform, the name held beside a kernel
|
|
172
|
+
* of a server. Re-offered whenever another thing is picked, and left alone
|
|
173
|
+
* while the user edits it.
|
|
174
|
+
*/
|
|
175
|
+
const [givenName, setGivenName] = useState('');
|
|
176
|
+
const givenNameFor = useRef(undefined);
|
|
177
|
+
const [userStorage, setUserStorage] = useState(false);
|
|
178
|
+
const asksForNewSandbox = withNewSandboxControls || !!onTransferChange;
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (asksForNewSandbox) {
|
|
181
|
+
refreshCredits();
|
|
182
|
+
}
|
|
183
|
+
}, [asksForNewSandbox]);
|
|
184
|
+
const resolvedBurningRate = runtimeDesc?.burningRate ??
|
|
185
|
+
multiServiceManager.remote?.environments
|
|
186
|
+
.get()
|
|
187
|
+
.find(env => env.name === runtimeDesc?.name)?.burning_rate;
|
|
188
|
+
const allowance = useNewCodeSandboxAllowance(resolvedBurningRate);
|
|
189
|
+
const isNewSandbox = !!runtimeDesc && !runtimeDesc.kernelId;
|
|
190
|
+
// Offer a name for what was just picked, and leave it alone afterwards: the
|
|
191
|
+
// key is the thing picked, so re-picking the same one keeps what was typed.
|
|
192
|
+
const pickedKey = isNewSandbox
|
|
193
|
+
? `${runtimeDesc?.location}:${runtimeDesc?.name}`
|
|
194
|
+
: undefined;
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!pickedKey || givenNameFor.current === pickedKey) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
givenNameFor.current = pickedKey;
|
|
200
|
+
const taken = [
|
|
201
|
+
...(multiServiceManager.remote?.runtimesManager
|
|
202
|
+
.get()
|
|
203
|
+
.map(runtime => runtime.given_name)
|
|
204
|
+
.filter(Boolean) ?? []),
|
|
205
|
+
...listCodeSandboxGivenNames(),
|
|
206
|
+
];
|
|
207
|
+
setGivenName(nextCodeSandboxGivenName(runtimeDesc?.displayName ?? '', taken));
|
|
208
|
+
}, [multiServiceManager, pickedKey, runtimeDesc?.displayName]);
|
|
209
|
+
const effectiveMaxMinutes = runtimeDesc?.location === 'remote'
|
|
210
|
+
? allowance.effectiveMaxMinutes
|
|
211
|
+
: Math.max(1, allowance.maxFromCredits ?? -1);
|
|
212
|
+
const outOfCredits = allowance.hasKnownCredits &&
|
|
213
|
+
allowance.hasKnownRunAllowance &&
|
|
214
|
+
!allowance.hasRemainingRuns &&
|
|
215
|
+
(allowance.maxFromCredits ?? -1) < Number.EPSILON;
|
|
216
|
+
const variables = useCodeSandboxVariablesTransfer(sessionContext, runtimeDesc, withVariablesTransfer);
|
|
217
|
+
const selectedVariables = variables.selected;
|
|
218
|
+
useEffect(() => {
|
|
219
|
+
if (!onTransferChange) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const maxMinutes = runtimeDesc?.location === 'remote' ? allowance.maxFromCredits : undefined;
|
|
223
|
+
const effectiveTimeLimit = runtimeDesc?.location === 'remote'
|
|
224
|
+
? Math.max(1, Math.min(timeLimit, maxMinutes && maxMinutes > 0 ? maxMinutes : 10))
|
|
225
|
+
: timeLimit;
|
|
226
|
+
const creditsLimit = runtimeDesc?.location === 'remote' && resolvedBurningRate
|
|
227
|
+
? creditsLimitFor(effectiveTimeLimit, resolvedBurningRate)
|
|
228
|
+
: undefined;
|
|
229
|
+
// A new remote sandbox that the account cannot pay for is no answer at
|
|
230
|
+
// all: the dialog is left with nothing to assign rather than with a
|
|
231
|
+
// start that would be refused.
|
|
232
|
+
if (isNewSandbox && runtimeDesc?.location === 'remote') {
|
|
233
|
+
if (!resolvedBurningRate || !Number.isFinite(resolvedBurningRate)) {
|
|
234
|
+
onTransferChange({ runtime: null, selectedVariables });
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (allowance.hasKnownCredits &&
|
|
238
|
+
allowance.hasKnownRunAllowance &&
|
|
239
|
+
!allowance.hasRemainingRuns &&
|
|
240
|
+
(!creditsLimit || creditsLimit <= 0)) {
|
|
241
|
+
onTransferChange({ runtime: null, selectedVariables });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
onTransferChange({
|
|
246
|
+
runtime: runtimeDesc
|
|
247
|
+
? {
|
|
248
|
+
environmentName: ['browser', 'remote'].includes(runtimeDesc.location)
|
|
249
|
+
? `${runtimeDesc.location}-${runtimeDesc.name}`
|
|
250
|
+
: runtimeDesc.name,
|
|
251
|
+
id: runtimeDesc.kernelId,
|
|
252
|
+
// The name the sandbox is given, carried with the choice.
|
|
253
|
+
//
|
|
254
|
+
// Everything else about the pick travelled in this object and the
|
|
255
|
+
// name did not, so whoever created the sandbox had nothing to name
|
|
256
|
+
// it with and fell back to the kernelspec: the launcher honoured
|
|
257
|
+
// what was typed, the picker silently dropped it.
|
|
258
|
+
displayName: runtimeDesc.displayName,
|
|
259
|
+
creditsLimit,
|
|
260
|
+
capabilities: userStorage ? ['user_storage'] : undefined,
|
|
261
|
+
}
|
|
262
|
+
: null,
|
|
263
|
+
selectedVariables,
|
|
264
|
+
});
|
|
265
|
+
}, [
|
|
266
|
+
allowance,
|
|
267
|
+
isNewSandbox,
|
|
268
|
+
onTransferChange,
|
|
269
|
+
resolvedBurningRate,
|
|
270
|
+
runtimeDesc,
|
|
271
|
+
selectedVariables,
|
|
272
|
+
timeLimit,
|
|
273
|
+
userStorage,
|
|
274
|
+
]);
|
|
112
275
|
// For cell using submenu instead of group would be nice unfortunately the feature
|
|
113
276
|
// is not yet implemented in the component there has been a not-great demo story.
|
|
114
277
|
// https://github.com/primer/react/pull/3585
|
|
115
|
-
return (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
278
|
+
return (_jsxs(_Fragment, { children: [display === 'menu' ? (
|
|
279
|
+
/*
|
|
280
|
+
* Section for Menu display.
|
|
281
|
+
*/
|
|
282
|
+
_jsxs(ActionMenu, { children: [variant === 'cell' ? (_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { disabled: disabled || groupedRuntimeDescs === null,
|
|
283
|
+
// icon={() => <kernelIcon.react className="dla-Cell-runtime-icon" tag={'span'} />}
|
|
284
|
+
icon: () => (_jsx("span", { className: "dla-Cell-runtime-icon", children: _jsx(CpuIcon, {}) })), "aria-label": trans.__('Assign a Runtime to the Cell.'), title: trans.__('Assign a Runtime to the Cell.'), size: "small", variant: "invisible" }) })) : (_jsxs(ActionMenu.Button, { variant: "default", disabled: disabled || groupedRuntimeDescs === null, children: [_jsx(Text, { fontWeight: 'bold', children: trans.__('Runtime:') }), ' ' + (runtimeDesc?.displayName ?? trans.__('No Runtime'))] })), _jsx(ActionMenu.Overlay, { ...overlayProps, width: "medium", sx: { overflowY: 'auto' }, side: variant === 'cell' ? 'outside-left' : 'outside-right', children: _jsxs(ActionList, { selectionVariant: "single", children: [variant === 'cell' && (_jsxs(ActionList.Item, { selected: runtimeDesc === undefined, onSelect: () => {
|
|
285
|
+
setRuntimeDesc(undefined);
|
|
286
|
+
}, children: [preference?.location && (_jsx(ActionList.LeadingVisual, { children: preference.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : preference.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) })), trans.__('Assign the Notebook Runtime')] }, 'null')), !!preActions && preActions, Object.entries(groupedRuntimeDescs ?? {}).map(([group, runtimeDescs]) => (_jsxs(ActionList.Group, { children: [_jsx(ActionList.GroupHeading, { children: group }), runtimeDescs.map(candidateRuntimeDesc => {
|
|
287
|
+
const annotation = candidateRuntimeDesc.podName
|
|
288
|
+
? ` - ${candidateRuntimeDesc.podName.split('-', 2).reverse()[0]}`
|
|
289
|
+
: candidateRuntimeDesc.kernelId
|
|
290
|
+
? ` - ${candidateRuntimeDesc.kernelId}`
|
|
291
|
+
: '';
|
|
292
|
+
const fullDisplayName = (candidateRuntimeDesc.displayName ?? '') + annotation;
|
|
293
|
+
const displayName = (candidateRuntimeDesc.displayName?.length ?? 0) >
|
|
294
|
+
RUNTIME_DISPLAY_NAME_MAX_LENGTH
|
|
295
|
+
? candidateRuntimeDesc.displayName.slice(0, RUNTIME_DISPLAY_NAME_MAX_LENGTH) + '…'
|
|
296
|
+
: (candidateRuntimeDesc.displayName ?? '');
|
|
297
|
+
return (_jsxs(ActionList.Item, { title: fullDisplayName, selected: (candidateRuntimeDesc.location ===
|
|
298
|
+
runtimeDesc?.location ||
|
|
299
|
+
(isRuntimeRemote(candidateRuntimeDesc.location) &&
|
|
300
|
+
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
301
|
+
(candidateRuntimeDesc.kernelId ??
|
|
302
|
+
candidateRuntimeDesc.name) ===
|
|
303
|
+
(runtimeDesc?.kernelId ?? runtimeDesc?.name), onSelect: () => {
|
|
304
|
+
setRuntimeDesc(candidateRuntimeDesc);
|
|
305
|
+
}, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10)] }, candidateRuntimeDesc.name));
|
|
306
|
+
})] }, group))), !!postActions && (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), postActions] }))] }) })] })) : (
|
|
307
|
+
/*
|
|
308
|
+
* Section for Radio display.
|
|
309
|
+
*/
|
|
310
|
+
_jsxs(_Fragment, { children: [defaultSet && (_jsx(RadioGroup, { name: "kernel-options", "aria-labelledby": "kernel-options", children: Object.entries(groupedRuntimeDescs ?? {}).map(([group, runtimeDescs]) => (_jsxs(Box, { children: [_jsx(Box, { as: "h4", style: { marginTop: 0 }, children: group }), runtimeDescs.map(k => {
|
|
311
|
+
return (
|
|
312
|
+
// A kernel identifies a runtime that already runs; an
|
|
313
|
+
// environment to start one in has none, and is named
|
|
314
|
+
// by where it runs and what it runs.
|
|
315
|
+
_jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
|
|
316
|
+
setRuntimeDesc(k);
|
|
317
|
+
}, checked: (k.location === runtimeDesc?.location ||
|
|
318
|
+
(isRuntimeRemote(k.location) &&
|
|
319
|
+
isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
|
|
320
|
+
(k.kernelId ?? k.name) ===
|
|
321
|
+
(runtimeDesc?.kernelId ?? runtimeDesc?.name) }), _jsx(FormControl.Label, { children: _jsxs(Box, { display: "flex", sx: { alignItems: 'baseline' }, children: [_jsx(Box, { children: k.displayName }), k.kernelId && (_jsx(Text, { sx: {
|
|
322
|
+
ml: 2,
|
|
323
|
+
fontSize: 0,
|
|
324
|
+
color: 'fg.muted',
|
|
325
|
+
fontFamily: 'mono'
|
|
326
|
+
}, title: k.kernelId, children: k.kernelId.slice(0, 8) })), k.kernelId && k.location === 'remote' && (_jsx(Box, { ml: 3, mt: 1, children: _jsx(CreditsIndicator, { kernelId: k.kernelId, serviceManager: multiServiceManager.remote }, "credits-indicator") }))] }) }), _jsx(FormControl.Caption, { children: _jsxs(LabelGroup, { sx: { marginTop: 1 }, children: [_jsx(Label, { variant: "secondary", children: k.name }), _jsx(Label, { variant: "secondary", sx: { marginLeft: 1 }, children: k.location }), k.burningRate && (_jsxs(Label, { variant: "sponsors", sx: { marginLeft: 1 }, children: [k.burningRate, " credits/second"] })), k.gpu && (_jsx(Label, { variant: "success", sx: { marginLeft: 1 }, children: "GPU" }))] }) })] }), _jsx(ActionList.Divider, {})] }, `${k.location}:${k.kernelId ?? k.name}`));
|
|
327
|
+
})] }, group))) })), !!postActions && _jsx(_Fragment, { children: postActions })] })), withNewSandboxControls && isNewSandbox && (
|
|
328
|
+
// The user asked for a NEW sandbox: the questions are the launcher's,
|
|
329
|
+
// asked through the launcher's own controls. A sandbox of this page
|
|
330
|
+
// or of this server reserves nothing, so only the time of a remote
|
|
331
|
+
// one is asked for.
|
|
332
|
+
_jsx(NewCodeSandboxControls, { withReservation: runtimeDesc?.location === 'remote', burningRate: resolvedBurningRate, timeLimit: timeLimit, onTimeChange: setTimeLimit, max: effectiveMaxMinutes, disabled: outOfCredits, error: runtimeDesc?.location !== 'remote'
|
|
333
|
+
? undefined
|
|
334
|
+
: outOfCredits && (allowance.maxFromCredits ?? -1) >= 0
|
|
335
|
+
? 'You must add credits to your account.'
|
|
336
|
+
: timeLimit === 0
|
|
337
|
+
? 'You must set a time limit.'
|
|
338
|
+
: undefined, givenName: givenName, onGivenNameChange: value => {
|
|
339
|
+
setGivenName(value);
|
|
340
|
+
if (runtimeDesc) {
|
|
341
|
+
// The description is what the caller creates the sandbox from:
|
|
342
|
+
// the name travels with it, rather than beside it.
|
|
343
|
+
setRuntimeDesc({ ...runtimeDesc, displayName: value });
|
|
344
|
+
}
|
|
345
|
+
}, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) })), variables.available && (_jsx(CodeSandboxVariables, { selectedVariables: variables.selected, setSelectVariable: variables.setSelected, transferVariables: variables.transfer, setTransferVariable: variables.setTransfer, kernelVariables: variables.variables, translator: translator }))] }));
|
|
160
346
|
}
|
|
161
|
-
// Backward-compatible aliases (deprecated: use CodeSandboxPicker).
|
|
162
|
-
export const RuntimePickerBase = CodeSandboxPicker;
|
|
163
347
|
export default CodeSandboxPicker;
|
package/lib/components/{runtimes/RuntimePickerCell.d.ts → code-sandboxes/CodeSandboxPickerCell.d.ts}
RENAMED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ISessionContext } from '@jupyterlab/apputils';
|
|
2
2
|
import { CodeCellModel } from '@jupyterlab/cells';
|
|
3
3
|
import type { IMarkdownParser, IRenderMime } from '@jupyterlab/rendermime';
|
|
4
|
-
import { type ICodeSandboxPickerProps } from '
|
|
4
|
+
import { type ICodeSandboxPickerProps } from './CodeSandboxPicker';
|
|
5
5
|
/**
|
|
6
|
-
* {@link
|
|
6
|
+
* {@link CodeSandboxPickerCell} properties
|
|
7
7
|
*/
|
|
8
|
-
export type
|
|
8
|
+
export type ICodeSandboxPickerCellProps = Pick<ICodeSandboxPickerProps, 'multiServiceManager' | 'preference' | 'translator'> & {
|
|
9
9
|
/**
|
|
10
10
|
* Callback to allow the user to login
|
|
11
11
|
*/
|
|
@@ -30,5 +30,5 @@ export type IRuntimePickerCellProps = Pick<ICodeSandboxPickerProps, 'multiServic
|
|
|
30
30
|
/**
|
|
31
31
|
* Runtime picker component for a cell.
|
|
32
32
|
*/
|
|
33
|
-
export declare function
|
|
34
|
-
export default
|
|
33
|
+
export declare function CodeSandboxPickerCell(props: ICodeSandboxPickerCellProps): JSX.Element;
|
|
34
|
+
export default CodeSandboxPickerCell;
|
package/lib/components/{runtimes/RuntimePickerCell.js → code-sandboxes/CodeSandboxPickerCell.js}
RENAMED
|
@@ -12,13 +12,13 @@ import { isRuntimeRemote } from '../../runtimes';
|
|
|
12
12
|
import { RuntimeSnippetsFacade } from '../../jupyter';
|
|
13
13
|
import { ExternalTokenSilentLogin } from '@datalayer/core/lib/components/iam';
|
|
14
14
|
import { SnippetDialog } from '../../components/snippets/SnippetDialog';
|
|
15
|
-
import { CodeSandboxPicker, } from '
|
|
16
|
-
import { CodeSandboxLauncher } from '
|
|
17
|
-
import {
|
|
15
|
+
import { CodeSandboxPicker, } from './CodeSandboxPicker';
|
|
16
|
+
import { CodeSandboxLauncher } from './CodeSandboxLauncher';
|
|
17
|
+
import { CodeSandboxCellVariablesDialog } from './CodeSandboxCellVariablesDialog';
|
|
18
18
|
/**
|
|
19
19
|
* Runtime picker component for a cell.
|
|
20
20
|
*/
|
|
21
|
-
export function
|
|
21
|
+
export function CodeSandboxPickerCell(props) {
|
|
22
22
|
const { logIn, markdownParser, model, preference, sanitizer, multiServiceManager, sessionContext, translator, } = props;
|
|
23
23
|
const { token } = useIAMStore();
|
|
24
24
|
const { configuration } = useCoreStore();
|
|
@@ -99,6 +99,6 @@ export function RuntimePickerCell(props) {
|
|
|
99
99
|
const datalayerMeta = model.getMetadata('datalayer') ?? {};
|
|
100
100
|
return (_jsxs(_Fragment, { children: [_jsx(CodeSandboxPicker, { display: "menu", filterRuntime: filterKernel, preference: preference, multiServiceManager: multiServiceManager, runtimeDesc: datalayerMeta.kernel ? datalayerMeta.kernel : undefined, setRuntimeDesc: setSelectedRuntimeDesc, variant: 'cell', preActions: _jsxs(ActionList.Item, { disabled: !multiServiceManager.remote, onSelect: setCell, selected: hasCellRuntime, title: !multiServiceManager.remote
|
|
101
101
|
? 'You are not connected with Datalayer.'
|
|
102
|
-
: 'Assign a new temporary Runtime on each Cell execution.', children: [_jsx(ActionList.LeadingVisual, { children: _jsx(CloudUploadIcon, {}) }), trans.__('Assign a Cell Runtime')] }), postActions: token || !logIn ? (_jsxs(_Fragment, { children: [RuntimeSnippetsFacade.supports(preference?.language ?? '') && (_jsx(ActionList.Item, { onSelect: openVariableDialog, disabled: !isForeign, title: trans.__('Define variables to transfer between the document kernel and the cell kernel.'), children: trans.__('Define Cell Variables Transfer') })), !configuration.whiteLabel && (_jsx(ActionList.Item, { onSelect: openSnippetDialog, disabled: snippets.length === 0, title: trans.__('Inject a code snippet at the end of the cell.'), children: trans.__('Inject Code Snippet') }))] })) : (_jsx(ActionList.Item, { onSelect: props.logIn, title: 'Runtime Provider.', children: _jsx(ExternalTokenSilentLogin, { message: "Click to sign in" }) })), translator: translator }), isVariableDialogOpen && (_jsx(
|
|
102
|
+
: 'Assign a new temporary Runtime on each Cell execution.', children: [_jsx(ActionList.LeadingVisual, { children: _jsx(CloudUploadIcon, {}) }), trans.__('Assign a Cell Runtime')] }), postActions: token || !logIn ? (_jsxs(_Fragment, { children: [RuntimeSnippetsFacade.supports(preference?.language ?? '') && (_jsx(ActionList.Item, { onSelect: openVariableDialog, disabled: !isForeign, title: trans.__('Define variables to transfer between the document kernel and the cell kernel.'), children: trans.__('Define Cell Variables Transfer') })), !configuration.whiteLabel && (_jsx(ActionList.Item, { onSelect: openSnippetDialog, disabled: snippets.length === 0, title: trans.__('Inject a code snippet at the end of the cell.'), children: trans.__('Inject Code Snippet') }))] })) : (_jsx(ActionList.Item, { onSelect: props.logIn, title: 'Runtime Provider.', children: _jsx(ExternalTokenSilentLogin, { message: "Click to sign in" }) })), translator: translator }), isVariableDialogOpen && (_jsx(CodeSandboxCellVariablesDialog, { model: model, onClose: closeVariableDialog, preference: preference, sessionContext: sessionContext, translator: translator })), isSnippetDialogOpen && (_jsx(SnippetDialog, { language: language, model: model, snippets: snippets, onClose: closeSnippetDialog, markdownParser: markdownParser, sanitizer: sanitizer })), isKernelDialogOpen && (_jsx(CodeSandboxLauncher, { manager: multiServiceManager.remote, onSubmit: onStartRemote, startRuntime: 'defer', submitLabel: "Assign", markdownParser: markdownParser, sanitizer: sanitizer }))] }));
|
|
103
103
|
}
|
|
104
|
-
export default
|
|
104
|
+
export default CodeSandboxPickerCell;
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* TODO this should be configurable
|
|
5
5
|
*/
|
|
6
|
-
export declare const
|
|
6
|
+
export declare const MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES: number;
|
|
7
7
|
/**
|
|
8
8
|
* Time control properties
|
|
9
9
|
*/
|
|
10
|
-
export interface
|
|
10
|
+
export interface ICodeSandboxReservationControlProps {
|
|
11
11
|
/**
|
|
12
12
|
* Callback on add credits button
|
|
13
13
|
*/
|
|
@@ -44,4 +44,4 @@ export interface IKernelReservationControlProps {
|
|
|
44
44
|
/**
|
|
45
45
|
* Runtime reservation control
|
|
46
46
|
*/
|
|
47
|
-
export declare function
|
|
47
|
+
export declare function CodeSandboxReservationControl(props: ICodeSandboxReservationControlProps): JSX.Element;
|
|
@@ -12,11 +12,11 @@ import { Slider } from '@datalayer/primer-addons';
|
|
|
12
12
|
*
|
|
13
13
|
* TODO this should be configurable
|
|
14
14
|
*/
|
|
15
|
-
export const
|
|
15
|
+
export const MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES = 24 * 60;
|
|
16
16
|
/**
|
|
17
17
|
* Runtime reservation control
|
|
18
18
|
*/
|
|
19
|
-
export function
|
|
19
|
+
export function CodeSandboxReservationControl(props) {
|
|
20
20
|
const { addCredits, burningRate, disabled, error, label, max: maxProps, onTimeChange, time, } = props;
|
|
21
21
|
// The caller computes its maximum from the credits and the burning rate of
|
|
22
22
|
// an environment, either of which may not be known yet — and a slider given
|
|
@@ -24,8 +24,8 @@ export function RuntimeReservationControl(props) {
|
|
|
24
24
|
// hold in any case.
|
|
25
25
|
const requestedMax = Number.isFinite(maxProps)
|
|
26
26
|
? maxProps
|
|
27
|
-
:
|
|
28
|
-
const max = Math.max(1, Math.min(requestedMax,
|
|
27
|
+
: MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES;
|
|
28
|
+
const max = Math.max(1, Math.min(requestedMax, MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES));
|
|
29
29
|
const displayedTime = Number.isFinite(time)
|
|
30
30
|
? Math.min(max, Math.max(1, time))
|
|
31
31
|
: 1;
|
|
@@ -45,5 +45,5 @@ export function RuntimeReservationControl(props) {
|
|
|
45
45
|
gridTemplateColumns: 'max-content 1fr max-content max-content',
|
|
46
46
|
gridGap: 'var(--stack-gap-condensed)',
|
|
47
47
|
width: '100%',
|
|
48
|
-
}, children: [_jsx(Slider, { step: 1, min: 1, max: max, value: displayedTime, onChange: handleTimeChange, disabled: disabled, label: "", displayValue: false }), _jsx(TextInput, { type: "number", step: "1", min: "1", max: max, disabled: disabled, value: displayedTime.toFixed(2), onChange: handleTimeChange }), (max === 0 || max > Number.EPSILON) && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["out of ", maxProps, " available minutes"] }), addCredits && (_jsx(IconButton, { icon: PlusIcon, "aria-label": "Add credits", onClick: () => addCredits() }))] }))] }), _jsxs(FormControl.Caption, { children: ["Maximum execution time that can be consumed by the runtime. It must be less than ", (
|
|
48
|
+
}, children: [_jsx(Slider, { step: 1, min: 1, max: max, value: displayedTime, onChange: handleTimeChange, disabled: disabled, label: "", displayValue: false }), _jsx(TextInput, { type: "number", step: "1", min: "1", max: max, disabled: disabled, value: displayedTime.toFixed(2), onChange: handleTimeChange }), (max === 0 || max > Number.EPSILON) && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["out of ", maxProps, " available minutes"] }), addCredits && (_jsx(IconButton, { icon: PlusIcon, "aria-label": "Add credits", onClick: () => addCredits() }))] }))] }), _jsxs(FormControl.Caption, { children: ["Maximum execution time that can be consumed by the runtime. It must be less than ", (MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES / 60).toFixed(0), ' ', "hours.", burningRate && (_jsxs(_Fragment, { children: [_jsx("br", {}), `With the current value, the runtime execution will consume at most ${(time * burningRate * 60).toFixed(2)} credits.`] }))] }), error && (_jsx(FormControl.Validation, { variant: "error", children: error }))] })) : (_jsx(_Fragment, {}));
|
|
49
49
|
}
|
package/lib/components/{runtimes/RuntimeTransfer.d.ts → code-sandboxes/CodeSandboxTransfer.d.ts}
RENAMED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { type IRuntimeOptions } from '../../runtimes';
|
|
2
|
-
export interface
|
|
2
|
+
export interface CodeSandboxTransfer {
|
|
3
3
|
/**
|
|
4
4
|
* Selected Kernel.
|
|
5
|
+
*
|
|
6
|
+
* `displayName` is the name the user gave the sandbox in the picker, carried
|
|
7
|
+
* with the choice so whoever creates it can create it named — the kernels
|
|
8
|
+
* API of a server has no field for one, and a runtime of the platform takes
|
|
9
|
+
* it as its `given_name`.
|
|
5
10
|
*/
|
|
6
11
|
runtime: Partial<Omit<IRuntimeOptions, 'kernelType'> & {
|
|
7
12
|
id: string;
|
|
13
|
+
displayName: string;
|
|
8
14
|
}> | null;
|
|
9
15
|
/**
|
|
10
16
|
* List of selected variables
|
|
@@ -2,14 +2,14 @@ import { SessionContext } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { ITranslator } from '@jupyterlab/translation';
|
|
3
3
|
import { IMultiServiceManager } from '../../runtimes';
|
|
4
4
|
import { IRuntimeDesc } from '../../models';
|
|
5
|
-
export type
|
|
5
|
+
export type IDatalayerCodeSandboxDesc = IRuntimeDesc & {
|
|
6
6
|
gpu?: string;
|
|
7
7
|
};
|
|
8
8
|
/**
|
|
9
9
|
* Create the grouped runtime descriptions.
|
|
10
10
|
*/
|
|
11
|
-
export declare function
|
|
12
|
-
[g: string]:
|
|
11
|
+
export declare function getGroupedCodeSandboxDescs(multiServiceManager: IMultiServiceManager, kernelId?: string, translator?: ITranslator, filterKernels?: (desc: IDatalayerCodeSandboxDesc) => boolean, variant?: 'document' | 'cell'): {
|
|
12
|
+
[g: string]: IDatalayerCodeSandboxDesc[];
|
|
13
13
|
} | undefined;
|
|
14
14
|
/**
|
|
15
15
|
* Get the default kernel name given a selector.
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
* Copyright (c) 2025-2026 Datalayer, Inc.
|
|
3
3
|
* Distributed under the terms of the Modified BSD License.
|
|
4
4
|
*/
|
|
5
|
-
import { PathExt } from '@jupyterlab/coreutils';
|
|
6
5
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
7
|
-
const ASSIGN_NEW_RUNTIME_LABEL = 'Assign a new
|
|
8
|
-
|
|
9
|
-
const
|
|
6
|
+
const ASSIGN_NEW_RUNTIME_LABEL = 'Assign a new Code Sandbox';
|
|
7
|
+
import { ensureCodeSandboxGivenName } from './CodeSandboxNames';
|
|
8
|
+
const ASSIGN_EXISTING_REMOTE_RUNTIME_LABEL = 'Assign an existing Remote Code Sandbox';
|
|
9
|
+
const ASSIGN_EXISTING_RUNTIME_LABEL = 'Assign an existing Code Sandbox';
|
|
10
10
|
/**
|
|
11
11
|
* Create the grouped runtime descriptions.
|
|
12
12
|
*/
|
|
13
|
-
export function
|
|
13
|
+
export function getGroupedCodeSandboxDescs(multiServiceManager, kernelId, translator, filterKernels = () => true, variant) {
|
|
14
14
|
translator = translator ?? nullTranslator;
|
|
15
15
|
const trans = translator.load('jupyterlab');
|
|
16
16
|
// The specifications land asynchronously, and a runtime that already runs
|
|
@@ -29,7 +29,7 @@ export function getGroupedRuntimeDescs(multiServiceManager, kernelId, translator
|
|
|
29
29
|
kernelId: session.kernel.id,
|
|
30
30
|
name: spec?.name ?? session.kernel.name,
|
|
31
31
|
language: spec?.language ?? '',
|
|
32
|
-
displayName: session.
|
|
32
|
+
displayName: ensureCodeSandboxGivenName(session.kernel.id, spec?.display_name ?? session.kernel.name),
|
|
33
33
|
location: 'local',
|
|
34
34
|
};
|
|
35
35
|
})
|
|
@@ -43,7 +43,7 @@ export function getGroupedRuntimeDescs(multiServiceManager, kernelId, translator
|
|
|
43
43
|
kernelId: session.kernel.id,
|
|
44
44
|
name: spec?.name ?? session.kernel.name,
|
|
45
45
|
language: spec?.language ?? '',
|
|
46
|
-
displayName: session.
|
|
46
|
+
displayName: ensureCodeSandboxGivenName(session.kernel.id, spec?.display_name ?? session.kernel.name),
|
|
47
47
|
location: 'browser',
|
|
48
48
|
};
|
|
49
49
|
}))
|
|
@@ -58,7 +58,7 @@ export function getGroupedRuntimeDescs(multiServiceManager, kernelId, translator
|
|
|
58
58
|
kernelId: k.id,
|
|
59
59
|
name: spec?.name ?? k.name,
|
|
60
60
|
language: spec?.language ?? '',
|
|
61
|
-
displayName: spec?.display_name ?? k.name,
|
|
61
|
+
displayName: ensureCodeSandboxGivenName(k.id, spec?.display_name ?? k.name),
|
|
62
62
|
location: 'local',
|
|
63
63
|
};
|
|
64
64
|
})
|
|
@@ -72,7 +72,12 @@ export function getGroupedRuntimeDescs(multiServiceManager, kernelId, translator
|
|
|
72
72
|
kernelId: runtime.id,
|
|
73
73
|
name: environment.name,
|
|
74
74
|
language: environment.language,
|
|
75
|
-
displayName:
|
|
75
|
+
displayName:
|
|
76
|
+
// The name it was GIVEN first: the title of the environment is
|
|
77
|
+
// what it runs, and every sandbox of that environment answers
|
|
78
|
+
// it — the launcher asks for a name precisely so two of them
|
|
79
|
+
// can be told apart here.
|
|
80
|
+
runtime.given_name || environment.title || environment.name,
|
|
76
81
|
location: 'remote',
|
|
77
82
|
podName: runtime.pod_name,
|
|
78
83
|
gpu: environment.resources?.['nvidia.com/gpu'],
|
|
@@ -86,7 +91,7 @@ export function getGroupedRuntimeDescs(multiServiceManager, kernelId, translator
|
|
|
86
91
|
kernelId: k.id,
|
|
87
92
|
name: spec?.name ?? k.name,
|
|
88
93
|
language: spec?.language ?? '',
|
|
89
|
-
displayName: spec?.display_name ?? k.name,
|
|
94
|
+
displayName: ensureCodeSandboxGivenName(k.id, spec?.display_name ?? k.name),
|
|
90
95
|
location: 'browser',
|
|
91
96
|
};
|
|
92
97
|
}))
|