@notebook-intelligence/notebook-intelligence
Version:
AI coding assistant for JupyterLab
1,017 lines • 89.8 kB
JavaScript
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
import { ReactWidget } from '@jupyterlab/apputils';
import { UUID } from '@lumino/coreutils';
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
import { NBIAPI, GitHubCopilotLoginStatus } from './api';
import { BackendMessageType, BuiltinToolsetType, CLAUDE_CODE_CHAT_PARTICIPANT_ID, ContextType, RequestDataType, ResponseStreamDataType, TelemetryEventType } from './tokens';
import { MarkdownRenderer as OriginalMarkdownRenderer } from './markdown-renderer';
const MarkdownRenderer = memo(OriginalMarkdownRenderer);
import copySvgstr from '../style/icons/copy.svg';
import copilotSvgstr from '../style/icons/copilot.svg';
import copilotWarningSvgstr from '../style/icons/copilot-warning.svg';
import { VscSend, VscStopCircle, VscEye, VscEyeClosed, VscTriangleRight, VscTriangleDown, VscSettingsGear, VscPassFilled, VscTools, VscTrash } from 'react-icons/vsc';
import { extractLLMGeneratedCode, isDarkTheme } from './utils';
import { CheckBoxItem } from './components/checkbox';
import { mcpServerSettingsToEnabledState } from './components/mcp-util';
import claudeSvgStr from '../style/icons/claude.svg';
import { AskUserQuestion } from './components/ask-user-question';
export var RunChatCompletionType;
(function (RunChatCompletionType) {
RunChatCompletionType[RunChatCompletionType["Chat"] = 0] = "Chat";
RunChatCompletionType[RunChatCompletionType["ExplainThis"] = 1] = "ExplainThis";
RunChatCompletionType[RunChatCompletionType["FixThis"] = 2] = "FixThis";
RunChatCompletionType[RunChatCompletionType["GenerateCode"] = 3] = "GenerateCode";
RunChatCompletionType[RunChatCompletionType["ExplainThisOutput"] = 4] = "ExplainThisOutput";
RunChatCompletionType[RunChatCompletionType["TroubleshootThisOutput"] = 5] = "TroubleshootThisOutput";
})(RunChatCompletionType || (RunChatCompletionType = {}));
export class ChatSidebar extends ReactWidget {
constructor(options) {
super();
this._options = options;
this.node.style.height = '100%';
}
render() {
return (React.createElement(SidebarComponent, { getCurrentDirectory: this._options.getCurrentDirectory, getActiveDocumentInfo: this._options.getActiveDocumentInfo, getActiveSelectionContent: this._options.getActiveSelectionContent, getCurrentCellContents: this._options.getCurrentCellContents, openFile: this._options.openFile, getApp: this._options.getApp, getTelemetryEmitter: this._options.getTelemetryEmitter }));
}
}
export class InlinePromptWidget extends ReactWidget {
constructor(rect, options) {
super();
this.node.classList.add('inline-prompt-widget');
this.node.style.top = `${rect.top + 32}px`;
this.node.style.left = `${rect.left}px`;
this.node.style.width = rect.width + 'px';
this.node.style.height = '48px';
this._options = options;
this.node.addEventListener('focusout', (event) => {
if (this.node.contains(event.relatedTarget)) {
return;
}
this._options.onRequestCancelled();
});
}
updatePosition(rect) {
this.node.style.top = `${rect.top + 32}px`;
this.node.style.left = `${rect.left}px`;
this.node.style.width = rect.width + 'px';
}
_onResponse(response) {
var _a, _b, _c, _d, _e;
if (response.type === BackendMessageType.StreamMessage) {
const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
if (!delta) {
return;
}
const responseMessage = (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
if (!responseMessage) {
return;
}
this._options.onContentStream(responseMessage);
}
else if (response.type === BackendMessageType.StreamEnd) {
this._options.onContentStreamEnd();
const timeElapsed = (new Date().getTime() - this._requestTime.getTime()) / 1000;
this._options.telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.InlineChatResponse,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
timeElapsed
}
});
}
}
_onRequestSubmitted(prompt) {
// code update
if (this._options.existingCode !== '') {
this.node.style.height = '300px';
}
// save the prompt in case of a rerender
this._options.prompt = prompt;
this._options.onRequestSubmitted(prompt);
this._requestTime = new Date();
this._options.telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.InlineChatRequest,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
prompt: prompt
}
});
}
render() {
return (React.createElement(InlinePopoverComponent, { prompt: this._options.prompt, existingCode: this._options.existingCode, onRequestSubmitted: this._onRequestSubmitted.bind(this), onRequestCancelled: this._options.onRequestCancelled, onResponseEmit: this._onResponse.bind(this), prefix: this._options.prefix, suffix: this._options.suffix, onUpdatedCodeChange: this._options.onUpdatedCodeChange, onUpdatedCodeAccepted: this._options.onUpdatedCodeAccepted }));
}
}
export class GitHubCopilotStatusBarItem extends ReactWidget {
constructor(options) {
super();
this._getApp = options.getApp;
}
render() {
return React.createElement(GitHubCopilotStatusComponent, { getApp: this._getApp });
}
}
export class GitHubCopilotLoginDialogBody extends ReactWidget {
constructor(options) {
super();
this._onLoggedIn = options.onLoggedIn;
}
render() {
return (React.createElement(GitHubCopilotLoginDialogBodyComponent, { onLoggedIn: () => this._onLoggedIn() }));
}
}
const answeredForms = new Map();
function ChatResponseHTMLFrame(props) {
const iframSrc = useMemo(() => URL.createObjectURL(new Blob([props.source], { type: 'text/html' })), []);
return (React.createElement("div", { className: "chat-response-html-frame", key: `key-${props.index}` },
React.createElement("iframe", { className: "chat-response-html-frame-iframe", height: props.height, sandbox: "allow-scripts", src: iframSrc })));
}
// Memoize ChatResponse for performance
function ChatResponse(props) {
var _a, _b, _c;
const [renderCount, setRenderCount] = useState(0);
const msg = props.message;
const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
const openNotebook = (event) => {
const notebookPath = event.target.dataset['ref'];
props.openFile(notebookPath);
};
const markFormConfirmed = (contentId) => {
answeredForms.set(contentId, 'confirmed');
setRenderCount(prev => prev + 1);
};
const markFormCanceled = (contentId) => {
answeredForms.set(contentId, 'canceled');
setRenderCount(prev => prev + 1);
};
const runCommand = (commandId, args) => {
props.getApp().commands.execute(commandId, args);
};
// group messages by type
const groupedContents = [];
let lastItemType;
const responseDetailTags = [
'<think>',
'</think>',
'<terminal-output>',
'</terminal-output>'
];
const extractReasoningContent = (item) => {
let currentContent = item.content;
if (typeof currentContent !== 'string') {
return false;
}
let reasoningContent = '';
let reasoningStartTime = new Date();
const reasoningEndTime = new Date();
let startPos = -1;
let startTag = '';
for (const tag of responseDetailTags) {
startPos = currentContent.indexOf(tag);
if (startPos >= 0) {
startTag = tag;
break;
}
}
const hasStart = startPos >= 0;
reasoningStartTime = new Date(item.created);
if (hasStart) {
currentContent = currentContent.substring(startPos + startTag.length);
}
let endPos = -1;
for (const tag of responseDetailTags) {
endPos = currentContent.indexOf(tag);
if (endPos >= 0) {
break;
}
}
const hasEnd = endPos >= 0;
if (hasEnd) {
reasoningContent += currentContent.substring(0, endPos);
currentContent = currentContent.substring(endPos + startTag.length);
}
else {
if (hasStart) {
reasoningContent += currentContent;
currentContent = '';
}
}
item.content = currentContent;
item.reasoningTag = startTag;
item.reasoningContent = reasoningContent;
item.reasoningFinished = hasEnd;
item.reasoningTime =
(reasoningEndTime.getTime() - reasoningStartTime.getTime()) / 1000;
return hasStart && !hasEnd; // is thinking
};
for (let i = 0; i < msg.contents.length; i++) {
const item = msg.contents[i];
if (item.type === lastItemType &&
lastItemType === ResponseStreamDataType.MarkdownPart) {
const lastItem = groupedContents[groupedContents.length - 1];
lastItem.content += item.content;
}
else {
groupedContents.push(structuredClone(item));
lastItemType = item.type;
}
}
const [thinkingInProgress, setThinkingInProgress] = useState(false);
for (const item of groupedContents) {
const isThinking = extractReasoningContent(item);
if (isThinking && !thinkingInProgress) {
setThinkingInProgress(true);
}
}
useEffect(() => {
let intervalId = undefined;
if (thinkingInProgress) {
intervalId = setInterval(() => {
setRenderCount(prev => prev + 1);
setThinkingInProgress(false);
}, 1000);
}
return () => clearInterval(intervalId);
}, [thinkingInProgress]);
const onExpandCollapseClick = (event) => {
const parent = event.currentTarget.parentElement;
if (parent.classList.contains('expanded')) {
parent.classList.remove('expanded');
}
else {
parent.classList.add('expanded');
}
};
const getReasoningTitle = (item) => {
if (item.reasoningTag === '<think>') {
return item.reasoningFinished
? 'Thought'
: `Thinking (${Math.floor(item.reasoningTime)} s)`;
}
else if (item.reasoningTag === '<terminal-output>') {
return item.reasoningFinished
? 'Output'
: `Running (${Math.floor(item.reasoningTime)} s)`;
}
return item.reasoningFinished
? 'Output'
: `Output (${Math.floor(item.reasoningTime)} s)`;
};
const chatParticipantId = ((_a = msg.participant) === null || _a === void 0 ? void 0 : _a.id) || 'default';
return (React.createElement("div", { className: `chat-message chat-message-${msg.from}`, "data-render-count": renderCount },
React.createElement("div", { className: "chat-message-header" },
React.createElement("div", { className: "chat-message-from" },
((_b = msg.participant) === null || _b === void 0 ? void 0 : _b.iconPath) && (React.createElement("div", { className: `chat-message-from-icon chat-message-from-icon-${chatParticipantId} ${isDarkTheme() ? 'dark' : ''}` },
React.createElement("img", { src: msg.participant.iconPath }))),
React.createElement("div", { className: "chat-message-from-title" }, msg.from === 'user'
? 'User'
: ((_c = msg.participant) === null || _c === void 0 ? void 0 : _c.name) || 'AI Assistant'),
React.createElement("div", { className: "chat-message-from-progress", style: { display: `${props.showGenerating ? 'visible' : 'none'}` } },
React.createElement("div", { className: "loading-ellipsis" }, "Generating"))),
React.createElement("div", { className: "chat-message-timestamp" }, timestamp)),
React.createElement("div", { className: "chat-message-content" },
groupedContents.map((item, index) => {
switch (item.type) {
case ResponseStreamDataType.Markdown:
case ResponseStreamDataType.MarkdownPart:
return (React.createElement(React.Fragment, null,
item.reasoningContent && (React.createElement("div", { className: "expandable-content expanded" },
React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
React.createElement(VscTriangleDown, { className: "expanded-icon" }),
' ',
getReasoningTitle(item)),
React.createElement("div", { className: "expandable-content-text" },
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.reasoningContent)))),
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.content),
item.contentDetail ? (React.createElement("div", { className: "expandable-content expanded" },
React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
React.createElement(VscTriangleDown, { className: "expanded-icon" }),
' ',
item.contentDetail.title),
React.createElement("div", { className: "expandable-content-text" },
React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.contentDetail.content)))) : null));
case ResponseStreamDataType.Image:
return (React.createElement("div", { className: "chat-response-img", key: `key-${index}` },
React.createElement("img", { src: item.content })));
case ResponseStreamDataType.HTMLFrame:
return (React.createElement(ChatResponseHTMLFrame, { index: index, source: item.content.source, height: item.content.height }));
case ResponseStreamDataType.Button:
return (React.createElement("div", { className: "chat-response-button", key: `key-${index}` },
React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => runCommand(item.content.commandId, item.content.args) },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.title))));
case ResponseStreamDataType.Anchor:
return (React.createElement("div", { className: "chat-response-anchor", key: `key-${index}` },
React.createElement("a", { href: item.content.uri, target: "_blank" }, item.content.title)));
case ResponseStreamDataType.Progress:
// show only if no more message available
return index === groupedContents.length - 1 ? (React.createElement("div", { className: "chat-response-progress", key: `key-${index}` },
"\u2713 ",
item.content)) : null;
case ResponseStreamDataType.Confirmation:
return answeredForms.get(item.id) ===
'confirmed' ? null : answeredForms.get(item.id) ===
'canceled' ? (React.createElement("div", null, "\u2716 Canceled")) : (React.createElement("div", { className: "chat-confirmation-form", key: `key-${index}` },
item.content.title ? (React.createElement("div", null,
React.createElement("b", null, item.content.title))) : null,
item.content.message ? (React.createElement("div", null, item.content.message)) : null,
React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => {
markFormConfirmed(item.id);
runCommand('notebook-intelligence:chat-user-input', item.content.confirmArgs);
} },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.confirmLabel)),
item.content.confirmSessionArgs ? (React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => {
markFormConfirmed(item.id);
runCommand('notebook-intelligence:chat-user-input', item.content.confirmSessionArgs);
} },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.confirmSessionLabel))) : null,
React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
markFormCanceled(item.id);
runCommand('notebook-intelligence:chat-user-input', item.content.cancelArgs);
} },
React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.cancelLabel))));
case ResponseStreamDataType.AskUserQuestion:
return answeredForms.get(item.id) ===
'confirmed' ? null : answeredForms.get(item.id) ===
'canceled' ? (React.createElement("div", null, "\u2716 Canceled")) : (React.createElement("div", { className: "chat-confirmation-form ask-user-question", key: `key-${index}` },
React.createElement(AskUserQuestion, { userQuestions: item, onSubmit: (selectedAnswers) => {
markFormConfirmed(item.id);
runCommand('notebook-intelligence:chat-user-input', {
id: item.content.identifier.id,
data: {
callback_id: item.content.identifier.callback_id,
data: {
confirmed: true,
selectedAnswers
}
}
});
}, onCancel: () => {
markFormCanceled(item.id);
runCommand('notebook-intelligence:chat-user-input', {
id: item.content.identifier.id,
data: {
callback_id: item.content.identifier.callback_id,
data: { confirmed: false }
}
});
} })));
}
return null;
}),
msg.notebookLink && (React.createElement("a", { className: "copilot-generated-notebook-link", "data-ref": msg.notebookLink, onClick: openNotebook }, "open notebook")))));
}
const MemoizedChatResponse = memo(ChatResponse);
async function submitCompletionRequest(request, responseEmitter) {
switch (request.type) {
case RunChatCompletionType.Chat:
return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || '', request.additionalContext || [], request.chatMode, request.toolSelections || {}, responseEmitter);
case RunChatCompletionType.ExplainThis:
case RunChatCompletionType.FixThis:
case RunChatCompletionType.ExplainThisOutput:
case RunChatCompletionType.TroubleshootThisOutput: {
return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || '', [], 'ask', {}, responseEmitter);
}
case RunChatCompletionType.GenerateCode:
return NBIAPI.generateCode(request.chatId, request.content, request.prefix || '', request.suffix || '', request.existingCode || '', request.language || 'python', request.filename || '', responseEmitter);
}
}
function SidebarComponent(props) {
const [chatMessages, setChatMessages] = useState([]);
const [prompt, setPrompt] = useState('');
const [draftPrompt, setDraftPrompt] = useState('');
const messagesEndRef = useRef(null);
const [ghLoginStatus, setGHLoginStatus] = useState(GitHubCopilotLoginStatus.NotLoggedIn);
const [loginClickCount, _setLoginClickCount] = useState(0);
const [copilotRequestInProgress, setCopilotRequestInProgress] = useState(false);
const [showPopover, setShowPopover] = useState(false);
const [originalPrefixes, setOriginalPrefixes] = useState([]);
const [prefixSuggestions, setPrefixSuggestions] = useState([]);
const [selectedPrefixSuggestionIndex, setSelectedPrefixSuggestionIndex] = useState(0);
const promptInputRef = useRef(null);
const [promptHistory, setPromptHistory] = useState([]);
// position on prompt history stack
const [promptHistoryIndex, setPromptHistoryIndex] = useState(0);
const [chatId, setChatId] = useState(UUID.uuid4());
const lastMessageId = useRef('');
const lastRequestTime = useRef(new Date());
const [contextOn, setContextOn] = useState(false);
const [activeDocumentInfo, setActiveDocumentInfo] = useState(null);
const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
const telemetryEmitter = props.getTelemetryEmitter();
const [chatMode, setChatMode] = useState(NBIAPI.config.defaultChatMode);
const [toolSelectionTitle, setToolSelectionTitle] = useState('Tool selection');
const [selectedToolCount, setSelectedToolCount] = useState(0);
const [unsafeToolSelected, setUnsafeToolSelected] = useState(false);
const [renderCount, setRenderCount] = useState(1);
const toolConfigRef = useRef({
builtinToolsets: [
{ id: BuiltinToolsetType.NotebookEdit, name: 'Notebook edit' },
{ id: BuiltinToolsetType.NotebookExecute, name: 'Notebook execute' }
],
mcpServers: [],
extensions: []
});
const mcpServerSettingsRef = useRef(NBIAPI.config.mcpServerSettings);
const [mcpServerEnabledState, setMCPServerEnabledState] = useState(new Map(mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current)));
const [showModeTools, setShowModeTools] = useState(false);
const toolSelectionsInitial = {
builtinToolsets: [],
mcpServers: {},
extensions: {}
};
const toolSelectionsEmpty = {
builtinToolsets: [],
mcpServers: {},
extensions: {}
};
const [toolSelections, setToolSelections] = useState(structuredClone(toolSelectionsInitial));
const [hasExtensionTools, setHasExtensionTools] = useState(false);
const [lastScrollTime, setLastScrollTime] = useState(0);
const [scrollPending, setScrollPending] = useState(false);
const cleanupRemovedToolsFromToolSelections = () => {
const newToolSelections = { ...toolSelections };
// if servers or tool is not in mcpServerEnabledState, remove it from newToolSelections
for (const serverId in newToolSelections.mcpServers) {
if (!mcpServerEnabledState.has(serverId)) {
delete newToolSelections.mcpServers[serverId];
}
else {
for (const tool of newToolSelections.mcpServers[serverId]) {
if (!mcpServerEnabledState.get(serverId).has(tool)) {
newToolSelections.mcpServers[serverId].splice(newToolSelections.mcpServers[serverId].indexOf(tool), 1);
}
}
}
}
for (const extensionId in newToolSelections.extensions) {
if (!mcpServerEnabledState.has(extensionId)) {
delete newToolSelections.extensions[extensionId];
}
else {
for (const toolsetId in newToolSelections.extensions[extensionId]) {
for (const tool of newToolSelections.extensions[extensionId][toolsetId]) {
if (!mcpServerEnabledState.get(extensionId).has(tool)) {
newToolSelections.extensions[extensionId][toolsetId].splice(newToolSelections.extensions[extensionId][toolsetId].indexOf(tool), 1);
}
}
}
}
}
setToolSelections(newToolSelections);
setRenderCount(renderCount => renderCount + 1);
};
useEffect(() => {
cleanupRemovedToolsFromToolSelections();
}, [mcpServerEnabledState]);
useEffect(() => {
NBIAPI.configChanged.connect(() => {
toolConfigRef.current = NBIAPI.config.toolConfig;
mcpServerSettingsRef.current = NBIAPI.config.mcpServerSettings;
const newMcpServerEnabledState = mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current);
setMCPServerEnabledState(newMcpServerEnabledState);
setRenderCount(renderCount => renderCount + 1);
});
}, []);
useEffect(() => {
let hasTools = false;
for (const extension of toolConfigRef.current.extensions) {
if (extension.toolsets.length > 0) {
hasTools = true;
break;
}
}
setHasExtensionTools(hasTools);
}, [toolConfigRef.current]);
useEffect(() => {
const builtinToolSelCount = toolSelections.builtinToolsets.length;
let mcpServerToolSelCount = 0;
let extensionToolSelCount = 0;
for (const serverId in toolSelections.mcpServers) {
const mcpServerTools = toolSelections.mcpServers[serverId];
mcpServerToolSelCount += mcpServerTools.length;
}
for (const extensionId in toolSelections.extensions) {
const extensionToolsets = toolSelections.extensions[extensionId];
for (const toolsetId in extensionToolsets) {
const toolsetTools = extensionToolsets[toolsetId];
extensionToolSelCount += toolsetTools.length;
}
}
const typeCounts = [];
if (builtinToolSelCount > 0) {
typeCounts.push(`${builtinToolSelCount} built-in`);
}
if (mcpServerToolSelCount > 0) {
typeCounts.push(`${mcpServerToolSelCount} mcp`);
}
if (extensionToolSelCount > 0) {
typeCounts.push(`${extensionToolSelCount} ext`);
}
setSelectedToolCount(builtinToolSelCount + mcpServerToolSelCount + extensionToolSelCount);
setUnsafeToolSelected(toolSelections.builtinToolsets.some((toolsetName) => [
BuiltinToolsetType.NotebookEdit,
BuiltinToolsetType.NotebookExecute,
BuiltinToolsetType.PythonFileEdit,
BuiltinToolsetType.FileEdit,
BuiltinToolsetType.CommandExecute
].includes(toolsetName)));
setToolSelectionTitle(typeCounts.length === 0
? 'Tool selection'
: `Tool selection (${typeCounts.join(', ')})`);
}, [toolSelections]);
const onClearToolsButtonClicked = () => {
setToolSelections(toolSelectionsEmpty);
};
const getBuiltinToolsetState = (toolsetName) => {
return toolSelections.builtinToolsets.includes(toolsetName);
};
const setBuiltinToolsetState = (toolsetName, enabled) => {
const newConfig = { ...toolSelections };
if (enabled) {
if (!toolSelections.builtinToolsets.includes(toolsetName)) {
newConfig.builtinToolsets.push(toolsetName);
}
}
else {
const index = newConfig.builtinToolsets.indexOf(toolsetName);
if (index !== -1) {
newConfig.builtinToolsets.splice(index, 1);
}
}
setToolSelections(newConfig);
};
const anyMCPServerToolSelected = (id) => {
if (!(id in toolSelections.mcpServers)) {
return false;
}
return toolSelections.mcpServers[id].length > 0;
};
const getMCPServerState = (id) => {
if (!(id in toolSelections.mcpServers)) {
return false;
}
const mcpServer = toolConfigRef.current.mcpServers.find(server => server.id === id);
const selectedServerTools = toolSelections.mcpServers[id];
for (const tool of mcpServer.tools) {
if (!selectedServerTools.includes(tool.name)) {
return false;
}
}
return true;
};
const onMCPServerClicked = (id) => {
if (anyMCPServerToolSelected(id)) {
const newConfig = { ...toolSelections };
delete newConfig.mcpServers[id];
setToolSelections(newConfig);
}
else {
const mcpServer = toolConfigRef.current.mcpServers.find(server => server.id === id);
const newConfig = { ...toolSelections };
newConfig.mcpServers[id] = structuredClone(mcpServer.tools
.filter((tool) => mcpServerEnabledState.get(mcpServer.id).has(tool.name))
.map((tool) => tool.name));
setToolSelections(newConfig);
}
};
const getMCPServerToolState = (serverId, toolId) => {
if (!(serverId in toolSelections.mcpServers)) {
return false;
}
const selectedServerTools = toolSelections.mcpServers[serverId];
return selectedServerTools.includes(toolId);
};
const setMCPServerToolState = (serverId, toolId, checked) => {
const newConfig = { ...toolSelections };
if (checked && !(serverId in newConfig.mcpServers)) {
newConfig.mcpServers[serverId] = [];
}
const selectedServerTools = newConfig.mcpServers[serverId];
if (checked) {
selectedServerTools.push(toolId);
}
else {
const index = selectedServerTools.indexOf(toolId);
if (index !== -1) {
selectedServerTools.splice(index, 1);
}
}
setToolSelections(newConfig);
};
// all toolsets and tools of the extension are selected
const getExtensionState = (extensionId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
const extension = toolConfigRef.current.extensions.find(extension => extension.id === extensionId);
for (const toolset of extension.toolsets) {
if (!getExtensionToolsetState(extensionId, toolset.id)) {
return false;
}
}
return true;
};
const getExtensionToolsetState = (extensionId, toolsetId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
if (!(toolsetId in toolSelections.extensions[extensionId])) {
return false;
}
const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
const selectedToolsetTools = toolSelections.extensions[extensionId][toolsetId];
for (const tool of extensionToolset.tools) {
if (!selectedToolsetTools.includes(tool.name)) {
return false;
}
}
return true;
};
const anyExtensionToolsetSelected = (extensionId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
return Object.keys(toolSelections.extensions[extensionId]).length > 0;
};
const onExtensionClicked = (extensionId) => {
if (anyExtensionToolsetSelected(extensionId)) {
const newConfig = { ...toolSelections };
delete newConfig.extensions[extensionId];
setToolSelections(newConfig);
}
else {
const newConfig = { ...toolSelections };
const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
if (extensionId in newConfig.extensions) {
delete newConfig.extensions[extensionId];
}
newConfig.extensions[extensionId] = {};
for (const toolset of extension.toolsets) {
newConfig.extensions[extensionId][toolset.id] = structuredClone(toolset.tools.map((tool) => tool.name));
}
setToolSelections(newConfig);
}
};
const anyExtensionToolsetToolSelected = (extensionId, toolsetId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
if (!(toolsetId in toolSelections.extensions[extensionId])) {
return false;
}
return toolSelections.extensions[extensionId][toolsetId].length > 0;
};
const onExtensionToolsetClicked = (extensionId, toolsetId) => {
if (anyExtensionToolsetToolSelected(extensionId, toolsetId)) {
const newConfig = { ...toolSelections };
if (toolsetId in newConfig.extensions[extensionId]) {
delete newConfig.extensions[extensionId][toolsetId];
}
setToolSelections(newConfig);
}
else {
const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
const newConfig = { ...toolSelections };
if (!(extensionId in newConfig.extensions)) {
newConfig.extensions[extensionId] = {};
}
newConfig.extensions[extensionId][toolsetId] = structuredClone(extensionToolset.tools.map((tool) => tool.name));
setToolSelections(newConfig);
}
};
const getExtensionToolsetToolState = (extensionId, toolsetId, toolId) => {
if (!(extensionId in toolSelections.extensions)) {
return false;
}
const selectedExtensionToolsets = toolSelections.extensions[extensionId];
if (!(toolsetId in selectedExtensionToolsets)) {
return false;
}
const selectedServerTools = selectedExtensionToolsets[toolsetId];
return selectedServerTools.includes(toolId);
};
const setExtensionToolsetToolState = (extensionId, toolsetId, toolId, checked) => {
const newConfig = { ...toolSelections };
if (checked && !(extensionId in newConfig.extensions)) {
newConfig.extensions[extensionId] = {};
}
if (checked && !(toolsetId in newConfig.extensions[extensionId])) {
newConfig.extensions[extensionId][toolsetId] = [];
}
const selectedTools = newConfig.extensions[extensionId][toolsetId];
if (checked) {
selectedTools.push(toolId);
}
else {
const index = selectedTools.indexOf(toolId);
if (index !== -1) {
selectedTools.splice(index, 1);
}
}
setToolSelections(newConfig);
};
useEffect(() => {
var _a;
const prefixes = [];
if (NBIAPI.config.isInClaudeCodeMode) {
const claudeChatParticipant = NBIAPI.config.chatParticipants.find(participant => participant.id === CLAUDE_CODE_CHAT_PARTICIPANT_ID);
if (claudeChatParticipant) {
const commands = claudeChatParticipant.commands;
for (const command of commands) {
prefixes.push(`/${command}`);
}
}
prefixes.push('/enter-plan-mode');
prefixes.push('/exit-plan-mode');
}
else {
if (chatMode === 'ask') {
const chatParticipants = NBIAPI.config.chatParticipants;
for (const participant of chatParticipants) {
const id = participant.id;
const commands = participant.commands;
const participantPrefix = id === 'default' ? '' : `@${id}`;
if (participantPrefix !== '') {
prefixes.push(participantPrefix);
}
const commandPrefix = participantPrefix === '' ? '' : `${participantPrefix} `;
for (const command of commands) {
prefixes.push(`${commandPrefix}/${command}`);
}
}
}
else {
prefixes.push('/clear');
}
}
const mcpServers = NBIAPI.config.toolConfig.mcpServers;
const mcpServerSettings = NBIAPI.config.mcpServerSettings;
for (const mcpServer of mcpServers) {
if (((_a = mcpServerSettings[mcpServer.id]) === null || _a === void 0 ? void 0 : _a.disabled) !== true) {
for (const prompt of mcpServer.prompts) {
prefixes.push(`/mcp:${mcpServer.id}:${prompt.name}`);
}
}
}
setOriginalPrefixes(prefixes);
setPrefixSuggestions(prefixes);
}, [chatMode, renderCount]);
useEffect(() => {
const fetchData = () => {
setGHLoginStatus(NBIAPI.getLoginStatus());
};
fetchData();
const intervalId = setInterval(fetchData, 1000);
return () => clearInterval(intervalId);
}, [loginClickCount]);
useEffect(() => {
setSelectedPrefixSuggestionIndex(0);
}, [prefixSuggestions]);
const onPromptChange = (event) => {
const newPrompt = event.target.value;
setPrompt(newPrompt);
const trimmedPrompt = newPrompt.trimStart();
if (trimmedPrompt === '@' || trimmedPrompt === '/') {
setShowPopover(true);
filterPrefixSuggestions(trimmedPrompt);
}
else if (trimmedPrompt.startsWith('@') ||
trimmedPrompt.startsWith('/') ||
trimmedPrompt === '') {
filterPrefixSuggestions(trimmedPrompt);
}
else {
setShowPopover(false);
}
};
const applyPrefixSuggestion = async (prefix) => {
var _a;
let mcpArguments = '';
if (prefix.startsWith('/mcp:')) {
mcpArguments = ':';
const serverId = prefix.split(':')[1];
const promptName = prefix.split(':')[2];
const promptConfig = NBIAPI.config.getMCPServerPrompt(serverId, promptName);
if (promptConfig &&
promptConfig.arguments &&
promptConfig.arguments.length > 0) {
const result = await props
.getApp()
.commands.execute('notebook-intelligence:show-form-input-dialog', {
title: 'Input Parameters',
fields: promptConfig.arguments
});
const argumentValues = [];
for (const argument of promptConfig.arguments) {
if (result[argument.name] !== undefined) {
argumentValues.push(`${argument.name}=${result[argument.name]}`);
}
}
mcpArguments = `(${argumentValues.join(', ')}):`;
}
}
if (prefix.includes(prompt)) {
setPrompt(`${prefix}${mcpArguments} `);
}
else {
setPrompt(`${prefix} ${prompt}${mcpArguments} `);
}
setShowPopover(false);
(_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
setSelectedPrefixSuggestionIndex(0);
};
const prefixSuggestionSelected = (event) => {
const prefix = event.target.dataset['value'];
applyPrefixSuggestion(prefix);
};
const handleSubmitStopChatButtonClick = async () => {
setShowModeTools(false);
if (!copilotRequestInProgress) {
handleUserInputSubmit();
}
else {
handleUserInputCancel();
}
};
const handleSettingsButtonClick = async () => {
setShowModeTools(false);
props
.getApp()
.commands.execute('notebook-intelligence:open-configuration-dialog');
};
const handleChatToolsButtonClick = async () => {
if (!showModeTools) {
NBIAPI.fetchCapabilities().then(() => {
toolConfigRef.current = NBIAPI.config.toolConfig;
mcpServerSettingsRef.current = NBIAPI.config.mcpServerSettings;
const newMcpServerEnabledState = mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current);
setMCPServerEnabledState(newMcpServerEnabledState);
setRenderCount(renderCount => renderCount + 1);
});
}
setShowModeTools(!showModeTools);
};
const handleUserInputSubmit = async () => {
setPromptHistoryIndex(promptHistory.length + 1);
setPromptHistory([...promptHistory, prompt]);
setShowPopover(false);
const promptPrefixParts = [];
const promptParts = prompt.split(' ');
if (promptParts.length > 1) {
for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
const part = promptParts[i];
if (part.startsWith('@') || part.startsWith('/')) {
promptPrefixParts.push(part);
}
}
}
lastMessageId.current = UUID.uuid4();
lastRequestTime.current = new Date();
const newList = [
...chatMessages,
{
id: lastMessageId.current,
date: new Date(),
from: 'user',
contents: [
{
id: UUID.uuid4(),
type: ResponseStreamDataType.Markdown,
content: prompt,
created: new Date()
}
]
}
];
setChatMessages(newList);
if (prompt.startsWith('/clear')) {
setChatMessages([]);
setPrompt('');
resetChatId();
resetPrefixSuggestions();
setPromptHistory([]);
setPromptHistoryIndex(0);
NBIAPI.sendWebSocketMessage(UUID.uuid4(), RequestDataType.ClearChatHistory, { chatId });
return;
}
setCopilotRequestInProgress(true);
const activeDocInfo = props.getActiveDocumentInfo();
const extractedPrompt = prompt;
const contents = [];
const app = props.getApp();
const additionalContext = [];
if (contextOn && (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename)) {
const selection = activeDocumentInfo.selection;
const textSelected = selection &&
!(selection.start.line === selection.end.line &&
selection.start.column === selection.end.column);
additionalContext.push({
type: ContextType.CurrentFile,
content: props.getActiveSelectionContent(),
currentCellContents: textSelected
? null
: props.getCurrentCellContents(),
filePath: activeDocumentInfo.filePath,
cellIndex: activeDocumentInfo.activeCellIndex,
startLine: selection ? selection.start.line + 1 : 1,
endLine: selection ? selection.end.line + 1 : 1
});
}
submitCompletionRequest({
messageId: lastMessageId.current,
chatId,
type: RunChatCompletionType.Chat,
content: extractedPrompt,
language: activeDocInfo.language,
currentDirectory: props.getCurrentDirectory(),
filename: activeDocInfo.filePath,
additionalContext,
chatMode,
toolSelections: toolSelections
}, {
emit: async (response) => {
var _a, _b, _c, _d, _e;
if (response.id !== lastMessageId.current) {
return;
}
let responseMessage = '';
if (response.type === BackendMessageType.StreamMessage) {
const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
if (!delta) {
return;
}
if (delta['nbiContent']) {
const nbiContent = delta['nbiContent'];
contents.push({
id: UUID.uuid4(),
type: nbiContent.type,
content: nbiContent.content,
contentDetail: nbiContent.detail,
created: new Date(response.created)
});
}
else {
responseMessage =
(_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
if (!responseMessage) {
return;
}
contents.push({
id: UUID.uuid4(),
type: ResponseStreamDataType.MarkdownPart,
content: responseMessage,
created: new Date(response.created)
});
}
}
else if (response.type === BackendMessageType.StreamEnd) {
setCopilotRequestInProgress(false);
const timeElapsed = (new Date().getTime() - lastRequestTime.current.getTime()) / 1000;
telemetryEmitter.emitTelemetryEvent({
type: TelemetryEventType.ChatResponse,
data: {
chatModel: {
provider: NBIAPI.config.chatModel.provider,
model: NBIAPI.config.chatModel.model
},
timeElapsed
}
});
}
else if (response.type === BackendMessageType.RunUICommand) {
const messageId = response.id;
let result = 'void';
try {
result = await app.commands.execute(response.data.commandId, response.data.args);
}
catch (error) {
result = `Error executing command: ${error}`;
}
const data = {
callback_id: response.data.callback_id,
result: result || 'void'
};
try {
JSON.stringify(data);
}
catch (error) {
data.result = 'Could not serialize the result';
}
NBIAPI.sendWebSocketMessage(messageId, RequestDataType.RunUICommandResponse, data);
}
setChatMessages([
...newList,
{
id: UUID.uuid4(),
date: new Date(),
from: