@notebook-intelligence/notebook-intelligence 2.3.2 → 2.4.1

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
@@ -62,7 +62,7 @@ for the frontend extension.
62
62
  Notebook Intelligence can remember your GitHub Copilot login so that you don't need to re-login after a JupyterLab or system restart. Please be aware of the security implications of using this feature.
63
63
 
64
64
  > [!CAUTION]
65
- > If you configure NBI to remember your GitHub Copilot login, it will encrypt the token and store into a data file at `~/.jupyter/nbi-data.json`. You should never share this file with others as they can access your tokens.
65
+ > If you configure NBI to remember your GitHub Copilot login, it will encrypt the token and store into a data file at `~/.jupyter/nbi/user-data.json`. You should never share this file with others as they can access your tokens.
66
66
  > Even though the token is encrypted, it is done so by using a default password and that's why it can be decrypted by others. In order to prevent that you can specify a custom password using the environment variable `NBI_GH_ACCESS_TOKEN_PASSWORD`.
67
67
 
68
68
  ```bash
package/lib/api.d.ts CHANGED
@@ -12,10 +12,11 @@ export interface IDeviceVerificationInfo {
12
12
  }
13
13
  export declare class NBIConfig {
14
14
  get userHomeDir(): string;
15
- get configFilePath(): string;
15
+ get userConfigDir(): string;
16
16
  get llmProviders(): [any];
17
17
  get chatModels(): [any];
18
18
  get inlineCompletionModels(): [any];
19
+ get defaultChatMode(): string;
19
20
  get chatModel(): any;
20
21
  get inlineCompletionModel(): any;
21
22
  get usingGitHubCopilotModel(): boolean;
@@ -32,6 +33,7 @@ export declare class NBIAPI {
32
33
  static _messageReceived: Signal<unknown, any>;
33
34
  static config: NBIConfig;
34
35
  static configChanged: Signal<NBIConfig, void>;
36
+ static githubLoginStatusChanged: Signal<unknown, void>;
35
37
  static initialize(): Promise<void>;
36
38
  static initializeWebsocket(): Promise<void>;
37
39
  static getLoginStatus(): GitHubCopilotLoginStatus;
@@ -43,6 +45,8 @@ export declare class NBIAPI {
43
45
  static setConfig(config: any): Promise<void>;
44
46
  static updateOllamaModelList(): Promise<void>;
45
47
  static reloadMCPServerList(): Promise<any>;
48
+ static getMCPConfigFile(): Promise<any>;
49
+ static setMCPConfigFile(config: any): Promise<any>;
46
50
  static chatRequest(messageId: string, chatId: string, prompt: string, language: string, filename: string, additionalContext: IContextItem[], chatMode: string, toolSelections: IToolSelections, responseEmitter: IChatCompletionResponseEmitter): Promise<void>;
47
51
  static generateCode(chatId: string, prompt: string, prefix: string, suffix: string, existingCode: string, language: string, filename: string, responseEmitter: IChatCompletionResponseEmitter): Promise<void>;
48
52
  static sendChatUserInput(messageId: string, data: any): Promise<void>;
package/lib/api.js CHANGED
@@ -5,8 +5,7 @@ import { requestAPI } from './handler';
5
5
  import { URLExt } from '@jupyterlab/coreutils';
6
6
  import { UUID } from '@lumino/coreutils';
7
7
  import { Signal } from '@lumino/signaling';
8
- import { GITHUB_COPILOT_PROVIDER_ID, RequestDataType } from './tokens';
9
- const LOGIN_STATUS_UPDATE_INTERVAL = 2000;
8
+ import { GITHUB_COPILOT_PROVIDER_ID, RequestDataType, BackendMessageType } from './tokens';
10
9
  export var GitHubCopilotLoginStatus;
11
10
  (function (GitHubCopilotLoginStatus) {
12
11
  GitHubCopilotLoginStatus["NotLoggedIn"] = "NOT_LOGGED_IN";
@@ -23,8 +22,8 @@ export class NBIConfig {
23
22
  get userHomeDir() {
24
23
  return this.capabilities.user_home_dir;
25
24
  }
26
- get configFilePath() {
27
- return this.capabilities.config_file_path;
25
+ get userConfigDir() {
26
+ return this.capabilities.nbi_user_config_dir;
28
27
  }
29
28
  get llmProviders() {
30
29
  return this.capabilities.llm_providers;
@@ -35,6 +34,9 @@ export class NBIConfig {
35
34
  get inlineCompletionModels() {
36
35
  return this.capabilities.inline_completion_models;
37
36
  }
37
+ get defaultChatMode() {
38
+ return this.capabilities.default_chat_mode;
39
+ }
38
40
  get chatModel() {
39
41
  return this.capabilities.chat_model;
40
42
  }
@@ -56,10 +58,15 @@ class NBIAPI {
56
58
  static async initialize() {
57
59
  await this.fetchCapabilities();
58
60
  this.updateGitHubLoginStatus();
59
- setInterval(() => {
60
- this.updateGitHubLoginStatus();
61
- }, LOGIN_STATUS_UPDATE_INTERVAL);
62
61
  NBIAPI.initializeWebsocket();
62
+ this._messageReceived.connect((_, msg) => {
63
+ msg = JSON.parse(msg);
64
+ if (msg.type === BackendMessageType.GitHubCopilotLoginStatusChange) {
65
+ this.updateGitHubLoginStatus().then(() => {
66
+ this.githubLoginStatusChanged.emit();
67
+ });
68
+ }
69
+ });
63
70
  }
64
71
  static async initializeWebsocket() {
65
72
  const serverSettings = ServerConnection.makeSettings();
@@ -189,6 +196,33 @@ class NBIAPI {
189
196
  });
190
197
  });
191
198
  }
199
+ static async getMCPConfigFile() {
200
+ return new Promise((resolve, reject) => {
201
+ requestAPI('mcp-config-file', { method: 'GET' })
202
+ .then(async (data) => {
203
+ resolve(data);
204
+ })
205
+ .catch(reason => {
206
+ console.error(`Failed to get MCP config file.\n${reason}`);
207
+ reject(reason);
208
+ });
209
+ });
210
+ }
211
+ static async setMCPConfigFile(config) {
212
+ return new Promise((resolve, reject) => {
213
+ requestAPI('mcp-config-file', {
214
+ method: 'POST',
215
+ body: JSON.stringify(config)
216
+ })
217
+ .then(async (data) => {
218
+ resolve(data);
219
+ })
220
+ .catch(reason => {
221
+ console.error(`Failed to set MCP config file.\n${reason}`);
222
+ reject(reason);
223
+ });
224
+ });
225
+ }
192
226
  static async chatRequest(messageId, chatId, prompt, language, filename, additionalContext, chatMode, toolSelections, responseEmitter) {
193
227
  this._messageReceived.connect((_, msg) => {
194
228
  msg = JSON.parse(msg);
@@ -286,4 +320,5 @@ NBIAPI._deviceVerificationInfo = {
286
320
  NBIAPI._messageReceived = new Signal(_a);
287
321
  NBIAPI.config = new NBIConfig();
288
322
  NBIAPI.configChanged = _a.config.changed;
323
+ NBIAPI.githubLoginStatusChanged = new Signal(_a);
289
324
  export { NBIAPI };
@@ -76,7 +76,9 @@ export declare class GitHubCopilotLoginDialogBody extends ReactWidget {
76
76
  export declare class ConfigurationDialogBody extends ReactWidget {
77
77
  constructor(options: {
78
78
  onSave: () => void;
79
+ onEditMCPConfigClicked: () => void;
79
80
  });
80
81
  render(): JSX.Element;
82
+ private _onEditMCPConfigClicked;
81
83
  private _onSave;
82
84
  }
@@ -13,6 +13,7 @@ import copilotWarningSvgstr from '../style/icons/copilot-warning.svg';
13
13
  import { VscSend, VscStopCircle, VscEye, VscEyeClosed, VscTriangleRight, VscTriangleDown, VscWarning, VscSettingsGear, VscPassFilled, VscTools, VscTrash } from 'react-icons/vsc';
14
14
  import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from 'react-icons/md';
15
15
  import { extractLLMGeneratedCode, isDarkTheme } from './utils';
16
+ import * as path from 'path';
16
17
  const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
17
18
  const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
18
19
  const OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID = 'openai-compatible-inline-completion-model';
@@ -130,10 +131,11 @@ export class GitHubCopilotLoginDialogBody extends ReactWidget {
130
131
  export class ConfigurationDialogBody extends ReactWidget {
131
132
  constructor(options) {
132
133
  super();
134
+ this._onEditMCPConfigClicked = options.onEditMCPConfigClicked;
133
135
  this._onSave = options.onSave;
134
136
  }
135
137
  render() {
136
- return React.createElement(ConfigurationDialogBodyComponent, { onSave: this._onSave });
138
+ return (React.createElement(ConfigurationDialogBodyComponent, { onEditMCPConfigClicked: this._onEditMCPConfigClicked, onSave: this._onSave }));
137
139
  }
138
140
  }
139
141
  const answeredForms = new Map();
@@ -358,7 +360,7 @@ function SidebarComponent(props) {
358
360
  const [activeDocumentInfo, setActiveDocumentInfo] = useState(null);
359
361
  const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
360
362
  const telemetryEmitter = props.getTelemetryEmitter();
361
- const [chatMode, setChatMode] = useState('ask');
363
+ const [chatMode, setChatMode] = useState(NBIAPI.config.defaultChatMode);
362
364
  const [toolSelectionTitle, setToolSelectionTitle] = useState('Tool selection');
363
365
  const [selectedToolCount, setSelectedToolCount] = useState(0);
364
366
  const [notebookExecuteToolSelected, setNotebookExecuteToolSelected] = useState(false);
@@ -1508,6 +1510,7 @@ function ConfigurationDialogBodyComponent(props) {
1508
1510
  const [mcpServerNames, setMcpServerNames] = useState(((_a = nbiConfig.toolConfig.mcpServers) === null || _a === void 0 ? void 0 : _a.map((server) => server.id)) || []);
1509
1511
  const handleSaveClick = async () => {
1510
1512
  const config = {
1513
+ default_chat_mode: defaultChatMode,
1511
1514
  chat_model: {
1512
1515
  provider: chatModelProvider,
1513
1516
  model: chatModel,
@@ -1532,6 +1535,7 @@ function ConfigurationDialogBodyComponent(props) {
1532
1535
  };
1533
1536
  const [chatModelProvider, setChatModelProvider] = useState(nbiConfig.chatModel.provider || 'none');
1534
1537
  const [inlineCompletionModelProvider, setInlineCompletionModelProvider] = useState(nbiConfig.inlineCompletionModel.provider || 'none');
1538
+ const [defaultChatMode, setDefaultChatMode] = useState(nbiConfig.defaultChatMode);
1535
1539
  const [chatModel, setChatModel] = useState(nbiConfig.chatModel.model);
1536
1540
  const [chatModelProperties, setChatModelProperties] = useState([]);
1537
1541
  const [inlineCompletionModelProperties, setInlineCompletionModelProperties] = useState([]);
@@ -1608,6 +1612,16 @@ function ConfigurationDialogBodyComponent(props) {
1608
1612
  }, []);
1609
1613
  return (React.createElement("div", { className: "config-dialog" },
1610
1614
  React.createElement("div", { className: "config-dialog-body" },
1615
+ React.createElement("div", { className: "model-config-section" },
1616
+ React.createElement("div", { className: "model-config-section-header" }, "Default chat mode"),
1617
+ React.createElement("div", { className: "model-config-section-body" },
1618
+ React.createElement("div", { className: "model-config-section-row" },
1619
+ React.createElement("div", { className: "model-config-section-column" },
1620
+ React.createElement("div", null,
1621
+ React.createElement("select", { className: "jp-mod-styled", value: defaultChatMode, onChange: event => setDefaultChatMode(event.target.value) },
1622
+ React.createElement("option", { value: "ask" }, "Ask"),
1623
+ React.createElement("option", { value: "agent" }, "Agent")))),
1624
+ React.createElement("div", { className: "model-config-section-column" }, " ")))),
1611
1625
  React.createElement("div", { className: "model-config-section" },
1612
1626
  React.createElement("div", { className: "model-config-section-header" }, "Chat model"),
1613
1627
  React.createElement("div", { className: "model-config-section-body" },
@@ -1689,8 +1703,10 @@ function ConfigurationDialogBodyComponent(props) {
1689
1703
  "Remember my GitHub Copilot access token")))))),
1690
1704
  React.createElement("div", { className: "model-config-section" },
1691
1705
  React.createElement("div", { className: "model-config-section-header" },
1692
- "MCP Servers [",
1706
+ "MCP Servers (",
1693
1707
  mcpServerNames.length,
1708
+ ") [",
1709
+ React.createElement("a", { href: "javascript:void(0)", onClick: props.onEditMCPConfigClicked }, "edit"),
1694
1710
  "]"),
1695
1711
  React.createElement("div", { className: "model-config-section-body" },
1696
1712
  React.createElement("div", { className: "model-config-section-row" },
@@ -1706,10 +1722,21 @@ function ConfigurationDialogBodyComponent(props) {
1706
1722
  React.createElement("div", { className: "model-config-section-row" },
1707
1723
  React.createElement("div", { className: "model-config-section-column" },
1708
1724
  React.createElement("span", { className: "user-code-span", onClick: () => {
1709
- navigator.clipboard.writeText(NBIAPI.config.configFilePath);
1725
+ navigator.clipboard.writeText(path.join(NBIAPI.config.userConfigDir, 'config.json'));
1726
+ return true;
1727
+ } },
1728
+ path.join(NBIAPI.config.userConfigDir, 'config.json'),
1729
+ ' ',
1730
+ React.createElement("span", { className: "copy-icon", dangerouslySetInnerHTML: { __html: copySvgstr } }))))),
1731
+ React.createElement("div", { className: "model-config-section-header" }, "MCP config file path"),
1732
+ React.createElement("div", { className: "model-config-section-body" },
1733
+ React.createElement("div", { className: "model-config-section-row" },
1734
+ React.createElement("div", { className: "model-config-section-column" },
1735
+ React.createElement("span", { className: "user-code-span", onClick: () => {
1736
+ navigator.clipboard.writeText(path.join(NBIAPI.config.userConfigDir, 'mcp.json'));
1710
1737
  return true;
1711
1738
  } },
1712
- NBIAPI.config.configFilePath,
1739
+ path.join(NBIAPI.config.userConfigDir, 'mcp.json'),
1713
1740
  ' ',
1714
1741
  React.createElement("span", { className: "copy-icon", dangerouslySetInnerHTML: { __html: copySvgstr } }))))))),
1715
1742
  React.createElement("div", { className: "config-dialog-footer" },
package/lib/index.js CHANGED
@@ -50,6 +50,7 @@ var CommandIDs;
50
50
  CommandIDs.runCellAtIndex = 'notebook-intelligence:run-cell-at-index';
51
51
  CommandIDs.getCurrentFileContent = 'notebook-intelligence:get-current-file-content';
52
52
  CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content';
53
+ CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor';
53
54
  })(CommandIDs || (CommandIDs = {}));
54
55
  const DOCUMENT_WATCH_INTERVAL = 1000;
55
56
  const MAX_TOKENS = 4096;
@@ -356,6 +357,68 @@ class TelemetryEmitter {
356
357
  });
357
358
  }
358
359
  }
360
+ class MCPConfigEditor {
361
+ constructor(docManager) {
362
+ this._docWidget = null;
363
+ this._tmpMCPConfigFilename = 'nbi.mcp.temp.json';
364
+ this._isOpen = false;
365
+ this._docManager = docManager;
366
+ }
367
+ async open() {
368
+ const contents = new ContentsManager();
369
+ const newJSONFile = await contents.newUntitled({
370
+ ext: '.json'
371
+ });
372
+ const mcpConfig = await NBIAPI.getMCPConfigFile();
373
+ try {
374
+ await contents.delete(this._tmpMCPConfigFilename);
375
+ }
376
+ catch (error) {
377
+ // ignore
378
+ }
379
+ await contents.save(newJSONFile.path, {
380
+ content: JSON.stringify(mcpConfig, null, 2),
381
+ format: 'text',
382
+ type: 'file'
383
+ });
384
+ await contents.rename(newJSONFile.path, this._tmpMCPConfigFilename);
385
+ this._docWidget = this._docManager.openOrReveal(this._tmpMCPConfigFilename, 'Editor');
386
+ this._addListeners();
387
+ // tab closed
388
+ this._docWidget.disposed.connect((_, args) => {
389
+ this._removeListeners();
390
+ contents.delete(this._tmpMCPConfigFilename);
391
+ });
392
+ this._isOpen = true;
393
+ }
394
+ close() {
395
+ if (!this._isOpen) {
396
+ return;
397
+ }
398
+ this._isOpen = false;
399
+ this._docWidget.dispose();
400
+ this._docWidget = null;
401
+ }
402
+ get isOpen() {
403
+ return this._isOpen;
404
+ }
405
+ _addListeners() {
406
+ this._docWidget.context.model.stateChanged.connect(this._onStateChanged, this);
407
+ }
408
+ _removeListeners() {
409
+ this._docWidget.context.model.stateChanged.disconnect(this._onStateChanged, this);
410
+ }
411
+ _onStateChanged(model, args) {
412
+ if (args.name === 'dirty' && args.newValue === false) {
413
+ this._onSave();
414
+ }
415
+ }
416
+ async _onSave() {
417
+ const mcpConfig = this._docWidget.context.model.toJSON();
418
+ await NBIAPI.setMCPConfigFile(mcpConfig);
419
+ await NBIAPI.fetchCapabilities();
420
+ }
421
+ }
359
422
  /**
360
423
  * Initialization data for the @notebook-intelligence/notebook-intelligence extension.
361
424
  */
@@ -392,6 +455,7 @@ const plugin = {
392
455
  };
393
456
  await NBIAPI.initialize();
394
457
  let openPopover = null;
458
+ let mcpConfigEditor = null;
395
459
  completionManager.registerInlineProvider(new NBIInlineCompletionProvider(telemetryEmitter));
396
460
  if (settingRegistry) {
397
461
  settingRegistry
@@ -866,6 +930,10 @@ const plugin = {
866
930
  onSave: () => {
867
931
  dialog === null || dialog === void 0 ? void 0 : dialog.dispose();
868
932
  NBIAPI.fetchCapabilities();
933
+ },
934
+ onEditMCPConfigClicked: () => {
935
+ dialog === null || dialog === void 0 ? void 0 : dialog.dispose();
936
+ app.commands.execute('notebook-intelligence:open-mcp-config-editor');
869
937
  }
870
938
  });
871
939
  dialog = new Dialog({
@@ -878,6 +946,16 @@ const plugin = {
878
946
  dialog.launch();
879
947
  }
880
948
  });
949
+ app.commands.addCommand(CommandIDs.openMCPConfigEditor, {
950
+ label: 'Open MCP Config Editor',
951
+ execute: args => {
952
+ if (mcpConfigEditor && mcpConfigEditor.isOpen) {
953
+ mcpConfigEditor.close();
954
+ }
955
+ mcpConfigEditor = new MCPConfigEditor(docManager);
956
+ mcpConfigEditor.open();
957
+ }
958
+ });
881
959
  palette.addItem({
882
960
  command: CommandIDs.openConfigurationDialog,
883
961
  category: 'Notebook Intelligence'
package/lib/tokens.d.ts CHANGED
@@ -25,7 +25,8 @@ export declare enum RequestDataType {
25
25
  export declare enum BackendMessageType {
26
26
  StreamMessage = "stream-message",
27
27
  StreamEnd = "stream-end",
28
- RunUICommand = "run-ui-command"
28
+ RunUICommand = "run-ui-command",
29
+ GitHubCopilotLoginStatusChange = "github-copilot-login-status-change"
29
30
  }
30
31
  export declare enum ResponseStreamDataType {
31
32
  LLMRaw = "llm-raw",
package/lib/tokens.js CHANGED
@@ -16,6 +16,7 @@ export var BackendMessageType;
16
16
  BackendMessageType["StreamMessage"] = "stream-message";
17
17
  BackendMessageType["StreamEnd"] = "stream-end";
18
18
  BackendMessageType["RunUICommand"] = "run-ui-command";
19
+ BackendMessageType["GitHubCopilotLoginStatusChange"] = "github-copilot-login-status-change";
19
20
  })(BackendMessageType || (BackendMessageType = {}));
20
21
  export var ResponseStreamDataType;
21
22
  (function (ResponseStreamDataType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notebook-intelligence/notebook-intelligence",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "AI coding assistant for JupyterLab",
5
5
  "keywords": [
6
6
  "AI",
package/src/api.ts CHANGED
@@ -12,11 +12,10 @@ import {
12
12
  IContextItem,
13
13
  ITelemetryEvent,
14
14
  IToolSelections,
15
- RequestDataType
15
+ RequestDataType,
16
+ BackendMessageType
16
17
  } from './tokens';
17
18
 
18
- const LOGIN_STATUS_UPDATE_INTERVAL = 2000;
19
-
20
19
  export enum GitHubCopilotLoginStatus {
21
20
  NotLoggedIn = 'NOT_LOGGED_IN',
22
21
  ActivatingDevice = 'ACTIVATING_DEVICE',
@@ -34,8 +33,8 @@ export class NBIConfig {
34
33
  return this.capabilities.user_home_dir;
35
34
  }
36
35
 
37
- get configFilePath(): string {
38
- return this.capabilities.config_file_path;
36
+ get userConfigDir(): string {
37
+ return this.capabilities.nbi_user_config_dir;
39
38
  }
40
39
 
41
40
  get llmProviders(): [any] {
@@ -50,6 +49,10 @@ export class NBIConfig {
50
49
  return this.capabilities.inline_completion_models;
51
50
  }
52
51
 
52
+ get defaultChatMode(): string {
53
+ return this.capabilities.default_chat_mode;
54
+ }
55
+
53
56
  get chatModel(): any {
54
57
  return this.capabilities.chat_model;
55
58
  }
@@ -89,16 +92,22 @@ export class NBIAPI {
89
92
  static _messageReceived = new Signal<unknown, any>(this);
90
93
  static config = new NBIConfig();
91
94
  static configChanged = this.config.changed;
95
+ static githubLoginStatusChanged = new Signal<unknown, void>(this);
92
96
 
93
97
  static async initialize() {
94
98
  await this.fetchCapabilities();
95
99
  this.updateGitHubLoginStatus();
96
100
 
97
- setInterval(() => {
98
- this.updateGitHubLoginStatus();
99
- }, LOGIN_STATUS_UPDATE_INTERVAL);
100
-
101
101
  NBIAPI.initializeWebsocket();
102
+
103
+ this._messageReceived.connect((_, msg) => {
104
+ msg = JSON.parse(msg);
105
+ if (msg.type === BackendMessageType.GitHubCopilotLoginStatusChange) {
106
+ this.updateGitHubLoginStatus().then(() => {
107
+ this.githubLoginStatusChanged.emit();
108
+ });
109
+ }
110
+ });
102
111
  }
103
112
 
104
113
  static async initializeWebsocket() {
@@ -250,6 +259,35 @@ export class NBIAPI {
250
259
  });
251
260
  }
252
261
 
262
+ static async getMCPConfigFile(): Promise<any> {
263
+ return new Promise<any>((resolve, reject) => {
264
+ requestAPI<any>('mcp-config-file', { method: 'GET' })
265
+ .then(async data => {
266
+ resolve(data);
267
+ })
268
+ .catch(reason => {
269
+ console.error(`Failed to get MCP config file.\n${reason}`);
270
+ reject(reason);
271
+ });
272
+ });
273
+ }
274
+
275
+ static async setMCPConfigFile(config: any): Promise<any> {
276
+ return new Promise<any>((resolve, reject) => {
277
+ requestAPI<any>('mcp-config-file', {
278
+ method: 'POST',
279
+ body: JSON.stringify(config)
280
+ })
281
+ .then(async data => {
282
+ resolve(data);
283
+ })
284
+ .catch(reason => {
285
+ console.error(`Failed to set MCP config file.\n${reason}`);
286
+ reject(reason);
287
+ });
288
+ });
289
+ }
290
+
253
291
  static async chatRequest(
254
292
  messageId: string,
255
293
  chatId: string,
@@ -56,6 +56,7 @@ import {
56
56
  import { MdOutlineCheckBoxOutlineBlank, MdCheckBox } from 'react-icons/md';
57
57
 
58
58
  import { extractLLMGeneratedCode, isDarkTheme } from './utils';
59
+ import * as path from 'path';
59
60
 
60
61
  const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
61
62
  const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
@@ -264,16 +265,26 @@ export class GitHubCopilotLoginDialogBody extends ReactWidget {
264
265
  }
265
266
 
266
267
  export class ConfigurationDialogBody extends ReactWidget {
267
- constructor(options: { onSave: () => void }) {
268
+ constructor(options: {
269
+ onSave: () => void;
270
+ onEditMCPConfigClicked: () => void;
271
+ }) {
268
272
  super();
269
273
 
274
+ this._onEditMCPConfigClicked = options.onEditMCPConfigClicked;
270
275
  this._onSave = options.onSave;
271
276
  }
272
277
 
273
278
  render(): JSX.Element {
274
- return <ConfigurationDialogBodyComponent onSave={this._onSave} />;
279
+ return (
280
+ <ConfigurationDialogBodyComponent
281
+ onEditMCPConfigClicked={this._onEditMCPConfigClicked}
282
+ onSave={this._onSave}
283
+ />
284
+ );
275
285
  }
276
286
 
287
+ private _onEditMCPConfigClicked: () => void;
277
288
  private _onSave: () => void;
278
289
  }
279
290
 
@@ -725,7 +736,8 @@ function SidebarComponent(props: any) {
725
736
  useState<IActiveDocumentInfo | null>(null);
726
737
  const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
727
738
  const telemetryEmitter: ITelemetryEmitter = props.getTelemetryEmitter();
728
- const [chatMode, setChatMode] = useState('ask');
739
+ const [chatMode, setChatMode] = useState(NBIAPI.config.defaultChatMode);
740
+
729
741
  const [toolSelectionTitle, setToolSelectionTitle] =
730
742
  useState('Tool selection');
731
743
  const [selectedToolCount, setSelectedToolCount] = useState(0);
@@ -2497,6 +2509,7 @@ function ConfigurationDialogBodyComponent(props: any) {
2497
2509
 
2498
2510
  const handleSaveClick = async () => {
2499
2511
  const config: any = {
2512
+ default_chat_mode: defaultChatMode,
2500
2513
  chat_model: {
2501
2514
  provider: chatModelProvider,
2502
2515
  model: chatModel,
@@ -2531,6 +2544,9 @@ function ConfigurationDialogBodyComponent(props: any) {
2531
2544
  );
2532
2545
  const [inlineCompletionModelProvider, setInlineCompletionModelProvider] =
2533
2546
  useState(nbiConfig.inlineCompletionModel.provider || 'none');
2547
+ const [defaultChatMode, setDefaultChatMode] = useState<string>(
2548
+ nbiConfig.defaultChatMode
2549
+ );
2534
2550
  const [chatModel, setChatModel] = useState<string>(nbiConfig.chatModel.model);
2535
2551
  const [chatModelProperties, setChatModelProperties] = useState<any[]>([]);
2536
2552
  const [inlineCompletionModelProperties, setInlineCompletionModelProperties] =
@@ -2629,6 +2645,27 @@ function ConfigurationDialogBodyComponent(props: any) {
2629
2645
  return (
2630
2646
  <div className="config-dialog">
2631
2647
  <div className="config-dialog-body">
2648
+ <div className="model-config-section">
2649
+ <div className="model-config-section-header">Default chat mode</div>
2650
+ <div className="model-config-section-body">
2651
+ <div className="model-config-section-row">
2652
+ <div className="model-config-section-column">
2653
+ <div>
2654
+ <select
2655
+ className="jp-mod-styled"
2656
+ value={defaultChatMode}
2657
+ onChange={event => setDefaultChatMode(event.target.value)}
2658
+ >
2659
+ <option value="ask">Ask</option>
2660
+ <option value="agent">Agent</option>
2661
+ </select>
2662
+ </div>
2663
+ </div>
2664
+ <div className="model-config-section-column"> </div>
2665
+ </div>
2666
+ </div>
2667
+ </div>
2668
+
2632
2669
  <div className="model-config-section">
2633
2670
  <div className="model-config-section-header">Chat model</div>
2634
2671
  <div className="model-config-section-body">
@@ -2889,7 +2926,11 @@ function ConfigurationDialogBodyComponent(props: any) {
2889
2926
 
2890
2927
  <div className="model-config-section">
2891
2928
  <div className="model-config-section-header">
2892
- MCP Servers [{mcpServerNames.length}]
2929
+ MCP Servers ({mcpServerNames.length}) [
2930
+ <a href="javascript:void(0)" onClick={props.onEditMCPConfigClicked}>
2931
+ edit
2932
+ </a>
2933
+ ]
2893
2934
  </div>
2894
2935
  <div className="model-config-section-body">
2895
2936
  <div className="model-config-section-row">
@@ -2927,11 +2968,37 @@ function ConfigurationDialogBodyComponent(props: any) {
2927
2968
  <span
2928
2969
  className="user-code-span"
2929
2970
  onClick={() => {
2930
- navigator.clipboard.writeText(NBIAPI.config.configFilePath);
2971
+ navigator.clipboard.writeText(
2972
+ path.join(NBIAPI.config.userConfigDir, 'config.json')
2973
+ );
2974
+ return true;
2975
+ }}
2976
+ >
2977
+ {path.join(NBIAPI.config.userConfigDir, 'config.json')}{' '}
2978
+ <span
2979
+ className="copy-icon"
2980
+ dangerouslySetInnerHTML={{ __html: copySvgstr }}
2981
+ ></span>
2982
+ </span>
2983
+ </div>
2984
+ </div>
2985
+ </div>
2986
+ <div className="model-config-section-header">
2987
+ MCP config file path
2988
+ </div>
2989
+ <div className="model-config-section-body">
2990
+ <div className="model-config-section-row">
2991
+ <div className="model-config-section-column">
2992
+ <span
2993
+ className="user-code-span"
2994
+ onClick={() => {
2995
+ navigator.clipboard.writeText(
2996
+ path.join(NBIAPI.config.userConfigDir, 'mcp.json')
2997
+ );
2931
2998
  return true;
2932
2999
  }}
2933
3000
  >
2934
- {NBIAPI.config.configFilePath}{' '}
3001
+ {path.join(NBIAPI.config.userConfigDir, 'mcp.json')}{' '}
2935
3002
  <span
2936
3003
  className="copy-icon"
2937
3004
  dangerouslySetInnerHTML={{ __html: copySvgstr }}
package/src/index.ts CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  } from '@jupyterlab/application';
8
8
 
9
9
  import { IDocumentManager } from '@jupyterlab/docmanager';
10
- import { DocumentWidget } from '@jupyterlab/docregistry';
10
+ import { DocumentWidget, IDocumentWidget } from '@jupyterlab/docregistry';
11
11
 
12
12
  import { Dialog, ICommandPalette } from '@jupyterlab/apputils';
13
13
  import { IMainMenu } from '@jupyterlab/mainmenu';
@@ -124,6 +124,8 @@ namespace CommandIDs {
124
124
  'notebook-intelligence:get-current-file-content';
125
125
  export const setCurrentFileContent =
126
126
  'notebook-intelligence:set-current-file-content';
127
+ export const openMCPConfigEditor =
128
+ 'notebook-intelligence:open-mcp-config-editor';
127
129
  }
128
130
 
129
131
  const DOCUMENT_WATCH_INTERVAL = 1000;
@@ -514,6 +516,88 @@ class TelemetryEmitter implements ITelemetryEmitter {
514
516
  private _listeners: Set<ITelemetryListener> = new Set<ITelemetryListener>();
515
517
  }
516
518
 
519
+ class MCPConfigEditor {
520
+ constructor(docManager: IDocumentManager) {
521
+ this._docManager = docManager;
522
+ }
523
+
524
+ async open() {
525
+ const contents = new ContentsManager();
526
+ const newJSONFile = await contents.newUntitled({
527
+ ext: '.json'
528
+ });
529
+ const mcpConfig = await NBIAPI.getMCPConfigFile();
530
+
531
+ try {
532
+ await contents.delete(this._tmpMCPConfigFilename);
533
+ } catch (error) {
534
+ // ignore
535
+ }
536
+
537
+ await contents.save(newJSONFile.path, {
538
+ content: JSON.stringify(mcpConfig, null, 2),
539
+ format: 'text',
540
+ type: 'file'
541
+ });
542
+ await contents.rename(newJSONFile.path, this._tmpMCPConfigFilename);
543
+ this._docWidget = this._docManager.openOrReveal(
544
+ this._tmpMCPConfigFilename,
545
+ 'Editor'
546
+ );
547
+ this._addListeners();
548
+ // tab closed
549
+ this._docWidget.disposed.connect((_, args) => {
550
+ this._removeListeners();
551
+ contents.delete(this._tmpMCPConfigFilename);
552
+ });
553
+ this._isOpen = true;
554
+ }
555
+
556
+ close() {
557
+ if (!this._isOpen) {
558
+ return;
559
+ }
560
+ this._isOpen = false;
561
+ this._docWidget.dispose();
562
+ this._docWidget = null;
563
+ }
564
+
565
+ get isOpen(): boolean {
566
+ return this._isOpen;
567
+ }
568
+
569
+ private _addListeners() {
570
+ this._docWidget.context.model.stateChanged.connect(
571
+ this._onStateChanged,
572
+ this
573
+ );
574
+ }
575
+
576
+ private _removeListeners() {
577
+ this._docWidget.context.model.stateChanged.disconnect(
578
+ this._onStateChanged,
579
+ this
580
+ );
581
+ }
582
+
583
+ private _onStateChanged(model: any, args: any) {
584
+ if (args.name === 'dirty' && args.newValue === false) {
585
+ this._onSave();
586
+ }
587
+ }
588
+
589
+ private async _onSave() {
590
+ const mcpConfig = this._docWidget.context.model.toJSON();
591
+ await NBIAPI.setMCPConfigFile(mcpConfig);
592
+ await NBIAPI.fetchCapabilities();
593
+ }
594
+
595
+ private _docManager: IDocumentManager;
596
+ private _docWidget: IDocumentWidget = null;
597
+ private _tmpMCPConfigFilename = 'nbi.mcp.temp.json';
598
+ private _isOpen = false;
599
+ }
600
+
517
601
  /**
518
602
  * Initialization data for the @notebook-intelligence/notebook-intelligence extension.
519
603
  */
@@ -567,6 +651,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
567
651
  await NBIAPI.initialize();
568
652
 
569
653
  let openPopover: InlinePromptWidget | null = null;
654
+ let mcpConfigEditor: MCPConfigEditor | null = null;
570
655
 
571
656
  completionManager.registerInlineProvider(
572
657
  new NBIInlineCompletionProvider(telemetryEmitter)
@@ -1152,6 +1237,12 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1152
1237
  onSave: () => {
1153
1238
  dialog?.dispose();
1154
1239
  NBIAPI.fetchCapabilities();
1240
+ },
1241
+ onEditMCPConfigClicked: () => {
1242
+ dialog?.dispose();
1243
+ app.commands.execute(
1244
+ 'notebook-intelligence:open-mcp-config-editor'
1245
+ );
1155
1246
  }
1156
1247
  });
1157
1248
  dialog = new Dialog({
@@ -1166,6 +1257,17 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1166
1257
  }
1167
1258
  });
1168
1259
 
1260
+ app.commands.addCommand(CommandIDs.openMCPConfigEditor, {
1261
+ label: 'Open MCP Config Editor',
1262
+ execute: args => {
1263
+ if (mcpConfigEditor && mcpConfigEditor.isOpen) {
1264
+ mcpConfigEditor.close();
1265
+ }
1266
+ mcpConfigEditor = new MCPConfigEditor(docManager);
1267
+ mcpConfigEditor.open();
1268
+ }
1269
+ });
1270
+
1169
1271
  palette.addItem({
1170
1272
  command: CommandIDs.openConfigurationDialog,
1171
1273
  category: 'Notebook Intelligence'
package/src/tokens.ts CHANGED
@@ -31,7 +31,8 @@ export enum RequestDataType {
31
31
  export enum BackendMessageType {
32
32
  StreamMessage = 'stream-message',
33
33
  StreamEnd = 'stream-end',
34
- RunUICommand = 'run-ui-command'
34
+ RunUICommand = 'run-ui-command',
35
+ GitHubCopilotLoginStatusChange = 'github-copilot-login-status-change'
35
36
  }
36
37
 
37
38
  export enum ResponseStreamDataType {
package/style/base.css CHANGED
@@ -30,7 +30,7 @@
30
30
  }
31
31
 
32
32
  .sidebar-user-input {
33
- height: 80px;
33
+ height: auto;
34
34
  padding: 5px;
35
35
  display: flex;
36
36
  flex-direction: column;
@@ -67,6 +67,11 @@
67
67
  border: none;
68
68
  resize: none;
69
69
  background: none;
70
+ /* stylelint-disable */
71
+ field-sizing: content;
72
+ /* stylelint-enable */
73
+ min-height: 36px;
74
+ max-height: 200px;
70
75
  }
71
76
 
72
77
  .sidebar-user-input .user-input-context-row {