@jupyterlite/services 0.8.0-alpha.0 → 0.8.0-alpha.2

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.
@@ -36,6 +36,104 @@ const N_CHECKPOINTS = 5;
36
36
  const encoder = new TextEncoder();
37
37
  const decoder = new TextDecoder('utf-8');
38
38
 
39
+ /**
40
+ * Converts a contents model into JSON
41
+ *
42
+ * @param model the model to convert
43
+ * @returns the converted model
44
+ */
45
+ function convertToJSON(model: Contents.IModel): Contents.IModel {
46
+ switch (model.format) {
47
+ case 'json': {
48
+ return model;
49
+ }
50
+ case 'text': {
51
+ return {
52
+ ...model,
53
+ content: JSON.parse(model.content),
54
+ format: 'json',
55
+ };
56
+ }
57
+ case 'base64': {
58
+ const binary = atob(model.content);
59
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
60
+ const decoded = new TextDecoder('utf-8').decode(bytes);
61
+
62
+ return {
63
+ ...model,
64
+ content: JSON.parse(decoded),
65
+ format: 'json',
66
+ };
67
+ }
68
+ }
69
+
70
+ throw new Error(`Invalid format ${model.format}`);
71
+ }
72
+
73
+ /**
74
+ * Converts a contents model into Text
75
+ *
76
+ * @param model the model to convert
77
+ * @returns the converted model
78
+ */
79
+ function convertToText(model: Contents.IModel): Contents.IModel {
80
+ switch (model.format) {
81
+ case 'json': {
82
+ return {
83
+ ...model,
84
+ content: JSON.stringify(model.content),
85
+ format: 'text',
86
+ };
87
+ }
88
+ case 'text': {
89
+ return model;
90
+ }
91
+ case 'base64': {
92
+ const binary = atob(model.content);
93
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
94
+ const decoded = new TextDecoder('utf-8').decode(bytes);
95
+
96
+ return {
97
+ ...model,
98
+ content: decoded,
99
+ format: 'text',
100
+ };
101
+ }
102
+ }
103
+
104
+ throw new Error(`Invalid format ${model.format}`);
105
+ }
106
+
107
+ /**
108
+ * Converts a contents model into Base64
109
+ *
110
+ * @param model the model to convert
111
+ * @returns the converted model
112
+ */
113
+ function convertToBase64(model: Contents.IModel): Contents.IModel {
114
+ switch (model.format) {
115
+ case 'json': {
116
+ return {
117
+ ...model,
118
+ content: btoa(JSON.stringify(model.content)),
119
+ format: 'base64',
120
+ };
121
+ }
122
+ case 'text': {
123
+ return {
124
+ ...model,
125
+ content: btoa(model.content),
126
+ format: 'base64',
127
+ };
128
+ }
129
+ case 'base64': {
130
+ return model;
131
+ }
132
+ }
133
+
134
+ throw new Error(`Invalid format ${model.format}`);
135
+ }
136
+
39
137
  /**
40
138
  * A custom drive to store files in the browser storage.
41
139
  */
@@ -258,27 +356,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
258
356
  const type = options?.type ?? 'notebook';
259
357
  const created = new Date().toISOString();
260
358
 
261
- let dirname = PathExt.dirname(path);
262
- const basename = PathExt.basename(path);
263
- const extname = PathExt.extname(path);
264
- const item = await this.get(dirname).catch(() => null);
265
-
266
- // handle the case of "Save As", where the path points to the new file
267
- // to create, e.g. subfolder/example-copy.ipynb
268
- let name = '';
269
- if (path && !extname && item) {
270
- // directory
271
- dirname = `${path}/`;
272
- name = '';
273
- } else if (dirname && basename) {
274
- // file in a subfolder
275
- dirname = `${dirname}/`;
276
- name = basename;
277
- } else {
278
- // file at the top level
279
- dirname = '';
280
- name = path;
281
- }
359
+ let name: string | undefined = undefined;
282
360
 
283
361
  let file: IModel;
