UNPKG

@notebook-intelligence/notebook-intelligence

Version:
1,142 lines 66.4 kB
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com> import { IDocumentManager } from '@jupyterlab/docmanager'; import { Dialog, ICommandPalette, MainAreaWidget } from '@jupyterlab/apputils'; import { IMainMenu } from '@jupyterlab/mainmenu'; import { IEditorLanguageRegistry } from '@jupyterlab/codemirror'; import { CodeCell } from '@jupyterlab/cells'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ICompletionProviderManager } from '@jupyterlab/completer'; import { NotebookPanel } from '@jupyterlab/notebook'; import { FileEditorWidget } from '@jupyterlab/fileeditor'; import { IDefaultFileBrowser } from '@jupyterlab/filebrowser'; import { ContentsManager, KernelSpecManager } from '@jupyterlab/services'; import { LabIcon } from '@jupyterlab/ui-components'; import { Menu, Panel, Widget } from '@lumino/widgets'; import { CommandRegistry } from '@lumino/commands'; import { IStatusBar } from '@jupyterlab/statusbar'; import stripAnsi from 'strip-ansi'; import { ChatSidebar, FormInputDialogBody, GitHubCopilotLoginDialogBody, GitHubCopilotStatusBarItem, InlinePromptWidget, RunChatCompletionType } from './chat-sidebar'; import { NBIAPI, GitHubCopilotLoginStatus } from './api'; import { BackendMessageType, GITHUB_COPILOT_PROVIDER_ID, INotebookIntelligence, RequestDataType, TelemetryEventType } from './tokens'; import sparklesSvgstr from '../style/icons/sparkles.svg'; import copilotSvgstr from '../style/icons/copilot.svg'; import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg'; import claudeSvgstr from '../style/icons/claude.svg'; import { applyCodeToSelectionInEditor, cellOutputAsText, compareSelections, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils'; import { UUID } from '@lumino/coreutils'; import * as path from 'path'; import { SettingsPanel } from './components/settings-panel'; var CommandIDs; (function (CommandIDs) { CommandIDs.chatuserInput = 'notebook-intelligence:chat-user-input'; CommandIDs.insertAtCursor = 'notebook-intelligence:insert-at-cursor'; CommandIDs.addCodeAsNewCell = 'notebook-intelligence:add-code-as-new-cell'; CommandIDs.createNewFile = 'notebook-intelligence:create-new-file'; CommandIDs.createNewNotebookFromPython = 'notebook-intelligence:create-new-notebook-from-py'; CommandIDs.renameNotebook = 'notebook-intelligence:rename-notebook'; CommandIDs.addCodeCellToNotebook = 'notebook-intelligence:add-code-cell-to-notebook'; CommandIDs.addMarkdownCellToNotebook = 'notebook-intelligence:add-markdown-cell-to-notebook'; CommandIDs.editorGenerateCode = 'notebook-intelligence:editor-generate-code'; CommandIDs.editorExplainThisCode = 'notebook-intelligence:editor-explain-this-code'; CommandIDs.editorFixThisCode = 'notebook-intelligence:editor-fix-this-code'; CommandIDs.editorExplainThisOutput = 'notebook-intelligence:editor-explain-this-output'; CommandIDs.editorTroubleshootThisOutput = 'notebook-intelligence:editor-troubleshoot-this-output'; CommandIDs.openGitHubCopilotLoginDialog = 'notebook-intelligence:open-github-copilot-login-dialog'; CommandIDs.openConfigurationDialog = 'notebook-intelligence:open-configuration-dialog'; CommandIDs.addMarkdownCellToActiveNotebook = 'notebook-intelligence:add-markdown-cell-to-active-notebook'; CommandIDs.addCodeCellToActiveNotebook = 'notebook-intelligence:add-code-cell-to-active-notebook'; CommandIDs.deleteCellAtIndex = 'notebook-intelligence:delete-cell-at-index'; CommandIDs.insertCellAtIndex = 'notebook-intelligence:insert-cell-at-index'; CommandIDs.getCellTypeAndSource = 'notebook-intelligence:get-cell-type-and-source'; CommandIDs.setCellTypeAndSource = 'notebook-intelligence:set-cell-type-and-source'; CommandIDs.getNumberOfCells = 'notebook-intelligence:get-number-of-cells'; CommandIDs.getCellOutput = 'notebook-intelligence:get-cell-output'; CommandIDs.runCellAtIndex = 'notebook-intelligence:run-cell-at-index'; CommandIDs.getCurrentFileContent = 'notebook-intelligence:get-current-file-content'; CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content'; CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor'; CommandIDs.showFormInputDialog = 'notebook-intelligence:show-form-input-dialog'; CommandIDs.runCommandInTerminal = 'notebook-intelligence:run-command-in-terminal'; })(CommandIDs || (CommandIDs = {})); const DOCUMENT_WATCH_INTERVAL = 1000; const MAX_TOKENS = 4096; const githubCopilotIcon = new LabIcon({ name: 'notebook-intelligence:github-copilot-icon', svgstr: copilotSvgstr }); const sparkleIcon = new LabIcon({ name: 'notebook-intelligence:sparkles-icon', svgstr: sparklesSvgstr }); const claudeIcon = new LabIcon({ name: 'notebook-intelligence:claude-icon', svgstr: claudeSvgstr }); const sparkleWarningIcon = new LabIcon({ name: 'notebook-intelligence:sparkles-warning-icon', svgstr: sparklesWarningSvgstr }); const emptyNotebookContent = { cells: [], metadata: {}, nbformat: 4, nbformat_minor: 5 }; const BACKEND_TELEMETRY_LISTENER_NAME = 'backend-telemetry-listener'; class ActiveDocumentWatcher { static initialize(app, languageRegistry, fileBrowser) { var _a; ActiveDocumentWatcher._languageRegistry = languageRegistry; (_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.connect((_sender, args) => { ActiveDocumentWatcher.watchDocument(args.newValue); }); ActiveDocumentWatcher.activeDocumentInfo.activeWidget = app.shell.currentWidget; ActiveDocumentWatcher.handleWatchDocument(); if (fileBrowser) { const onPathChanged = (model) => { ActiveDocumentWatcher.currentDirectory = model.path; }; fileBrowser.model.pathChanged.connect(onPathChanged); } } static watchDocument(widget) { if (ActiveDocumentWatcher.activeDocumentInfo.activeWidget === widget) { return; } clearInterval(ActiveDocumentWatcher._watchTimer); ActiveDocumentWatcher.activeDocumentInfo.activeWidget = widget; ActiveDocumentWatcher._watchTimer = setInterval(() => { ActiveDocumentWatcher.handleWatchDocument(); }, DOCUMENT_WATCH_INTERVAL); ActiveDocumentWatcher.handleWatchDocument(); } static handleWatchDocument() { var _a, _b, _c, _d, _e, _f, _g; const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo; const previousDocumentInfo = { ...activeDocumentInfo, ...{ activeWidget: null } }; const activeWidget = activeDocumentInfo.activeWidget; if (activeWidget instanceof NotebookPanel) { const np = activeWidget; activeDocumentInfo.filename = np.sessionContext.name; activeDocumentInfo.filePath = np.sessionContext.path; activeDocumentInfo.language = ((_d = (_c = (_b = (_a = np.model) === null || _a === void 0 ? void 0 : _a.sharedModel) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.kernelspec) === null || _d === void 0 ? void 0 : _d.language) || 'python'; const { activeCellIndex, activeCell } = np.content; activeDocumentInfo.activeCellIndex = activeCellIndex; activeDocumentInfo.selection = (_e = activeCell === null || activeCell === void 0 ? void 0 : activeCell.editor) === null || _e === void 0 ? void 0 : _e.getSelection(); } else if (activeWidget) { const dw = activeWidget; const contentsModel = (_f = dw.context) === null || _f === void 0 ? void 0 : _f.contentsModel; if ((contentsModel === null || contentsModel === void 0 ? void 0 : contentsModel.format) === 'text') { const fileName = contentsModel.name; const filePath = contentsModel.path; const language = ActiveDocumentWatcher._languageRegistry.findByMIME(contentsModel.mimetype) || ActiveDocumentWatcher._languageRegistry.findByFileName(fileName); activeDocumentInfo.language = (language === null || language === void 0 ? void 0 : language.name) || 'unknown'; activeDocumentInfo.filename = fileName; activeDocumentInfo.filePath = filePath; if (activeWidget instanceof FileEditorWidget) { const fe = activeWidget; activeDocumentInfo.selection = (_g = fe.content.editor) === null || _g === void 0 ? void 0 : _g.getSelection(); } else { activeDocumentInfo.selection = undefined; } } else { activeDocumentInfo.filename = ''; activeDocumentInfo.filePath = ''; activeDocumentInfo.language = ''; } } if (ActiveDocumentWatcher.documentInfoChanged(previousDocumentInfo, activeDocumentInfo)) { ActiveDocumentWatcher.fireActiveDocumentChangedEvent(); } } static documentInfoChanged(lhs, rhs) { if (!lhs || !rhs) { return true; } return (lhs.filename !== rhs.filename || lhs.filePath !== rhs.filePath || lhs.language !== rhs.language || lhs.activeCellIndex !== rhs.activeCellIndex || !compareSelections(lhs.selection, rhs.selection)); } static getActiveSelectionContent() { var _a, _b; const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo; const activeWidget = activeDocumentInfo.activeWidget; if (activeWidget instanceof NotebookPanel) { const np = activeWidget; const editor = np.content.activeCell.editor; if (isSelectionEmpty(editor.getSelection())) { return getWholeNotebookContent(np); } else { return getSelectionInEditor(editor); } } else if (activeWidget instanceof FileEditorWidget) { const fe = activeWidget; const editor = fe.content.editor; if (isSelectionEmpty(editor.getSelection())) { return editor.model.sharedModel.getSource(); } else { return getSelectionInEditor(editor); } } else { const dw = activeWidget; const content = (_b = (_a = dw === null || dw === void 0 ? void 0 : dw.context) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.toString(); const maxContext = 0.5 * MAX_TOKENS; return content.substring(0, maxContext); } } static getCurrentCellContents() { const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo; const activeWidget = activeDocumentInfo.activeWidget; if (activeWidget instanceof NotebookPanel) { const np = activeWidget; const activeCell = np.content.activeCell; const input = activeCell.model.sharedModel.source.trim(); let output = ''; if (activeCell instanceof CodeCell) { output = cellOutputAsText(np.content.activeCell); } return { input, output }; } return null; } static fireActiveDocumentChangedEvent() { document.dispatchEvent(new CustomEvent('copilotSidebar:activeDocumentChanged', { detail: { activeDocumentInfo: ActiveDocumentWatcher.activeDocumentInfo } })); } } ActiveDocumentWatcher.currentDirectory = ''; ActiveDocumentWatcher.activeDocumentInfo = { language: 'python', filename: 'nb-doesnt-exist.ipynb', filePath: 'nb-doesnt-exist.ipynb', activeWidget: null, activeCellIndex: -1, selection: null }; class NBIInlineCompletionProvider { constructor(telemetryEmitter) { this._lastRequestInfo = null; this._telemetryEmitter = telemetryEmitter; } get schema() { return { default: { debouncerDelay: 200, timeout: 15000 } }; } fetch(request, context) { let preContent = ''; let postContent = ''; const preCursor = request.text.substring(0, request.offset); const postCursor = request.text.substring(request.offset); let language = ActiveDocumentWatcher.activeDocumentInfo.language; let editorType = 'file-editor'; if (context.widget instanceof NotebookPanel) { editorType = 'notebook'; const activeCell = context.widget.content.activeCell; if (activeCell.model.sharedModel.cell_type === 'markdown') { language = 'markdown'; } let activeCellReached = false; for (const cell of context.widget.content.widgets) { const cellModel = cell.model.sharedModel; if (cell === activeCell) { activeCellReached = true; } else if (!activeCellReached) { if (cellModel.cell_type === 'code') { preContent += cellModel.source + '\n'; } else if (cellModel.cell_type === 'markdown') { preContent += markdownToComment(cellModel.source) + '\n'; } } else { if (cellModel.cell_type === 'code') { postContent += cellModel.source + '\n'; } else if (cellModel.cell_type === 'markdown') { postContent += markdownToComment(cellModel.source) + '\n'; } } } } const nbiConfig = NBIAPI.config; const inlineCompletionsEnabled = nbiConfig.isInClaudeCodeMode || (nbiConfig.inlineCompletionModel.provider === GITHUB_COPILOT_PROVIDER_ID ? NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.LoggedIn : nbiConfig.inlineCompletionModel.provider !== 'none'); this._telemetryEmitter.emitTelemetryEvent({ type: TelemetryEventType.InlineCompletionRequest, data: { inlineCompletionModel: { provider: NBIAPI.config.inlineCompletionModel.provider, model: NBIAPI.config.inlineCompletionModel.model }, editorType } }); return new Promise((resolve, reject) => { const items = []; if (!inlineCompletionsEnabled) { resolve({ items }); return; } if (this._lastRequestInfo) { NBIAPI.sendWebSocketMessage(this._lastRequestInfo.messageId, RequestDataType.CancelInlineCompletionRequest, { chatId: this._lastRequestInfo.chatId }); } const messageId = UUID.uuid4(); const chatId = UUID.uuid4(); this._lastRequestInfo = { chatId, messageId, requestTime: new Date() }; NBIAPI.inlineCompletionsRequest(chatId, messageId, preContent + preCursor, postCursor + postContent, language, ActiveDocumentWatcher.activeDocumentInfo.filename, { emit: (response) => { if (response.type === BackendMessageType.StreamMessage && response.id === this._lastRequestInfo.messageId) { items.push({ insertText: response.data.completions }); const timeElapsed = (new Date().getTime() - this._lastRequestInfo.requestTime.getTime()) / 1000; this._telemetryEmitter.emitTelemetryEvent({ type: TelemetryEventType.InlineCompletionResponse, data: { inlineCompletionModel: { provider: NBIAPI.config.inlineCompletionModel.provider, model: NBIAPI.config.inlineCompletionModel.model }, timeElapsed } }); resolve({ items }); } else { reject(); } } }); }); } get name() { return 'Notebook Intelligence'; } get identifier() { return '@notebook-intelligence/notebook-intelligence'; } get icon() { return NBIAPI.config.isInClaudeCodeMode ? claudeIcon : NBIAPI.config.usingGitHubCopilotModel ? githubCopilotIcon : sparkleIcon; } } class TelemetryEmitter { constructor() { this._listeners = new Set(); } registerTelemetryListener(listener) { const listenerName = listener.name; if (listenerName !== BACKEND_TELEMETRY_LISTENER_NAME) { console.warn(`Notebook Intelligence telemetry listener '${listenerName}' registered. Make sure it is from a trusted source.`); } let listenerAlreadyExists = false; this._listeners.forEach(existingListener => { if (existingListener.name === listenerName) { listenerAlreadyExists = true; } }); if (listenerAlreadyExists) { console.error(`Notebook Intelligence telemetry listener '${listenerName}' already exists!`); return; } this._listeners.add(listener); } unregisterTelemetryListener(listener) { this._listeners.delete(listener); } emitTelemetryEvent(event) { this._listeners.forEach(listener => { listener.onTelemetryEvent(event); }); } } class MCPConfigEditor { constructor(docManager) { this._docWidget = null; this._tmpMCPConfigFilename = 'nbi.mcp.temp.json'; this._isOpen = false; this._docManager = docManager; } async open() { const contents = new ContentsManager(); const newJSONFile = await contents.newUntitled({ ext: '.json' }); const mcpConfig = await NBIAPI.getMCPConfigFile(); try { await contents.delete(this._tmpMCPConfigFilename); } catch (error) { // ignore } await contents.save(newJSONFile.path, { content: JSON.stringify(mcpConfig, null, 2), format: 'text', type: 'file' }); await contents.rename(newJSONFile.path, this._tmpMCPConfigFilename); this._docWidget = this._docManager.openOrReveal(this._tmpMCPConfigFilename, 'Editor'); this._addListeners(); // tab closed this._docWidget.disposed.connect((_, args) => { this._removeListeners(); contents.delete(this._tmpMCPConfigFilename); }); this._isOpen = true; } close() { if (!this._isOpen) { return; } this._isOpen = false; this._docWidget.dispose(); this._docWidget = null; } get isOpen() { return this._isOpen; } _addListeners() { this._docWidget.context.model.stateChanged.connect(this._onStateChanged, this); } _removeListeners() { this._docWidget.context.model.stateChanged.disconnect(this._onStateChanged, this); } _onStateChanged(model, args) { if (args.name === 'dirty' && args.newValue === false) { this._onSave(); } } async _onSave() { const mcpConfig = this._docWidget.context.model.toJSON(); await NBIAPI.setMCPConfigFile(mcpConfig); await NBIAPI.fetchCapabilities(); } } /** * Initialization data for the @notebook-intelligence/notebook-intelligence extension. */ const plugin = { id: '@notebook-intelligence/notebook-intelligence:plugin', description: 'Notebook Intelligence', autoStart: true, requires: [ ICompletionProviderManager, IDocumentManager, IDefaultFileBrowser, IEditorLanguageRegistry, ICommandPalette, IMainMenu ], optional: [ISettingRegistry, IStatusBar], provides: INotebookIntelligence, activate: async (app, completionManager, docManager, defaultBrowser, languageRegistry, palette, mainMenu, settingRegistry, statusBar) => { console.log('JupyterLab extension @notebook-intelligence/notebook-intelligence is activated!'); const telemetryEmitter = new TelemetryEmitter(); telemetryEmitter.registerTelemetryListener({ name: BACKEND_TELEMETRY_LISTENER_NAME, onTelemetryEvent: event => { NBIAPI.emitTelemetryEvent(event); } }); const extensionService = { registerTelemetryListener: (listener) => { telemetryEmitter.registerTelemetryListener(listener); }, unregisterTelemetryListener: (listener) => { telemetryEmitter.unregisterTelemetryListener(listener); } }; await NBIAPI.initialize(); let openPopover = null; let mcpConfigEditor = null; completionManager.registerInlineProvider(new NBIInlineCompletionProvider(telemetryEmitter)); if (settingRegistry) { settingRegistry .load(plugin.id) .then(settings => { // }) .catch(reason => { console.error('Failed to load settings for @notebook-intelligence/notebook-intelligence.', reason); }); } const waitForFileToBeActive = async (filePath) => { const isNotebook = filePath.endsWith('.ipynb'); return new Promise((resolve, reject) => { const checkIfActive = () => { const activeFilePath = ActiveDocumentWatcher.activeDocumentInfo.filePath; const filePathToCheck = filePath; const currentWidget = app.shell.currentWidget; if (activeFilePath === filePathToCheck && ((isNotebook && currentWidget instanceof NotebookPanel && currentWidget.content.activeCell && currentWidget.content.activeCell.node.contains(document.activeElement)) || (!isNotebook && currentWidget instanceof FileEditorWidget && currentWidget.content.editor.hasFocus()))) { resolve(true); } else { setTimeout(checkIfActive, 200); } }; checkIfActive(); waitForDuration(10000).then(() => { resolve(false); }); }); }; const panel = new Panel(); panel.id = 'notebook-intelligence-tab'; panel.title.caption = 'Notebook Intelligence'; const sidebarIcon = new LabIcon({ name: 'notebook-intelligence:sidebar-icon', svgstr: sparklesSvgstr }); panel.title.icon = sidebarIcon; const sidebar = new ChatSidebar({ getCurrentDirectory: () => { return ActiveDocumentWatcher.currentDirectory; }, getActiveDocumentInfo: () => { return ActiveDocumentWatcher.activeDocumentInfo; }, getActiveSelectionContent: () => { return ActiveDocumentWatcher.getActiveSelectionContent(); }, getCurrentCellContents: () => { return ActiveDocumentWatcher.getCurrentCellContents(); }, openFile: (path) => { docManager.openOrReveal(path); }, getApp() { return app; }, getTelemetryEmitter() { return telemetryEmitter; } }); panel.addWidget(sidebar); app.shell.add(panel, 'right', { rank: 1000 }); app.shell.activateById(panel.id); const updateSidebarIcon = () => { if (NBIAPI.getChatEnabled()) { panel.title.icon = sidebarIcon; } else { panel.title.icon = sparkleWarningIcon; } }; NBIAPI.githubLoginStatusChanged.connect((_, args) => { updateSidebarIcon(); }); NBIAPI.configChanged.connect((_, args) => { updateSidebarIcon(); }); setTimeout(() => { updateSidebarIcon(); }, 2000); app.commands.addCommand(CommandIDs.chatuserInput, { execute: args => { NBIAPI.sendChatUserInput(args.id, args.data); } }); app.commands.addCommand(CommandIDs.insertAtCursor, { execute: args => { const currentWidget = app.shell.currentWidget; if (currentWidget instanceof NotebookPanel) { const activeCell = currentWidget.content.activeCell; if (activeCell) { applyCodeToSelectionInEditor(activeCell.editor, args.code); return; } } else if (currentWidget instanceof FileEditorWidget) { applyCodeToSelectionInEditor(currentWidget.content.editor, args.code); return; } app.commands.execute('apputils:notify', { message: 'Failed to insert at cursor. Open a notebook or file to insert the code.', type: 'error', options: { autoClose: true } }); } }); app.commands.addCommand(CommandIDs.addCodeAsNewCell, { execute: args => { var _a; const currentWidget = app.shell.currentWidget; if (currentWidget instanceof NotebookPanel) { let activeCellIndex = currentWidget.content.activeCellIndex; activeCellIndex = activeCellIndex === -1 ? currentWidget.content.widgets.length : activeCellIndex + 1; (_a = currentWidget.model) === null || _a === void 0 ? void 0 : _a.sharedModel.insertCell(activeCellIndex, { cell_type: 'code', metadata: { trusted: true }, source: args.code }); currentWidget.content.activeCellIndex = activeCellIndex; } else { app.commands.execute('apputils:notify', { message: 'Open a notebook to insert the code as new cell', type: 'error', options: { autoClose: true } }); } } }); app.commands.addCommand(CommandIDs.createNewFile, { execute: async (args) => { const contents = new ContentsManager(); const newPyFile = await contents.newUntitled({ ext: '.py', path: defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path }); contents.save(newPyFile.path, { content: extractLLMGeneratedCode(args.code), format: 'text', type: 'file' }); docManager.openOrReveal(newPyFile.path); await waitForFileToBeActive(newPyFile.path); return newPyFile; } }); app.commands.addCommand(CommandIDs.showFormInputDialog, { execute: async (args) => { const title = args.title; const fields = args.fields; return new Promise((resolve, reject) => { let dialog = null; const dialogBody = new FormInputDialogBody({ fields: fields, onDone: (formData) => { dialog.dispose(); resolve(formData); } }); dialog = new Dialog({ title: title, hasClose: true, body: dialogBody, buttons: [] }); dialog .launch() .then((result) => { reject(); }) .catch(() => { reject(new Error('Failed to show form input dialog')); }); }); } }); app.commands.addCommand(CommandIDs.createNewNotebookFromPython, { execute: async (args) => { var _a; let pythonKernelSpec = null; const contents = new ContentsManager(); const kernels = new KernelSpecManager(); await kernels.ready; const kernelspecs = (_a = kernels.specs) === null || _a === void 0 ? void 0 : _a.kernelspecs; if (kernelspecs) { for (const key in kernelspecs) { const kernelspec = kernelspecs[key]; if ((kernelspec === null || kernelspec === void 0 ? void 0 : kernelspec.language) === 'python') { pythonKernelSpec = kernelspec; break; } } } const newNBFile = await contents.newUntitled({ ext: '.ipynb', path: defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path }); const nbFileContent = structuredClone(emptyNotebookContent); if (pythonKernelSpec) { nbFileContent.metadata = { kernelspec: { language: 'python', name: pythonKernelSpec.name, display_name: pythonKernelSpec.display_name } }; } if (args.code) { nbFileContent.cells.push({ cell_type: 'code', metadata: { trusted: true }, source: [args.code], outputs: [] }); } contents.save(newNBFile.path, { content: nbFileContent, format: 'json', type: 'notebook' }); docManager.openOrReveal(newNBFile.path); await waitForFileToBeActive(newNBFile.path); return newNBFile; } }); app.commands.addCommand(CommandIDs.renameNotebook, { execute: async (args) => { const activeWidget = app.shell.currentWidget; if (activeWidget instanceof NotebookPanel) { const oldPath = activeWidget.context.path; const oldParentPath = path.dirname(oldPath); let newPath = path.join(oldParentPath, args.newName); if (path.extname(newPath) !== '.ipynb') { newPath += '.ipynb'; } if (path.dirname(newPath) !== oldParentPath) { return 'Failed to rename notebook. New path is outside the old parent directory'; } try { await app.serviceManager.contents.rename(oldPath, newPath); return 'Successfully renamed notebook'; } catch (error) { return `Failed to rename notebook: ${error}`; } } else { return 'Cannot rename non notebook files'; } } }); app.commands.addCommand(CommandIDs.runCommandInTerminal, { execute: async (args) => { var _a; const command = args.command; const terminal = await app.commands.execute('terminal:create-new', { cwd: args.cwd || ActiveDocumentWatcher.currentDirectory }); const session = (_a = terminal === null || terminal === void 0 ? void 0 : terminal.content) === null || _a === void 0 ? void 0 : _a.session; if (!session) { return 'Failed to execute command in Jupyter terminal'; } return new Promise((resolve, reject) => { let lastMessageReceivedTime = Date.now(); let lastMessageCheckInterval = null; const messageCheckTimeout = 5000; const messageCheckInterval = 1000; let output = ''; session.messageReceived.connect((sender, message) => { const content = stripAnsi(message.content.join('')); output += content; lastMessageReceivedTime = Date.now(); }); session.send({ type: 'stdin', content: [command + '\n'] // Add newline to execute the command }); // wait for the messageCheckInterval and if no message received, return the output. // otherwise wait for the next message. lastMessageCheckInterval = setInterval(() => { if (Date.now() - lastMessageReceivedTime > messageCheckTimeout) { clearInterval(lastMessageCheckInterval); resolve(`Command executed in Jupyter terminal, output: ${output}`); } }, messageCheckInterval); }); } }); const isNewEmptyNotebook = (model) => { return (model.cells.length === 1 && model.cells[0].cell_type === 'code' && model.cells[0].source === ''); }; const githubLoginRequired = () => { return (NBIAPI.config.usingGitHubCopilotModel && NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn); }; const isChatEnabled = () => { return (NBIAPI.config.isInClaudeCodeMode || (NBIAPI.config.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID ? !githubLoginRequired() : NBIAPI.config.chatModel.provider !== 'none')); }; const isActiveCellCodeCell = () => { if (!(app.shell.currentWidget instanceof NotebookPanel)) { return false; } const np = app.shell.currentWidget; const activeCell = np.content.activeCell; return activeCell instanceof CodeCell; }; const isCurrentWidgetFileEditor = () => { return app.shell.currentWidget instanceof FileEditorWidget; }; const addCellToNotebook = (filePath, cellType, source) => { const currentWidget = app.shell.currentWidget; const notebookOpen = currentWidget instanceof NotebookPanel && currentWidget.sessionContext.path === filePath && currentWidget.model; if (!notebookOpen) { app.commands.execute('apputils:notify', { message: `Failed to access the notebook: ${filePath}`, type: 'error', options: { autoClose: true } }); return false; } const model = currentWidget.model.sharedModel; const newCellIndex = isNewEmptyNotebook(model) ? 0 : model.cells.length - 1; model.insertCell(newCellIndex, { cell_type: cellType, metadata: { trusted: true }, source }); return true; }; app.commands.addCommand(CommandIDs.addCodeCellToNotebook, { execute: args => { return addCellToNotebook(args.path, 'code', args.code); } }); app.commands.addCommand(CommandIDs.addMarkdownCellToNotebook, { execute: args => { return addCellToNotebook(args.path, 'markdown', args.markdown); } }); const ensureANotebookIsActive = () => { const currentWidget = app.shell.currentWidget; const notebookOpen = currentWidget instanceof NotebookPanel && currentWidget.model; if (!notebookOpen) { app.commands.execute('apputils:notify', { message: 'Failed to find active notebook', type: 'error', options: { autoClose: true } }); return false; } return true; }; const ensureAFileEditorIsActive = () => { const currentWidget = app.shell.currentWidget; const textFileOpen = currentWidget instanceof FileEditorWidget; if (!textFileOpen) { app.commands.execute('apputils:notify', { message: 'Failed to find active file', type: 'error', options: { autoClose: true } }); return false; } return true; }; app.commands.addCommand(CommandIDs.addMarkdownCellToActiveNotebook, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; const newCellIndex = isNewEmptyNotebook(model) ? 0 : model.cells.length - 1; model.insertCell(newCellIndex, { cell_type: 'markdown', metadata: { trusted: true }, source: args.source }); return true; } }); app.commands.addCommand(CommandIDs.addCodeCellToActiveNotebook, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; const newCellIndex = isNewEmptyNotebook(model) ? 0 : model.cells.length - 1; model.insertCell(newCellIndex, { cell_type: 'code', metadata: { trusted: true }, source: args.source }); return true; } }); app.commands.addCommand(CommandIDs.getCellTypeAndSource, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; return { type: model.cells[args.cellIndex].cell_type, source: model.cells[args.cellIndex].source }; } }); app.commands.addCommand(CommandIDs.setCellTypeAndSource, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; const cellIndex = args.cellIndex; const cellType = args.cellType; const cell = model.getCell(cellIndex); model.deleteCell(cellIndex); model.insertCell(cellIndex, { cell_type: cellType, metadata: cell.metadata, source: args.source }); return true; } }); app.commands.addCommand(CommandIDs.getNumberOfCells, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; return model.cells.length; } }); app.commands.addCommand(CommandIDs.getCellOutput, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const cellIndex = args.cellIndex; const cell = np.content.widgets[cellIndex]; if (!(cell instanceof CodeCell)) { return ''; } const content = cellOutputAsText(cell); return content; } }); app.commands.addCommand(CommandIDs.insertCellAtIndex, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; const cellIndex = args.cellIndex; const cellType = args.cellType; model.insertCell(cellIndex, { cell_type: cellType, metadata: { trusted: true }, source: args.source }); return true; } }); app.commands.addCommand(CommandIDs.deleteCellAtIndex, { execute: args => { if (!ensureANotebookIsActive()) { return false; } const np = app.shell.currentWidget; const model = np.model.sharedModel; const cellIndex = args.cellIndex; model.deleteCell(cellIndex); return true; } }); app.commands.addCommand(CommandIDs.runCellAtIndex, { execute: async (args) => { if (!ensureANotebookIsActive()) { return false; } const currentWidget = app.shell.currentWidget; currentWidget.content.activeCellIndex = args.cellIndex; await app.commands.execute('notebook:run-cell'); } }); app.commands.addCommand(CommandIDs.getCurrentFileContent, { execute: async (args) => { if (!ensureAFileEditorIsActive()) { return false; } const currentWidget = app.shell.currentWidget; const editor = currentWidget.content.editor; return editor.model.sharedModel.getSource(); } }); app.commands.addCommand(CommandIDs.setCurrentFileContent, { execute: async (args) => { if (!ensureAFileEditorIsActive()) { return false; } const currentWidget = app.shell.currentWidget; const editor = currentWidget.content.editor; editor.model.sharedModel.setSource(args.content); return editor.model.sharedModel.getSource(); } }); app.commands.addCommand(CommandIDs.openGitHubCopilotLoginDialog, { execute: args => { let dialog = null; const dialogBody = new GitHubCopilotLoginDialogBody({ onLoggedIn: () => dialog === null || dialog === void 0 ? void 0 : dialog.dispose() }); dialog = new Dialog({ title: 'GitHub Copilot Status', hasClose: true, body: dialogBody, buttons: [] }); dialog.launch(); } }); const createNewSettingsWidget = () => { const settingsPanel = new SettingsPanel({ onSave: () => { NBIAPI.fetchCapabilities(); }, onEditMCPConfigClicked: () => { app.commands.execute('notebook-intelligence:open-mcp-config-editor'); } }); const widget = new MainAreaWidget({ content: settingsPanel }); widget.id = 'nbi-settings'; widget.title.label = 'NBI Settings'; widget.title.closable = true; return widget; }; let settingsWidget = createNewSettingsWidget(); app.commands.addCommand(CommandIDs.openConfigurationDialog, { label: 'Notebook Intelligence Settings', execute: args => { if (settingsWidget.isDisposed) { settingsWidget = createNewSettingsWidget(); } if (!settingsWidget.isAttached) { app.shell.add(settingsWidget, 'main'); } app.shell.activateById(settingsWidget.id); } }); app.commands.addCommand(CommandIDs.openMCPConfigEditor, { label: 'Open MCP Config Editor', execute: args => { if (mcpConfigEditor && mcpConfigEditor.isOpen) { mcpConfigEditor.close(); } mcpConfigEditor = new MCPConfigEditor(docManager); mcpConfigEditor.open(); } }); palette.addItem({ command: CommandIDs.openConfigurationDialog, category: 'Notebook Intelligence' }); mainMenu.settingsMenu.addGroup([ { command: CommandIDs.openConfigurationDialog } ]); const getPrefixAndSuffixForActiveCell = () => { let prefix = ''; let suffix = ''; const currentWidget = app.shell.currentWidget; if (!(currentWidget instanceof NotebookPanel && currentWidget.content.activeCell)) { return { prefix, suffix }; } const activeCellIndex = currentWidget.content.activeCellIndex; const numCells = currentWidget.content.widgets.length; const maxContext = 0.7 * MAX_TOKENS; for (let d = 1; d < numCells; ++d) { const above = activeCellIndex - d; const below = activeCellIndex + d; if ((above < 0 && below >= numCells) || getTokenCount(`${prefix} ${suffix}`) >= maxContext) { break; } if (above >= 0) { const aboveCell = currentWidget.content.widgets[above]; const cellModel = aboveCell.model.sharedModel; if (cellModel.cell_type === 'code') { prefix = cellModel.source + '\n' + prefix; } else if (cellModel.cell_type === 'markdown') { prefix = markdownToComment(cellModel.source) + '\n' + prefix; } } if (below < numCells) { const belowCell = currentWidget.content.widgets[below]; const cellModel = belowCell.model.sharedModel; if (cellModel.cell_type === 'code') { suffix += cellModel.source + '\n'; } else if (cellModel.cell_type === 'markdown') { suffix += markdownToComment(cellModel.source) + '\n'; } } } return { prefix, suffix }; }; const getPrefixAndSuffixForFileEditor = () => { let prefix = ''; let suffix = ''; const currentWidget = app.shell.currentWidget; if (!(currentWidget instanceof FileEditorWidget)) { return { prefix, suffix }; } const fe = currentWidget; const cursor = fe.content.editor.getCursorPosition(); const offset = fe.content.editor.getOffsetAt(cursor); const source = fe.content.editor.model.sharedModel.getSource(); prefix = source.substring(0, offset); suffix = source.substring(offset); return { prefix, suffix }; }; const generateCodeForCellOrFileEditor = () => { const isCodeCell = isActiveCellCodeCell(); const curr