@jupyterlite/ai 0.9.0-a3 → 0.9.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.
Files changed (50) hide show
  1. package/README.md +20 -89
  2. package/lib/agent.d.ts +10 -4
  3. package/lib/agent.js +30 -17
  4. package/lib/chat-model.d.ts +6 -0
  5. package/lib/chat-model.js +144 -17
  6. package/lib/completion/completion-provider.js +1 -13
  7. package/lib/components/completion-status.d.ts +20 -0
  8. package/lib/components/completion-status.js +51 -0
  9. package/lib/components/index.d.ts +1 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/components/model-select.js +1 -2
  12. package/lib/diff-manager.d.ts +25 -0
  13. package/lib/diff-manager.js +60 -0
  14. package/lib/icons.d.ts +0 -1
  15. package/lib/icons.js +2 -6
  16. package/lib/index.d.ts +2 -2
  17. package/lib/index.js +54 -23
  18. package/lib/models/settings-model.d.ts +4 -0
  19. package/lib/models/settings-model.js +24 -2
  20. package/lib/providers/built-in-providers.d.ts +0 -4
  21. package/lib/providers/built-in-providers.js +17 -23
  22. package/lib/tokens.d.ts +74 -0
  23. package/lib/tokens.js +4 -0
  24. package/lib/tools/commands.js +36 -35
  25. package/lib/tools/file.d.ts +10 -1
  26. package/lib/tools/file.js +235 -146
  27. package/lib/tools/notebook.d.ts +2 -3
  28. package/lib/tools/notebook.js +11 -11
  29. package/lib/widgets/ai-settings.js +78 -13
  30. package/lib/widgets/provider-config-dialog.js +15 -8
  31. package/package.json +5 -3
  32. package/schema/settings-model.json +25 -0
  33. package/src/agent.ts +35 -20
  34. package/src/chat-model.ts +182 -19
  35. package/src/completion/completion-provider.ts +1 -14
  36. package/src/components/completion-status.tsx +79 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/components/model-select.tsx +0 -3
  39. package/src/diff-manager.ts +81 -0
  40. package/src/icons.ts +2 -7
  41. package/src/index.ts +74 -24
  42. package/src/models/settings-model.ts +28 -2
  43. package/src/providers/built-in-providers.ts +17 -24
  44. package/src/tokens.ts +78 -0
  45. package/src/tools/commands.ts +45 -40
  46. package/src/tools/file.ts +295 -164
  47. package/src/tools/notebook.ts +13 -14
  48. package/src/widgets/ai-settings.tsx +184 -35
  49. package/src/widgets/provider-config-dialog.tsx +43 -16
  50. package/style/base.css +14 -0
package/src/tools/file.ts CHANGED
@@ -1,11 +1,14 @@
1
+ import { PathExt } from '@jupyterlab/coreutils';
1
2
  import { CommandRegistry } from '@lumino/commands';
2
3
  import { IDocumentManager } from '@jupyterlab/docmanager';
4
+ import { IDocumentWidget } from '@jupyterlab/docregistry';
5
+ import { IEditorTracker } from '@jupyterlab/fileeditor';
3
6
 
4
7
  import { tool } from '@openai/agents';
5
8
 
6
9
  import { z } from 'zod';
7
10
 
8
- import { ITool } from '../tokens';
11
+ import { IDiffManager, ITool } from '../tokens';
9
12
 
