@jupyterlite/ai 0.10.0 → 0.11.0

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.
@@ -1,927 +0,0 @@
1
- import {
2
- CodeCell,
3
- CodeCellModel,
4
- ICodeCellModel,
5
- MarkdownCell
6
- } from '@jupyterlab/cells';
7
- import { IDocumentManager } from '@jupyterlab/docmanager';
8
- import { DocumentWidget } from '@jupyterlab/docregistry';
9
- import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
10
- import { KernelSpec } from '@jupyterlab/services';
11
-
12
- import { tool } from 'ai';
13
-
14
- import { z } from 'zod';
15
-
16
- import { IDiffManager, ITool } from '../tokens';
17
-
18
- /**
19
- * Find a kernel name that matches the specified language
20
- */
21
- async function findKernelByLanguage(
22
- kernelSpecManager: KernelSpec.IManager,
23
- language?: string | null
24
- ): Promise<string> {
25
- try {
26
- await kernelSpecManager.ready;
27
- const specs = kernelSpecManager.specs;
28
-
29
- if (!specs || !specs.kernelspecs) {
30
- return 'python3'; // Final fallback
31
- }
32
-
33
- // If no language specified, return the default kernel
34
- if (!language) {
35
- return specs.default || Object.keys(specs.kernelspecs)[0] || 'python3';
36
- }
37
-
38
- // Normalize the language name for comparison
39
- const normalizedLanguage = language.toLowerCase().trim();
40
-
41
- // Find kernels that match the requested language
42
- for (const [kernelName, kernelSpec] of Object.entries(specs.kernelspecs)) {
43
- if (!kernelSpec) {
44
- continue;
45
- }
46
-
47
- const kernelLanguage = kernelSpec.language?.toLowerCase() || '';
48
-
49
- // Direct language match
50
- if (kernelLanguage === normalizedLanguage) {
51
- return kernelName;
52
- }
53
- }
54
-
55
- // No matching kernel found, return default
56
- console.warn(`No kernel found for language '${language}', using default`);
57
- return specs.default || Object.keys(specs.kernelspecs)[0] || 'python3';
58
- } catch (error) {
59
- console.warn('Failed to find kernel by language:', error);
60
- return 'python3';
61
- }
62
- }
63
-
64
- /**
65
- * Helper function to get a notebook widget by path or use the active one
66
- */
67
- async function getNotebookWidget(
68
- notebookPath: string | null | undefined,
69
- docManager: IDocumentManager,
70
- notebookTracker?: INotebookTracker
71
- ): Promise<NotebookPanel | null> {
72
- if (notebookPath) {
73
- // Open specific notebook by path using document manager
74
-
75
- let widget = docManager.findWidget(notebookPath);
76
- if (!widget) {
77
- widget = docManager.openOrReveal(notebookPath);
78
- }
79
-
80
- if (!(widget instanceof NotebookPanel)) {
81
- throw new Error(`Widget for ${notebookPath} is not a notebook panel`);
82
- }
83
-
84
- return widget ?? null;
85
- } else {
86
- // Use current active notebook
87
- return notebookTracker?.currentWidget || null;
88
- }
89
- }
90
-
91
- /**
92
- * Create a notebook creation tool
93
- */
94
- export function createNotebookCreationTool(
95
- docManager: IDocumentManager,
96
- kernelSpecManager: KernelSpec.IManager
97
- ): ITool {
98
- return tool({
99
- title: 'Create Notebook',
100
- description:
101
- 'Create a new Jupyter notebook with a kernel for the specified programming language',
102
- inputSchema: z.object({
103
- language: z
104
- .string()
105
- .optional()
106
- .nullable()
107
- .describe(
108
- 'The programming language for the notebook (e.g., python, r, julia, javascript, etc.). Will use system default if not specified.'
109
- ),
110
- name: z
111
- .string()
112
- .optional()
113
- .nullable()
114
- .describe(
115
- 'Optional name for the notebook file (without .ipynb extension)'
116
- )
117
- }),
118
- execute: async (input: {
119
- language?: string | null;
120
- name?: string | null;
121
- }) => {
122
- const kernel = await findKernelByLanguage(
123
- kernelSpecManager,
124
- input.language
125
- );
126
- const { name } = input;
127
-
128
- if (!name) {
129
- return {
130
- success: false,
131
- error: 'A name must be provided to create a notebook'
132
- };
133
- }
134
-
135
- // TODO: handle cwd / path?
136
- const fileName = name.endsWith('.ipynb') ? name : `${name}.ipynb`;
137
-
138
- // Create untitled notebook first
139
- const notebookModel = await docManager.newUntitled({
140
- type: 'notebook'
141
- });
142
-
143
- // Rename to desired filename
144
- await docManager.services.contents.rename(notebookModel.path, fileName);
145
-
146
- // Create widget with specific kernel
147
- const notebook = docManager.createNew(fileName, 'default', {
148
- name: kernel
149
- });
150
-
151
- if (!(notebook instanceof DocumentWidget)) {
152
- return {
153
- success: false,
154
- error: 'Failed to create notebook widget'
155
- };
156
- }
157
-
158
- await notebook.context.ready;
159
- await notebook.context.save();
160
-
161
- docManager.openOrReveal(fileName);
162
-
163
- return {
164
- success: true,
165
- message: `Successfully created notebook ${fileName} with ${kernel} kernel${input.language ? ` for ${input.language}` : ''}`,
166
- notebookPath: fileName,
167
- notebookName: fileName,
168
- kernel,
169
- language: input.language
170
- };
171
- }
172
- });
173
- }
174
-
175
- /**
176
- * Create a tool for adding cells to a specific notebook
177
- */
178
- export function createAddCellTool(
179
- docManager: IDocumentManager,
180
- notebookTracker?: INotebookTracker
181
- ): ITool {
182
- return tool({
183
- title: 'Add Cell',
184
- description: 'Add a cell to the current notebook with optional content',
185
- inputSchema: z.object({
186
- notebookPath: z
187
- .string()
188
- .optional()
189
- .nullable()
190
- .describe(
191
- 'Path to the notebook file. If not provided, uses the currently active notebook'
192
- ),
193
- content: z
194
- .string()
195
- .optional()
196
- .nullable()
197
- .describe('Content to add to the cell'),
198
- cellType: z
199
- .enum(['code', 'markdown', 'raw'])
200
- .default('code')
201
- .describe('Type of cell to add'),
202
- position: z
203
- .enum(['above', 'below'])
204
- .optional()
205
- .default('below')
206
- .describe('Position relative to current cell')
207
- }),
208
- execute: async ({
209
- notebookPath,
210
- content,
211
- cellType = 'code',
212
- position = 'below'
213
- }) => {
214
- const currentWidget = await getNotebookWidget(
215
- notebookPath,
216
- docManager,
217
- notebookTracker
218
- );
219
- if (!currentWidget) {
220
- return {
221
- success: false,
222
- error: notebookPath
223
- ? `Failed to open notebook at path: ${notebookPath}`
224
- : 'No active notebook and no notebook path provided'
225
- };
226
- }
227
-
228
- const notebook = currentWidget.content;
229
- const model = notebook.model;
230
-
231
- if (!model) {
232
- return {
233
- success: false,
234
- error: 'No notebook model available'
235
- };
236
- }
237
-
238
- // Check if we should replace the first empty cell instead of adding
239
- const shouldReplaceFirstCell =
240
- model.cells.length === 1 &&
241
- model.cells.get(0).sharedModel.getSource().trim() === '';
242
-
243
- if (shouldReplaceFirstCell) {
244
- // Replace the first empty cell by removing it and adding new one
245
- model.sharedModel.deleteCell(0);
246
- }
247
-
248
- // Create the new cell using shared model
249
- const newCellData = {
250
- cell_type: cellType,
251
- source: content || '',
252
- metadata: cellType === 'code' ? { trusted: true } : {}
253
- };
254
-
255
- model.sharedModel.addCell(newCellData);
256
-
257
- // Execute markdown cells after creation to render them
258
- if (cellType === 'markdown' && content) {
259
- const cellIndex = model.cells.length - 1;
260
- const cellWidget = notebook.widgets[cellIndex];
261
- if (cellWidget && cellWidget instanceof MarkdownCell) {
262
- try {
263
- await cellWidget.ready;
264
- cellWidget.rendered = true;
265
- } catch (error) {
266
- console.warn('Failed to render markdown cell:', error);
267
- }
268
- }
269
- }
270
-
271
- return {
272
- success: true,
273
- message: `${cellType} cell added successfully`,
274
- content: content || '',
275
- cellType,
276
- position
277
- };
278
- }
279
- });
280
- }
281
-
282
- /**
283
- * Create a tool for getting notebook information
284
- */
285
- export function createGetNotebookInfoTool(
286
- docManager: IDocumentManager,
287
- notebookTracker?: INotebookTracker
288
- ): ITool {
289
- return tool({
290
- title: 'Get Notebook Info',
291
- description:
292
- 'Get information about a notebook including number of cells and active cell index',
293
- inputSchema: z.object({
294
- notebookPath: z
295
- .string()
296
- .optional()
297
- .nullable()
298
- .describe(
299
- 'Path to the notebook file. If not provided, uses the currently active notebook'
300
- )
301
- }),
302
- execute: async (input: { notebookPath?: string | null }) => {
303
- const { notebookPath } = input;
304
-
305
- const currentWidget = await getNotebookWidget(
306
- notebookPath,
307
- docManager,
308
- notebookTracker
309
- );
310
- if (!currentWidget) {
311
- return JSON.stringify({
312
- success: false,
313
- error: notebookPath
314
- ? `Failed to open notebook at path: ${notebookPath}`
315
- : 'No active notebook and no notebook path provided'
316
- });
317
- }
318
-
319
- const notebook = currentWidget.content;
320
- const model = notebook.model;
321
-
322
- if (!model) {
323
- return JSON.stringify({
324
- success: false,
325
- error: 'No notebook model available'
326
- });
327
- }
328
-
329
- const cellCount = model.cells.length;
330
- const activeCellIndex = notebook.activeCellIndex;
331
- const activeCell = notebook.activeCell;
332
- const activeCellType = activeCell?.model.type || 'unknown';
333
-
334
- return JSON.stringify({
335
- success: true,
336
- notebookName: currentWidget.title.label,
337
- notebookPath: currentWidget.context.path,
338
- cellCount,
339
- activeCellIndex,
340
- activeCellType,
341
- isDirty: model.dirty
342
- });
343
- }
344
- });
345
- }
346
-
347
- /**
348
- * Create a tool for getting cell information by index
349
- */
350
- export function createGetCellInfoTool(
351
- docManager: IDocumentManager,
352
- notebookTracker?: INotebookTracker
353
- ): ITool {
354
- return tool({
355
- title: 'Get Cell Info',
356
- description:
357
- 'Get information about a specific cell including its type, source content, and outputs',
358
- inputSchema: z.object({
359
- notebookPath: z
360
- .string()
361
- .optional()
362
- .nullable()
363
- .describe(
364
- 'Path to the notebook file. If not provided, uses the currently active notebook'
365
- ),
366
- cellIndex: z
367
- .number()
368
- .optional()
369
- .nullable()
370
- .describe(
371
- 'Index of the cell to get information for (0-based). If not provided, uses the currently active cell'
372
- )
373
- }),
374
- execute: async (input: {
375
- notebookPath?: string | null;
376
- cellIndex?: number | null;
377
- }) => {
378
- const { notebookPath } = input;
379
- let { cellIndex } = input;
380
-
381
- const currentWidget = await getNotebookWidget(
382
- notebookPath,
383
- docManager,
384
- notebookTracker
385
- );
386
- if (!currentWidget) {
387
- return JSON.stringify({
388
- success: false,
389
- error: notebookPath
390
- ? `Failed to open notebook at path: ${notebookPath}`
391
- : 'No active notebook and no notebook path provided'
392
- });
393
- }
394
-
395
- const notebook = currentWidget.content;
396
- const model = notebook.model;
397
-
398
- if (!model) {
399
- return JSON.stringify({
400
- success: false,
401
- error: 'No notebook model available'
402
- });
403
- }
404
-
405
- if (cellIndex === undefined || cellIndex === null) {
406
- cellIndex = notebook.activeCellIndex;
407
- }
408
-
409
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
410
- return JSON.stringify({
411
- success: false,
412
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
413
- });
414
- }
415
-
416
- const cell = model.cells.get(cellIndex);
417
- const cellType = cell.type;
418
- const sharedModel = cell.sharedModel;
419
- const source = sharedModel.getSource();
420
-
421
- // Get outputs for code cells
422
- let outputs: any[] = [];
423
- if (cellType === 'code') {
424
- const rawOutputs = sharedModel.toJSON().outputs;
425
- outputs = Array.isArray(rawOutputs) ? rawOutputs : [];
426
- }
427
-
428
- return JSON.stringify({
429
- success: true,
430
- cellId: cell.id,
431
- cellIndex,
432
- cellType,
433
- source,
434
- outputs,
435
- executionCount:
436
- cellType === 'code' ? (cell as CodeCellModel).executionCount : null
437
- });
438
- }
439
- });
440
- }
441
-
442
- /**
443
- * Create a tool for setting cell content and type
444
- */
445
- export function createSetCellContentTool(
446
- docManager: IDocumentManager,
447
- notebookTracker?: INotebookTracker,
448
- diffManager?: IDiffManager
449
- ): ITool {
450
- return tool({
451
- title: 'Set Cell Content',
452
- description:
453
- 'Set the content of a specific cell and return both the previous and new content',
454
- inputSchema: z.object({
455
- notebookPath: z
456
- .string()
457
- .optional()
458
- .nullable()
459
- .describe(
460
- 'Path to the notebook file. If not provided, uses the currently active notebook'
461
- ),
462
- cellId: z
463
- .string()
464
- .optional()
465
- .nullable()
466
- .describe(
467
- 'ID of the cell to modify. If provided, takes precedence over cellIndex'
468
- ),
469
- cellIndex: z
470
- .number()
471
- .optional()
472
- .nullable()
473
- .describe(
474
- 'Index of the cell to modify (0-based). Used if cellId is not provided. If neither is provided, targets the active cell'
475
- ),
476
- content: z.string().describe('New content for the cell')
477
- }),
478
- execute: async (input: {
479
- notebookPath?: string | null;
480
- cellId?: string | null;
481
- cellIndex?: number | null;
482
- content: string;
483
- }) => {
484
- const { notebookPath, cellId, cellIndex, content } = input;
485
-
486
- const notebookWidget = await getNotebookWidget(
487
- notebookPath,
488
- docManager,
489
- notebookTracker
490
- );
491
- if (!notebookWidget) {
492
- return JSON.stringify({
493
- success: false,
494
- error: notebookPath
495
- ? `Failed to open notebook at path: ${notebookPath}`
496
- : 'No active notebook and no notebook path provided'
497
- });
498
- }
499
-
500
- const notebook = notebookWidget.content;
501
- const targetNotebookPath = notebookWidget.context.path;
502
-
503
- const model = notebook.model;
504
-
505
- if (!model) {
506
- return JSON.stringify({
507
- success: false,
508
- error: 'No notebook model available'
509
- });
510
- }
511
-
512
- // Determine target cell index
513
- let targetCellIndex: number;
514
- if (cellId !== undefined && cellId !== null) {
515
- // Find cell by ID
516
- targetCellIndex = -1;
517
- for (let i = 0; i < model.cells.length; i++) {
518
- if (model.cells.get(i).id === cellId) {
519
- targetCellIndex = i;
520
- break;
521
- }
522
- }
523
- if (targetCellIndex === -1) {
524
- return JSON.stringify({
525
- success: false,
526
- error: `Cell with ID '${cellId}' not found in notebook`
527
- });
528
- }
529
- } else if (cellIndex !== undefined && cellIndex !== null) {
530
- // Use provided cell index
531
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
532
- return JSON.stringify({
533
- success: false,
534
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
535
- });
536
- }
537
- targetCellIndex = cellIndex;
538
- } else {
539
- // Use active cell
540
- targetCellIndex = notebook.activeCellIndex;
541
- if (targetCellIndex === -1 || targetCellIndex >= model.cells.length) {
542
- return JSON.stringify({
543
- success: false,
544
- error: 'No active cell or invalid active cell index'
545
- });
546
- }
547
- }
548
-
549
- // Get the target cell
550
- const targetCell = model.cells.get(targetCellIndex);
551
- if (!targetCell) {
552
- return JSON.stringify({
553
- success: false,
554
- error: `Cell at index ${targetCellIndex} not found`
555
- });
556
- }
557
-
558
- const sharedModel = targetCell.sharedModel;
559
-
560
- // Get previous content and type
561
- const previousContent = sharedModel.getSource();
562
- const previousCellType = targetCell.type;
563
- const retrievedCellId = targetCell.id;
564
-
565
- sharedModel.setSource(content);
566
-
567
- // Show the cell diff using the diff manager if available
568
- if (diffManager) {
569
- await diffManager.showCellDiff({
570
- original: previousContent,
571
- modified: content,
572
- cellId: retrievedCellId,
573
- notebookPath: targetNotebookPath
574
- });
575
- }
576
-
577
- return JSON.stringify({
578
- success: true,
579
- message:
580
- cellId !== undefined && cellId !== null
581
- ? `Cell with ID '${cellId}' content replaced successfully`
582
- : cellIndex !== undefined && cellIndex !== null
583
- ? `Cell ${targetCellIndex} content replaced successfully`
584
- : 'Active cell content replaced successfully',
585
- notebookPath: targetNotebookPath,
586
- cellId: retrievedCellId,
587
- cellIndex: targetCellIndex,
588
- previousContent,
589
- previousCellType,
590
- newContent: content,
591
- wasActiveCell: cellId === undefined && cellIndex === undefined
592
- });
593
- }
594
- });
595
- }
596
-
597
- /**
598
- * Create a tool for running a specific cell
599
- */
600
- export function createRunCellTool(
601
- docManager: IDocumentManager,
602
- notebookTracker?: INotebookTracker
603
- ): ITool {
604
- return tool({
605
- title: 'Run Cell',
606
- description: 'Run a specific cell in the notebook by index',
607
- inputSchema: z.object({
608
- notebookPath: z
609
- .string()
610
- .optional()
611
- .nullable()
612
- .describe(
613
- 'Path to the notebook file. If not provided, uses the currently active notebook'
614
- ),
615
- cellIndex: z.number().describe('Index of the cell to run (0-based)'),
616
- recordTiming: z
617
- .boolean()
618
- .default(true)
619
- .describe('Whether to record execution timing')
620
- }),
621
- needsApproval: true,
622
- execute: async (input: {
623
- notebookPath?: string | null;
624
- cellIndex: number;
625
- recordTiming?: boolean;
626
- }) => {
627
- const { notebookPath, cellIndex, recordTiming = true } = input;
628
-
629
- const currentWidget = await getNotebookWidget(
630
- notebookPath,
631
- docManager,
632
- notebookTracker
633
- );
634
- if (!currentWidget) {
635
- return JSON.stringify({
636
- success: false,
637
- error: notebookPath
638
- ? `Failed to open notebook at path: ${notebookPath}`
639
- : 'No active notebook and no notebook path provided'
640
- });
641
- }
642
-
643
- const notebook = currentWidget.content;
644
- const model = notebook.model;
645
-
646
- if (!model) {
647
- return JSON.stringify({
648
- success: false,
649
- error: 'No notebook model available'
650
- });
651
- }
652
-
653
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
654
- return JSON.stringify({
655
- success: false,
656
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
657
- });
658
- }
659
-
660
- // Get the target cell widget
661
- const cellWidget = notebook.widgets[cellIndex];
662
- if (!cellWidget) {
663
- return JSON.stringify({
664
- success: false,
665
- error: `Cell widget at index ${cellIndex} not found`
666
- });
667
- }
668
-
669
- // Execute using shared model approach (non-disruptive)
670
- if (cellWidget instanceof CodeCell) {
671
- // Use direct CodeCell.execute() method
672
- const sessionCtx = currentWidget.sessionContext;
673
- await CodeCell.execute(cellWidget, sessionCtx, {
674
- recordTiming,
675
- deletedCells: model.deletedCells
676
- });
677
-
678
- const codeModel = cellWidget.model as ICodeCellModel;
679
- return JSON.stringify({
680
- success: true,
681
- message: `Cell ${cellIndex} executed successfully`,
682
- cellIndex,
683
- executionCount: codeModel.executionCount,
684
- hasOutput: codeModel.outputs.length > 0
685
- });
686
- } else {
687
- // For non-code cells, just return success
688
- return JSON.stringify({
689
- success: true,
690
- message: `Cell ${cellIndex} is not a code cell, no execution needed`,
691
- cellIndex,
692
- cellType: cellWidget.model.type
693
- });
694
- }
695
- }
696
- });
697
- }
698
-
699
- /**
700
- * Create a tool for deleting a specific cell
701
- */
702
- export function createDeleteCellTool(
703
- docManager: IDocumentManager,
704
- notebookTracker?: INotebookTracker
705
- ): ITool {
706
- return tool({
707
- title: 'Delete Cell',
708
- description: 'Delete a specific cell from the notebook by index',
709
- inputSchema: z.object({
710
- notebookPath: z
711
- .string()
712
- .optional()
713
- .nullable()
714
- .describe(
715
- 'Path to the notebook file. If not provided, uses the currently active notebook'
716
- ),
717
- cellIndex: z.number().describe('Index of the cell to delete (0-based)')
718
- }),
719
- execute: async (input: {
720
- notebookPath?: string | null;
721
- cellIndex: number;
722
- }) => {
723
- const { notebookPath, cellIndex } = input;
724
-
725
- const currentWidget = await getNotebookWidget(
726
- notebookPath,
727
- docManager,
728
- notebookTracker
729
- );
730
- if (!currentWidget) {
731
- return JSON.stringify({
732
- success: false,
733
- error: notebookPath
734
- ? `Failed to open notebook at path: ${notebookPath}`
735
- : 'No active notebook and no notebook path provided'
736
- });
737
- }
738
-
739
- const notebook = currentWidget.content;
740
- const model = notebook.model;
741
-
742
- if (!model) {
743
- return JSON.stringify({
744
- success: false,
745
- error: 'No notebook model available'
746
- });
747
- }
748
-
749
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
750
- return JSON.stringify({
751
- success: false,
752
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
753
- });
754
- }
755
-
756
- // Validate cell exists
757
- const targetCell = model.cells.get(cellIndex);
758
- if (!targetCell) {
759
- return JSON.stringify({
760
- success: false,
761
- error: `Cell at index ${cellIndex} not found`
762
- });
763
- }
764
-
765
- // Delete cell using shared model (non-disruptive)
766
- model.sharedModel.deleteCell(cellIndex);
767
-
768
- return JSON.stringify({
769
- success: true,
770
- message: `Cell ${cellIndex} deleted successfully`,
771
- cellIndex,
772
- remainingCells: model.cells.length
773
- });
774
- }
775
- });
776
- }
777
-
778
- /**
779
- * Create a tool for executing code in the active cell (non-disruptive alternative to mcp__ide__executeCode)
780
- */
781
- export function createExecuteActiveCellTool(
782
- docManager: IDocumentManager,
783
- notebookTracker?: INotebookTracker
784
- ): ITool {
785
- return tool({
786
- title: 'Execute Active Cell',
787
- description:
788
- 'Execute the currently active cell in the notebook without disrupting user focus',
789
- inputSchema: z.object({
790
- notebookPath: z
791
- .string()
792
- .optional()
793
- .nullable()
794
- .describe(
795
- 'Path to the notebook file. If not provided, uses the currently active notebook'
796
- ),
797
- code: z
798
- .string()
799
- .optional()
800
- .nullable()
801
- .describe('Optional: set cell content before executing'),
802
- recordTiming: z
803
- .boolean()
804
- .default(true)
805
- .describe('Whether to record execution timing')
806
- }),
807
- execute: async (input: {
808
- notebookPath?: string | null;
809
- code?: string | null;
810
- recordTiming?: boolean;
811
- }) => {
812
- const { notebookPath, code, recordTiming = true } = input;
813
-
814
- const currentWidget = await getNotebookWidget(
815
- notebookPath,
816
- docManager,
817
- notebookTracker
818
- );
819
- if (!currentWidget) {
820
- return JSON.stringify({
821
- success: false,
822
- error: notebookPath
823
- ? `Failed to open notebook at path: ${notebookPath}`
824
- : 'No active notebook and no notebook path provided'
825
- });
826
- }
827
-
828
- const notebook = currentWidget.content;
829
- const model = notebook.model;
830
- const activeCellIndex = notebook.activeCellIndex;
831
-
832
- if (!model || activeCellIndex === -1) {
833
- return JSON.stringify({
834
- success: false,
835
- error: 'No notebook model or active cell available'
836
- });
837
- }
838
-
839
- const activeCell = model.cells.get(activeCellIndex);
840
- if (!activeCell) {
841
- return JSON.stringify({
842
- success: false,
843
- error: 'Active cell not found'
844
- });
845
- }
846
-
847
- // Set code content if provided
848
- if (code) {
849
- activeCell.sharedModel.setSource(code);
850
- }
851
-
852
- // Get the cell widget for execution
853
- const cellWidget = notebook.widgets[activeCellIndex];
854
- if (!cellWidget || !(cellWidget instanceof CodeCell)) {
855
- return JSON.stringify({
856
- success: false,
857
- error: 'Active cell is not a code cell'
858
- });
859
- }
860
-
861
- // Execute using shared model approach (non-disruptive)
862
- const sessionCtx = currentWidget.sessionContext;
863
- await CodeCell.execute(cellWidget, sessionCtx, {
864
- recordTiming,
865
- deletedCells: model.deletedCells
866
- });
867
-
868
- const codeModel = cellWidget.model as ICodeCellModel;
869
- return JSON.stringify({
870
- success: true,
871
- message: 'Code executed successfully in active cell',
872
- cellIndex: activeCellIndex,
873
- executionCount: codeModel.executionCount,
874
- hasOutput: codeModel.outputs.length > 0,
875
- code: code || activeCell.sharedModel.getSource()
876
- });
877
- }
878
- });
879
- }
880
-
881
- /**
882
- * Create a tool for saving a specific notebook
883
- */
884
- export function createSaveNotebookTool(
885
- docManager: IDocumentManager,
886
- notebookTracker?: INotebookTracker
887
- ): ITool {
888
- return tool({
889
- title: 'Save Notebook',
890
- description: 'Save a specific notebook to disk',
891
- inputSchema: z.object({
892
- notebookPath: z
893
- .string()
894
- .optional()
895
- .nullable()
896
- .describe(
897
- 'Path to the notebook file. If not provided, uses the currently active notebook'
898
- )
899
- }),
900
- execute: async (input: { notebookPath?: string | null }) => {
901
- const { notebookPath } = input;
902
-
903
- const currentWidget = await getNotebookWidget(
904
- notebookPath,
905
- docManager,
906
- notebookTracker
907
- );
908
- if (!currentWidget) {
909
- return JSON.stringify({
910
- success: false,
911
- error: notebookPath
912
- ? `Failed to open notebook at path: ${notebookPath}`
913
- : 'No active notebook and no notebook path provided'
914
- });
915
- }
916
-
917
- await currentWidget.context.save();
918
-
919
- return JSON.stringify({
920
- success: true,
921
- message: 'Notebook saved successfully',
922
- notebookName: currentWidget.title.label,
923
- notebookPath: currentWidget.context.path
924
- });
925
- }
926
- });
927
- }