From 474563d8c8eb8f93d44f6c694a15fba37a0e06eb Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Thu, 30 Oct 2025 22:05:01 +0100 Subject: [PATCH 1/6] switch to jupyterlab-ai-commands --- pyproject.toml | 1 + schema/settings-model.json | 2 +- src/agent.ts | 32 +- src/index.ts | 100 +--- src/tools/file.ts | 438 ---------------- src/tools/notebook.ts | 986 ------------------------------------- 6 files changed, 25 insertions(+), 1534 deletions(-) delete mode 100644 src/tools/file.ts delete mode 100644 src/tools/notebook.ts diff --git a/pyproject.toml b/pyproject.toml index 100005d..75f7db0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ classifiers = [ ] dependencies = [ "jupyter-secrets-manager >=0.4,<0.5", + "jupyterlab-ai-commands >=0.1.1,<0.2", "jupyterlab-diff >=0.6.0,<0.7", ] dynamic = ["version", "description", "authors", "urls", "keywords"] diff --git a/schema/settings-model.json b/schema/settings-model.json index 79c5b78..5648793 100644 --- a/schema/settings-model.json +++ b/schema/settings-model.json @@ -162,7 +162,7 @@ "title": "System Prompt", "description": "Instructions that define how the AI should behave and respond", "type": "string", - "default": "You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.\n\n## Your Core Mission\nYou're designed to be a capable partner for data science, research, and development work in Jupyter notebooks. You can help with everything from quick code snippets to complex multi-notebook projects.\n\n## Your Capabilities\n**📁 File & Project Management:**\n- Create, read, edit, and organize Python files and notebooks\n- Manage project structure and navigate file systems\n- Help with version control and project organization\n\n**📊 Notebook Operations:**\n- Create new notebooks and manage existing ones\n- Add, edit, delete, and run cells (both code and markdown)\n- Help with notebook structure and organization\n- Retrieve and analyze cell outputs and execution results\n\n**🧠 Coding & Development:**\n- Write, debug, and optimize Python code\n- Explain complex algorithms and data structures\n- Help with data analysis, visualization, and machine learning\n- Support for scientific computing libraries (numpy, pandas, matplotlib, etc.)\n- Code reviews and best practices recommendations\n\n**💡 Adaptive Assistance:**\n- Understand context from your current work environment\n- Provide suggestions tailored to your specific use case\n- Help with both quick fixes and long-term project planning\n\n## How I Work\nI can actively interact with your JupyterLab environment using specialized tools. When you ask me to perform actions, I can:\n- Execute operations directly in your notebooks\n- Create and modify files as needed\n- Run code and analyze results\n- Make systematic changes across multiple files\n\n## My Approach\n- **Context-aware**: I understand you're working in a data science/research environment\n- **Practical**: I focus on actionable solutions that work in your current setup\n- **Educational**: I explain my reasoning and teach best practices along the way\n- **Collaborative**: Think of me as a pair programming partner, not just a code generator\n\n## Communication Style & Agent Behavior\n- **Conversational**: I maintain a friendly, natural conversation flow throughout our interaction\n- **Progress Updates**: I write brief progress messages between tool uses that appear directly in our conversation\n- **No Filler**: I avoid empty acknowledgments like \"Sounds good!\" or \"Okay, I will...\" - I get straight to work\n- **Purposeful Communication**: I start with what I'm doing, use tools, then share what I found and what's next\n- **Active Narration**: I actively write progress updates like \"Looking at the current code structure...\" or \"Found the issue in the notebook...\" between tool calls\n- **Checkpoint Updates**: After several operations, I summarize what I've accomplished and what remains\n- **Natural Flow**: My explanations and progress reports appear as normal conversation text, not just in tool blocks\n\n## IMPORTANT: Always write progress messages between tools that explain what you're doing and what you found. These should be conversational updates that help the user follow along with your work.\n\n## Technical Communication\n- Code is formatted in proper markdown blocks with syntax highlighting\n- Mathematical notation uses LaTeX formatting: \\\\(equations\\\\) and \\\\[display math\\\\]\n- I provide context for my actions and explain my reasoning as I work\n- When creating or modifying multiple files, I give brief summaries of changes\n- I keep users informed of progress while staying focused on the task\n\n## Multi-Step Task Handling\nWhen users request complex tasks that require multiple steps (like \"create a notebook with example cells\"), I use tools in sequence to accomplish the complete task. For example:\n- First use create_notebook to create the notebook\n- Then use add_code_cell or add_markdown_cell to add cells\n- Use set_cell_content to add content to cells as needed\n- Use run_cell to execute code when appropriate\n\nAlways think through multi-step tasks and use tools to fully complete the user's request rather than stopping after just one action.\n\nReady to help you build something great! What are you working on?" + "default": "You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.\n\n## Your Core Mission\nYou're designed to be a capable partner for data science, research, and development work in Jupyter notebooks. You can help with everything from quick code snippets to complex multi-notebook projects.\n\n## Your Capabilities\n**📁 File & Project Management:**\n- Create, read, edit, and organize Python files and notebooks\n- Manage project structure and navigate file systems\n- Help with version control and project organization\n\n**📊 Notebook Operations:**\n- Create new notebooks and manage existing ones\n- Add, edit, delete, and run cells (both code and markdown)\n- Help with notebook structure and organization\n- Retrieve and analyze cell outputs and execution results\n\n**🧠 Coding & Development:**\n- Write, debug, and optimize Python code\n- Explain complex algorithms and data structures\n- Help with data analysis, visualization, and machine learning\n- Support for scientific computing libraries (numpy, pandas, matplotlib, etc.)\n- Code reviews and best practices recommendations\n\n**💡 Adaptive Assistance:**\n- Understand context from your current work environment\n- Provide suggestions tailored to your specific use case\n- Help with both quick fixes and long-term project planning\n\n## How I Work\nI interact with your JupyterLab environment primarily through the command system:\n- I use 'discover_commands' to find available JupyterLab commands\n- I use 'execute_command' to perform operations\n- For file and notebook operations, I use commands from the jupyterlab-ai-commands extension (prefixed with 'jupyterlab-ai-commands:')\n- These commands provide comprehensive file and notebook manipulation: create, read, edit files/notebooks, manage cells, run code, etc.\n- I can make systematic changes across multiple files and perform complex multi-step operations\n\n## My Approach\n- **Context-aware**: I understand you're working in a data science/research environment\n- **Practical**: I focus on actionable solutions that work in your current setup\n- **Educational**: I explain my reasoning and teach best practices along the way\n- **Collaborative**: Think of me as a pair programming partner, not just a code generator\n\n## Communication Style & Agent Behavior\n- **Conversational**: I maintain a friendly, natural conversation flow throughout our interaction\n- **Progress Updates**: I write brief progress messages between tool uses that appear directly in our conversation\n- **No Filler**: I avoid empty acknowledgments like \"Sounds good!\" or \"Okay, I will...\" - I get straight to work\n- **Purposeful Communication**: I start with what I'm doing, use tools, then share what I found and what's next\n- **Active Narration**: I actively write progress updates like \"Looking at the current code structure...\" or \"Found the issue in the notebook...\" between tool calls\n- **Checkpoint Updates**: After several operations, I summarize what I've accomplished and what remains\n- **Natural Flow**: My explanations and progress reports appear as normal conversation text, not just in tool blocks\n\n## IMPORTANT: Always write progress messages between tools that explain what you're doing and what you found. These should be conversational updates that help the user follow along with your work.\n\n## Technical Communication\n- Code is formatted in proper markdown blocks with syntax highlighting\n- Mathematical notation uses LaTeX formatting: \\\\(equations\\\\) and \\\\[display math\\\\]\n- I provide context for my actions and explain my reasoning as I work\n- When creating or modifying multiple files, I give brief summaries of changes\n- I keep users informed of progress while staying focused on the task\n\n## Multi-Step Task Handling\nWhen users request complex tasks, I use the command system to accomplish them:\n- Use discover_commands to find relevant commands (e.g., query 'notebook', 'file', 'cell')\n- For file and notebook operations, execute jupyterlab-ai-commands: prefixed commands using execute_command\n- For example, to create a notebook with cells:\n 1. discover_commands with query 'notebook' to find available commands\n 2. execute_command with 'jupyterlab-ai-commands:create-notebook' and required arguments\n 3. execute_command with 'jupyterlab-ai-commands:add-cell' multiple times to add cells\n 4. execute_command with 'jupyterlab-ai-commands:set-cell-content' to add content to cells\n 5. execute_command with 'jupyterlab-ai-commands:run-cell' when appropriate\n\nAlways think through multi-step tasks and use commands to fully complete the user's request rather than stopping after just one action.\n\nReady to help you build something great! What are you working on?" }, "completionSystemPrompt": { "title": "Completion System Prompt", diff --git a/src/agent.ts b/src/agent.ts index 514efd9..0da3c7e 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -933,17 +933,27 @@ Guidelines: - End with a brief summary of accomplishments - Use natural, conversational tone throughout -COMMAND DISCOVERY: -- When you want to execute JupyterLab commands, ALWAYS use the 'discover_commands' tool first to find available commands and their metadata, with the optional query parameter. -- The query should typically be a single word, e.g., 'terminal', 'notebook', 'cell', 'file', 'edit', 'view', 'run', etc, to find relevant commands. -- If searching with a query does not yield the desired command, try again with a different query or use an empty query to list all commands. -- This ensures you have complete information about command IDs, descriptions, and required arguments before attempting to execute them. Only after discovering the available commands should you use the 'execute_command' tool with the correct command ID and arguments. - -TOOL SELECTION GUIDELINES: -- For file operations (create, read, write, modify files and directories): Use dedicated file manipulation tools -- For general JupyterLab UI interactions (opening panels, running commands, navigating interface): Use the general command tool (execute_command) -- Examples of file operations: Creating notebooks, editing code files, managing project structure -- Examples of UI interactions: Opening terminal, switching tabs, running notebook cells, accessing menus +PRIMARY TOOL USAGE - COMMAND-BASED OPERATIONS: +Most operations in JupyterLab should be performed using the command system: +1. Use 'discover_commands' to find available commands and their metadata + - The query parameter helps filter commands (e.g., 'notebook', 'file', 'cell', 'terminal') + - Use specific keywords for better results, or omit query to see all commands + - For file and notebook operations, look for commands prefixed with 'jupyterlab-ai-commands:' + +2. Use 'execute_command' to perform the actual operation + - After discovering commands, execute them with the correct command ID and arguments + - File and notebook operations use jupyterlab-ai-commands: prefixed commands + - Other UI operations use standard JupyterLab command IDs + +COMMAND DISCOVERY WORKFLOW: +- ALWAYS use 'discover_commands' first when you need to perform file/notebook operations or JupyterLab actions +- For file and notebook operations, use commands from the jupyterlab-ai-commands extension: + * File operations: jupyterlab-ai-commands:create-file, jupyterlab-ai-commands:open-file, jupyterlab-ai-commands:delete-file, jupyterlab-ai-commands:rename-file, jupyterlab-ai-commands:copy-file, jupyterlab-ai-commands:get-file-info, jupyterlab-ai-commands:set-file-content + * Notebook operations: jupyterlab-ai-commands:create-notebook, jupyterlab-ai-commands:add-cell, jupyterlab-ai-commands:get-notebook-info, jupyterlab-ai-commands:get-cell-info, jupyterlab-ai-commands:set-cell-content, jupyterlab-ai-commands:run-cell, jupyterlab-ai-commands:delete-cell, jupyterlab-ai-commands:save-notebook + * Directory navigation: jupyterlab-ai-commands:navigate-to-directory +- For other UI operations, use standard JupyterLab commands: terminal:create-new, launcher:create, filebrowser:go-to-path, etc. +- The query parameter should typically be a single relevant word to find appropriate commands +- If the first query doesn't find what you need, try alternative keywords or search all commands `; return baseSystemPrompt + progressReportingPrompt; diff --git a/src/index.ts b/src/index.ts index d5abb4c..f888585 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 { @@ -92,29 +88,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 @@ -794,80 +767,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/file.ts b/src/tools/file.ts deleted file mode 100644 index b7fffc5..0000000 --- a/src/tools/file.ts +++ /dev/null @@ -1,438 +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 '@openai/agents'; - -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({ - name: 'create_file', - description: - 'Create a new file of specified type (text, python, markdown, json, etc.)', - parameters: 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)') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to create file: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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({ - name: 'open_file', - description: 'Open a file in the editor', - parameters: z.object({ - filePath: z.string().describe('Path to the file to open') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to open file: ${error instanceof Error ? error.message : String(error)}` - }); - }, - execute: async (input: { filePath: string }) => { - const { filePath } = input; - - const widget = docManager.openOrReveal(filePath); - - if (!widget) { - throw new 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({ - name: 'delete_file', - description: 'Delete a file from the file system', - parameters: z.object({ - filePath: z.string().describe('Path to the file to delete') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to delete file: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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({ - name: 'rename_file', - description: 'Rename a file or move it to a different location', - parameters: z.object({ - oldPath: z.string().describe('Current path of the file'), - newPath: z.string().describe('New path/name for the file') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to rename file: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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({ - name: 'copy_file', - description: 'Copy a file to a new location', - parameters: z.object({ - sourcePath: z.string().describe('Path of the file to copy'), - destinationPath: z - .string() - .describe('Destination path for the copied file') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to copy file: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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({ - name: 'navigate_to_directory', - description: 'Navigate to a specific directory in the file browser', - parameters: z.object({ - directoryPath: z.string().describe('Path to the directory to navigate to') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to navigate to directory: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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({ - name: '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.', - parameters: 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.' - ) - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to get file info: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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) { - throw new Error(`Failed to open file at path: ${filePath}`); - } - } else { - widget = editorTracker?.currentWidget ?? null; - - if (!widget) { - throw new Error( - 'No active file in the editor and no file path provided' - ); - } - } - - if (!widget.context) { - throw new Error('Widget is not a document'); - } - - await widget.context.ready; - - const model = widget.context.model; - - if (!model) { - throw new 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({ - name: '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.', - parameters: 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)') - }), - errorFunction: (context, error) => { - return JSON.stringify({ - success: false, - error: `Failed to set file content: ${error instanceof Error ? error.message : String(error)}` - }); - }, - 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) { - throw new Error(`Failed to open file at path: ${filePath}`); - } - - await widget.context.ready; - - const model = widget.context.model; - - if (!model) { - throw new Error('File model not available'); - } - - if (model.readOnly) { - throw new 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 8a80554..0000000 --- a/src/tools/notebook.ts +++ /dev/null @@ -1,986 +0,0 @@ -import { CodeCell, 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 '@openai/agents'; - -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({ - name: 'create_notebook', - description: - 'Create a new Jupyter notebook with a kernel for the specified programming language', - parameters: 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) { - throw new Error('A name must be provided to create a notebook'); - } - - try { - // 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)) { - throw new 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 - }; - } catch (error) { - return { - success: false, - error: `Failed to create notebook: ${(error as Error).message}` - }; - } - } - }); -} - -/** - * Create a tool for adding cells to a specific notebook - */ -export function createAddCellTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'add_cell', - description: 'Add a cell to the current notebook with optional content', - parameters: 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') - }), - async execute({ - notebookPath, - content, - cellType = 'code', - position = 'below' - }) { - try { - 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 - }; - } catch (error) { - return { - success: false, - error: `Failed to add ${cellType} cell: ${(error as Error).message}` - }; - } - } - }); -} - -/** - * Create a tool for getting notebook information - */ -export function createGetNotebookInfoTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'get_notebook_info', - description: - 'Get information about a notebook including number of cells and active cell index', - parameters: 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; - - try { - 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 - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to get notebook info: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * Create a tool for getting cell information by index - */ -export function createGetCellInfoTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'get_cell_info', - description: - 'Get information about a specific cell including its type, source content, and outputs', - parameters: 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; - try { - 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 any).executionCount : null - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to get cell info: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * Create a tool for setting cell content and type - */ -export function createSetCellContentTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker, - diffManager?: IDiffManager -): ITool { - return tool({ - name: 'set_cell_content', - description: - 'Set the content of a specific cell and return both the previous and new content', - parameters: 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; - - try { - 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 - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to replace cell content: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * Create a tool for running a specific cell - */ -export function createRunCellTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'run_cell', - description: 'Run a specific cell in the notebook by index', - parameters: 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; - - try { - 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) - try { - 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 - }); - } - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to execute cell: ${(error as Error).message}`, - cellIndex - }); - } - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to run cell: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * Create a tool for deleting a specific cell - */ -export function createDeleteCellTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'delete_cell', - description: 'Delete a specific cell from the notebook by index', - parameters: 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; - - try { - 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 - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to delete cell: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * 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({ - name: 'execute_active_cell', - description: - 'Execute the currently active cell in the notebook without disrupting user focus', - parameters: 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; - - try { - 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() - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to execute code: ${(error as Error).message}` - }); - } - } - }); -} - -/** - * Create a tool for saving a specific notebook - */ -export function createSaveNotebookTool( - docManager: IDocumentManager, - notebookTracker?: INotebookTracker -): ITool { - return tool({ - name: 'save_notebook', - description: 'Save a specific notebook to disk', - parameters: 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; - - try { - 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 - }); - } catch (error) { - return JSON.stringify({ - success: false, - error: `Failed to save notebook: ${(error as Error).message}` - }); - } - } - }); -} From b4955aa9304e0f9ba88bde2367dc340485ff89c2 Mon Sep 17 00:00:00 2001 From: Jeremy Tuloup Date: Thu, 30 Oct 2025 22:29:56 +0100 Subject: [PATCH 2/6] improve tool call rendering --- src/chat-model.ts | 73 +++++++++++++++++++++++++++++++++++++++++------ style/base.css | 11 +++++++ 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/src/chat-model.ts b/src/chat-model.ts index f3379e5..f370876 100644 --- a/src/chat-model.ts +++ b/src/chat-model.ts @@ -363,6 +363,34 @@ export class AIChatModel extends AbstractChatModel { } } + /** + * Extracts a human-readable summary from tool input for display in the header. + * @param toolName The name of the tool being called + * @param input The formatted JSON input string + * @returns A short summary string or empty string if none available + */ + private _extractToolSummary(toolName: string, input: string): string { + try { + const parsedInput = JSON.parse(input); + + switch (toolName) { + case 'execute_command': + if (parsedInput.commandId) { + return parsedInput.commandId; + } + break; + case 'discover_commands': + if (parsedInput.query) { + return `query: "${parsedInput.query}"`; + } + break; + } + } catch { + // If parsing fails, return empty string + } + return ''; + } + /** * Handles the start of a tool call execution. * @param event Event containing the tool call start data @@ -371,11 +399,20 @@ export class AIChatModel extends AbstractChatModel { event: IAgentEvent<'tool_call_start'> ): void { const toolCallMessageId = UUID.uuid4(); + const toolSummary = this._extractToolSummary( + event.data.toolName, + event.data.input + ); + + const toolTitleHtml = toolSummary + ? `` + : ``; + const toolCallMessage: IChatMessage = { body: `