@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.
Files changed (55) hide show
  1. package/lib/components/agents/index.d.ts +6 -0
  2. package/lib/components/{students → agents}/index.js +6 -1
  3. package/lib/components/{NotificationEventCard.js → cards/NotificationEventCard.js} +1 -1
  4. package/lib/components/{OutputCard.js → cards/OutputCard.js} +1 -1
  5. package/lib/components/cards/index.d.ts +9 -0
  6. package/lib/components/cards/index.js +13 -0
  7. package/lib/components/{runtimes/RuntimeCellVariables.d.ts → code-sandboxes/CodeSandboxCellVariables.d.ts} +3 -3
  8. package/lib/components/{runtimes/RuntimeCellVariables.js → code-sandboxes/CodeSandboxCellVariables.js} +2 -2
  9. package/lib/components/{runtimes/RuntimeCellVariablesDialog.d.ts → code-sandboxes/CodeSandboxCellVariablesDialog.d.ts} +4 -4
  10. package/lib/components/{runtimes/RuntimeCellVariablesDialog.js → code-sandboxes/CodeSandboxCellVariablesDialog.js} +4 -4
  11. package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +0 -2
  12. package/lib/components/code-sandboxes/CodeSandboxLauncher.js +98 -11
  13. package/lib/components/code-sandboxes/CodeSandboxNames.d.ts +47 -0
  14. package/lib/components/code-sandboxes/CodeSandboxNames.js +166 -0
  15. package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +30 -5
  16. package/lib/components/code-sandboxes/CodeSandboxPicker.js +239 -55
  17. package/lib/components/{runtimes/RuntimePickerCell.d.ts → code-sandboxes/CodeSandboxPickerCell.d.ts} +5 -5
  18. package/lib/components/{runtimes/RuntimePickerCell.js → code-sandboxes/CodeSandboxPickerCell.js} +6 -6
  19. package/lib/components/{runtimes/RuntimeReservationControl.d.ts → code-sandboxes/CodeSandboxReservationControl.d.ts} +3 -3
  20. package/lib/components/{runtimes/RuntimeReservationControl.js → code-sandboxes/CodeSandboxReservationControl.js} +5 -5
  21. package/lib/components/{runtimes/RuntimeTransfer.d.ts → code-sandboxes/CodeSandboxTransfer.d.ts} +7 -1
  22. package/lib/components/{runtimes/RuntimeUtils.d.ts → code-sandboxes/CodeSandboxUtils.d.ts} +3 -3
  23. package/lib/components/{runtimes/RuntimeUtils.js → code-sandboxes/CodeSandboxUtils.js} +15 -10
  24. package/lib/components/code-sandboxes/CodeSandboxVariables.d.ts +71 -0
  25. package/lib/components/{runtimes/RuntimeVariables.js → code-sandboxes/CodeSandboxVariables.js} +77 -2
  26. package/lib/components/code-sandboxes/NewCodeSandboxControls.d.ts +17 -0
  27. package/lib/components/code-sandboxes/NewCodeSandboxControls.js +5 -5
  28. package/lib/components/code-sandboxes/index.d.ts +20 -1
  29. package/lib/components/code-sandboxes/index.js +20 -1
  30. package/lib/components/index.d.ts +4 -6
  31. package/lib/components/index.js +4 -6
  32. package/lib/components/progress/CreditsIndicator.js +1 -1
  33. package/package.json +2 -2
  34. package/lib/components/runtimes/RuntimeLauncherDialog.d.ts +0 -7
  35. package/lib/components/runtimes/RuntimeLauncherDialog.js +0 -11
  36. package/lib/components/runtimes/RuntimePickerBase.d.ts +0 -8
  37. package/lib/components/runtimes/RuntimePickerBase.js +0 -12
  38. package/lib/components/runtimes/RuntimePickerNotebook.d.ts +0 -42
  39. package/lib/components/runtimes/RuntimePickerNotebook.js +0 -197
  40. package/lib/components/runtimes/RuntimeSimplePicker.d.ts +0 -38
  41. package/lib/components/runtimes/RuntimeSimplePicker.js +0 -156
  42. package/lib/components/runtimes/RuntimeVariables.d.ts +0 -38
  43. package/lib/components/runtimes/index.d.ts +0 -12
  44. package/lib/components/runtimes/index.js +0 -17
  45. package/lib/components/students/StudentItemStatus.d.ts +0 -7
  46. package/lib/components/students/StudentItemStatus.js +0 -68
  47. package/lib/components/students/index.d.ts +0 -1
  48. package/scripts/codegen/__pycache__/versioning.cpython-313.pyc +0 -0
  49. /package/lib/components/{AgentSummary.d.ts → agents/AgentSummary.d.ts} +0 -0
  50. /package/lib/components/{AgentSummary.js → agents/AgentSummary.js} +0 -0
  51. /package/lib/components/{NotificationEventCard.d.ts → cards/NotificationEventCard.d.ts} +0 -0
  52. /package/lib/components/{OutputCard.d.ts → cards/OutputCard.d.ts} +0 -0
  53. /package/lib/components/{ToolApprovalCard.d.ts → cards/ToolApprovalCard.d.ts} +0 -0
  54. /package/lib/components/{ToolApprovalCard.js → cards/ToolApprovalCard.js} +0 -0
  55. /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 { getGroupedRuntimeDescs, } from '../runtimes/RuntimeUtils';
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, runtimeDesc, sessionContext, setRuntimeDesc, translator, variant, } = props;
26
- const [groupedRuntimeDescs, setGroupedRuntimeDescs] = useState(getGroupedRuntimeDescs(multiServiceManager, preference?.id, translator, filterRuntime, variant));
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(getGroupedRuntimeDescs(multiServiceManager, preference?.id, translator, filterRuntime, variant));
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 && !remoteModelsReady) {
100
- return;
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 (_jsx(_Fragment, { children: display === 'menu' ? (
116
- /*
117
- * Section for Menu display.
118
- */
119
- _jsxs(ActionMenu, { children: [variant === 'cell' ? (_jsx(ActionMenu.Anchor, { children: _jsx(IconButton, { disabled: disabled || groupedRuntimeDescs === null,
120
- // icon={() => <kernelIcon.react className="dla-Cell-runtime-icon" tag={'span'} />}
121
- 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: () => {
122
- setRuntimeDesc(undefined);
123
- }, 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 => {
124
- const annotation = candidateRuntimeDesc.podName
125
- ? ` - ${candidateRuntimeDesc.podName.split('-', 2).reverse()[0]}`
126
- : candidateRuntimeDesc.kernelId
127
- ? ` - ${candidateRuntimeDesc.kernelId}`
128
- : '';
129
- const fullDisplayName = (candidateRuntimeDesc.displayName ?? '') + annotation;
130
- const displayName = (candidateRuntimeDesc.displayName?.length ?? 0) >
131
- RUNTIME_DISPLAY_NAME_MAX_LENGTH
132
- ? candidateRuntimeDesc.displayName.slice(0, RUNTIME_DISPLAY_NAME_MAX_LENGTH) + '…'
133
- : (candidateRuntimeDesc.displayName ?? '');
134
- return (_jsxs(ActionList.Item, { title: fullDisplayName, selected: (candidateRuntimeDesc.location ===
135
- runtimeDesc?.location ||
136
- (isRuntimeRemote(candidateRuntimeDesc.location) &&
137
- isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
138
- (candidateRuntimeDesc.kernelId ??
139
- candidateRuntimeDesc.name) ===
140
- (runtimeDesc?.kernelId ?? runtimeDesc?.name), onSelect: () => {
141
- setRuntimeDesc(candidateRuntimeDesc);
142
- }, children: [_jsx(ActionList.LeadingVisual, { children: candidateRuntimeDesc.location === 'local' ? (_jsx(LaptopSimpleIcon, {})) : candidateRuntimeDesc.location === 'browser' ? (_jsx(BrowserIcon, {})) : (_jsx(CloudUploadIcon, {})) }), displayName + annotation.slice(0, 10)] }, candidateRuntimeDesc.name));
143
- })] }, group))), !!postActions && (_jsxs(_Fragment, { children: [_jsx(ActionList.Divider, {}), postActions] }))] }) })] })) : (
144
- /*
145
- * Section for Radio display.
146
- */
147
- _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 => {
148
- return (
149
- // A kernel identifies a runtime that already runs; an
150
- // environment to start one in has none, and is named
151
- // by where it runs and what it runs.
152
- _jsxs(Box, { title: k.name, children: [_jsxs(FormControl, { children: [_jsx(Radio, { value: k.kernelId, onChange: () => {
153
- setRuntimeDesc(k);
154
- }, checked: (k.location === runtimeDesc?.location ||
155
- (isRuntimeRemote(k.location) &&
156
- isRuntimeRemote(runtimeDesc?.location ?? 'local'))) &&
157
- (k.kernelId ?? k.name) ===
158
- (runtimeDesc?.kernelId ?? runtimeDesc?.name) }), _jsx(FormControl.Label, { children: _jsxs(Box, { display: "flex", children: [_jsx(Box, { children: k.displayName }), 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}`));
159
- })] }, group))) })), !!postActions && _jsx(_Fragment, { children: postActions })] })) }));
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;
@@ -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 '../code-sandboxes/CodeSandboxPicker';
4
+ import { type ICodeSandboxPickerProps } from './CodeSandboxPicker';
5
5
  /**
6
- * {@link RuntimePickerCell} properties
6
+ * {@link CodeSandboxPickerCell} properties
7
7
  */
8
- export type IRuntimePickerCellProps = Pick<ICodeSandboxPickerProps, 'multiServiceManager' | 'preference' | 'translator'> & {
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 RuntimePickerCell(props: IRuntimePickerCellProps): JSX.Element;
34
- export default RuntimePickerCell;
33
+ export declare function CodeSandboxPickerCell(props: ICodeSandboxPickerCellProps): JSX.Element;
34
+ export default CodeSandboxPickerCell;
@@ -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 '../code-sandboxes/CodeSandboxPicker';
16
- import { CodeSandboxLauncher } from '../code-sandboxes/CodeSandboxLauncher';
17
- import { RuntimeCellVariablesDialog } from './RuntimeCellVariablesDialog';
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 RuntimePickerCell(props) {
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(RuntimeCellVariablesDialog, { 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 }))] }));
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 RuntimePickerCell;
104
+ export default CodeSandboxPickerCell;
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * TODO this should be configurable
5
5
  */
6
- export declare const MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES: number;
6
+ export declare const MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES: number;
7
7
  /**
8
8
  * Time control properties
9
9
  */
10
- export interface IKernelReservationControlProps {
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 RuntimeReservationControl(props: IKernelReservationControlProps): JSX.Element;
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 MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES = 24 * 60;
15
+ export const MAXIMAL_CODE_SANDBOX_TIME_RESERVATION_MINUTES = 24 * 60;
16
16
  /**
17
17
  * Runtime reservation control
18
18
  */
19
- export function RuntimeReservationControl(props) {
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
- : MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES;
28
- const max = Math.max(1, Math.min(requestedMax, MAXIMAL_RUNTIME_TIME_RESERVATION_MINUTES));
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 ", (MAXIMAL_RUNTIME_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, {}));
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
  }
@@ -1,10 +1,16 @@
1
1
  import { type IRuntimeOptions } from '../../runtimes';
2
- export interface RuntimeTransfer {
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 IDatalayerRuntimeDesc = IRuntimeDesc & {
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 getGroupedRuntimeDescs(multiServiceManager: IMultiServiceManager, kernelId?: string, translator?: ITranslator, filterKernels?: (desc: IDatalayerRuntimeDesc) => boolean, variant?: 'document' | 'cell'): {
12
- [g: string]: IDatalayerRuntimeDesc[];
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 Runtime';
8
- const ASSIGN_EXISTING_REMOTE_RUNTIME_LABEL = 'Assign an existing Remote Runtime';
9
- const ASSIGN_EXISTING_RUNTIME_LABEL = 'Assign an existing Runtime';
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 getGroupedRuntimeDescs(multiServiceManager, kernelId, translator, filterKernels = () => true, variant) {
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.name || PathExt.basename(session.path),
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.name || PathExt.basename(session.path),
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: environment.title || runtime.given_name || environment.name,
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
  }))