@datalayer/agent-runtimes 1.0.26 → 1.0.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 (34) hide show
  1. package/lib/api/index.js +16 -4
  2. package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.d.ts +41 -0
  3. package/lib/components/code-sandboxes/CodeSandboxEnvironmentSelect.js +83 -0
  4. package/lib/components/code-sandboxes/CodeSandboxLauncher.d.ts +11 -0
  5. package/lib/components/code-sandboxes/CodeSandboxLauncher.js +190 -17
  6. package/lib/components/code-sandboxes/CodeSandboxNames.d.ts +47 -0
  7. package/lib/components/code-sandboxes/CodeSandboxNames.js +211 -0
  8. package/lib/components/code-sandboxes/CodeSandboxPicker.d.ts +14 -1
  9. package/lib/components/code-sandboxes/CodeSandboxPicker.js +203 -18
  10. package/lib/components/code-sandboxes/CodeSandboxTransfer.d.ts +6 -0
  11. package/lib/components/code-sandboxes/CodeSandboxUtils.d.ts +3 -0
  12. package/lib/components/code-sandboxes/CodeSandboxUtils.js +76 -20
  13. package/lib/components/code-sandboxes/NewCodeSandboxControls.d.ts +17 -0
  14. package/lib/components/code-sandboxes/NewCodeSandboxControls.js +4 -4
  15. package/lib/components/code-sandboxes/codeSandboxProviders.d.ts +29 -0
  16. package/lib/components/code-sandboxes/codeSandboxProviders.js +81 -0
  17. package/lib/components/code-sandboxes/index.d.ts +3 -0
  18. package/lib/components/code-sandboxes/index.js +3 -0
  19. package/lib/examples/AgentspecsExample.js +3 -3
  20. package/lib/identity/index.d.ts +1 -1
  21. package/lib/identity/index.js +1 -1
  22. package/lib/identity/types.d.ts +1 -1
  23. package/lib/identity/types.js +1 -1
  24. package/lib/models/CodeSandboxVariant.d.ts +45 -0
  25. package/lib/models/CodeSandboxVariant.js +65 -0
  26. package/lib/models/index.d.ts +1 -0
  27. package/lib/models/index.js +1 -0
  28. package/lib/runtimes/actions.js +14 -2
  29. package/lib/specs/envvars.d.ts +1 -1
  30. package/lib/specs/envvars.js +3 -3
  31. package/lib/specs/mcpServers.js +2 -2
  32. package/lib/state/substates/RuntimesState.js +4 -1
  33. package/package.json +1 -1
  34. package/scripts/gen_workers.py +1 -1
package/lib/api/index.js CHANGED
@@ -22,9 +22,21 @@ export * as notifications from './notifications';
22
22
  export * as output from './output';
23
23
  export * from '../runtimes';
24
24
  import { iamStore } from '@datalayer/core/lib/state';
25
+ import { runtimesStore } from '../state/substates';
25
26
  import { listEnvironments } from './runtimes/environments';
26
27
  import { listSnapshots, createSnapshot, deleteSnapshot, } from './runtimes/snapshots';
27
28
  import { asCodeSandboxSnapshot, } from '../models/CodeSandboxSnapshot';
