${bodyContent}
diff --git a/src/index.ts b/src/index.ts
index 40fa419..dc89a65 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -26,14 +26,10 @@ import { ICompletionProviderManager } from '@jupyterlab/completer';
import { IDocumentManager } from '@jupyterlab/docmanager';
-import { IEditorTracker } from '@jupyterlab/fileeditor';
-
import { INotebookTracker } from '@jupyterlab/notebook';
import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
-import { IKernelSpecManager, KernelSpec } from '@jupyterlab/services';
-
import { ISettingRegistry } from '@jupyterlab/settingregistry';
import { IStatusBar } from '@jupyterlab/statusbar';
@@ -100,29 +96,6 @@ import { DiffManager } from './diff-manager';
import { ToolRegistry } from './tools/tool-registry';
-import {
- createAddCellTool,
- createDeleteCellTool,
- createExecuteActiveCellTool,
- createGetCellInfoTool,
- createGetNotebookInfoTool,
- createNotebookCreationTool,
- createRunCellTool,
- createSaveNotebookTool,
- createSetCellContentTool
-} from './tools/notebook';
-
-import {
- createCopyFileTool,
- createDeleteFileTool,
- createGetFileInfoTool,
- createNavigateToDirectoryTool,
- createNewFileTool,
- createOpenFileTool,
- createRenameFileTool,
- createSetFileContentTool
-} from './tools/file';
-
import {
createDiscoverCommandsTool,
createExecuteCommandTool
@@ -820,80 +793,11 @@ const toolRegistry: JupyterFrontEndPlugin = {
id: '@jupyterlite/ai:tool-registry',
description: 'Provide the AI tool registry',
autoStart: true,
- requires: [IAISettingsModel, IDocumentManager, IKernelSpecManager],
- optional: [INotebookTracker, IDiffManager, IEditorTracker],
+ requires: [IAISettingsModel],
provides: IToolRegistry,
- activate: (
- app: JupyterFrontEnd,
- settingsModel: AISettingsModel,
- docManager: IDocumentManager,
- kernelSpecManager: KernelSpec.IManager,
- notebookTracker?: INotebookTracker,
- diffManager?: IDiffManager,
- editorTracker?: IEditorTracker
- ) => {
+ activate: (app: JupyterFrontEnd, settingsModel: AISettingsModel) => {
const toolRegistry = new ToolRegistry();
- const notebookCreationTool = createNotebookCreationTool(
- docManager,
- kernelSpecManager
- );
- toolRegistry.add('create_notebook', notebookCreationTool);
-
- // Add high-level notebook operation tools
- const addCellTool = createAddCellTool(docManager, notebookTracker);
- const getNotebookInfoTool = createGetNotebookInfoTool(
- docManager,
- notebookTracker
- );
- const getCellInfoTool = createGetCellInfoTool(docManager, notebookTracker);
- const setCellContentTool = createSetCellContentTool(
- docManager,
- notebookTracker,
- diffManager
- );
- const runCellTool = createRunCellTool(docManager, notebookTracker);
- const deleteCellTool = createDeleteCellTool(docManager, notebookTracker);
- const saveNotebookTool = createSaveNotebookTool(
- docManager,
- notebookTracker
- );
- const executeActiveCellTool = createExecuteActiveCellTool(
- docManager,
- notebookTracker
- );
-
- toolRegistry.add('add_cell', addCellTool);
- toolRegistry.add('get_notebook_info', getNotebookInfoTool);
- toolRegistry.add('get_cell_info', getCellInfoTool);
- toolRegistry.add('set_cell_content', setCellContentTool);
- toolRegistry.add('run_cell', runCellTool);
- toolRegistry.add('delete_cell', deleteCellTool);
- toolRegistry.add('save_notebook', saveNotebookTool);
- toolRegistry.add('execute_active_cell', executeActiveCellTool);
-
- // Add file operation tools
- const newFileTool = createNewFileTool(docManager);
- const openFileTool = createOpenFileTool(docManager);
- const deleteFileTool = createDeleteFileTool(docManager);
- const renameFileTool = createRenameFileTool(docManager);
- const copyFileTool = createCopyFileTool(docManager);
- const navigateToDirectoryTool = createNavigateToDirectoryTool(app.commands);
- const getFileInfoTool = createGetFileInfoTool(docManager, editorTracker);
- const setFileContentTool = createSetFileContentTool(
- docManager,
- diffManager
- );
-
- toolRegistry.add('create_file', newFileTool);
- toolRegistry.add('open_file', openFileTool);
- toolRegistry.add('delete_file', deleteFileTool);
- toolRegistry.add('rename_file', renameFileTool);
- toolRegistry.add('copy_file', copyFileTool);
- toolRegistry.add('navigate_to_directory', navigateToDirectoryTool);
- toolRegistry.add('get_file_info', getFileInfoTool);
- toolRegistry.add('set_file_content', setFileContentTool);
-
// Add command operation tools
const discoverCommandsTool = createDiscoverCommandsTool(app.commands);
const executeCommandTool = createExecuteCommandTool(
diff --git a/src/tools/commands.ts b/src/tools/commands.ts
index 861e13e..b678402 100644
--- a/src/tools/commands.ts
+++ b/src/tools/commands.ts
@@ -111,18 +111,12 @@ export function createExecuteCommandTool(
// Execute the command
const result = await commands.execute(commandId, args);
- // Handle Widget objects specially (including subclasses like DocumentWidget)
+ // Handle Widget objects specially by extracting id and title
let serializedResult;
- if (
- result &&
- typeof result === 'object' &&
- (result.constructor?.name?.includes('Widget') || result.id)
- ) {
+ if (result && typeof result === 'object' && result.id) {
serializedResult = {
- type: result.constructor?.name || 'Widget',
id: result.id,
- title: result.title?.label || result.title,
- className: result.className
+ title: result.title?.label || result.title
};
} else {
// For other objects, try JSON serialization with fallback
diff --git a/src/tools/file.ts b/src/tools/file.ts
deleted file mode 100644
index ecf26dd..0000000
--- a/src/tools/file.ts
+++ /dev/null
@@ -1,412 +0,0 @@
-import { PathExt } from '@jupyterlab/coreutils';
-import { CommandRegistry } from '@lumino/commands';
-import { IDocumentManager } from '@jupyterlab/docmanager';
-import { IDocumentWidget } from '@jupyterlab/docregistry';
-import { IEditorTracker } from '@jupyterlab/fileeditor';
-
-import { tool } from 'ai';
-
-import { z } from 'zod';
-
-import { IDiffManager, ITool } from '../tokens';
-
-/**
- * Create a tool for creating new files of various types
- */
-export function createNewFileTool(docManager: IDocumentManager): ITool {
- return tool({
- title: 'New File',
- description:
- 'Create a new file of specified type (text, python, markdown, json, etc.)',
- inputSchema: z.object({
- fileName: z.string().describe('Name of the file to create'),
- fileType: z
- .string()
- .default('text')
- .describe(
- 'Type of file to create. Common examples: text, python, markdown, json, javascript, typescript, yaml, julia, r, csv'
- ),
- content: z
- .string()
- .optional()
- .nullable()
- .describe('Initial content for the file (optional)'),
- cwd: z
- .string()
- .optional()
- .nullable()
- .describe('Directory where to create the file (optional)')
- }),
- execute: async (input: {
- fileName: string;
- fileType?: string;
- content?: string | null;
- cwd?: string | null;
- }) => {
- const { fileName, content = '', cwd, fileType = 'text' } = input;
-
- const registeredFileType = docManager.registry.getFileType(fileType);
- const ext = registeredFileType?.extensions[0] || '.txt';
-
- const existingExt = PathExt.extname(fileName);
- const fullFileName = existingExt ? fileName : `${fileName}${ext}`;
-
- const fullPath = cwd ? `${cwd}/${fullFileName}` : fullFileName;
-
- const model = await docManager.services.contents.newUntitled({
- path: cwd || '',
- type: 'file',
- ext
- });
-
- let finalPath = model.path;
- if (model.name !== fullFileName) {
- const renamed = await docManager.services.contents.rename(
- model.path,
- fullPath
- );
- finalPath = renamed.path;
- }
-
- if (content) {
- await docManager.services.contents.save(finalPath, {
- type: 'file',
- format: 'text',
- content
- });
- }
-
- let opened = false;
- if (!docManager.findWidget(finalPath)) {
- docManager.openOrReveal(finalPath);
- opened = true;
- }
-
- return {
- success: true,
- message: `${fileType} file '${fullFileName}' created and opened successfully`,
- fileName: fullFileName,
- filePath: finalPath,
- fileType,
- hasContent: !!content,
- opened
- };
- }
- });
-}
-
-/**
- * Create a tool for opening files
- */
-export function createOpenFileTool(docManager: IDocumentManager): ITool {
- return tool({
- title: 'Open File',
- description: 'Open a file in the editor',
- inputSchema: z.object({
- filePath: z.string().describe('Path to the file to open')
- }),
- execute: async (input: { filePath: string }) => {
- const { filePath } = input;
-
- const widget = docManager.openOrReveal(filePath);
-
- if (!widget) {
- return {
- success: false,
- error: `Could not open file: ${filePath}`
- };
- }
-
- return {
- success: true,
- message: `File '${filePath}' opened successfully`,
- filePath,
- widgetId: widget.id
- };
- }
- });
-}
-
-/**
- * Create a tool for deleting files
- */
-export function createDeleteFileTool(docManager: IDocumentManager): ITool {
- return tool({
- title: 'Delete File',
- description: 'Delete a file from the file system',
- inputSchema: z.object({
- filePath: z.string().describe('Path to the file to delete')
- }),
- execute: async (input: { filePath: string }) => {
- const { filePath } = input;
-
- await docManager.services.contents.delete(filePath);
-
- return {
- success: true,
- message: `File '${filePath}' deleted successfully`,
- filePath
- };
- }
- });
-}
-
-/**
- * Create a tool for renaming files
- */
-export function createRenameFileTool(docManager: IDocumentManager): ITool {
- return tool({
- title: 'Rename File',
- description: 'Rename a file or move it to a different location',
- inputSchema: z.object({
- oldPath: z.string().describe('Current path of the file'),
- newPath: z.string().describe('New path/name for the file')
- }),
- execute: async (input: { oldPath: string; newPath: string }) => {
- const { oldPath, newPath } = input;
-
- await docManager.services.contents.rename(oldPath, newPath);
-
- return {
- success: true,
- message: `File renamed from '${oldPath}' to '${newPath}' successfully`,
- oldPath,
- newPath
- };
- }
- });
-}
-
-/**
- * Create a tool for copying files
- */
-export function createCopyFileTool(docManager: IDocumentManager): ITool {
- return tool({
- title: 'Copy File',
- description: 'Copy a file to a new location',
- inputSchema: z.object({
- sourcePath: z.string().describe('Path of the file to copy'),
- destinationPath: z
- .string()
- .describe('Destination path for the copied file')
- }),
- execute: async (input: { sourcePath: string; destinationPath: string }) => {
- const { sourcePath, destinationPath } = input;
-
- await docManager.services.contents.copy(sourcePath, destinationPath);
-
- return {
- success: true,
- message: `File copied from '${sourcePath}' to '${destinationPath}' successfully`,
- sourcePath,
- destinationPath
- };
- }
- });
-}
-
-/**
- * Create a tool for navigating to directories in the file browser
- */
-export function createNavigateToDirectoryTool(
- commands: CommandRegistry
-): ITool {
- return tool({
- title: 'Navigate to Directory',
- description: 'Navigate to a specific directory in the file browser',
- inputSchema: z.object({
- directoryPath: z.string().describe('Path to the directory to navigate to')
- }),
- execute: async (input: { directoryPath: string }) => {
- const { directoryPath } = input;
-
- await commands.execute('filebrowser:go-to-path', {
- path: directoryPath
- });
-
- return {
- success: true,
- message: `Navigated to directory '${directoryPath}' successfully`,
- directoryPath
- };
- }
- });
-}
-
-/**
- * Create a tool for getting file information and content
- */
-export function createGetFileInfoTool(
- docManager: IDocumentManager,
- editorTracker?: IEditorTracker
-): ITool {
- return tool({
- title: 'Get File Info',
- description:
- '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.',
- inputSchema: z.object({
- filePath: z
- .string()
- .optional()
- .nullable()
- .describe(
- '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.'
- )
- }),
- execute: async (input: { filePath?: string | null }) => {
- const { filePath } = input;
-
- let widget: IDocumentWidget | null = null;
-
- if (filePath) {
- widget =
- docManager.findWidget(filePath) ??
- docManager.openOrReveal(filePath) ??
- null;
-
- if (!widget) {
- return JSON.stringify({
- success: false,
- error: `Failed to open file at path: ${filePath}`
- });
- }
- } else {
- widget = editorTracker?.currentWidget ?? null;
-
- if (!widget) {
- return JSON.stringify({
- success: false,
- error: 'No active file in the editor and no file path provided'
- });
- }
- }
-
- if (!widget.context) {
- return JSON.stringify({
- success: false,
- error: 'Widget is not a document'
- });
- }
-
- await widget.context.ready;
-
- const model = widget.context.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'File model not available'
- });
- }
-
- const sharedModel = model.sharedModel;
- const content = sharedModel.getSource();
- const resolvedFilePath = widget.context.path;
- const fileName = widget.title.label;
- const fileExtension = PathExt.extname(resolvedFilePath) || 'unknown';
-
- return JSON.stringify({
- success: true,
- filePath: resolvedFilePath,
- fileName,
- fileExtension,
- content,
- isDirty: model.dirty,
- readOnly: model.readOnly,
- widgetType: widget.constructor.name
- });
- }
- });
-}
-
-/**
- * Create a tool for setting the content of a file
- */
-export function createSetFileContentTool(
- docManager: IDocumentManager,
- diffManager?: IDiffManager
-): ITool {
- return tool({
- title: 'Set File Content',
- description:
- '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.',
- inputSchema: z.object({
- filePath: z
- .string()
- .describe(
- 'Path to the file to update (e.g., "script.py", "README.md", "config.json")'
- ),
- content: z.string().describe('The new content to set for the file'),
- save: z
- .boolean()
- .optional()
- .default(true)
- .describe('Whether to save the file after updating (default: true)')
- }),
- execute: async (input: {
- filePath: string;
- content: string;
- save?: boolean;
- }) => {
- const { filePath, content, save = true } = input;
-
- let widget = docManager.findWidget(filePath);
-
- if (!widget) {
- widget = docManager.openOrReveal(filePath);
- }
-
- if (!widget) {
- return JSON.stringify({
- success: false,
- error: `Failed to open file at path: ${filePath}`
- });
- }
-
- await widget.context.ready;
-
- const model = widget.context.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'File model not available'
- });
- }
-
- if (model.readOnly) {
- return JSON.stringify({
- success: false,
- error: 'File is read-only and cannot be modified'
- });
- }
-
- const sharedModel = model.sharedModel;
- const originalContent = sharedModel.getSource();
-
- sharedModel.setSource(content);
-
- // Show the file diff using the diff manager if available
- if (diffManager) {
- await diffManager.showFileDiff({
- original: String(originalContent),
- modified: content,
- filePath
- });
- }
-
- if (save) {
- await widget.context.save();
- }
-
- return JSON.stringify({
- success: true,
- filePath,
- fileName: widget.title.label,
- contentLength: content.length,
- saved: save,
- isDirty: model.dirty
- });
- }
- });
-}
diff --git a/src/tools/notebook.ts b/src/tools/notebook.ts
deleted file mode 100644
index ce73404..0000000
--- a/src/tools/notebook.ts
+++ /dev/null
@@ -1,927 +0,0 @@
-import {
- CodeCell,
- CodeCellModel,
- ICodeCellModel,
- MarkdownCell
-} from '@jupyterlab/cells';
-import { IDocumentManager } from '@jupyterlab/docmanager';
-import { DocumentWidget } from '@jupyterlab/docregistry';
-import { INotebookTracker, NotebookPanel } from '@jupyterlab/notebook';
-import { KernelSpec } from '@jupyterlab/services';
-
-import { tool } from 'ai';
-
-import { z } from 'zod';
-
-import { IDiffManager, ITool } from '../tokens';
-
-/**
- * Find a kernel name that matches the specified language
- */
-async function findKernelByLanguage(
- kernelSpecManager: KernelSpec.IManager,
- language?: string | null
-): Promise {
- try {
- await kernelSpecManager.ready;
- const specs = kernelSpecManager.specs;
-
- if (!specs || !specs.kernelspecs) {
- return 'python3'; // Final fallback
- }
-
- // If no language specified, return the default kernel
- if (!language) {
- return specs.default || Object.keys(specs.kernelspecs)[0] || 'python3';
- }
-
- // Normalize the language name for comparison
- const normalizedLanguage = language.toLowerCase().trim();
-
- // Find kernels that match the requested language
- for (const [kernelName, kernelSpec] of Object.entries(specs.kernelspecs)) {
- if (!kernelSpec) {
- continue;
- }
-
- const kernelLanguage = kernelSpec.language?.toLowerCase() || '';
-
- // Direct language match
- if (kernelLanguage === normalizedLanguage) {
- return kernelName;
- }
- }
-
- // No matching kernel found, return default
- console.warn(`No kernel found for language '${language}', using default`);
- return specs.default || Object.keys(specs.kernelspecs)[0] || 'python3';
- } catch (error) {
- console.warn('Failed to find kernel by language:', error);
- return 'python3';
- }
-}
-
-/**
- * Helper function to get a notebook widget by path or use the active one
- */
-async function getNotebookWidget(
- notebookPath: string | null | undefined,
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): Promise {
- if (notebookPath) {
- // Open specific notebook by path using document manager
-
- let widget = docManager.findWidget(notebookPath);
- if (!widget) {
- widget = docManager.openOrReveal(notebookPath);
- }
-
- if (!(widget instanceof NotebookPanel)) {
- throw new Error(`Widget for ${notebookPath} is not a notebook panel`);
- }
-
- return widget ?? null;
- } else {
- // Use current active notebook
- return notebookTracker?.currentWidget || null;
- }
-}
-
-/**
- * Create a notebook creation tool
- */
-export function createNotebookCreationTool(
- docManager: IDocumentManager,
- kernelSpecManager: KernelSpec.IManager
-): ITool {
- return tool({
- title: 'Create Notebook',
- description:
- 'Create a new Jupyter notebook with a kernel for the specified programming language',
- inputSchema: z.object({
- language: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'The programming language for the notebook (e.g., python, r, julia, javascript, etc.). Will use system default if not specified.'
- ),
- name: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Optional name for the notebook file (without .ipynb extension)'
- )
- }),
- execute: async (input: {
- language?: string | null;
- name?: string | null;
- }) => {
- const kernel = await findKernelByLanguage(
- kernelSpecManager,
- input.language
- );
- const { name } = input;
-
- if (!name) {
- return {
- success: false,
- error: 'A name must be provided to create a notebook'
- };
- }
-
- // TODO: handle cwd / path?
- const fileName = name.endsWith('.ipynb') ? name : `${name}.ipynb`;
-
- // Create untitled notebook first
- const notebookModel = await docManager.newUntitled({
- type: 'notebook'
- });
-
- // Rename to desired filename
- await docManager.services.contents.rename(notebookModel.path, fileName);
-
- // Create widget with specific kernel
- const notebook = docManager.createNew(fileName, 'default', {
- name: kernel
- });
-
- if (!(notebook instanceof DocumentWidget)) {
- return {
- success: false,
- error: 'Failed to create notebook widget'
- };
- }
-
- await notebook.context.ready;
- await notebook.context.save();
-
- docManager.openOrReveal(fileName);
-
- return {
- success: true,
- message: `Successfully created notebook ${fileName} with ${kernel} kernel${input.language ? ` for ${input.language}` : ''}`,
- notebookPath: fileName,
- notebookName: fileName,
- kernel,
- language: input.language
- };
- }
- });
-}
-
-/**
- * Create a tool for adding cells to a specific notebook
- */
-export function createAddCellTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Add Cell',
- description: 'Add a cell to the current notebook with optional content',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- content: z
- .string()
- .optional()
- .nullable()
- .describe('Content to add to the cell'),
- cellType: z
- .enum(['code', 'markdown', 'raw'])
- .default('code')
- .describe('Type of cell to add'),
- position: z
- .enum(['above', 'below'])
- .optional()
- .default('below')
- .describe('Position relative to current cell')
- }),
- execute: async ({
- notebookPath,
- content,
- cellType = 'code',
- position = 'below'
- }) => {
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return {
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- };
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
-
- if (!model) {
- return {
- success: false,
- error: 'No notebook model available'
- };
- }
-
- // Check if we should replace the first empty cell instead of adding
- const shouldReplaceFirstCell =
- model.cells.length === 1 &&
- model.cells.get(0).sharedModel.getSource().trim() === '';
-
- if (shouldReplaceFirstCell) {
- // Replace the first empty cell by removing it and adding new one
- model.sharedModel.deleteCell(0);
- }
-
- // Create the new cell using shared model
- const newCellData = {
- cell_type: cellType,
- source: content || '',
- metadata: cellType === 'code' ? { trusted: true } : {}
- };
-
- model.sharedModel.addCell(newCellData);
-
- // Execute markdown cells after creation to render them
- if (cellType === 'markdown' && content) {
- const cellIndex = model.cells.length - 1;
- const cellWidget = notebook.widgets[cellIndex];
- if (cellWidget && cellWidget instanceof MarkdownCell) {
- try {
- await cellWidget.ready;
- cellWidget.rendered = true;
- } catch (error) {
- console.warn('Failed to render markdown cell:', error);
- }
- }
- }
-
- return {
- success: true,
- message: `${cellType} cell added successfully`,
- content: content || '',
- cellType,
- position
- };
- }
- });
-}
-
-/**
- * Create a tool for getting notebook information
- */
-export function createGetNotebookInfoTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Get Notebook Info',
- description:
- 'Get information about a notebook including number of cells and active cell index',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- )
- }),
- execute: async (input: { notebookPath?: string | null }) => {
- const { notebookPath } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model available'
- });
- }
-
- const cellCount = model.cells.length;
- const activeCellIndex = notebook.activeCellIndex;
- const activeCell = notebook.activeCell;
- const activeCellType = activeCell?.model.type || 'unknown';
-
- return JSON.stringify({
- success: true,
- notebookName: currentWidget.title.label,
- notebookPath: currentWidget.context.path,
- cellCount,
- activeCellIndex,
- activeCellType,
- isDirty: model.dirty
- });
- }
- });
-}
-
-/**
- * Create a tool for getting cell information by index
- */
-export function createGetCellInfoTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Get Cell Info',
- description:
- 'Get information about a specific cell including its type, source content, and outputs',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- cellIndex: z
- .number()
- .optional()
- .nullable()
- .describe(
- 'Index of the cell to get information for (0-based). If not provided, uses the currently active cell'
- )
- }),
- execute: async (input: {
- notebookPath?: string | null;
- cellIndex?: number | null;
- }) => {
- const { notebookPath } = input;
- let { cellIndex } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model available'
- });
- }
-
- if (cellIndex === undefined || cellIndex === null) {
- cellIndex = notebook.activeCellIndex;
- }
-
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
- return JSON.stringify({
- success: false,
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
- });
- }
-
- const cell = model.cells.get(cellIndex);
- const cellType = cell.type;
- const sharedModel = cell.sharedModel;
- const source = sharedModel.getSource();
-
- // Get outputs for code cells
- let outputs: any[] = [];
- if (cellType === 'code') {
- const rawOutputs = sharedModel.toJSON().outputs;
- outputs = Array.isArray(rawOutputs) ? rawOutputs : [];
- }
-
- return JSON.stringify({
- success: true,
- cellId: cell.id,
- cellIndex,
- cellType,
- source,
- outputs,
- executionCount:
- cellType === 'code' ? (cell as CodeCellModel).executionCount : null
- });
- }
- });
-}
-
-/**
- * Create a tool for setting cell content and type
- */
-export function createSetCellContentTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker,
- diffManager?: IDiffManager
-): ITool {
- return tool({
- title: 'Set Cell Content',
- description:
- 'Set the content of a specific cell and return both the previous and new content',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- cellId: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'ID of the cell to modify. If provided, takes precedence over cellIndex'
- ),
- cellIndex: z
- .number()
- .optional()
- .nullable()
- .describe(
- 'Index of the cell to modify (0-based). Used if cellId is not provided. If neither is provided, targets the active cell'
- ),
- content: z.string().describe('New content for the cell')
- }),
- execute: async (input: {
- notebookPath?: string | null;
- cellId?: string | null;
- cellIndex?: number | null;
- content: string;
- }) => {
- const { notebookPath, cellId, cellIndex, content } = input;
-
- const notebookWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!notebookWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = notebookWidget.content;
- const targetNotebookPath = notebookWidget.context.path;
-
- const model = notebook.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model available'
- });
- }
-
- // Determine target cell index
- let targetCellIndex: number;
- if (cellId !== undefined && cellId !== null) {
- // Find cell by ID
- targetCellIndex = -1;
- for (let i = 0; i < model.cells.length; i++) {
- if (model.cells.get(i).id === cellId) {
- targetCellIndex = i;
- break;
- }
- }
- if (targetCellIndex === -1) {
- return JSON.stringify({
- success: false,
- error: `Cell with ID '${cellId}' not found in notebook`
- });
- }
- } else if (cellIndex !== undefined && cellIndex !== null) {
- // Use provided cell index
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
- return JSON.stringify({
- success: false,
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
- });
- }
- targetCellIndex = cellIndex;
- } else {
- // Use active cell
- targetCellIndex = notebook.activeCellIndex;
- if (targetCellIndex === -1 || targetCellIndex >= model.cells.length) {
- return JSON.stringify({
- success: false,
- error: 'No active cell or invalid active cell index'
- });
- }
- }
-
- // Get the target cell
- const targetCell = model.cells.get(targetCellIndex);
- if (!targetCell) {
- return JSON.stringify({
- success: false,
- error: `Cell at index ${targetCellIndex} not found`
- });
- }
-
- const sharedModel = targetCell.sharedModel;
-
- // Get previous content and type
- const previousContent = sharedModel.getSource();
- const previousCellType = targetCell.type;
- const retrievedCellId = targetCell.id;
-
- sharedModel.setSource(content);
-
- // Show the cell diff using the diff manager if available
- if (diffManager) {
- await diffManager.showCellDiff({
- original: previousContent,
- modified: content,
- cellId: retrievedCellId,
- notebookPath: targetNotebookPath
- });
- }
-
- return JSON.stringify({
- success: true,
- message:
- cellId !== undefined && cellId !== null
- ? `Cell with ID '${cellId}' content replaced successfully`
- : cellIndex !== undefined && cellIndex !== null
- ? `Cell ${targetCellIndex} content replaced successfully`
- : 'Active cell content replaced successfully',
- notebookPath: targetNotebookPath,
- cellId: retrievedCellId,
- cellIndex: targetCellIndex,
- previousContent,
- previousCellType,
- newContent: content,
- wasActiveCell: cellId === undefined && cellIndex === undefined
- });
- }
- });
-}
-
-/**
- * Create a tool for running a specific cell
- */
-export function createRunCellTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Run Cell',
- description: 'Run a specific cell in the notebook by index',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- cellIndex: z.number().describe('Index of the cell to run (0-based)'),
- recordTiming: z
- .boolean()
- .default(true)
- .describe('Whether to record execution timing')
- }),
- needsApproval: true,
- execute: async (input: {
- notebookPath?: string | null;
- cellIndex: number;
- recordTiming?: boolean;
- }) => {
- const { notebookPath, cellIndex, recordTiming = true } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model available'
- });
- }
-
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
- return JSON.stringify({
- success: false,
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
- });
- }
-
- // Get the target cell widget
- const cellWidget = notebook.widgets[cellIndex];
- if (!cellWidget) {
- return JSON.stringify({
- success: false,
- error: `Cell widget at index ${cellIndex} not found`
- });
- }
-
- // Execute using shared model approach (non-disruptive)
- if (cellWidget instanceof CodeCell) {
- // Use direct CodeCell.execute() method
- const sessionCtx = currentWidget.sessionContext;
- await CodeCell.execute(cellWidget, sessionCtx, {
- recordTiming,
- deletedCells: model.deletedCells
- });
-
- const codeModel = cellWidget.model as ICodeCellModel;
- return JSON.stringify({
- success: true,
- message: `Cell ${cellIndex} executed successfully`,
- cellIndex,
- executionCount: codeModel.executionCount,
- hasOutput: codeModel.outputs.length > 0
- });
- } else {
- // For non-code cells, just return success
- return JSON.stringify({
- success: true,
- message: `Cell ${cellIndex} is not a code cell, no execution needed`,
- cellIndex,
- cellType: cellWidget.model.type
- });
- }
- }
- });
-}
-
-/**
- * Create a tool for deleting a specific cell
- */
-export function createDeleteCellTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Delete Cell',
- description: 'Delete a specific cell from the notebook by index',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- cellIndex: z.number().describe('Index of the cell to delete (0-based)')
- }),
- execute: async (input: {
- notebookPath?: string | null;
- cellIndex: number;
- }) => {
- const { notebookPath, cellIndex } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
-
- if (!model) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model available'
- });
- }
-
- if (cellIndex < 0 || cellIndex >= model.cells.length) {
- return JSON.stringify({
- success: false,
- error: `Invalid cell index: ${cellIndex}. Notebook has ${model.cells.length} cells.`
- });
- }
-
- // Validate cell exists
- const targetCell = model.cells.get(cellIndex);
- if (!targetCell) {
- return JSON.stringify({
- success: false,
- error: `Cell at index ${cellIndex} not found`
- });
- }
-
- // Delete cell using shared model (non-disruptive)
- model.sharedModel.deleteCell(cellIndex);
-
- return JSON.stringify({
- success: true,
- message: `Cell ${cellIndex} deleted successfully`,
- cellIndex,
- remainingCells: model.cells.length
- });
- }
- });
-}
-
-/**
- * Create a tool for executing code in the active cell (non-disruptive alternative to mcp__ide__executeCode)
- */
-export function createExecuteActiveCellTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Execute Active Cell',
- description:
- 'Execute the currently active cell in the notebook without disrupting user focus',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- ),
- code: z
- .string()
- .optional()
- .nullable()
- .describe('Optional: set cell content before executing'),
- recordTiming: z
- .boolean()
- .default(true)
- .describe('Whether to record execution timing')
- }),
- execute: async (input: {
- notebookPath?: string | null;
- code?: string | null;
- recordTiming?: boolean;
- }) => {
- const { notebookPath, code, recordTiming = true } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- const notebook = currentWidget.content;
- const model = notebook.model;
- const activeCellIndex = notebook.activeCellIndex;
-
- if (!model || activeCellIndex === -1) {
- return JSON.stringify({
- success: false,
- error: 'No notebook model or active cell available'
- });
- }
-
- const activeCell = model.cells.get(activeCellIndex);
- if (!activeCell) {
- return JSON.stringify({
- success: false,
- error: 'Active cell not found'
- });
- }
-
- // Set code content if provided
- if (code) {
- activeCell.sharedModel.setSource(code);
- }
-
- // Get the cell widget for execution
- const cellWidget = notebook.widgets[activeCellIndex];
- if (!cellWidget || !(cellWidget instanceof CodeCell)) {
- return JSON.stringify({
- success: false,
- error: 'Active cell is not a code cell'
- });
- }
-
- // Execute using shared model approach (non-disruptive)
- const sessionCtx = currentWidget.sessionContext;
- await CodeCell.execute(cellWidget, sessionCtx, {
- recordTiming,
- deletedCells: model.deletedCells
- });
-
- const codeModel = cellWidget.model as ICodeCellModel;
- return JSON.stringify({
- success: true,
- message: 'Code executed successfully in active cell',
- cellIndex: activeCellIndex,
- executionCount: codeModel.executionCount,
- hasOutput: codeModel.outputs.length > 0,
- code: code || activeCell.sharedModel.getSource()
- });
- }
- });
-}
-
-/**
- * Create a tool for saving a specific notebook
- */
-export function createSaveNotebookTool(
- docManager: IDocumentManager,
- notebookTracker?: INotebookTracker
-): ITool {
- return tool({
- title: 'Save Notebook',
- description: 'Save a specific notebook to disk',
- inputSchema: z.object({
- notebookPath: z
- .string()
- .optional()
- .nullable()
- .describe(
- 'Path to the notebook file. If not provided, uses the currently active notebook'
- )
- }),
- execute: async (input: { notebookPath?: string | null }) => {
- const { notebookPath } = input;
-
- const currentWidget = await getNotebookWidget(
- notebookPath,
- docManager,
- notebookTracker
- );
- if (!currentWidget) {
- return JSON.stringify({
- success: false,
- error: notebookPath
- ? `Failed to open notebook at path: ${notebookPath}`
- : 'No active notebook and no notebook path provided'
- });
- }
-
- await currentWidget.context.save();
-
- return JSON.stringify({
- success: true,
- message: 'Notebook saved successfully',
- notebookName: currentWidget.title.label,
- notebookPath: currentWidget.context.path
- });
- }
- });
-}
diff --git a/style/base.css b/style/base.css
index 75ee8d1..f56cf72 100644
--- a/style/base.css
+++ b/style/base.css
@@ -151,6 +151,17 @@
flex: 1;
}
+.jp-ai-tool-summary {
+ font-weight: 400;
+ opacity: 0.7;
+ font-size: var(--jp-ui-font-size0);
+}
+
+.jp-ai-tool-summary::before {
+ content: ' ';
+ white-space: pre;
+}
+
.jp-ai-tool-status {
font-size: var(--jp-ui-font-size0);
font-weight: 500;
diff --git a/ui-tests/tests/commands-tool.spec.ts b/ui-tests/tests/commands-tool.spec.ts
index d46e6ea..705fc69 100644
--- a/ui-tests/tests/commands-tool.spec.ts
+++ b/ui-tests/tests/commands-tool.spec.ts
@@ -54,10 +54,12 @@ test.describe('#commandsTool', () => {
const toolCall = panel.locator('.jp-ai-tool-call');
await expect(toolCall).toHaveCount(1, { timeout: EXPECT_TIMEOUT });
- // Verify the tool was called
await expect(toolCall).toContainText('discover_commands', {
timeout: EXPECT_TIMEOUT
});
+ await expect(toolCall).toContainText('query: "notebook"', {
+ timeout: EXPECT_TIMEOUT
+ });
// Click to expand the tool call
await toolCall.click();
@@ -110,6 +112,11 @@ test.describe('#commandsTool', () => {
timeout: EXPECT_TIMEOUT
});
+ // Verify no query is displayed when not provided
+ await expect(toolCall).not.toContainText('query:', {
+ timeout: EXPECT_TIMEOUT
+ });
+
// Click to expand the tool call
await toolCall.click();
@@ -124,4 +131,42 @@ test.describe('#commandsTool', () => {
// Should have many commands (typically 400+)
expect(count).toBeGreaterThan(400);
});
+
+ test('should display command name when executing command', async ({
+ page
+ }) => {
+ test.setTimeout(120 * 1000);
+
+ const panel = await openChatPanel(page);
+ const input = panel
+ .locator('.jp-chat-input-container')
+ .getByRole('combobox');
+ const sendButton = panel.locator(
+ '.jp-chat-input-container .jp-chat-send-button'
+ );
+
+ // Prompt to execute a specific command
+ const PROMPT =
+ 'Use the execute_command tool with commandId parameter set to "notebook:create-new" to create a new notebook';
+
+ await input.pressSequentially(PROMPT);
+ await sendButton.click();
+
+ // Wait for AI response
+ await expect(
+ panel.locator('.jp-chat-message-header:has-text("Jupyternaut")')
+ ).toHaveCount(1, { timeout: EXPECT_TIMEOUT });
+
+ // Wait for tool call to appear
+ const toolCall = panel.locator('.jp-ai-tool-call');
+ await expect(toolCall).toHaveCount(1, { timeout: EXPECT_TIMEOUT });
+
+ // Verify the tool was called and the command name is displayed in the summary
+ await expect(toolCall).toContainText('execute_command', {
+ timeout: EXPECT_TIMEOUT
+ });
+ await expect(toolCall).toContainText('notebook:create-new', {
+ timeout: EXPECT_TIMEOUT
+ });
+ });
});
diff --git a/ui-tests/tests/test-utils.ts b/ui-tests/tests/test-utils.ts
index 408229c..251ffeb 100644
--- a/ui-tests/tests/test-utils.ts
+++ b/ui-tests/tests/test-utils.ts
@@ -18,7 +18,10 @@ export const DEFAULT_GENERIC_PROVIDER_SETTINGS = {
name: DEFAULT_MODEL_NAME,
provider: 'generic',
model: 'qwen2.5:0.5b',
- baseURL: 'http://localhost:11434/v1'
+ baseURL: 'http://localhost:11434/v1',
+ parameters: {
+ temperature: 0
+ }
}
],
showTokenUsage: false,