10
13
  /**
11
14
  * Create a tool for creating new files of various types
@@ -18,16 +21,11 @@ export function createNewFileTool(docManager: IDocumentManager): ITool {
18
21
  parameters: z.object({
19
22
  fileName: z.string().describe('Name of the file to create'),
20
23
  fileType: z
21
- .enum([
22
- 'text',
23
- 'python',
24
- 'markdown',
25
- 'json',
26
- 'javascript',
27
- 'typescript'
28
- ])
24
+ .string()
29
25
  .default('text')
30
- .describe('Type of file to create'),
26
+ .describe(
27
+ 'Type of file to create. Common examples: text, python, markdown, json, javascript, typescript, yaml, julia, r, csv'
28
+ ),
31
29
  content: z
32
30
  .string()
33
31
  .optional()
@@ -39,96 +37,66 @@ export function createNewFileTool(docManager: IDocumentManager): ITool {
39
37
  .nullable()
40
38
  .describe('Directory where to create the file (optional)')
41
39
  }),
40
+ errorFunction: (context, error) => {
41
+ return JSON.stringify({
42
+ success: false,
43
+ error: `Failed to create file: ${error instanceof Error ? error.message : String(error)}`
44
+ });
45
+ },
42
46
  execute: async (input: {
43
47
  fileName: string;
44
- fileType?:
45
- | 'text'
46
- | 'python'
47
- | 'markdown'
48
- | 'json'
49
- | 'javascript'
50
- | 'typescript';
48
+ fileType?: string;
51
49
  content?: string | null;
52
50
  cwd?: string | null;
53
51
  }) => {
54
52
  const { fileName, content = '', cwd, fileType = 'text' } = input;
55
53
 
56
- try {
57
- // Determine file extension based on type
58
- const extensions: Record<string, string> = {
59
- python: 'py',
60
- markdown: 'md',
61
- json: 'json',
62
- text: 'txt',
63
- javascript: 'js',
64
- typescript: 'ts'
65
- };
66
-
67
- const ext = extensions[fileType] || 'txt';
68
-
69
- // If fileName already has an extension, use it as-is, otherwise add the extension
70
- const fullFileName = fileName.includes('.')
71
- ? fileName
72
- : `${fileName}.${ext}`;
73
-
74
- // For Python files, ensure .py extension if fileType is python
75
- const finalFileName =
76
- fileType === 'python' &&
77
- !fileName.endsWith('.py') &&
78
- !fileName.includes('.')
79
- ? `${fileName}.py`
80
- : fullFileName;
81
-
82
- const fullPath = cwd ? `${cwd}/${finalFileName}` : finalFileName;
83
-
84
- // Create file with content using document manager
85
- const model = await docManager.services.contents.newUntitled({
86
- path: cwd || '',
87
- type: 'file',
88
- ext
89
- });
54
+ const registeredFileType = docManager.registry.getFileType(fileType);
55
+ const ext = registeredFileType?.extensions[0] || '.txt';
90
56
 
91
- // Rename to desired name if needed
92
- let finalPath = model.path;
93
- if (model.name !== finalFileName) {
94
- const renamed = await docManager.services.contents.rename(
95
- model.path,
96
- fullPath
97
- );
98
- finalPath = renamed.path;
99
- }
57
+ const existingExt = PathExt.extname(fileName);
58
+ const fullFileName = existingExt ? fileName : `${fileName}${ext}`;
100
59
 
101
- // Set content if provided
102
- if (content) {
103
- await docManager.services.contents.save(finalPath, {
104
- type: 'file',
105
- format: 'text',
106
- content
107
- });
108
- }
60
+ const fullPath = cwd ? `${cwd}/${fullFileName}` : fullFileName;
109
61
 
110
- // Open the newly created file
111
- let opened = false;
112
- if (!docManager.findWidget(finalPath)) {
113
- docManager.openOrReveal(finalPath);
114
- opened = true;
115
- }
62
+ const model = await docManager.services.contents.newUntitled({
63
+ path: cwd || '',
64
+ type: 'file',
65
+ ext
66
+ });
67
+
68
+ let finalPath = model.path;
69
+ if (model.name !== fullFileName) {
70
+ const renamed = await docManager.services.contents.rename(
71
+ model.path,
72
+ fullPath
73
+ );
74
+ finalPath = renamed.path;
75
+ }
116
76
 
117
- return {
118
- success: true,
119
- message: `${fileType} file '${finalFileName}' created and opened successfully`,
120
- fileName: finalFileName,
121
- filePath: finalPath,
122
- fileType,
123
- hasContent: !!content,
124
- opened
125
- };
126
- } catch (error) {
127
- return {
128
- success: false,
129
- error: `Failed to create file: ${(error as Error).message}`
130
- };
77
+ if (content) {
78
+ await docManager.services.contents.save(finalPath, {
79
+ type: 'file',
80
+ format: 'text',
81
+ content
82
+ });
131
83
  }
84
+
85
+ let opened = false;
86
+ if (!docManager.findWidget(finalPath)) {
87
+ docManager.openOrReveal(finalPath);
88
+ opened = true;
89
+ }
90
+
91
+ return {
92
+ success: true,
93
+ message: `${fileType} file '${fullFileName}' created and opened successfully`,
94
+ fileName: fullFileName,
95
+ filePath: finalPath,
96
+ fileType,
97
+ hasContent: !!content,
98
+ opened
99
+ };
132
100
  }
133
101
  });
134
102
  }
@@ -143,31 +111,27 @@ export function createOpenFileTool(docManager: IDocumentManager): ITool {
143
111
  parameters: z.object({
144
112
  filePath: z.string().describe('Path to the file to open')
145
113
  }),
114
+ errorFunction: (context, error) => {
115
+ return JSON.stringify({
116
+ success: false,
117
+ error: `Failed to open file: ${error instanceof Error ? error.message : String(error)}`
118
+ });
119
+ },
146
120
  execute: async (input: { filePath: string }) => {
147
121
  const { filePath } = input;
148
122
 
149
- try {
150
- const widget = docManager.openOrReveal(filePath);
151
-
152
- if (!widget) {
153
- return {
154
- success: false,
155
- error: `Failed to open file: ${filePath}`
156
- };
157
- }
123
+ const widget = docManager.openOrReveal(filePath);
158
124
 
159
- return {
160
- success: true,
161
- message: `File '${filePath}' opened successfully`,
162
- filePath,
163
- widgetId: widget.id
164
- };
165
- } catch (error) {
166
- return {
167
- success: false,
168
- error: `Failed to open file: ${(error as Error).message}`
169
- };
125
+ if (!widget) {
126
+ throw new Error(`Could not open file: ${filePath}`);
170
127
  }
128
+
129
+ return {
130
+ success: true,
131
+ message: `File '${filePath}' opened successfully`,
132
+ filePath,
133
+ widgetId: widget.id
134
+ };
171
135
  }
172
136
  });
173
137
  }
@@ -182,23 +146,22 @@ export function createDeleteFileTool(docManager: IDocumentManager): ITool {
182
146
  parameters: z.object({
183
147
  filePath: z.string().describe('Path to the file to delete')
184
148
  }),
149
+ errorFunction: (context, error) => {
150
+ return JSON.stringify({
151
+ success: false,
152
+ error: `Failed to delete file: ${error instanceof Error ? error.message : String(error)}`
153
+ });
154
+ },
185
155
  execute: async (input: { filePath: string }) => {
186
156
  const { filePath } = input;
187
157
 
188
- try {
189
- await docManager.services.contents.delete(filePath);
158
+ await docManager.services.contents.delete(filePath);
190
159
 
191
- return {
192
- success: true,
193
- message: `File '${filePath}' deleted successfully`,
194
- filePath
195
- };
196
- } catch (error) {
197
- return {
198
- success: false,
199
- error: `Failed to delete file: ${(error as Error).message}`
200
- };
201
- }
160
+ return {
161
+ success: true,
162
+ message: `File '${filePath}' deleted successfully`,
163
+ filePath
164
+ };
202
165
  }
203
166
  });
204
167
  }
@@ -214,24 +177,23 @@ export function createRenameFileTool(docManager: IDocumentManager): ITool {
214
177
  oldPath: z.string().describe('Current path of the file'),
215
178
  newPath: z.string().describe('New path/name for the file')
216
179
  }),
180
+ errorFunction: (context, error) => {
181
+ return JSON.stringify({
182
+ success: false,
183
+ error: `Failed to rename file: ${error instanceof Error ? error.message : String(error)}`
184
+ });
185
+ },
217
186
  execute: async (input: { oldPath: string; newPath: string }) => {
218
187
  const { oldPath, newPath } = input;
219
188
 
220
- try {
221
- await docManager.services.contents.rename(oldPath, newPath);
222
-
223
- return {
224
- success: true,
225
- message: `File renamed from '${oldPath}' to '${newPath}' successfully`,
226
- oldPath,
227
- newPath
228
- };
229
- } catch (error) {
230
- return {
231
- success: false,
232
- error: `Failed to rename file: ${(error as Error).message}`
233
- };
234
- }
189
+ await docManager.services.contents.rename(oldPath, newPath);
190
+
191
+ return {
192
+ success: true,
193
+ message: `File renamed from '${oldPath}' to '${newPath}' successfully`,
194
+ oldPath,
195
+ newPath
196
+ };
235
197
  }
236
198
  });
237
199
  }
@@ -249,24 +211,23 @@ export function createCopyFileTool(docManager: IDocumentManager): ITool {
249
211
  .string()
250
212
  .describe('Destination path for the copied file')
251
213
  }),
214
+ errorFunction: (context, error) => {
215
+ return JSON.stringify({
216
+ success: false,
217
+ error: `Failed to copy file: ${error instanceof Error ? error.message : String(error)}`
218
+ });
219
+ },
252
220
  execute: async (input: { sourcePath: string; destinationPath: string }) => {
253
221
  const { sourcePath, destinationPath } = input;
254
222
 
255
- try {
256
- await docManager.services.contents.copy(sourcePath, destinationPath);
257
-
258
- return {
259
- success: true,
260
- message: `File copied from '${sourcePath}' to '${destinationPath}' successfully`,
261
- sourcePath,
262
- destinationPath
263
- };
264
- } catch (error) {
265
- return {
266
- success: false,
267
- error: `Failed to copy file: ${(error as Error).message}`
268
- };
269
- }
223
+ await docManager.services.contents.copy(sourcePath, destinationPath);
224
+
225
+ return {
226
+ success: true,
227
+ message: `File copied from '${sourcePath}' to '${destinationPath}' successfully`,
228
+ sourcePath,
229
+ destinationPath
230
+ };
270
231
  }
271
232
  });
272
233
  }
@@ -283,25 +244,195 @@ export function createNavigateToDirectoryTool(
283
244
  parameters: z.object({
284
245
  directoryPath: z.string().describe('Path to the directory to navigate to')
285
246
  }),
247
+ errorFunction: (context, error) => {
248
+ return JSON.stringify({
249
+ success: false,
250
+ error: `Failed to navigate to directory: ${error instanceof Error ? error.message : String(error)}`
251
+ });
252
+ },
286
253
  execute: async (input: { directoryPath: string }) => {
287
254
  const { directoryPath } = input;
288
255
 
289
- try {
290
- await commands.execute('filebrowser:go-to-path', {
291
- path: directoryPath
256
+ await commands.execute('filebrowser:go-to-path', {
257
+ path: directoryPath
258
+ });
259
+
260
+ return {
261
+ success: true,
262
+ message: `Navigated to directory '${directoryPath}' successfully`,
263
+ directoryPath
264
+ };
265
+ }
266
+ });
267
+ }
268
+
269
+ /**
270
+ * Create a tool for getting file information and content
271
+ */
272
+ export function createGetFileInfoTool(
273
+ docManager: IDocumentManager,
274
+ editorTracker?: IEditorTracker
275
+ ): ITool {
276
+ return tool({
277
+ name: 'get_file_info',
278
+ description:
279
+ 'Get information about a file including its path, name, extension, and content. Works with text-based files like Python files, markdown, JSON, etc. For Jupyter notebooks, use dedicated notebook tools instead. If no file path is provided, returns information about the currently active file in the editor.',
280
+ parameters: z.object({
281
+ filePath: z
282
+ .string()
283
+ .optional()
284
+ .nullable()
285
+ .describe(
286
+ 'Path to the file to read (e.g., "script.py", "README.md", "config.json"). If not provided, uses the currently active file in the editor.'
287
+ )
288
+ }),
289
+ errorFunction: (context, error) => {
290
+ return JSON.stringify({
291
+ success: false,
292
+ error: `Failed to get file info: ${error instanceof Error ? error.message : String(error)}`
293
+ });
294
+ },
295
+ execute: async (input: { filePath?: string | null }) => {
296
+ const { filePath } = input;
297
+
298
+ let widget: IDocumentWidget | null = null;
299
+
300
+ if (filePath) {
301
+ widget =
302
+ docManager.findWidget(filePath) ??
303
+ docManager.openOrReveal(filePath) ??
304
+ null;
305
+
306
+ if (!widget) {
307
+ throw new Error(`Failed to open file at path: ${filePath}`);
308
+ }
309
+ } else {
310
+ widget = editorTracker?.currentWidget ?? null;
311
+
312
+ if (!widget) {
313
+ throw new Error(
314
+ 'No active file in the editor and no file path provided'
315
+ );
316
+ }
317
+ }
318
+
319
+ if (!widget.context) {
320
+ throw new Error('Widget is not a document');
321
+ }
322
+
323
+ await widget.context.ready;
324
+
325
+ const model = widget.context.model;
326
+
327
+ if (!model) {
328
+ throw new Error('File model not available');
329
+ }
330
+
331
+ const sharedModel = model.sharedModel;
332
+ const content = sharedModel.getSource();
333
+ const resolvedFilePath = widget.context.path;
334
+ const fileName = widget.title.label;
335
+ const fileExtension = PathExt.extname(resolvedFilePath) || 'unknown';
336
+
337
+ return JSON.stringify({
338
+ success: true,
339
+ filePath: resolvedFilePath,
340
+ fileName,
341
+ fileExtension,
342
+ content,
343
+ isDirty: model.dirty,
344
+ readOnly: model.readOnly,
345
+ widgetType: widget.constructor.name
346
+ });
347
+ }
348
+ });
349
+ }
350
+
351
+ /**
352
+ * Create a tool for setting the content of a file
353
+ */
354
+ export function createSetFileContentTool(
355
+ docManager: IDocumentManager,
356
+ diffManager?: IDiffManager
357
+ ): ITool {
358
+ return tool({
359
+ name: 'set_file_content',
360
+ description:
361
+ 'Set or update the content of an existing file. This will replace the entire content of the file. For Jupyter notebooks, use dedicated notebook tools instead.',
362
+ parameters: z.object({
363
+ filePath: z
364
+ .string()
365
+ .describe(
366
+ 'Path to the file to update (e.g., "script.py", "README.md", "config.json")'
367
+ ),
368
+ content: z.string().describe('The new content to set for the file'),
369
+ save: z
370
+ .boolean()
371
+ .optional()
372
+ .default(true)
373
+ .describe('Whether to save the file after updating (default: true)')
374
+ }),
375
+ errorFunction: (context, error) => {
376
+ return JSON.stringify({
377
+ success: false,
378
+ error: `Failed to set file content: ${error instanceof Error ? error.message : String(error)}`
379
+ });
380
+ },
381
+ execute: async (input: {
382
+ filePath: string;
383
+ content: string;
384
+ save?: boolean;
385
+ }) => {
386
+ const { filePath, content, save = true } = input;
387
+
388
+ let widget = docManager.findWidget(filePath);
389
+
390
+ if (!widget) {
391
+ widget = docManager.openOrReveal(filePath);
392
+ }
393
+
394
+ if (!widget) {
395
+ throw new Error(`Failed to open file at path: ${filePath}`);
396
+ }
397
+
398
+ await widget.context.ready;
399
+
400
+ const model = widget.context.model;
401
+
402
+ if (!model) {
403
+ throw new Error('File model not available');
404
+ }
405
+
406
+ if (model.readOnly) {
407
+ throw new Error('File is read-only and cannot be modified');
408
+ }
409
+
410
+ const sharedModel = model.sharedModel;
411
+ const originalContent = sharedModel.getSource();
412
+
413
+ sharedModel.setSource(content);
414
+
415
+ // Show the file diff using the diff manager if available
416
+ if (diffManager) {
417
+ await diffManager.showFileDiff({
418
+ original: String(originalContent),
419
+ modified: content,
420
+ filePath
292
421
  });
422
+ }
293
423
 
294
- return {
295
- success: true,
296
- message: `Navigated to directory '${directoryPath}' successfully`,
297
- directoryPath
298
- };
299
- } catch (error) {
300
- return {
301
- success: false,
302
- error: `Failed to navigate to directory: ${(error as Error).message}`
303
- };
424
+ if (save) {
425
+ await widget.context.save();
304
426
  }
427
+
428
+ return JSON.stringify({
429
+ success: true,
430
+ filePath,
431
+ fileName: widget.title.label,
432
+ contentLength: content.length,
433
+ saved: save,
434
+ isDirty: model.dirty
435
+ });
305
436
  }
306
437
  });
307
438
  }