29
+ /*
30
+ * The runtimes URL of the PAGE's configuration, not the built-in default.
31
+ *
32
+ * Every function below takes an optional `baseUrl`, and the low-level API
33
+ * layer defaults it to the production URL. Callers all over the application
34
+ * pass nothing — so against a local plane they silently queried production,
35
+ * and the picker offered the environments of the wrong deployment.
36
+ */
37
+ function resolveRuntimesUrl(baseUrl) {
38
+ return baseUrl ?? runtimesStore.getState().runtimesUrl ?? undefined;
39
+ }
28
40
  function resolveToken(token) {
29
41
  const resolved = token ?? iamStore.getState()?.token;
30
42
  if (!resolved) {
@@ -33,14 +45,14 @@ function resolveToken(token) {
33
45
  return resolved;
34
46
  }
35
47
  export async function getEnvironments(token, baseUrl) {
36
- const response = await listEnvironments(resolveToken(token), baseUrl);
48
+ const response = await listEnvironments(resolveToken(token), resolveRuntimesUrl(baseUrl));
37
49
  return (response.environments ?? []).map(environment => ({
38
50
  ...environment,
39
51
  burningRate: environment.burningRate ?? environment.burning_rate,
40
52
  }));
41
53
  }
42
54
  export async function getSandboxSnapshots(token, baseUrl) {
43
- const response = await listSnapshots(resolveToken(token), baseUrl);
55
+ const response = await listSnapshots(resolveToken(token), resolveRuntimesUrl(baseUrl));
44
56
  return (response.snapshots ?? []).map(snapshot => asCodeSandboxSnapshot(snapshot));
45
57
  }
46
58
  export async function createSandboxSnapshot(data, token, baseUrl) {
@@ -49,11 +61,11 @@ export async function createSandboxSnapshot(data, token, baseUrl) {
49
61
  if (!('pod_name' in data)) {
50
62
  return data;
51
63
  }
52
- const response = await createSnapshot(resolveToken(token), data, baseUrl);
64
+ const response = await createSnapshot(resolveToken(token), data, resolveRuntimesUrl(baseUrl));
53
65
  return response.snapshot;
54
66
  }
55
67
  export async function deleteCodeSandboxSnapshot(snapshotId, token, baseUrl) {
56
- return deleteSnapshot(resolveToken(token), snapshotId, baseUrl);
68
+ return deleteSnapshot(resolveToken(token), snapshotId, resolveRuntimesUrl(baseUrl));
57
69
  }
58
70
  export async function exportCodeSandboxSnapshot(..._args) {
59
71
  throw new Error('exportCodeSandboxSnapshot is not implemented in @datalayer/agent-runtimes/api yet.');
@@ -0,0 +1,41 @@
1
+ /** One choice of the dropdown. */
2
+ export type ICodeSandboxEnvironmentOption = {
3
+ /** Identity of the choice, as the host knows it. */
4
+ key: string;
5
+ /** What it is called. */
6
+ title: string;
7
+ /** The environment name, shown as a label beside the title. */
8
+ name?: string;
9
+ /** Who runs it — Datalayer, Kaggle, Modal, the Jupyter Server… */
10
+ providerTitle?: string;
11
+ /** Where it runs: `local`, `remote`, `browser`. */
12
+ location?: string;
13
+ /** Credits per second, when it costs any. */
14
+ burningRate?: number;
15
+ /** The GPU the environment carries, when it carries one. */
16
+ gpu?: string | boolean;
17
+ /** The heading this choice is listed under, when the list is grouped. */
18
+ group?: string;
19
+ };
20
+ export type ICodeSandboxEnvironmentSelectProps = {
21
+ /** The choices, in the order they should read. */
22
+ options: ICodeSandboxEnvironmentOption[];
23
+ /** The `key` of the chosen one, if any. */
24
+ selectedKey?: string;
25
+ /** Called with the `key` of the choice made. */
26
+ onSelect: (key: string) => void;
27
+ /** Whether the control refuses to open. */
28
+ disabled?: boolean;
29
+ /** What the trigger reads when nothing is chosen. */
30
+ placeholder?: string;
31
+ /**
32
+ * Width of the overlay, `xlarge` by default.
33
+ *
34
+ * Wide because the entries carry their labels on the same line as their
35
+ * name, and a priced one — the credits it burns, the environment, the
36
+ * provider, where it runs — is nearly as wide as the row itself.
37
+ */
38
+ overlayWidth?: 'small' | 'medium' | 'large' | 'xlarge' | 'auto';
39
+ };
40
+ export declare function CodeSandboxEnvironmentSelect(props: ICodeSandboxEnvironmentSelectProps): JSX.Element;
41
+ export default CodeSandboxEnvironmentSelect;
@@ -0,0 +1,83 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ /*
3
+ * Copyright (c) 2025-2026 Datalayer, Inc.
4
+ * Distributed under the terms of the Modified BSD License.
5
+ */
6
+ /**
7
+ * The one dropdown for choosing where code runs.
8
+ *
9
+ * The launcher asks it of a new sandbox and the picker asks it of an editor
10
+ * that needs one; both are the same question, and were drawn twice — a menu
11
+ * here, a column of radio buttons there. This is that control, once: the
12
+ * chosen entry reads on the trigger exactly as the entries read in the list,
13
+ * name to the left and labels to the right, so the closed control and the
14
+ * open one show the same thing.
15
+ *
16
+ * An `ActionMenu` rather than a `<select>`: what tells two entries apart is
17
+ * their labels — the environment name, the provider, the credits it burns —
18
+ * and no `<option>` can carry those.
19
+ *
20
+ * @module components/code-sandboxes/CodeSandboxEnvironmentSelect
21
+ */
22
+ import { ActionList, ActionMenu, Box, Label, Text } from '@primer/react';
23
+ /**
24
+ * The labels of an option, on one line: cost, name, provider, where.
25
+ *
26
+ * The price is the widest of them — "0.0008 credits/second" outruns the
27
+ * name, the provider and the location together — so the room comes from the
28
+ * overlay rather than from a second line: see `overlayWidth`.
29
+ */
30
+ function OptionLabels(props) {
31
+ const { option } = props;
32
+ return (_jsxs(Box, { as: "span", sx: {
33
+ display: 'inline-flex',
34
+ alignItems: 'center',
35
+ gap: 1,
36
+ whiteSpace: 'nowrap',
37
+ }, children: [option.burningRate ? (_jsxs(Label, { size: "small", variant: "sponsors", children: [option.burningRate, " credits/second"] })) : null, option.gpu ? (_jsx(Label, { size: "small", variant: "success", children: "GPU" })) : null, option.name ? _jsx(Label, { size: "small", children: option.name }) : null, option.providerTitle ? (_jsx(Label, { size: "small", variant: "accent", children: option.providerTitle })) : null, option.location ? (_jsx(Label, { size: "small", variant: "secondary", children: option.location })) : null] }));
38
+ }
39
+ export function CodeSandboxEnvironmentSelect(props) {
40
+ const { disabled, onSelect, options, overlayWidth = 'xlarge', placeholder = 'Select an environment', selectedKey, } = props;
41
+ const selected = options.find(option => option.key === selectedKey);
42
+ /*
43
+ * What costs credits leads.
44
+ *
45
+ * The sandboxes of the platform and of its providers are the ones worth
46
+ * choosing between — a kernel of this server is the fallback, always
47
+ * there and free — so they are read first rather than found under a list
48
+ * of local specifications. A stable partition: within each half the order
49
+ * given by the caller is kept.
50
+ */
51
+ const ordered = [
52
+ ...options.filter(option => option.burningRate),
53
+ ...options.filter(option => !option.burningRate),
54
+ ];
55
+ const grouped = ordered.some(option => option.group);
56
+ const groups = grouped
57
+ ? Array.from(new Set(ordered.map(option => option.group ?? '')))
58
+ : [];
59
+ const rows = (subset) => subset.map(option => (_jsxs(ActionList.Item, { selected: option.key === selectedKey, onSelect: () => onSelect(option.key), children: [option.title, _jsx(ActionList.TrailingVisual, { children: _jsx(OptionLabels, { option: option }) })] }, option.key)));
60
+ return (_jsxs(ActionMenu, { children: [_jsx(ActionMenu.Button, { block: true, disabled: disabled || ordered.length === 0,
61
+ // The button centers its text by default; the trigger must read like
62
+ // the rows below it — name left, labels right.
63
+ sx: {
64
+ // The content of a Primer button is a grid whose middle track is
65
+ // content-sized, which hugs the labels against the title. The
66
+ // template areas stay "leadingVisual text trailingVisual": only
67
+ // that track changes, from content-sized to 1fr.
68
+ '& [data-component="buttonContent"]': {
69
+ flex: 1,
70
+ gridTemplateColumns: 'min-content minmax(0, 1fr) min-content',
71
+ },
72
+ '& [data-component="text"]': { width: '100%', textAlign: 'left' },
73
+ }, children: selected ? (_jsxs(Box, { as: "span", sx: {
74
+ display: 'flex',
75
+ width: '100%',
76
+ alignItems: 'center',
77
+ justifyContent: 'space-between',
78
+ gap: 2,
79
+ }, children: [_jsx(Text, { children: selected.title }), _jsx(OptionLabels, { option: selected })] })) : (placeholder) }), _jsx(ActionMenu.Overlay, { width: overlayWidth, children: _jsx(ActionList, { selectionVariant: "single", children: grouped
80
+ ? groups.map(group => (_jsxs(ActionList.Group, { children: [group ? (_jsx(ActionList.GroupHeading, { children: group })) : null, rows(ordered.filter(option => (option.group ?? '') === group))] }, group || 'ungrouped')))
81
+ : rows(ordered) }) })] }));
82
+ }
83
+ export default CodeSandboxEnvironmentSelect;
@@ -46,6 +46,17 @@ export interface ICodeSandboxLauncherProps {
46
46
  * Upgrade subscription URL
47
47
  */
48
48
  upgradeSubscription?: string;
49
+ /**
50
+ * Where "Add credits" navigates.
51
+ *
52
+ * ABSOLUTE, always: a relative route resolves against the CURRENT
53
+ * location — opened from `/…/code-sandboxes`, `usage` landed on
54
+ * `code-sandboxes/usage` and a `:sandboxId` route swallowed it as a
55
+ * sandbox called "usage". The default is the web application's `/plans`
56
+ * (which redirects to the plan overview); a nested surface passes its
57
+ * own (e.g. `/datalayer/jupyter-kernels/usage/plans`).
58
+ */
59
+ usageRoute?: string;
49
60
  /**
50
61
  * Optional submit button label override.
51
62
  */
@@ -1,18 +1,22 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /*
3
3
  * Copyright (c) 2025-2026 Datalayer, Inc.
4
4
  * Distributed under the terms of the Modified BSD License.
5
5
  */
6
6
  import { useCallback, useEffect, useMemo, useState } from 'react';
7
7
  import { useIsMounted } from 'usehooks-ts';
8
- import { Button, FormControl, Select, Spinner, Text, TextInput, ToggleSwitch, } from '@primer/react';
8
+ import { Button, FormControl, Spinner, Text, TextInput, ToggleSwitch, } from '@primer/react';
9
9
  import { Dialog } from '@primer/react/experimental';
10
10
  import { Box } from '@datalayer/primer-addons';
11
11
  import { useJupyterReactStore } from '@datalayer/jupyter-react';
12
- import { USAGE_ROUTE } from '@datalayer/core/lib/routes';
13
12
  import { useNavigate } from '@datalayer/core/lib/hooks';
14
13
  import { NO_RUNTIME_AVAILABLE_LABEL } from '@datalayer/core/lib/i18n';
15
- import { iamStore, useCoreStore, useIAMStore } from '../../state/substates';
14
+ import { isCodeSandboxProviderAvailable } from './codeSandboxProviders';
15
+ import { CodeSandboxVariant, codeSandboxVariantOf, codeSandboxVariantTitle, } from '../../models/CodeSandboxVariant';
16
+ import { CodeSandboxEnvironmentSelect, } from './CodeSandboxEnvironmentSelect';
17
+ import { getCodeSandboxGivenName, listCodeSandboxGivenNames, nextCodeSandboxGivenName, setCodeSandboxGivenName, } from './CodeSandboxNames';
18
+ import { iamStore, runtimesStore, useCoreStore, useIAMStore, } from '../../state/substates';
19
+ import { createRuntime as createRuntimeRecord } from '../../runtimes/actions';
16
20
  import { createNotebook, sleep } from '@datalayer/core/lib/utils';
17
21
  import { Markdown } from '@datalayer/core/lib/components/display';
18
22
  import { Timer } from '@datalayer/core/lib/components/progress';
@@ -41,11 +45,76 @@ const NOT_AVAILABLE_RETRIES = 5;
41
45
  * The launcher of a new code sandbox.
42
46
  */
43
47
  export function CodeSandboxLauncher(props) {
44
- const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, submitLabel, startRuntime = true, } = props;
48
+ const { dialogTitle, kernelSnapshot, manager, onSubmit, markdownParser, sanitizer, upgradeSubscription, usageRoute = '/plans', submitLabel, startRuntime = true, } = props;
45
49
  const hasExample = startRuntime === 'with-example';
46
50
  const shouldStartRuntime = startRuntime !== 'defer';
47
51
  const user = iamStore.getState().user;
52
+ /*
53
+ * Where a sandbox may be launched: every provider that can be reached.
54
+ *
55
+ * The environments of the platform are offered wherever the account can
56
+ * reach it — withholding them inside JupyterLab left `ai-agents-env` off
57
+ * the list and no way to launch a remote sandbox from there. The Jupyter
58
+ * Server is the provider that depends on where this runs, and it is gated
59
+ * below.
60
+ */
61
+ const jupyterAvailable = isCodeSandboxProviderAvailable('local', runtimesStore.getState().multiServiceManager);
48
62
  const environments = manager.environments.get();
63
+ /*
64
+ * The sandboxes of this Jupyter Server, offered beside those of the platform.
65
+ *
66
+ * Inside JupyterLab a sandbox can be a kernel of the server, and the picker
67
+ * has always offered both — the launcher offered only the environments of
68
+ * the platform, so "Launch a Code Sandbox" could not launch the very kind
69
+ * that costs nothing and starts at once. The value of a local choice is
70
+ * prefixed, since a kernelspec and an environment are free to share a name.
71
+ */
72
+ const localSpecs = useMemo(() => {
73
+ /*
74
+ * ...and only inside JupyterLab.
75
+ *
76
+ * The rule cuts both ways: the web application talks to no Jupyter Server
77
+ * of its own, so a kernelspec of one is not something it can start — the
78
+ * entries read as choices that lead nowhere.
79
+ */
80
+ if (!jupyterAvailable) {
81
+ return [];
82
+ }
83
+ const specs = runtimesStore.getState().multiServiceManager?.local?.kernelspecs.specs
84
+ ?.kernelspecs ?? {};
85
+ return Object.values(specs)
86
+ .filter(Boolean)
87
+ .map(spec => ({
88
+ name: spec.name,
89
+ title: spec.display_name,
90
+ language: spec.language,
91
+ }));
92
+ }, [jupyterAvailable]);
93
+ const LOCAL_PREFIX = 'local:';
94
+ /*
95
+ * The choices of the dropdown: what the platform and the external
96
+ * providers offer, then the kernelspecs of this Jupyter Server. Each says
97
+ * whose machine it is — two entries called "GPU" are told apart by nothing
98
+ * else.
99
+ */
100
+ const environmentOptions = useMemo(() => [
101
+ ...environments.map(spec => ({
102
+ key: spec.name,
103
+ title: spec.title || spec.name,
104
+ name: spec.name,
105
+ providerTitle: codeSandboxVariantTitle(codeSandboxVariantOf(spec?.owner)),
106
+ burningRate: spec.burning_rate,
107
+ })),
108
+ ...localSpecs.map(spec => ({
109
+ key: `${LOCAL_PREFIX}${spec.name}`,
110
+ title: spec.title || spec.name,
111
+ name: spec.name,
112
+ providerTitle: codeSandboxVariantTitle(CodeSandboxVariant.JupyterServer),
113
+ })),
114
+ ], [environments, localSpecs]);
115
+ const localSpecOf = (value) => value.startsWith(LOCAL_PREFIX)
116
+ ? localSpecs.find(spec => spec.name === value.slice(LOCAL_PREFIX.length))
117
+ : undefined;
49
118
  const { configuration } = useCoreStore();
50
119
  const { refreshCredits } = useIAMStore();
51
120
  let navigate;
@@ -60,8 +129,37 @@ export function CodeSandboxLauncher(props) {
60
129
  const jupyterReactStore = useJupyterReactStore();
61
130
  const jupyterLabAdapter = jupyterReactStore.jupyterLabAdapter;
62
131
  const [selection, setSelection] = useState((kernelSnapshot?.environment || environments[0]?.name) ?? '');
132
+ /*
133
+ * Something is always selected.
134
+ *
135
+ * With the environments of the platform withheld, the first choice of the
136
+ * list is a sandbox of this server — and nothing was selected at all, so the
137
+ * launcher opened on an empty dropdown that could not be submitted.
138
+ */
139
+ useEffect(() => {
140
+ if (!selection && localSpecs.length) {
141
+ setSelection(`${LOCAL_PREFIX}${localSpecs[0].name}`);
142
+ }
143
+ }, [localSpecs, selection]);
63
144
  const [timeLimit, setTimeLimit] = useState(10);
64
- const [runtimeName, setRuntimeName] = useState(environments[0]?.runtime?.givenNameTemplate || environments[0]?.title || '');
145
+ /*
146
+ * The name the sandbox is given, and what is offered for it.
147
+ *
148
+ * The environment says what to call one of its sandboxes; the sandboxes that
149
+ * already run say which of those names are taken — a second sandbox of an
150
+ * environment is offered that name numbered, exactly as an unnamed one is
151
+ * given one. Both go through `nextCodeSandboxGivenName`, so what is proposed
152
+ * here and what is assigned elsewhere never drift apart.
153
+ */
154
+ const takenGivenNames = useCallback(() => [
155
+ ...manager.runtimesManager
156
+ .get()
157
+ .map(runtime => runtime.given_name)
158
+ .filter(Boolean),
159
+ ...listCodeSandboxGivenNames(),
160
+ ], [manager]);
161
+ const givenNameFor = useCallback((spec) => nextCodeSandboxGivenName(spec?.runtime?.givenNameTemplate || spec?.title || '', takenGivenNames()), [takenGivenNames]);
162
+ const [runtimeName, setRuntimeName] = useState(() => givenNameFor(environments[0]));
65
163
  // Whether the runtim name has been changed by the user or not
66
164
  const [hasCustomRuntimeName, setHasCustomRuntimeName] = useState(false);
67
165
  const [userStorage, setUserStorage] = useState(false);
@@ -75,6 +173,7 @@ export function CodeSandboxLauncher(props) {
75
173
  refreshCredits();
76
174
  }
77
175
  }, [shouldStartRuntime]);
176
+ const isLocalSelection = selection.startsWith(LOCAL_PREFIX);
78
177
  const spec = useMemo(() => environments.find(spec => spec.name === selection), [environments, selection]);
79
178
  const description = spec?.description ?? '';
80
179
  const burningRate = spec?.burning_rate ?? 1;
@@ -82,15 +181,52 @@ export function CodeSandboxLauncher(props) {
82
181
  const allowance = useNewCodeSandboxAllowance(burningRate);
83
182
  const effectiveMaxMinutes = allowance.effectiveMaxMinutes;
84
183
  const outOfCredits = shouldStartRuntime && allowance.outOfCredits;
85
- const handleSelectionChange = useCallback((e) => {
86
- const selection = e.target.value;
87
- setSelection(selection);
184
+ const handleSelectionChange = useCallback((value) => {
185
+ setSelection(value);
88
186
  if (!hasCustomRuntimeName) {
89
- const spec = environments.find(env => env.name === selection);
90
- setRuntimeName(spec?.runtime?.givenNameTemplate || spec?.title || '');
187
+ const local = localSpecOf(value);
188
+ setRuntimeName(givenNameFor(local ?? environments.find(env => env.name === value)));
91
189
  }
92
- }, [setSelection, hasCustomRuntimeName]);
190
+ }, [environments, givenNameFor, setSelection, hasCustomRuntimeName]);
93
191
  const handleSubmitRuntime = useCallback(async () => {
192
+ const localSpec = localSpecOf(selection);
193
+ if (localSpec) {
194
+ /*
195
+ * A sandbox of this Jupyter Server: started here, named here.
196
+ *
197
+ * Nothing to reserve, no credits to weigh, no pod to wait for — the
198
+ * kernel is started and the choice is handed back as any other, with
199
+ * the name the user gave it, which is the local answer to `given_name`.
200
+ */
201
+ setError(undefined);
202
+ const kernels = runtimesStore.getState().multiServiceManager?.local?.kernels;
203
+ if (!kernels) {
204
+ setError(_jsx(_Fragment, { children: "The Jupyter Server of this page is not reachable." }));
205
+ return;
206
+ }
207
+ setWaitingForRuntime(true);
208
+ try {
209
+ const connection = await kernels.startNew({ name: localSpec.name });
210
+ setCodeSandboxGivenName(connection.id, runtimeName);
211
+ onSubmit({
212
+ kernelId: connection.id,
213
+ name: localSpec.name,
214
+ language: localSpec.language ?? '',
215
+ location: 'local',
216
+ displayName: getCodeSandboxGivenName(connection.id) ?? localSpec.title,
217
+ });
218
+ }
219
+ catch (reason) {
220
+ console.warn('Failed to start a code sandbox of this server.', reason);
221
+ setError(_jsx(_Fragment, { children: "The code sandbox could not be started." }));
222
+ }
223
+ finally {
224
+ if (isMounted()) {
225
+ setWaitingForRuntime(false);
226
+ }
227
+ }
228
+ return;
229
+ }
94
230
  if (selection) {
95
231
  setError(undefined);
96
232
  setWaitingForRuntime(shouldStartRuntime);
@@ -110,7 +246,36 @@ export function CodeSandboxLauncher(props) {
110
246
  desc.params['capabilities'] = ['user_storage'];
111
247
  }
112
248
  let success = true;
113
- if (shouldStartRuntime) {
249
+ const provider = codeSandboxVariantOf(spec?.owner);
250
+ if (shouldStartRuntime && provider !== CodeSandboxVariant.Datalayer) {
251
+ /*
252
+ * An EXTERNAL environment — Kaggle, Modal. The platform RECORDS such
253
+ * a sandbox rather than running it: the provider runs it under the
254
+ * user's own credentials, and what comes back is a record with no
255
+ * pod, no ingress and no kernel. Waiting for one, as the branch
256
+ * below does, would time out and then delete the record as broken.
257
+ */
258
+ success = false;
259
+ try {
260
+ await createRuntimeRecord({
261
+ environmentName: selection,
262
+ type: 'notebook',
263
+ givenName: runtimeName,
264
+ creditsLimit,
265
+ });
266
+ await manager.runtimesManager.refresh();
267
+ success = true;
268
+ }
269
+ catch (error) {
270
+ console.error('Failed to record the external sandbox.', error);
271
+ setFlashLevel('danger');
272
+ setError(_jsxs(Text, { children: ["The ", codeSandboxVariantTitle(provider), " sandbox could not be created."] }));
273
+ }
274
+ finally {
275
+ setWaitingForRuntime(false);
276
+ }
277
+ }
278
+ else if (shouldStartRuntime) {
114
279
  success = false;
115
280
  let availableTrial = 1;
116
281
  let retryDelay = NOT_AVAILABLE_INIT_RETRY;
@@ -195,6 +360,8 @@ export function CodeSandboxLauncher(props) {
195
360
  onSubmit(desc);
196
361
  }
197
362
  }
363
+ // `localSpecOf` closes over `localSpecs`, which is built once.
364
+ // eslint-disable-next-line react-hooks/exhaustive-deps
198
365
  }, [
199
366
  manager,
200
367
  selection,
@@ -251,7 +418,7 @@ export function CodeSandboxLauncher(props) {
251
418
  event.preventDefault();
252
419
  handleSubmitRuntime();
253
420
  }
254
- }, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsx(Select, { name: "environment", disabled: !!kernelSnapshot?.environment || environments.length === 0, value: selection, onChange: handleSelectionChange, block: true, children: environments.map(spec => (_jsxs(Select.Option, { value: spec.name, children: [spec.name, spec.title && (_jsxs(_Fragment, { children: [' - ', spec.title] }))] }, spec.name))) }), _jsx(FormControl.Caption, { children: _jsx(_Fragment, { children: markdownParser ? (_jsx(Box, { sx: { img: { maxWidth: '100%' } }, children: _jsx(Markdown, { text: description, markdownParser: markdownParser, sanitizer: sanitizer }) })) : sanitizer ? (
421
+ }, children: [_jsxs(FormControl, { disabled: !!kernelSnapshot?.environment || environments.length === 0, children: [_jsx(FormControl.Label, { children: "Environment" }), _jsx(CodeSandboxEnvironmentSelect, { disabled: !!kernelSnapshot?.environment || environments.length === 0, options: environmentOptions, selectedKey: selection, onSelect: handleSelectionChange }), _jsx(FormControl.Caption, { children: _jsx(_Fragment, { children: markdownParser ? (_jsx(Box, { sx: { img: { maxWidth: '100%' } }, children: _jsx(Markdown, { text: description, markdownParser: markdownParser, sanitizer: sanitizer }) })) : sanitizer ? (
255
422
  // The description of an environment carries markup. With no
256
423
  // parser to render it, the sanitizer of the application is
257
424
  // enough to show it as what it is rather than as its tags.
@@ -260,11 +427,17 @@ export function CodeSandboxLauncher(props) {
260
427
  } })) : (
261
428
  // Neither a parser nor a sanitizer: the text of the markup,
262
429
  // which is still better than the markup itself.
263
- asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls, { withReservation: !!startRuntime, burningRate: burningRate, timeLimit: timeLimit, onTimeChange: setTimeLimit, max: effectiveMaxMinutes, disabled: outOfCredits, error: outOfCredits ? 'You must add credits to your account.' : undefined, addCredits: navigate
430
+ asPlainText(description)) }) })] }), _jsx(NewCodeSandboxControls
431
+ // A sandbox of this server costs nothing and starts at once: it has
432
+ // no time to reserve and no storage of the platform to mount.
433
+ , {
434
+ // A sandbox of this server costs nothing and starts at once: it has
435
+ // no time to reserve and no storage of the platform to mount.
436
+ withReservation: !!startRuntime && !isLocalSelection, withUserStorage: !isLocalSelection, burningRate: burningRate, timeLimit: timeLimit, onTimeChange: setTimeLimit, max: effectiveMaxMinutes, disabled: outOfCredits, error: outOfCredits ? 'You must add credits to your account.' : undefined, addCredits: navigate
264
437
  ? () => {
265
- navigate(USAGE_ROUTE);
438
+ navigate(usageRoute);
266
439
  }
267
- : undefined, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Runtime name" }), _jsx(TextInput, { name: "name", value: runtimeName, onChange: handleKernelNameChange, block: true })] }), hasExample &&
440
+ : undefined, userStorage: userStorage, onUserStorageToggle: () => setUserStorage(current => !current) }), _jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { children: "Given Name" }), _jsx(TextInput, { name: "name", value: runtimeName, onChange: handleKernelNameChange, block: true })] }), hasExample &&
268
441
  jupyterLabAdapter?.jupyterLab &&
269
442
  !configuration.whiteLabel && (_jsxs(FormControl, { sx: { paddingTop: '10px' }, children: [_jsx(FormControl.Label, { id: "open-example-label", children: "Open example notebook" }), _jsx(ToggleSwitch, { disabled: !environments.find(spec => spec.name === selection)?.example, checked: openExample, size: "small", onClick: handleSwitchClick, "aria-labelledby": "open-example-label" })] })), error && (_jsx(FlashClosable, { variant: flashLevel, actions: navigate && upgradeSubscription && flashLevel === 'warning' ? (_jsx(Button, { onClick: handleUpgrade, title: 'Upgrade your subscription.', children: "Upgrade" })) : undefined, children: error }))] }) }));
270
443
  }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The given name of a sandbox, when it has one.
3
+ *
4
+ * @param kernelId Kernel of the sandbox
5
+ */
6
+ export declare function getCodeSandboxGivenName(kernelId?: string): string | undefined;
7
+ /**
8
+ * Name a sandbox, or rename it.
9
+ *
10
+ * @param kernelId Kernel of the sandbox
11
+ * @param givenName What to call it; empty forgets the name
12
+ */
13
+ export declare function setCodeSandboxGivenName(kernelId: string, givenName: string): void;
14
+ /**
15
+ * Drop the names of the sandboxes that are no longer running.
16
+ *
17
+ * @param runningKernelIds The kernels that are still there
18
+ */
19
+ export declare function pruneCodeSandboxGivenNames(runningKernelIds: Iterable<string>): void;
20
+ /**
21
+ * Every given name in use by a sandbox of a server or of the browser.
22
+ */
23
+ export declare function listCodeSandboxGivenNames(): string[];
24
+ /**
25
+ * The next free name built on a base, numbered when the base is taken.
26
+ *
27
+ * "Python 3 (ipykernel)", then "Python 3 (ipykernel) 2", then 3 — one rule for
28
+ * the name a sandbox is given on first sight and for the name the launcher
29
+ * offers, so what the launcher proposes is what an unnamed one would get.
30
+ *
31
+ * @param base What to call it, before any numbering
32
+ * @param taken The names already in use
33
+ */
34
+ export declare function nextCodeSandboxGivenName(base: string, taken: Iterable<string>): string;
35
+ /**
36
+ * The given name of a sandbox, giving it one when it has none.
37
+ *
38
+ * The name of what it runs, and a number when a sandbox of that kind is
39
+ * already named — the way two runtimes of one environment are told apart.
40
+ * Assigned once and kept: a name that changed with what else is running would
41
+ * be no name at all.
42
+ *
43
+ * @param kernelId Kernel of the sandbox
44
+ * @param specDisplayName What the kernelspec of the sandbox is called
45
+ */
46
+ export declare function ensureCodeSandboxGivenName(kernelId: string, specDisplayName: string): string;
47
+ export default ensureCodeSandboxGivenName;