@notebook-intelligence/notebook-intelligence 2.6.1 → 3.0.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(', ')})`);
@@ -810,6 +884,7 @@ function SidebarComponent(props) {
810
884
  type: RunChatCompletionType.Chat,
811
885
  content: extractedPrompt,
812
886
  language: activeDocInfo.language,
887
+ currentDirectory: props.getCurrentDirectory(),
813
888
  filename: activeDocInfo.filePath,
814
889
  additionalContext,
815
890
  chatMode,
@@ -1217,16 +1292,13 @@ function SidebarComponent(props) {
1217
1292
  if (event.target.value === 'ask') {
1218
1293
  setToolSelections(toolSelectionsEmpty);
1219
1294
  }
1220
- else if (event.target.value === 'agent') {
1221
- setToolSelections(structuredClone(toolSelectionsInitial));
1222
- }
1223
1295
  setShowModeTools(false);
1224
1296
  setChatMode(event.target.value);
1225
1297
  } },
1226
1298
  React.createElement("option", { value: "ask" }, "Ask"),
1227
1299
  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}`
1300
+ 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
1301
+ ? `Tool selection can cause irreversible changes! Review each tool execution carefully.\n${toolSelectionTitle}`
1230
1302
  : toolSelectionTitle },
1231
1303
  React.createElement(VscTools, null),
1232
1304
  selectedToolCount > 0 && React.createElement(React.Fragment, null, selectedToolCount)))),
@@ -1257,7 +1329,7 @@ function SidebarComponent(props) {
1257
1329
  React.createElement("div", null, "Done"))),
1258
1330
  React.createElement("div", { className: "mode-tools-popover-tool-list" },
1259
1331
  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: () => {
1332
+ 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
1333
  setBuiltinToolsetState(toolset.id, !getBuiltinToolsetState(toolset.id));
1262
1334
  } })))),
1263
1335
  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.0.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(
@@ -1311,6 +1400,7 @@ function SidebarComponent(props: any) {
1311
1400
  type: RunChatCompletionType.Chat,
1312
1401
  content: extractedPrompt,
1313
1402
  language: activeDocInfo.language,
1403
+ currentDirectory: props.getCurrentDirectory(),
1314
1404
  filename: activeDocInfo.filePath,
1315
1405
  additionalContext,
1316
1406
  chatMode,
@@ -1886,8 +1976,6 @@ function SidebarComponent(props: any) {
1886
1976
  onChange={event => {
1887
1977
  if (event.target.value === 'ask') {
1888
1978
  setToolSelections(toolSelectionsEmpty);
1889
- } else if (event.target.value === 'agent') {
1890
- setToolSelections(structuredClone(toolSelectionsInitial));
1891
1979
  }
1892
1980
  setShowModeTools(false);
1893
1981
  setChatMode(event.target.value);
@@ -1899,11 +1987,11 @@ function SidebarComponent(props: any) {
1899
1987
  </div>
1900
1988
  {chatMode !== 'ask' && (
1901
1989
  <div
1902
- className={`user-input-footer-button tools-button ${notebookExecuteToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`}
1990
+ className={`user-input-footer-button tools-button ${unsafeToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`}
1903
1991
  onClick={() => handleChatToolsButtonClick()}
1904
1992
  title={
1905
- notebookExecuteToolSelected
1906
- ? `Notebook execute tool selected!\n${toolSelectionTitle}`
1993
+ unsafeToolSelected
1994
+ ? `Tool selection can cause irreversible changes! Review each tool execution carefully.\n${toolSelectionTitle}`
1907
1995
  : toolSelectionTitle
1908
1996
  }
1909
1997
  >
@@ -1996,6 +2084,7 @@ function SidebarComponent(props: any) {
1996
2084
  key={toolset.id}
1997
2085
  label={toolset.name}
1998
2086
  checked={getBuiltinToolsetState(toolset.id)}
2087
+ tooltip={toolset.description}
1999
2088
  header={true}
2000
2089
  onClick={() => {
2001
2090
  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
+ }