@notebook-intelligence/notebook-intelligence
Version:
AI coding assistant for JupyterLab
1,142 lines • 66.4 kB
JavaScript
// 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