@notebook-intelligence/notebook-intelligence 2.6.1 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,21 +75,37 @@ To let Notebook Intelligence remember your GitHub access token, go to Notebook I
75
75
 
76
76
  If your stored access token fails to login (due to expiration or other reasons), you will be prompted to relogin on the UI.
77
77
 
78
- ### Notebook execute tool options
78
+ ## Built-in Tools
79
79
 
80
- Notebook execute tool is enabled by default in Agent Mode. However, you can disable it or make it controlled by an environment variable.
80
+ - **Notebook Edit** (nbi-notebook-edit): Edit notebook using the JupyterLab notebook editor.
81
+ - **Notebook Execute** (nbi-notebook-execute): Run notebooks in JupyterLab UI.
82
+ - **Python File Edit** (nbi-python-file-edit): Edit Python files using the JupyterLab file editor.
83
+ - **File Edit** (nbi-file-edit): Edit files in the Jupyter root directory.
84
+ - **File Read** (nbi-file-read): Read files in the Jupyter root directory.
85
+ - **Command Execute** (nbi-command-execute): Execute shell commands using embedded terminal in Agent UI or JupyterLab terminal.
81
86
 
82
- In order to disable Notebook execute tool:
87
+ ### Disabling Built-in tools
83
88
 
84
- ```bash
85
- jupyter lab --NotebookIntelligence.notebook_execute_tool=disabled
89
+ All built-in toolas are enabled by default in Agent Mode. However, you can disable them and make them controlled by an environment variable.
90
+
91
+ In order to disable any built-in tool use the `disabled_tools` config:
92
+
93
+ ```python
94
+ c.NotebookIntelligence.disabled_tools = ["nbi-notebook-execute","nbi-python-file-edit"]
95
+ ```
96
+
97
+ Valid built-in tool values are `nbi-notebook-edit`, `nbi-notebook-execute`, `nbi-python-file-edit`, `nbi-file-edit`, `nbi-file-read`, `nbi-command-execute`.
98
+
99
+ In order to disable a built-in tool by default but allow re-enabling using an environment variable use the `allow_enabling_tools_with_env` config:
100
+
101
+ ```python
102
+ c.NotebookIntelligence.allow_enabling_tools_with_env = True
86
103
  ```
87
104
 
88
- In order to disable Notebook execute tool by default but allow enabling using an environment variable:
105
+ Then the environment variable `NBI_ENABLED_BUILTIN_TOOLS` can be used to re-enable specific built-in tools.
89
106
 
90
107
  ```bash