284
362
  switch (type) {
@@ -287,7 +365,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
287
365
  name = `Untitled Folder${counter || ''}`;
288
366
  file = {
289
367
  name,
290
- path: `${dirname}${name}`,
368
+ path: PathExt.join(path, name),
291
369
  last_modified: created,
292
370
  created,
293
371
  format: 'json',
@@ -304,7 +382,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
304
382
  name = name || `Untitled${counter || ''}.ipynb`;
305
383
  file = {
306
384
  name,
307
- path: `${dirname}${name}`,
385
+ path: PathExt.join(path, name),
308
386
  last_modified: created,
309
387
  created,
310
388
  format: 'json',
@@ -317,15 +395,20 @@ export class BrowserStorageDrive implements Contents.IDrive {
317
395
  break;
318
396
  }
319
397
  default: {
320
- let ext = options?.ext ?? '.txt';
321
- if (!ext.startsWith('.')) {
398
+ let ext = options?.ext;
399
+ if (ext && !ext.startsWith('.')) {
322
400
  ext = `.${ext}`;
323
401
  }
324
402
  const counter = await this._incrementCounter('file');
325
- const mimetype = FILE.getType(ext) || MIME.OCTET_STREAM;
403
+ const mimetype = ext
404
+ ? FILE.getType(ext) || MIME.OCTET_STREAM
405
+ : MIME.OCTET_STREAM;
326
406
 
327
407
  let format: Contents.FileFormat;
328
- if (FILE.hasFormat(ext, 'text') || mimetype.indexOf('text') !== -1) {
408
+ if (!ext) {
409
+ format = 'base64';
410
+ ext = '';
411
+ } else if (FILE.hasFormat(ext, 'text') || mimetype.indexOf('text') !== -1) {
329
412
  format = 'text';
330
413
  } else if (ext.indexOf('json') !== -1 || ext.indexOf('ipynb') !== -1) {
331
414
  format = 'json';
@@ -336,7 +419,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
336
419
  name = name || `untitled${counter || ''}${ext}`;
337
420
  file = {
338
421
  name,
339
- path: `${dirname}${name}`,
422
+ path: PathExt.join(path, name),
340
423
  last_modified: created,
341
424
  created,
342
425
  format,
@@ -440,16 +523,36 @@ export class BrowserStorageDrive implements Contents.IDrive {
440
523
  const item = await storage.getItem(path);
441
524
  const serverItem = await this._getServerContents(path, options);
442
525
 
443
- const model = (item || serverItem) as IModel | null;
526
+ let model = (item || serverItem) as IModel | null;
444
527
 
445
528
  if (!model) {
446
529
  throw Error(`Could not find content with path ${path}`);
447
530
  }
448
531
 
449
- if (!options?.content) {
532
+ if (options?.content) {
533
+ // Fix model format if the requested format does not match
534
+ const requestedFormat = options?.format;
535
+ if (requestedFormat && model.format !== requestedFormat) {
536
+ switch (requestedFormat) {
537
+ case 'json': {
538
+ model = convertToJSON(model);
539
+ break;
540
+ }
541
+ case 'text': {
542
+ model = convertToText(model);
543
+ break;
544
+ }
545
+ case 'base64': {
546
+ model = convertToBase64(model);
547
+ break;
548
+ }
549
+ }
550
+ }
551
+ } else {
450
552
  return {
451
- size: 0,
452
553
  ...model,
554
+ size: 0,
555
+ format: options?.format ?? model.format,
453
556
  content: null,
454
557
  };
455
558
  }
@@ -574,7 +677,9 @@ export class BrowserStorageDrive implements Contents.IDrive {
574
677
  path = decodeURIComponent(path);
575
678
 
576
679
  // process the file if coming from an upload
577
- const ext = PathExt.extname(options.name ?? '');
680
+ const name = options.name ? options.name : PathExt.basename(path) ?? undefined;
681
+ const ext = name ? PathExt.extname(name) ?? undefined : undefined;
682
+ const mimetype = ext ? FILE.getType(ext) || MIME.OCTET_STREAM : MIME.OCTET_STREAM;
578
683
  const chunk = options.chunk;
579
684
 
580
685
  // retrieve the content if it is a later chunk or the last one
@@ -582,74 +687,79 @@ export class BrowserStorageDrive implements Contents.IDrive {
582
687
  const appendChunk = chunk ? chunk > 1 || chunk === -1 : false;
583
688
  item = await this.get(path, { content: appendChunk }).catch(() => null);
584
689
 
585
- if (!item) {
586
- item = await this.newUntitled({ path, ext, type: 'file' });
587
- }
690
+ const now = new Date().toISOString();
588
691
 
589
- if (!item) {
590
- throw Error(`Could not find file with path ${path}`);
692
+ let type = options.type || 'file';
693
+
694
+ // The Contents API treats Notebooks as a special case.
695
+ if (ext && ext.toLowerCase() === '.ipynb') {
696
+ type = 'notebook';
591
697
  }
592
698
 
593
- // keep a reference to the original content
594
- const originalContent = item.content;
699
+ const format = options?.format || 'base64';
700
+ const content = options?.content || '';
595
701
 
596
- const modified = new Date().toISOString();
597
- // override with the new values
598
- item = {
599
- ...item,
600
- ...options,
601
- last_modified: modified,
602
- };
702
+ // keep a reference to the original content
703
+ const originalContent = item?.content;
704
+
705
+ if (item) {
706
+ item = {
707
+ ...item,
708
+ last_modified: now,
709
+ format,
710
+ mimetype,
711
+ content,
712
+ writable: true,
713
+ type,
714
+ };
715
+ } else {
716
+ item = {
717
+ name,
718
+ path,
719
+ last_modified: now,
720
+ created: now,
721
+ format,
722
+ mimetype,
723
+ content,
724
+ writable: true,
725
+ type,
726
+ };
727
+ }
603
728
 
604
- if (options.content && options.format === 'base64') {
605
- const lastChunk = chunk ? chunk === -1 : true;
729
+ // Handle multichunks uploads
730
+ if (chunk) {
731
+ const lastChunk = chunk === -1;
606
732
 
607
733
  const contentBinaryString = this._handleUploadChunk(
608
- options.content,
734
+ content,
609
735
  originalContent,
610
736
  appendChunk,
611
737
  );
612
738
 
613
- if (ext === '.ipynb') {
614
- const content = lastChunk
615
- ? JSON.parse(decoder.decode(this._binaryStringToBytes(contentBinaryString)))
616
- : contentBinaryString;
617
- item = {
618
- ...item,
619
- content,
620
- format: 'json',
621
- type: 'notebook',
622
- size: contentBinaryString.length,
623
- };
624
- } else if (FILE.hasFormat(ext, 'json')) {
739
+ if (item.format === 'json') {
625
740
  const content = lastChunk
626
741
  ? JSON.parse(decoder.decode(this._binaryStringToBytes(contentBinaryString)))
627
742
  : contentBinaryString;
628
743
  item = {
629
744
  ...item,
630
745
  content,
631
- format: 'json',
632
- type: 'file',
633
746
  size: contentBinaryString.length,
634
747
  };
635
- } else if (FILE.hasFormat(ext, 'text')) {
748
+ } else if (item.format === 'text') {
636
749
  const content = lastChunk
637
750
  ? decoder.decode(this._binaryStringToBytes(contentBinaryString))
638
751
  : contentBinaryString;
639
752
  item = {
640
753
  ...item,
641
754
  content,
642
- format: 'text',
643
- type: 'file',
644
755
  size: contentBinaryString.length,
645
756
  };
646
757
  } else {
758
+ // item.format is base64
647
759
  const content = lastChunk ? btoa(contentBinaryString) : contentBinaryString;
648
760
  item = {
649
761
  ...item,
650
762
  content,
651
- format: 'base64',
652
- type: 'file',
653
763
  size: contentBinaryString.length,
654
764
  };
655
765
  }
@@ -657,7 +767,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
657
767
 
658
768
  // fixup content sizes if necessary
659
769
  if (item.content) {
660
- switch (options.format) {
770
+ switch (item.format) {
661
771
  case 'json': {
662
772
  item = { ...item, size: encoder.encode(JSON.stringify(item.content)).length };
663
773
  break;
@@ -666,8 +776,9 @@ export class BrowserStorageDrive implements Contents.IDrive {
666
776
  item = { ...item, size: encoder.encode(item.content).length };
667
777
  break;
668
778
  }
669
- // base64 save was already handled above
670
779
  case 'base64': {
780
+ const padding = (item.content.match(/=+$/) || [''])[0].length;
781
+ item = { ...item, size: (item.content.length * 3) / 4 - padding };
671
782
  break;
672
783
  }
673
784
  default: {
@@ -970,7 +1081,7 @@ export class BrowserStorageDrive implements Contents.IDrive {
970
1081
  };
971
1082
 
972
1083
  /**
973
- * retrieve the contents for this path from `__index__.json` in the appropriate
1084
+ * retrieve the contents for this path from the contents index file in the appropriate
974
1085
  * folder.
975
1086
  *
976
1087
  * @param newLocalPath - The new file path.
@@ -981,11 +1092,18 @@ export class BrowserStorageDrive implements Contents.IDrive {
981
1092
  const content = this._serverContents.get(path) || new Map();
982
1093
 
983
1094
  if (!this._serverContents.has(path)) {
1095
+ // Check if contents are indexed by looking for the filename in PageConfig
1096
+ const contentsAllJsonFile = PageConfig.getOption('contentsAllJsonFile');
1097
+ if (!contentsAllJsonFile) {
1098
+ this._serverContents.set(path, content);
1099
+ return content;
1100
+ }
1101
+
984
1102
  const apiURL = URLExt.join(
985
1103
  PageConfig.getBaseUrl(),
986
1104
  'api/contents',
987
1105
  path,
988
- 'all.json',
1106
+ contentsAllJsonFile,
989
1107
  );
990
1108
 
991
1109
  try {
@@ -2,7 +2,7 @@ import { PathExt } from '@jupyterlab/coreutils';
2
2
  import type { Contents } from '@jupyterlab/services';
3
3
  import type { TDriveMethod, TDriveRequest, TDriveResponse } from './drivefs';
4
4
  import { BLOCK_SIZE } from './drivefs';
5
- import { DIR_MODE, FILE_MODE } from './emscripten';
5
+ import { isDirMode, DIR_MODE, FILE_MODE } from './emscripten';
6
6
 
7
7
  export interface IDriveContentsProcessor {
8
8
  /**
@@ -168,7 +168,7 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
168
168
  ok: true,
169
169
  mode: model.type === 'directory' ? DIR_MODE : FILE_MODE,
170
170
  };
171
- } catch (e) {
171
+ } catch {
172
172
  response = { ok: false };
173
173
  }
174
174
 
@@ -176,12 +176,18 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
176
176
  }
177
177
 
178
178
  async mknod(request: TDriveRequest<'mknod'>): Promise<TDriveResponse<'mknod'>> {
179
- const model = await this.contentsManager.newUntitled({
180
- path: PathExt.dirname(request.path),
181
- type: request.data.mode === DIR_MODE ? 'directory' : 'file',
182
- ext: PathExt.extname(request.path),
183
- });
184
- await this.contentsManager.rename(model.path, request.path);
179
+ // Contents API does not permit creating folders with given name. We can only create untitled folder then rename.
180
+ if (isDirMode(request.data.mode)) {
181
+ const model = await this.contentsManager.newUntitled({
182
+ path: PathExt.dirname(request.path),
183
+ type: 'directory',
184
+ ext: PathExt.extname(request.path),
185
+ });
186
+ await this.contentsManager.rename(model.path, request.path);
187
+ return null;
188
+ }
189
+
190
+ await this.contentsManager.save(request.path, { type: 'file' });
185
191
  return null;
186
192
  }
187
193
 
@@ -211,7 +217,7 @@ export class DriveContentsProcessor implements IDriveContentsProcessor {
211
217
  let model: Contents.IModel;
212
218
  try {
213
219
  model = await this.contentsManager.get(request.path, { content: true });
214
- } catch (e) {
220
+ } catch {
215
221
  return null;
216
222
  }
217
223
 
@@ -168,7 +168,7 @@ export class DriveFSEmscriptenStreamOps implements IEmscriptenStreamOps {
168
168
  try {
169
169
  const file = this.fs.API.get(path);
170
170
  stream.file = file;
171
- } catch (e) {
171
+ } catch {
172
172
  // If we're opening a file for writing and the file does not exist, create it! Otherwise, throw the proper error
173
173
  // We need to do this because the current thread is thinking a file exist (isFile returns true)
174
174
  // whilst it was actually deleted in the main thread
@@ -342,7 +342,7 @@ export class DriveFSEmscriptenNodeOps implements IEmscriptenNodeOps {
342
342
  let file;
343
343
  try {
344
344
  file = this.fs.API.get(path);
345
- } catch (e) {
345
+ } catch {
346
346
  // TODO: Should do anything here? Should we create the file?
347
347
  break;
348
348
  }
@@ -13,13 +13,27 @@
13
13
  * Ideally, much more of these would be taken from `@types/emscripten`.
14
14
  */
15
15
 
16
- type EmscriptenFS = typeof FS;
16
+ type EmscriptenFS = typeof globalThis extends { FS: infer T } ? T : Record<string, any>;
17
+
18
+ // Those constants are standard that is very unlikely to change
19
+ // https://github.com/bminor/glibc/blob/04e750e75b73957cf1c791535a3f4319534a52fc/bits/stat.h#L66
20
+ const S_IFMT = 0o170000;
21
+ const S_IFDIR = 0o040000;
22
+ const S_IFREG = 0o100000;
17
23
 
18
24
  export const DIR_MODE = 16895; // 040777
19
25
  export const FILE_MODE = 33206; // 100666
20
26
  export const SEEK_CUR = 1;
21
27
  export const SEEK_END = 2;
22
28
 
29
+ export function isDirMode(mode: number): boolean {
30
+ return (mode & S_IFMT) === S_IFDIR;
31
+ }
32
+
33
+ export function isFileMode(mode: number): boolean {
34
+ return (mode & S_IFMT) === S_IFREG;
35
+ }
36
+
23
37
  export interface IStats {
24
38
  dev: number;
25
39
  ino?: number;
@@ -243,8 +243,7 @@ export abstract class BaseKernel implements IKernel {
243
243
  | KernelMessage.IHeader<KernelMessage.MessageType>
244
244
  | undefined = undefined,
245
245
  ): void {
246
- const parentHeaderValue =
247
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
246
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
248
247
  const message = KernelMessage.createMessage<KernelMessage.IStreamMsg>({
249
248
  channel: 'iopub',
250
249
  msgType: 'stream',
@@ -269,8 +268,7 @@ export abstract class BaseKernel implements IKernel {
269
268
  | undefined = undefined,
270
269
  ): void {
271
270
  // Make sure metadata is always set
272
- const parentHeaderValue =
273
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
271
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
274
272
  content.metadata = content.metadata ?? {};
275
273
 
276
274
  const message = KernelMessage.createMessage<KernelMessage.IDisplayDataMsg>({
@@ -296,8 +294,7 @@ export abstract class BaseKernel implements IKernel {
296
294
  | KernelMessage.IHeader<KernelMessage.MessageType>
297
295
  | undefined = undefined,
298
296
  ): void {
299
- const parentHeaderValue =
300
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
297
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
301
298
  const message = KernelMessage.createMessage<KernelMessage.IInputRequestMsg>({
302
299
  channel: 'stdin',
303
300
  msgType: 'input_request',
@@ -321,8 +318,7 @@ export abstract class BaseKernel implements IKernel {
321
318
  | KernelMessage.IHeader<KernelMessage.MessageType>
322
319
  | undefined = undefined,
323
320
  ): void {
324
- const parentHeaderValue =
325
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
321
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
326
322
  const message = KernelMessage.createMessage<KernelMessage.IExecuteResultMsg>({
327
323
  channel: 'iopub',
328
324
  msgType: 'execute_result',
@@ -346,8 +342,7 @@ export abstract class BaseKernel implements IKernel {
346
342
  | KernelMessage.IHeader<KernelMessage.MessageType>
347
343
  | undefined = undefined,
348
344
  ): void {
349
- const parentHeaderValue =
350
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
345
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
351
346
  const message = KernelMessage.createMessage<KernelMessage.IErrorMsg>({
352
347
  channel: 'iopub',
353
348
  msgType: 'error',
@@ -371,8 +366,7 @@ export abstract class BaseKernel implements IKernel {
371
366
  | KernelMessage.IHeader<KernelMessage.MessageType>
372
367
  | undefined = undefined,
373
368
  ): void {
374
- const parentHeaderValue =
375
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
369
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
376
370
  const message = KernelMessage.createMessage<KernelMessage.IUpdateDisplayDataMsg>({
377
371
  channel: 'iopub',
378
372
  msgType: 'update_display_data',
@@ -396,8 +390,7 @@ export abstract class BaseKernel implements IKernel {
396
390
  | KernelMessage.IHeader<KernelMessage.MessageType>
397
391
  | undefined = undefined,
398
392
  ): void {
399
- const parentHeaderValue =
400
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
393
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
401
394
  const message = KernelMessage.createMessage<KernelMessage.IClearOutputMsg>({
402
395
  channel: 'iopub',
403
396
  msgType: 'clear_output',
@@ -423,8 +416,7 @@ export abstract class BaseKernel implements IKernel {
423
416
  | KernelMessage.IHeader<KernelMessage.MessageType>
424
417
  | undefined = undefined,
425
418
  ): void {
426
- const parentHeaderValue =
427
- typeof parentHeader !== 'undefined' ? parentHeader : this._parentHeader;
419
+ const parentHeaderValue = parentHeader ?? this._parentHeader;
428
420
  const message = KernelMessage.createMessage<any>({
429
421
  channel: 'iopub',
430
422
  msgType: type,
@@ -55,8 +55,31 @@ export class NotebookExporter extends BaseExporter {
55
55
  * @param path The path to the notebook
56
56
  */
57
57
  async export(model: Contents.IModel, path: string): Promise<void> {
58
+ let content = '';
59
+ switch (model.format) {
60
+ case 'base64': {
61
+ const decoded = atob(model.content);
62
+ try {
63
+ // If it contains JSON, format it nicely
64
+ const parsed = JSON.parse(decoded);
65
+ content = JSON.stringify(parsed, null, 2);
66
+ } catch {
67
+ // If it's not JSON, just use decoded text
68
+ content = decoded;
69
+ }
70
+ break;
71
+ }
72
+ case 'json': {
73
+ content = JSON.stringify(model.content, null, 2);
74
+ break;
75
+ }
76
+ case 'text': {
77
+ content = model.content;
78
+ break;
79
+ }
80
+ }
81
+
58
82
  const mime = model.mimetype ?? 'application/json';
59
- const content = JSON.stringify(model.content, null, 2);
60
83
  this.triggerDownload(content, mime, path);
61
84
  }
62
85
  }
@@ -157,6 +157,30 @@ export class LiteSessionClient implements ISessionAPIClient {
157
157
  if (running) {
158
158
  return running;
159
159
  }
160
+
161
+ // Check if we should reuse an existing kernel (kernel.id takes precedence over name).
162
+ // Note: The Session.ISessionOptions type doesn't include kernel.id, but at runtime
163
+ // it may be passed (e.g., from SessionContext._changeKernel when sharing a kernel).
164
+ const requestedKernelId = (options.kernel as { id?: string } | undefined)?.id;
165
+ if (requestedKernelId) {
166
+ const existingSession = this._sessions.find(
167
+ (session) => session.kernel?.id === requestedKernelId,
168
+ );
169
+ if (existingSession) {
170
+ // Create a new session that shares the existing kernel
171
+ const id = UUID.uuid4();
172
+ const session: Session.IModel = {
173
+ id,
174
+ path,
175
+ name: name ?? path,
176
+ type: 'notebook',
177
+ kernel: existingSession.kernel,
178
+ };
179
+ this._sessions.push(session);
180
+ return session;
181
+ }
182
+ }
183
+
160
184
  const kernelName = options.kernel?.name ?? '';
161
185
  const id = UUID.uuid4();
162
186
  const nameOrPath = options.name ?? options.path;