@@ -3,13 +3,12 @@ import { IDocumentManager } from '@jupyterlab/docmanager';
3
3
  import { DocumentWidget } from '@jupyterlab/docregistry';
4
4
  import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
5
5
  import { KernelSpec } from '@jupyterlab/services';
6
- import { CommandRegistry } from '@lumino/commands';
7
6
 
8
7
  import { tool } from '@openai/agents';
9
8
 
10
9
  import { z } from 'zod';
11
10
 
12
- import { ITool } from '../tokens';
11
+ import { IDiffManager, ITool } from '../tokens';
13
12
 
14
13
  /**
15
14
  * Find a kernel name that matches the specified language
@@ -198,6 +197,7 @@ export function createAddCellTool(
198
197
  .describe('Type of cell to add'),
199
198
  position: z
200
199
  .enum(['above', 'below'])
200
+ .optional()
201
201
  .default('below')
202
202
  .describe('Position relative to current cell')
203
203
  }),
@@ -460,8 +460,8 @@ export function createGetCellInfoTool(
460
460
  */
461
461
  export function createSetCellContentTool(
462
462
  docManager: IDocumentManager,
463
- commands: CommandRegistry,
464
- notebookTracker?: INotebookTracker
463
+ notebookTracker?: INotebookTracker,
464
+ diffManager?: IDiffManager
465
465
  ): ITool {
466
466
  return tool({
467
467
  name: 'set_cell_content',
@@ -581,16 +581,15 @@ export function createSetCellContentTool(
581
581
 
582
582
  sharedModel.setSource(content);
583
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
- });
584
+ // Show the cell diff using the diff manager if available
585
+ if (diffManager) {
586
+ await diffManager.showCellDiff({
587
+ original: previousContent,
588
+ modified: content,
589
+ cellId: retrievedCellId,
590
+ notebookPath: targetNotebookPath
591
+ });
592
+ }
594
593
 
595
594
  return JSON.stringify({
596
595
  success: true,