91
- NBI_NOTEBOOK_EXECUTE_TOOL=enabled
92
- jupyter lab --NotebookIntelligence.notebook_execute_tool=env_enabled
108
+ export NBI_ENABLED_BUILTIN_TOOLS=nbi-notebook-execute,nbi-python-file-edit
93
109
  ```
94
110
 
95
111
  ### Configuration files
package/lib/api.d.ts CHANGED
@@ -53,7 +53,7 @@ export declare class NBIAPI {
53
53
  static updateOllamaModelList(): Promise<void>;
54
54
  static getMCPConfigFile(): Promise<any>;
55
55
  static setMCPConfigFile(config: any): Promise<any>;
56
- static chatRequest(messageId: string, chatId: string, prompt: string, language: string, filename: string, additionalContext: IContextItem[], chatMode: string, toolSelections: IToolSelections, responseEmitter: IChatCompletionResponseEmitter): Promise<void>;
56
+ static chatRequest(messageId: string, chatId: string, prompt: string, language: string, currentDirectory: string, filename: string, additionalContext: IContextItem[], chatMode: string, toolSelections: IToolSelections, responseEmitter: IChatCompletionResponseEmitter): Promise<void>;
57
57
  static reloadMCPServers(): Promise<any>;
58
58
  static generateCode(chatId: string, prompt: string, prefix: string, suffix: string, existingCode: string, language: string, filename: string, responseEmitter: IChatCompletionResponseEmitter): Promise<void>;
59
59
  static sendChatUserInput(messageId: string, data: any): Promise<void>;
package/lib/api.js CHANGED
@@ -254,7 +254,7 @@ class NBIAPI {
254
254
  });
255
255
  });
256
256
  }
257
- static async chatRequest(messageId, chatId, prompt, language, filename, additionalContext, chatMode, toolSelections, responseEmitter) {
257
+ static async chatRequest(messageId, chatId, prompt, language, currentDirectory, filename, additionalContext, chatMode, toolSelections, responseEmitter) {
258
258
  this._messageReceived.connect((_, msg) => {
259
259
  msg = JSON.parse(msg);
260
260
  if (msg.id === messageId) {
@@ -268,6 +268,7 @@ class NBIAPI {
268
268
  chatId,
269
269
  prompt,
270
270
  language,
271
+ currentDirectory,
271
272
  filename,
272
273
  additionalContext,
273
274
  chatMode,
@@ -16,6 +16,7 @@ export interface IRunChatCompletionRequest {
16
16
  type: RunChatCompletionType;
17
17
  content: string;
18
18
  language?: string;
19
+ currentDirectory?: string;
19
20
  filename?: string;
20
21
  prefix?: string;
21
22
  suffix?: string;
@@ -25,6 +26,7 @@ export interface IRunChatCompletionRequest {
25
26
  toolSelections?: IToolSelections;
26
27
  }
27
28
  export interface IChatSidebarOptions {
29
+ getCurrentDirectory: () => string;
28
30
  getActiveDocumentInfo: () => IActiveDocumentInfo;
29
31
  getActiveSelectionContent: () => string;
30
32
  getCurrentCellContents: () => ICellContents;
@@ -30,7 +30,7 @@ export class ChatSidebar extends ReactWidget {
30
30
  this.node.style.height = '100%';
31
31
  }
32
32
  render() {
33
- return (React.createElement(SidebarComponent, { getActiveDocumentInfo: this._options.getActiveDocumentInfo, getActiveSelectionContent: this._options.getActiveSelectionContent, getCurrentCellContents: this._options.getCurrentCellContents, openFile: this._options.openFile, getApp: this._options.getApp, getTelemetryEmitter: this._options.getTelemetryEmitter }));
33
+ 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 }));
34
34
  }
35
35
  }
36
36
  export class InlinePromptWidget extends ReactWidget {
@@ -154,6 +154,12 @@ function ChatResponse(props) {
154
154
  // group messages by type
155
155
  const groupedContents = [];
156
156
  let lastItemType;
157
+ const responseDetailTags = [
158
+ '<think>',
159
+ '</think>',
160
+ '<terminal-output>',
161
+ '</terminal-output>'
162
+ ];
157
163
  const extractReasoningContent = (item) => {
158
164
  let currentContent = item.content;
159
165
  if (typeof currentContent !== 'string') {
@@ -162,17 +168,31 @@ function ChatResponse(props) {
162
168
  let reasoningContent = '';
163
169
  let reasoningStartTime = new Date();
164
170
  const reasoningEndTime = new Date();
165
- const startPos = currentContent.indexOf('<think>');
171
+ let startPos = -1;
172
+ let startTag = '';
173
+ for (const tag of responseDetailTags) {
174
+ startPos = currentContent.indexOf(tag);
175
+ if (startPos >= 0) {
176
+ startTag = tag;
177
+ break;
178
+ }
179
+ }
166
180
  const hasStart = startPos >= 0;
167
181
  reasoningStartTime = new Date(item.created);
168
182
  if (hasStart) {
169
- currentContent = currentContent.substring(startPos + 7);
183
+ currentContent = currentContent.substring(startPos + startTag.length);
184
+ }
185
+ let endPos = -1;
186
+ for (const tag of responseDetailTags) {
187
+ endPos = currentContent.indexOf(tag);
188
+ if (endPos >= 0) {
189
+ break;
190
+ }
170
191
  }
171
- const endPos = currentContent.indexOf('</think>');
172
192
  const hasEnd = endPos >= 0;
173
193
  if (hasEnd) {
174
194
  reasoningContent += currentContent.substring(0, endPos);
175
- currentContent = currentContent.substring(endPos + 8);
195
+ currentContent = currentContent.substring(endPos + startTag.length);
176
196
  }
177
197
  else {
178
198
  if (hasStart) {
@@ -181,6 +201,7 @@ function ChatResponse(props) {
181
201
  }
182
202
  }
183
203
  item.content = currentContent;
204
+ item.reasoningTag = startTag;
184
205
  item.reasoningContent = reasoningContent;
185
206
  item.reasoningFinished = hasEnd;
186
207
  item.reasoningTime =
@@ -225,6 +246,21 @@ function ChatResponse(props) {
225
246
  parent.classList.add('expanded');
226
247
  }
227
248
  };
249
+ const getReasoningTitle = (item) => {
250
+ if (item.reasoningTag === '<think>') {
251
+ return item.reasoningFinished
252
+ ? 'Thought'
253
+ : `Thinking (${Math.floor(item.reasoningTime)} s)`;
254
+ }
255
+ else if (item.reasoningTag === '<terminal-output>') {
256
+ return item.reasoningFinished
257
+ ? 'Output'
258
+ : `Running (${Math.floor(item.reasoningTime)} s)`;
259
+ }
260
+ return item.reasoningFinished
261
+ ? 'Output'
262
+ : `Output (${Math.floor(item.reasoningTime)} s)`;
263
+ };
228
264
  return (React.createElement("div", { className: `chat-message chat-message-${msg.from}`, "data-render-count": renderCount },
229
265
  React.createElement("div", { className: "chat-message-header" },
230
266
  React.createElement("div", { className: "chat-message-from" },
@@ -242,18 +278,16 @@ function ChatResponse(props) {
242
278
  case ResponseStreamDataType.Markdown:
243
279
  case ResponseStreamDataType.MarkdownPart:
244
280
  return (React.createElement(React.Fragment, null,
245
- item.reasoningContent && (React.createElement("div", { className: "expandable-content" },
281
+ item.reasoningContent && (React.createElement("div", { className: "expandable-content expanded" },
246
282
  React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
247
283
  React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
248
284
  React.createElement(VscTriangleDown, { className: "expanded-icon" }),
249
285
  ' ',
250
- item.reasoningFinished
251
- ? 'Thought'
252
- : `Thinking (${Math.floor(item.reasoningTime)} s)`),
286
+ getReasoningTitle(item)),
253
287
  React.createElement("div", { className: "expandable-content-text" },
254
288
  React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.reasoningContent)))),
255
289
  React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.content),
256
- item.contentDetail ? (React.createElement("div", { className: "expandable-content" },
290
+ item.contentDetail ? (React.createElement("div", { className: "expandable-content expanded" },
257
291
  React.createElement("div", { className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event) },
258
292
  React.createElement(VscTriangleRight, { className: "collapsed-icon" }),
259
293
  React.createElement(VscTriangleDown, { className: "expanded-icon" }),
@@ -304,12 +338,12 @@ const MemoizedChatResponse = memo(ChatResponse);
304
338
  async function submitCompletionRequest(request, responseEmitter) {
305
339
  switch (request.type) {
306
340
  case RunChatCompletionType.Chat:
307
- return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.filename || 'Untitled.ipynb', request.additionalContext || [], request.chatMode, request.toolSelections || {}, responseEmitter);
341
+ return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || 'Untitled.ipynb', request.additionalContext || [], request.chatMode, request.toolSelections || {}, responseEmitter);
308
342
  case RunChatCompletionType.ExplainThis:
309
343
  case RunChatCompletionType.FixThis:
310
344
  case RunChatCompletionType.ExplainThisOutput:
311
345
  case RunChatCompletionType.TroubleshootThisOutput: {
312
- return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.filename || 'Untitled.ipynb', [], 'ask', {}, responseEmitter);
346
+ return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || 'Untitled.ipynb', [], 'ask', {}, responseEmitter);
313
347
  }
314
348
  case RunChatCompletionType.GenerateCode:
315
349
  return NBIAPI.generateCode(request.chatId, request.content, request.prefix || '', request.suffix || '', request.existingCode || '', request.language || 'python', request.filename || 'Untitled.ipynb', responseEmitter);
@@ -341,7 +375,7 @@ function SidebarComponent(props) {
341
375
  const [chatMode, setChatMode] = useState(NBIAPI.config.defaultChatMode);
342
376
  const [toolSelectionTitle, setToolSelectionTitle] = useState('Tool selection');
343
377
  const [selectedToolCount, setSelectedToolCount] = useState(0);
344
- const [notebookExecuteToolSelected, setNotebookExecuteToolSelected] = useState(false);
378
+ const [unsafeToolSelected, setUnsafeToolSelected] = useState(false);
345
379
  const [renderCount, setRenderCount] = useState(1);
346
380
  const toolConfigRef = useRef({
347
381
  builtinToolsets: [
@@ -355,7 +389,7 @@ function SidebarComponent(props) {
355
389
  const [mcpServerEnabledState, setMCPServerEnabledState] = useState(new Map(mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current)));
356
390
  const [showModeTools, setShowModeTools] = useState(false);
357
391
  const toolSelectionsInitial = {
358
- builtinToolsets: [BuiltinToolsetType.NotebookEdit],
392
+ builtinToolsets: [],
359
393
  mcpServers: {},
360
394
  extensions: {}
361
395
  };
@@ -368,13 +402,47 @@ function SidebarComponent(props) {
368
402
  const [hasExtensionTools, setHasExtensionTools] = useState(false);
369
403
  const [lastScrollTime, setLastScrollTime] = useState(0);
370
404
  const [scrollPending, setScrollPending] = useState(false);
405
+ const cleanupRemovedToolsFromToolSelections = () => {
406
+ const newToolSelections = { ...toolSelections };
407
+ // if servers or tool is not in mcpServerEnabledState, remove it from newToolSelections
408
+ for (const serverId in newToolSelections.mcpServers) {
409
+ if (!mcpServerEnabledState.has(serverId)) {
410
+ delete newToolSelections.mcpServers[serverId];
411
+ }
412
+ else {
413
+ for (const tool of newToolSelections.mcpServers[serverId]) {
414
+ if (!mcpServerEnabledState.get(serverId).has(tool)) {
415
+ newToolSelections.mcpServers[serverId].splice(newToolSelections.mcpServers[serverId].indexOf(tool), 1);
416
+ }
417
+ }
418
+ }
419
+ }
420
+ for (const extensionId in newToolSelections.extensions) {
421
+ if (!mcpServerEnabledState.has(extensionId)) {
422
+ delete newToolSelections.extensions[extensionId];
423
+ }
424
+ else {
425
+ for (const toolsetId in newToolSelections.extensions[extensionId]) {
426
+ for (const tool of newToolSelections.extensions[extensionId][toolsetId]) {
427
+ if (!mcpServerEnabledState.get(extensionId).has(tool)) {
428
+ newToolSelections.extensions[extensionId][toolsetId].splice(newToolSelections.extensions[extensionId][toolsetId].indexOf(tool), 1);
429
+ }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ setToolSelections(newToolSelections);
435
+ setRenderCount(renderCount => renderCount + 1);
436
+ };
437
+ useEffect(() => {
438
+ cleanupRemovedToolsFromToolSelections();
439
+ }, [mcpServerEnabledState]);
371
440
  useEffect(() => {
372
441
  NBIAPI.configChanged.connect(() => {
373
442
  toolConfigRef.current = NBIAPI.config.toolConfig;
374
443
  mcpServerSettingsRef.current = NBIAPI.config.mcpServerSettings;
375
444
  const newMcpServerEnabledState = mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current);
376
445
  setMCPServerEnabledState(newMcpServerEnabledState);
377
- setToolSelections(structuredClone(toolSelectionsInitial));
378
446
  setRenderCount(renderCount => renderCount + 1);
379
447
  });
380
448
  }, []);
@@ -414,7 +482,13 @@ function SidebarComponent(props) {
414
482
  typeCounts.push(`${extensionToolSelCount} ext`);
415
483
  }
416
484
  setSelectedToolCount(builtinToolSelCount + mcpServerToolSelCount + extensionToolSelCount);
417
- setNotebookExecuteToolSelected(toolSelections.builtinToolsets.includes(BuiltinToolsetType.NotebookExecute));
485
+ setUnsafeToolSelected(toolSelections.builtinToolsets.some((toolsetName) => [
486
+ BuiltinToolsetType.NotebookEdit,
487
+ BuiltinToolsetType.NotebookExecute,
488
+ BuiltinToolsetType.PythonFileEdit,
489
+ BuiltinToolsetType.FileEdit,
490
+ BuiltinToolsetType.CommandExecute
491
+ ].includes(toolsetName)));
418
492
  setToolSelectionTitle(typeCounts.length === 0
419
493
  ? 'Tool selection'
420
494
  : `Tool selection (${typeCounts.join(', ')})`);
@@ -615,7 +689,6 @@ function SidebarComponent(props) {
615
689
  useEffect(() => {
616
690
  var _a;
617
691
  const prefixes = [];
618
- prefixes.push('/clear');
619
692
  if (chatMode === 'ask') {
620
693
  const chatParticipants = NBIAPI.config.chatParticipants;
621
694
  for (const participant of chatParticipants) {
@@ -631,6 +704,9 @@ function SidebarComponent(props) {
631
704
  }
632
705
  }
633
706
  }
707
+ else {
708
+ prefixes.push('/clear');
709
+ }
634
710
  const mcpServers = NBIAPI.config.toolConfig.mcpServers;
635
711
  const mcpServerSettings = NBIAPI.config.mcpServerSettings;
636
712
  for (const mcpServer of mcpServers) {
@@ -810,6 +886,7 @@ function SidebarComponent(props) {
810
886
  type: RunChatCompletionType.Chat,
811
887
  content: extractedPrompt,
812
888
  language: activeDocInfo.language,
889
+ currentDirectory: props.getCurrentDirectory(),
813
890
  filename: activeDocInfo.filePath,
814
891
  additionalContext,
815
892
  chatMode,
@@ -1217,16 +1294,13 @@ function SidebarComponent(props) {
1217
1294
  if (event.target.value === 'ask') {
1218
1295
  setToolSelections(toolSelectionsEmpty);
1219
1296
  }
1220
- else if (event.target.value === 'agent') {
1221
- setToolSelections(structuredClone(toolSelectionsInitial));
1222
- }
1223
1297
  setShowModeTools(false);
1224
1298
  setChatMode(event.target.value);
1225
1299
  } },
1226
1300
  React.createElement("option", { value: "ask" }, "Ask"),
1227
1301
  React.createElement("option", { value: "agent" }, "Agent"))),
1228
- chatMode !== 'ask' && (React.createElement("div", { className: `user-input-footer-button tools-button ${notebookExecuteToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`, onClick: () => handleChatToolsButtonClick(), title: notebookExecuteToolSelected
1229
- ? `Notebook execute tool selected!\n${toolSelectionTitle}`
1302
+ chatMode !== 'ask' && (React.createElement("div", { className: `user-input-footer-button tools-button ${unsafeToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`, onClick: () => handleChatToolsButtonClick(), title: unsafeToolSelected
1303
+ ? `Tool selection can cause irreversible changes! Review each tool execution carefully.\n${toolSelectionTitle}`
1230
1304
  : toolSelectionTitle },
1231
1305
  React.createElement(VscTools, null),
1232
1306
  selectedToolCount > 0 && React.createElement(React.Fragment, null, selectedToolCount)))),
@@ -1257,7 +1331,7 @@ function SidebarComponent(props) {
1257
1331
  React.createElement("div", null, "Done"))),
1258
1332
  React.createElement("div", { className: "mode-tools-popover-tool-list" },
1259
1333
  React.createElement("div", { className: "mode-tools-group-header" }, "Built-in"),
1260
- React.createElement("div", { className: "mode-tools-group mode-tools-group-built-in" }, toolConfigRef.current.builtinToolsets.map((toolset) => (React.createElement(CheckBoxItem, { key: toolset.id, label: toolset.name, checked: getBuiltinToolsetState(toolset.id), header: true, onClick: () => {
1334
+ React.createElement("div", { className: "mode-tools-group mode-tools-group-built-in" }, toolConfigRef.current.builtinToolsets.map((toolset) => (React.createElement(CheckBoxItem, { key: toolset.id, label: toolset.name, checked: getBuiltinToolsetState(toolset.id), tooltip: toolset.description, header: true, onClick: () => {
1261
1335
  setBuiltinToolsetState(toolset.id, !getBuiltinToolsetState(toolset.id));
1262
1336
  } })))),
1263
1337
  renderCount > 0 &&
@@ -3,7 +3,7 @@ import React from 'react';
3
3
  import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from 'react-icons/md';
4
4
  export function CheckBoxItem(props) {
5
5
  const indent = props.indent || 0;
6
- return (React.createElement("div", { className: `checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`, title: props.title, onClick: event => props.onClick(event) },
6
+ return (React.createElement("div", { className: `checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`, title: props.tooltip || props.title || '', onClick: event => props.onClick(event) },
7
7
  React.createElement("div", { className: "checkbox-item-toggle" },
8
8
  props.checked ? (React.createElement(MdCheckBox, { className: "checkbox-icon" })) : (React.createElement(MdOutlineCheckBoxOutlineBlank, { className: "checkbox-icon" })),
9
9
  props.label),
@@ -364,9 +364,15 @@ function SettingsPanelComponentMCPServers(props) {
364
364
  setMCPServerEnabled(server.id, !getMCPServerEnabled(server.id));
365
365
  } }),
366
366
  React.createElement("div", { className: `server-status-indicator ${server.status}`, title: server.status })),
367
- getMCPServerEnabled(server.id) && (React.createElement("div", null, server.tools.map((tool) => (React.createElement(PillItem, { label: tool.name, title: tool.description, checked: getMCPServerToolEnabled(server.id, tool.name), onClick: () => {
368
- setMCPServerToolEnabled(server.id, tool.name, !getMCPServerToolEnabled(server.id, tool.name));
369
- } }))))))))))),
367
+ getMCPServerEnabled(server.id) && (React.createElement("div", null,
368
+ server.tools.length > 0 && (React.createElement("div", { className: "mcp-server-tools" },
369
+ React.createElement("div", { className: "mcp-server-tools-header" }, "Tools"),
370
+ React.createElement("div", null, server.tools.map((tool) => (React.createElement(PillItem, { label: tool.name, title: tool.description, checked: getMCPServerToolEnabled(server.id, tool.name), onClick: () => {
371
+ setMCPServerToolEnabled(server.id, tool.name, !getMCPServerToolEnabled(server.id, tool.name));
372
+ } })))))),
373
+ server.prompts.length > 0 && (React.createElement("div", { className: "mcp-server-prompts" },
374
+ React.createElement("div", { className: "mcp-server-prompts-header" }, "Prompts"),
375
+ React.createElement("div", null, server.prompts.map((prompt) => (React.createElement(PillItem, { label: prompt.name, title: prompt.description, checked: true })))))))))))))),
370
376
  React.createElement("div", { className: "model-config-section-row" },
371
377
  React.createElement("div", { className: "model-config-section-column", style: { flexGrow: 'initial' } },
372
378
  React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", style: { width: 'max-content' }, onClick: props.onEditMCPConfigClicked },
package/lib/index.js CHANGED
@@ -54,6 +54,7 @@ var CommandIDs;
54
54
  CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content';
55
55
  CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor';
56
56
  CommandIDs.showFormInputDialog = 'notebook-intelligence:show-form-input-dialog';
57
+ CommandIDs.runCommandInTerminal = 'notebook-intelligence:run-command-in-terminal';
57
58
  })(CommandIDs || (CommandIDs = {}));
58
59
  const DOCUMENT_WATCH_INTERVAL = 1000;
59
60
  const MAX_TOKENS = 4096;
@@ -77,7 +78,7 @@ const emptyNotebookContent = {
77
78
  };
78
79
  const BACKEND_TELEMETRY_LISTENER_NAME = 'backend-telemetry-listener';
79
80
  class ActiveDocumentWatcher {
80
- static initialize(app, languageRegistry) {
81
+ static initialize(app, languageRegistry, fileBrowser) {
81
82
  var _a;
82
83
  ActiveDocumentWatcher._languageRegistry = languageRegistry;
83
84
  (_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.connect((_sender, args) => {
@@ -86,6 +87,12 @@ class ActiveDocumentWatcher {
86
87
  ActiveDocumentWatcher.activeDocumentInfo.activeWidget =
87
88
  app.shell.currentWidget;
88
89
  ActiveDocumentWatcher.handleWatchDocument();
90
+ if (fileBrowser) {
91
+ const onPathChanged = (model) => {
92
+ ActiveDocumentWatcher.currentDirectory = model.path;
93
+ };
94
+ fileBrowser.model.pathChanged.connect(onPathChanged);
95
+ }
89
96
  }
90
97
  static watchDocument(widget) {
91
98
  if (ActiveDocumentWatcher.activeDocumentInfo.activeWidget === widget) {
@@ -209,6 +216,7 @@ class ActiveDocumentWatcher {
209
216
  }));
210
217
  }
211
218
  }
219
+ ActiveDocumentWatcher.currentDirectory = '';
212
220
  ActiveDocumentWatcher.activeDocumentInfo = {
213
221
  language: 'python',
214
222
  filename: 'nb-doesnt-exist.ipynb',
@@ -510,6 +518,9 @@ const plugin = {
510
518
  });
511
519
  panel.title.icon = sidebarIcon;
512
520
  const sidebar = new ChatSidebar({
521
+ getCurrentDirectory: () => {
522
+ return ActiveDocumentWatcher.currentDirectory;
523
+ },
513
524
  getActiveDocumentInfo: () => {
514
525
  return ActiveDocumentWatcher.activeDocumentInfo;
515
526
  },
@@ -530,7 +541,7 @@ const plugin = {
530
541
  }
531
542
  });
532
543
  panel.addWidget(sidebar);
533
- app.shell.add(panel, 'left', { rank: 1000 });
544
+ app.shell.add(panel, 'right', { rank: 1000 });
534
545
  app.shell.activateById(panel.id);
535
546
  const updateSidebarIcon = () => {
536
547
  if (NBIAPI.getChatEnabled()) {
@@ -723,6 +734,29 @@ const plugin = {
723
734
  }
724
735
  }
725
736
  });
737
+ app.commands.addCommand(CommandIDs.runCommandInTerminal, {
738
+ execute: async (args) => {
739
+ var _a;
740
+ const command = args.command;
741
+ const terminal = await app.commands.execute('terminal:create-new', {
742
+ cwd: args.cwd || ActiveDocumentWatcher.currentDirectory
743
+ });
744
+ const session = (_a = terminal === null || terminal === void 0 ? void 0 : terminal.content) === null || _a === void 0 ? void 0 : _a.session;
745
+ session.messageReceived.connect((sender, message) => {
746
+ console.log('Message received in Jupyter terminal:', message);
747
+ });
748
+ if (session) {
749
+ session.send({
750
+ type: 'stdin',
751
+ content: [command + '\n'] // Add newline to execute the command
752
+ });
753
+ return 'Command executed in Jupyter terminal';
754
+ }
755
+ else {
756
+ return 'Failed to execute command in Jupyter terminal';
757
+ }
758
+ }
759
+ });
726
760
  const isNewEmptyNotebook = (model) => {
727
761
  return (model.cells.length === 1 &&
728
762
  model.cells[0].cell_type === 'code' &&
@@ -1441,7 +1475,7 @@ const plugin = {
1441
1475
  });
1442
1476
  }
1443
1477
  const jlabApp = app;
1444
- ActiveDocumentWatcher.initialize(jlabApp, languageRegistry);
1478
+ ActiveDocumentWatcher.initialize(jlabApp, languageRegistry, defaultBrowser);
1445
1479
  return extensionService;
1446
1480
  }
1447
1481
  };
package/lib/tokens.d.ts CHANGED
@@ -86,7 +86,11 @@ export interface IToolSelections {
86
86
  }
87
87
  export declare enum BuiltinToolsetType {
88
88
  NotebookEdit = "nbi-notebook-edit",
89
- NotebookExecute = "nbi-notebook-execute"
89
+ NotebookExecute = "nbi-notebook-execute",
90
+ PythonFileEdit = "nbi-python-file-edit",
91
+ FileEdit = "nbi-file-edit",
92
+ FileRead = "nbi-file-read",
93
+ CommandExecute = "nbi-command-execute"
90
94
  }
91
95
  export declare const GITHUB_COPILOT_PROVIDER_ID = "github-copilot";
92
96
  export declare enum TelemetryEventType {
package/lib/tokens.js CHANGED
@@ -52,6 +52,10 @@ export var BuiltinToolsetType;
52
52
  (function (BuiltinToolsetType) {
53
53
  BuiltinToolsetType["NotebookEdit"] = "nbi-notebook-edit";
54
54
  BuiltinToolsetType["NotebookExecute"] = "nbi-notebook-execute";
55
+ BuiltinToolsetType["PythonFileEdit"] = "nbi-python-file-edit";
56
+ BuiltinToolsetType["FileEdit"] = "nbi-file-edit";
57
+ BuiltinToolsetType["FileRead"] = "nbi-file-read";
58
+ BuiltinToolsetType["CommandExecute"] = "nbi-command-execute";
55
59
  })(BuiltinToolsetType || (BuiltinToolsetType = {}));
56
60
  export const GITHUB_COPILOT_PROVIDER_ID = 'github-copilot';
57
61
  export var TelemetryEventType;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notebook-intelligence/notebook-intelligence",
3
- "version": "2.6.1",
3
+ "version": "3.1.0",
4
4
  "description": "AI coding assistant for JupyterLab",
5
5
  "keywords": [
6
6
  "AI",
package/src/api.ts CHANGED
@@ -339,6 +339,7 @@ export class NBIAPI {
339
339
  chatId: string,
340
340
  prompt: string,
341
341
  language: string,
342
+ currentDirectory: string,
342
343
  filename: string,
343
344
  additionalContext: IContextItem[],
344
345
  chatMode: string,
@@ -359,6 +360,7 @@ export class NBIAPI {
359
360
  chatId,
360
361
  prompt,
361
362
  language,
363
+ currentDirectory,
362
364
  filename,
363
365
  additionalContext,
364
366
  chatMode,
@@ -70,6 +70,7 @@ export interface IRunChatCompletionRequest {
70
70
  type: RunChatCompletionType;
71
71
  content: string;
72
72
  language?: string;
73
+ currentDirectory?: string;
73
74
  filename?: string;
74
75
  prefix?: string;
75
76
  suffix?: string;
@@ -80,6 +81,7 @@ export interface IRunChatCompletionRequest {
80
81
  }
81
82
 
82
83
  export interface IChatSidebarOptions {
84
+ getCurrentDirectory: () => string;
83
85
  getActiveDocumentInfo: () => IActiveDocumentInfo;
84
86
  getActiveSelectionContent: () => string;
85
87
  getCurrentCellContents: () => ICellContents;
@@ -99,6 +101,7 @@ export class ChatSidebar extends ReactWidget {
99
101
  render(): JSX.Element {
100
102
  return (
101
103
  <SidebarComponent
104
+ getCurrentDirectory={this._options.getCurrentDirectory}
102
105
  getActiveDocumentInfo={this._options.getActiveDocumentInfo}
103
106
  getActiveSelectionContent={this._options.getActiveSelectionContent}
104
107
  getCurrentCellContents={this._options.getCurrentCellContents}
@@ -262,6 +265,7 @@ interface IChatMessageContent {
262
265
  content: any;
263
266
  contentDetail?: any;
264
267
  created: Date;
268
+ reasoningTag?: string;
265
269
  reasoningContent?: string;
266
270
  reasoningFinished?: boolean;
267
271
  reasoningTime?: number;
@@ -323,6 +327,12 @@ function ChatResponse(props: any) {
323
327
  // group messages by type
324
328
  const groupedContents: IChatMessageContent[] = [];
325
329
  let lastItemType: ResponseStreamDataType | undefined;
330
+ const responseDetailTags = [
331
+ '<think>',
332
+ '</think>',
333
+ '<terminal-output>',
334
+ '</terminal-output>'
335
+ ];
326
336
 
327
337
  const extractReasoningContent = (item: IChatMessageContent) => {
328
338
  let currentContent = item.content as string;
@@ -334,21 +344,35 @@ function ChatResponse(props: any) {
334
344
  let reasoningStartTime = new Date();
335
345
  const reasoningEndTime = new Date();
336
346
 
337
- const startPos = currentContent.indexOf('<think>');
347
+ let startPos = -1;
348
+ let startTag = '';
349
+ for (const tag of responseDetailTags) {
350
+ startPos = currentContent.indexOf(tag);
351
+ if (startPos >= 0) {
352
+ startTag = tag;
353
+ break;
354
+ }
355
+ }
338
356
 
339
357
  const hasStart = startPos >= 0;
340
358
  reasoningStartTime = new Date(item.created);
341
359
 
342
360
  if (hasStart) {
343
- currentContent = currentContent.substring(startPos + 7);
361
+ currentContent = currentContent.substring(startPos + startTag.length);
344
362
  }
345
363
 
346
- const endPos = currentContent.indexOf('</think>');
364
+ let endPos = -1;
365
+ for (const tag of responseDetailTags) {
366
+ endPos = currentContent.indexOf(tag);
367
+ if (endPos >= 0) {
368
+ break;
369
+ }
370
+ }
347
371
  const hasEnd = endPos >= 0;
348
372
 
349
373
  if (hasEnd) {
350
374
  reasoningContent += currentContent.substring(0, endPos);
351
- currentContent = currentContent.substring(endPos + 8);
375
+ currentContent = currentContent.substring(endPos + startTag.length);
352
376
  } else {
353
377
  if (hasStart) {
354
378
  reasoningContent += currentContent;
@@ -357,6 +381,7 @@ function ChatResponse(props: any) {
357
381
  }
358
382
 
359
383
  item.content = currentContent;
384
+ item.reasoningTag = startTag;
360
385
  item.reasoningContent = reasoningContent;
361
386
  item.reasoningFinished = hasEnd;
362
387
  item.reasoningTime =
@@ -409,6 +434,21 @@ function ChatResponse(props: any) {
409
434
  }
410
435
  };
411
436
 
437
+ const getReasoningTitle = (item: IChatMessageContent) => {
438
+ if (item.reasoningTag === '<think>') {
439
+ return item.reasoningFinished
440
+ ? 'Thought'
441
+ : `Thinking (${Math.floor(item.reasoningTime)} s)`;
442
+ } else if (item.reasoningTag === '<terminal-output>') {
443
+ return item.reasoningFinished
444
+ ? 'Output'
445
+ : `Running (${Math.floor(item.reasoningTime)} s)`;
446
+ }
447
+ return item.reasoningFinished
448
+ ? 'Output'
449
+ : `Output (${Math.floor(item.reasoningTime)} s)`;
450
+ };
451
+
412
452
  return (
413
453
  <div
414
454
  className={`chat-message chat-message-${msg.from}`}
@@ -445,16 +485,14 @@ function ChatResponse(props: any) {
445
485
  return (
446
486
  <>
447
487
  {item.reasoningContent && (
448
- <div className="expandable-content">
488
+ <div className="expandable-content expanded">
449
489
  <div
450
490
  className="expandable-content-title"
451
491
  onClick={(event: any) => onExpandCollapseClick(event)}
452
492
  >
453
493
  <VscTriangleRight className="collapsed-icon"></VscTriangleRight>
454
494
  <VscTriangleDown className="expanded-icon"></VscTriangleDown>{' '}
455
- {item.reasoningFinished
456
- ? 'Thought'
457
- : `Thinking (${Math.floor(item.reasoningTime)} s)`}
495
+ {getReasoningTitle(item)}
458
496
  </div>
459
497
  <div className="expandable-content-text">
460
498
  <MarkdownRenderer
@@ -475,7 +513,7 @@ function ChatResponse(props: any) {
475
513
  {item.content}
476
514
  </MarkdownRenderer>
477
515
  {item.contentDetail ? (
478
- <div className="expandable-content">
516
+ <div className="expandable-content expanded">
479
517
  <div
480
518
  className="expandable-content-title"
481
519
  onClick={(event: any) => onExpandCollapseClick(event)}
@@ -616,6 +654,7 @@ async function submitCompletionRequest(
616
654
  request.chatId,
617
655
  request.content,
618
656
  request.language || 'python',
657
+ request.currentDirectory || '',
619
658
  request.filename || 'Untitled.ipynb',
620
659
  request.additionalContext || [],
621
660
  request.chatMode,
@@ -631,6 +670,7 @@ async function submitCompletionRequest(
631
670
  request.chatId,
632
671
  request.content,
633
672
  request.language || 'python',
673
+ request.currentDirectory || '',
634
674
  request.filename || 'Untitled.ipynb',
635
675
  [],
636
676
  'ask',
@@ -685,8 +725,7 @@ function SidebarComponent(props: any) {
685
725
  const [toolSelectionTitle, setToolSelectionTitle] =
686
726
  useState('Tool selection');
687
727
  const [selectedToolCount, setSelectedToolCount] = useState(0);
688
- const [notebookExecuteToolSelected, setNotebookExecuteToolSelected] =
689
- useState(false);
728
+ const [unsafeToolSelected, setUnsafeToolSelected] = useState(false);
690
729
 
691
730
  const [renderCount, setRenderCount] = useState(1);
692
731
  const toolConfigRef = useRef({
@@ -709,7 +748,7 @@ function SidebarComponent(props: any) {
709
748
 
710
749
  const [showModeTools, setShowModeTools] = useState(false);
711
750
  const toolSelectionsInitial: any = {
712
- builtinToolsets: [BuiltinToolsetType.NotebookEdit],
751
+ builtinToolsets: [],
713
752
  mcpServers: {},
714
753
  extensions: {}
715
754
  };
@@ -725,6 +764,51 @@ function SidebarComponent(props: any) {
725
764
  const [lastScrollTime, setLastScrollTime] = useState(0);
726
765
  const [scrollPending, setScrollPending] = useState(false);
727
766
 
767
+ const cleanupRemovedToolsFromToolSelections = () => {
768
+ const newToolSelections = { ...toolSelections };
769
+ // if servers or tool is not in mcpServerEnabledState, remove it from newToolSelections
770
+ for (const serverId in newToolSelections.mcpServers) {
771
+ if (!mcpServerEnabledState.has(serverId)) {
772
+ delete newToolSelections.mcpServers[serverId];
773
+ } else {
774
+ for (const tool of newToolSelections.mcpServers[serverId]) {
775
+ if (!mcpServerEnabledState.get(serverId).has(tool)) {
776
+ newToolSelections.mcpServers[serverId].splice(
777
+ newToolSelections.mcpServers[serverId].indexOf(tool),
778
+ 1
779
+ );
780
+ }
781
+ }
782
+ }
783
+ }
784
+ for (const extensionId in newToolSelections.extensions) {
785
+ if (!mcpServerEnabledState.has(extensionId)) {
786
+ delete newToolSelections.extensions[extensionId];
787
+ } else {
788
+ for (const toolsetId in newToolSelections.extensions[extensionId]) {
789
+ for (const tool of newToolSelections.extensions[extensionId][
790
+ toolsetId
791
+ ]) {
792
+ if (!mcpServerEnabledState.get(extensionId).has(tool)) {
793
+ newToolSelections.extensions[extensionId][toolsetId].splice(
794
+ newToolSelections.extensions[extensionId][toolsetId].indexOf(
795
+ tool
796
+ ),
797
+ 1
798
+ );
799
+ }
800
+ }
801
+ }
802
+ }
803
+ }
804
+ setToolSelections(newToolSelections);
805
+ setRenderCount(renderCount => renderCount + 1);
806
+ };
807
+
808
+ useEffect(() => {
809
+ cleanupRemovedToolsFromToolSelections();
810
+ }, [mcpServerEnabledState]);
811
+
728
812
  useEffect(() => {
729
813
  NBIAPI.configChanged.connect(() => {
730
814
  toolConfigRef.current = NBIAPI.config.toolConfig;
@@ -734,7 +818,6 @@ function SidebarComponent(props: any) {
734
818
  mcpServerSettingsRef.current
735
819
  );
736
820
  setMCPServerEnabledState(newMcpServerEnabledState);
737
- setToolSelections(structuredClone(toolSelectionsInitial));
738
821
  setRenderCount(renderCount => renderCount + 1);
739
822
  });
740
823
  }, []);
@@ -782,9 +865,15 @@ function SidebarComponent(props: any) {
782
865
  setSelectedToolCount(
783
866
  builtinToolSelCount + mcpServerToolSelCount + extensionToolSelCount
784
867
  );
785
- setNotebookExecuteToolSelected(
786
- toolSelections.builtinToolsets.includes(
787
- BuiltinToolsetType.NotebookExecute
868
+ setUnsafeToolSelected(
869
+ toolSelections.builtinToolsets.some((toolsetName: string) =>
870
+ [
871
+ BuiltinToolsetType.NotebookEdit,
872
+ BuiltinToolsetType.NotebookExecute,
873
+ BuiltinToolsetType.PythonFileEdit,
874
+ BuiltinToolsetType.FileEdit,
875
+ BuiltinToolsetType.CommandExecute
876
+ ].includes(toolsetName as unknown as BuiltinToolsetType)
788
877
  )
789
878
  );
790
879
  setToolSelectionTitle(
@@ -1079,7 +1168,6 @@ function SidebarComponent(props: any) {
1079
1168
 
1080
1169
  useEffect(() => {
1081
1170
  const prefixes: string[] = [];
1082
- prefixes.push('/clear');
1083
1171
 
1084
1172
  if (chatMode === 'ask') {
1085
1173
  const chatParticipants = NBIAPI.config.chatParticipants;
@@ -1096,6 +1184,8 @@ function SidebarComponent(props: any) {
1096
1184
  prefixes.push(`${commandPrefix}/${command}`);
1097
1185
  }
1098
1186
  }
1187
+ } else {
1188
+ prefixes.push('/clear');
1099
1189
  }
1100
1190
 
1101
1191
  const mcpServers = NBIAPI.config.toolConfig.mcpServers;
@@ -1311,6 +1401,7 @@ function SidebarComponent(props: any) {
1311
1401
  type: RunChatCompletionType.Chat,
1312
1402
  content: extractedPrompt,
1313
1403
  language: activeDocInfo.language,
1404
+ currentDirectory: props.getCurrentDirectory(),
1314
1405
  filename: activeDocInfo.filePath,
1315
1406
  additionalContext,
1316
1407
  chatMode,
@@ -1886,8 +1977,6 @@ function SidebarComponent(props: any) {
1886
1977
  onChange={event => {
1887
1978
  if (event.target.value === 'ask') {
1888
1979
  setToolSelections(toolSelectionsEmpty);
1889
- } else if (event.target.value === 'agent') {
1890
- setToolSelections(structuredClone(toolSelectionsInitial));
1891
1980
  }
1892
1981
  setShowModeTools(false);
1893
1982
  setChatMode(event.target.value);
@@ -1899,11 +1988,11 @@ function SidebarComponent(props: any) {
1899
1988
  </div>
1900
1989
  {chatMode !== 'ask' && (
1901
1990
  <div
1902
- className={`user-input-footer-button tools-button ${notebookExecuteToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`}
1991
+ className={`user-input-footer-button tools-button ${unsafeToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`}
1903
1992
  onClick={() => handleChatToolsButtonClick()}
1904
1993
  title={
1905
- notebookExecuteToolSelected
1906
- ? `Notebook execute tool selected!\n${toolSelectionTitle}`
1994
+ unsafeToolSelected
1995
+ ? `Tool selection can cause irreversible changes! Review each tool execution carefully.\n${toolSelectionTitle}`
1907
1996
  : toolSelectionTitle
1908
1997
  }
1909
1998
  >
@@ -1996,6 +2085,7 @@ function SidebarComponent(props: any) {
1996
2085
  key={toolset.id}
1997
2086
  label={toolset.name}
1998
2087
  checked={getBuiltinToolsetState(toolset.id)}
2088
+ tooltip={toolset.description}
1999
2089
  header={true}
2000
2090
  onClick={() => {
2001
2091
  setBuiltinToolsetState(
@@ -10,7 +10,7 @@ export function CheckBoxItem(props: any) {
10
10
  return (
11
11
  <div
12
12
  className={`checkbox-item checkbox-item-indent-${indent} ${props.header ? 'checkbox-item-header' : ''}`}
13
- title={props.title}
13
+ title={props.tooltip || props.title || ''}
14
14
  onClick={event => props.onClick(event)}
15
15
  >
16
16
  <div className="checkbox-item-toggle">
@@ -724,23 +724,51 @@ function SettingsPanelComponentMCPServers(props: any) {
724
724
  </div>
725
725
  {getMCPServerEnabled(server.id) && (
726
726
  <div>
727
- {server.tools.map((tool: any) => (
728
- <PillItem
729
- label={tool.name}
730
- title={tool.description}
731
- checked={getMCPServerToolEnabled(
732
- server.id,
733
- tool.name
734
- )}
735
- onClick={() => {
736
- setMCPServerToolEnabled(
737
- server.id,
738
- tool.name,
739
- !getMCPServerToolEnabled(server.id, tool.name)
740
- );
741
- }}
742
- ></PillItem>
743
- ))}
727
+ {server.tools.length > 0 && (
728
+ <div className="mcp-server-tools">
729
+ <div className="mcp-server-tools-header">
730
+ Tools
731
+ </div>
732
+ <div>
733
+ {server.tools.map((tool: any) => (
734
+ <PillItem
735
+ label={tool.name}
736
+ title={tool.description}
737
+ checked={getMCPServerToolEnabled(
738
+ server.id,
739
+ tool.name
740
+ )}
741
+ onClick={() => {
742
+ setMCPServerToolEnabled(
743
+ server.id,
744
+ tool.name,
745
+ !getMCPServerToolEnabled(
746
+ server.id,
747
+ tool.name
748
+ )
749
+ );
750
+ }}
751
+ ></PillItem>
752
+ ))}
753
+ </div>
754
+ </div>
755
+ )}
756
+ {server.prompts.length > 0 && (
757
+ <div className="mcp-server-prompts">
758
+ <div className="mcp-server-prompts-header">
759
+ Prompts
760
+ </div>
761
+ <div>
762
+ {server.prompts.map((prompt: any) => (
763
+ <PillItem
764
+ label={prompt.name}
765
+ title={prompt.description}
766
+ checked={true}
767
+ ></PillItem>
768
+ ))}
769
+ </div>
770
+ </div>
771
+ )}
744
772
  </div>
745
773
  )}
746
774
  </div>
package/src/index.ts CHANGED
@@ -32,7 +32,7 @@ import { NotebookPanel } from '@jupyterlab/notebook';
32
32
  import { CodeEditor } from '@jupyterlab/codeeditor';
33
33
  import { FileEditorWidget } from '@jupyterlab/fileeditor';
34
34
 
35
- import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
35
+ import { FileBrowserModel, IDefaultFileBrowser } from '@jupyterlab/filebrowser';
36
36
 
37
37
  import { ContentsManager, KernelSpecManager } from '@jupyterlab/services';
38
38
 
@@ -83,6 +83,7 @@ import { UUID } from '@lumino/coreutils';
83
83
 
84
84
  import * as path from 'path';
85
85
  import { SettingsPanel } from './components/settings-panel';
86
+ import { ITerminalConnection } from '@jupyterlab/services/lib/terminal/terminal';
86
87
 
87
88
  namespace CommandIDs {
88
89
  export const chatuserInput = 'notebook-intelligence:chat-user-input';
@@ -130,6 +131,8 @@ namespace CommandIDs {
130
131
  'notebook-intelligence:open-mcp-config-editor';
131
132
  export const showFormInputDialog =
132
133
  'notebook-intelligence:show-form-input-dialog';
134
+ export const runCommandInTerminal =
135
+ 'notebook-intelligence:run-command-in-terminal';
133
136
  }
134
137
 
135
138
  const DOCUMENT_WATCH_INTERVAL = 1000;
@@ -160,7 +163,8 @@ const BACKEND_TELEMETRY_LISTENER_NAME = 'backend-telemetry-listener';
160
163
  class ActiveDocumentWatcher {
161
164
  static initialize(
162
165
  app: JupyterLab,
163
- languageRegistry: IEditorLanguageRegistry
166
+ languageRegistry: IEditorLanguageRegistry,
167
+ fileBrowser: IDefaultFileBrowser
164
168
  ) {
165
169
  ActiveDocumentWatcher._languageRegistry = languageRegistry;
166
170
 
@@ -171,6 +175,13 @@ class ActiveDocumentWatcher {
171
175
  ActiveDocumentWatcher.activeDocumentInfo.activeWidget =
172
176
  app.shell.currentWidget;
173
177
  ActiveDocumentWatcher.handleWatchDocument();
178
+
179
+ if (fileBrowser) {
180
+ const onPathChanged = (model: FileBrowserModel) => {
181
+ ActiveDocumentWatcher.currentDirectory = model.path;
182
+ };
183
+ fileBrowser.model.pathChanged.connect(onPathChanged);
184
+ }
174
185
  }
175
186
 
176
187
  static watchDocument(widget: Widget) {
@@ -315,6 +326,8 @@ class ActiveDocumentWatcher {
315
326
  );
316
327
  }
317
328
 
329
+ static currentDirectory: string = '';
330
+
318
331
  static activeDocumentInfo: IActiveDocumentInfo = {
319
332
  language: 'python',
320
333
  filename: 'nb-doesnt-exist.ipynb',
@@ -724,6 +737,9 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
724
737
  });
725
738
  panel.title.icon = sidebarIcon;
726
739
  const sidebar = new ChatSidebar({
740
+ getCurrentDirectory: (): string => {
741
+ return ActiveDocumentWatcher.currentDirectory;
742
+ },
727
743
  getActiveDocumentInfo: (): IActiveDocumentInfo => {
728
744
  return ActiveDocumentWatcher.activeDocumentInfo;
729
745
  },
@@ -744,7 +760,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
744
760
  }
745
761
  });
746
762
  panel.addWidget(sidebar);
747
- app.shell.add(panel, 'left', { rank: 1000 });
763
+ app.shell.add(panel, 'right', { rank: 1000 });
748
764
  app.shell.activateById(panel.id);
749
765
 
750
766
  const updateSidebarIcon = () => {
@@ -962,6 +978,32 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
962
978
  }
963
979
  });
964
980
 
981
+ app.commands.addCommand(CommandIDs.runCommandInTerminal, {
982
+ execute: async args => {
983
+ const command = args.command as string;
984
+ const terminal = await app.commands.execute('terminal:create-new', {
985
+ cwd: (args.cwd as string) || ActiveDocumentWatcher.currentDirectory
986
+ });
987
+
988
+ const session: ITerminalConnection = terminal?.content?.session;
989
+
990
+ session.messageReceived.connect((sender, message) => {
991
+ console.log('Message received in Jupyter terminal:', message);
992
+ });
993
+
994
+ if (session) {
995
+ session.send({
996
+ type: 'stdin',
997
+ content: [command + '\n'] // Add newline to execute the command
998
+ });
999
+
1000
+ return 'Command executed in Jupyter terminal';
1001
+ } else {
1002
+ return 'Failed to execute command in Jupyter terminal';
1003
+ }
1004
+ }
1005
+ });
1006
+
965
1007
  const isNewEmptyNotebook = (model: ISharedNotebook) => {
966
1008
  return (
967
1009
  model.cells.length === 1 &&
@@ -1848,7 +1890,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1848
1890
  }
1849
1891
 
1850
1892
  const jlabApp = app as JupyterLab;
1851
- ActiveDocumentWatcher.initialize(jlabApp, languageRegistry);
1893
+ ActiveDocumentWatcher.initialize(jlabApp, languageRegistry, defaultBrowser);
1852
1894
 
1853
1895
  return extensionService;
1854
1896
  }
package/src/tokens.ts CHANGED
@@ -100,7 +100,11 @@ export interface IToolSelections {
100
100
 
101
101
  export enum BuiltinToolsetType {
102
102
  NotebookEdit = 'nbi-notebook-edit',
103
- NotebookExecute = 'nbi-notebook-execute'
103
+ NotebookExecute = 'nbi-notebook-execute',
104
+ PythonFileEdit = 'nbi-python-file-edit',
105
+ FileEdit = 'nbi-file-edit',
106
+ FileRead = 'nbi-file-read',
107
+ CommandExecute = 'nbi-command-execute'
104
108
  }
105
109
 
106
110
  export const GITHUB_COPILOT_PROVIDER_ID = 'github-copilot';
package/style/base.css CHANGED
@@ -733,8 +733,11 @@ body[data-jp-theme-light='false'] .inline-popover {
733
733
  display: none;
734
734
  margin: 5px;
735
735
  padding: 0 5px;
736
- border: 1px dashed var(--jp-border-color1);
736
+ border: 1px solid var(--jp-border-color2);
737
737
  border-radius: 5px;
738
+ max-height: 200px;
739
+ overflow-y: auto;
740
+ box-shadow: inset 0 0 15px 15px rgb(23 23 23 / 50%);
738
741
  }
739
742
 
740
743
  .expandable-content .collapsed-icon {
@@ -816,6 +819,14 @@ svg.access-token-warning {
816
819
  font-weight: bold;
817
820
  }
818
821
 
822
+ .mode-tools-group-built-in {
823
+ flex-flow: row wrap;
824
+ }
825
+
826
+ .mode-tools-group-built-in .checkbox-item {
827
+ width: 50%;
828
+ }
829
+
819
830
  .mode-tools-group-built-in .checkbox-item-toggle {
820
831
  font-weight: normal;
821
832
  }
@@ -930,3 +941,13 @@ svg.access-token-warning {
930
941
  .form-input-dialog-body-content-field-input {
931
942
  width: 50%;
932
943
  }
944
+
945
+ .mcp-server-prompts .pill-item {
946
+ cursor: default;
947
+ }
948
+
949
+ .mcp-server-tools-header,
950
+ .mcp-server-prompts-header {
951
+ margin: 5px;
952
+ font-style: italic;
953
+ }