@jupyterlite/ai 0.8.1 → 0.9.0-a1

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