UNPKG

@notebook-intelligence/notebook-intelligence

Version:
1,017 lines 89.8 kB
// 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: