@notebook-intelligence/notebook-intelligence 2.5.0 → 2.6.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
@@ -175,6 +175,119 @@ If you have multiple servers configured but you would like to disable some for a
175
175
  }
176
176
  ```
177
177
 
178
+ ### Ruleset System
179
+
180
+ NBI includes a powerful ruleset system that allows you to define custom guidelines and best practices that are automatically injected into AI prompts. This helps ensure consistent coding standards, project-specific conventions, and domain knowledge across all AI interactions.
181
+
182
+ #### How It Works
183
+
184
+ Rules are markdown files with optional YAML frontmatter stored in `~/.jupyter/nbi/rules/`. They are automatically discovered and applied based on context (file type, notebook kernel, chat mode).
185
+
186
+ #### Creating Rules
187
+
188
+ **Global Rules** - Apply to all contexts:
189
+
190
+ Create a file like `~/.jupyter/nbi/rules/01-coding-standards.md`:
191
+
192
+ ```markdown
193
+ ---
194
+ priority: 10
195
+ ---
196
+
197
+ # Coding Standards
198
+
199
+ - Always use type hints in Python functions
200
+ - Prefer list comprehensions over loops when appropriate
201
+ - Add docstrings to all public functions
202
+ ```
203
+
204
+ **Mode-Specific Rules** - Apply only to specific chat modes:
205
+
206
+ NBI supports mode-specific rules for three modes:
207
+
208
+ - **ask** - Question/answer mode
209
+ - **agent** - Autonomous agent mode with tool access
210
+ - **inline-chat** - Inline code generation and editing
211
+
212
+ Create a file like `~/.jupyter/nbi/rules/modes/agent/01-testing.md`:
213
+
214
+ ```markdown
215
+ ---
216
+ priority: 20
217
+ scope:
218
+ kernels: ['python3']
219
+ ---
220
+
221
+ # Testing Guidelines
222
+
223
+ When writing code in agent mode:
224
+
225
+ - Always include error handling
226
+ - Add logging for debugging
227
+ - Test edge cases
228
+ ```
229
+
230
+ #### Rule Frontmatter Options
231
+
232
+ ```yaml
233
+ ---
234
+ apply: always # 'always', 'auto', or 'manual'
235
+ active: true # Enable/disable the rule
236
+ priority: 10 # Lower numbers = higher priority
237
+ scope:
238
+ file_patterns: # Apply to specific file patterns
239
+ - '*.py'
240
+ - 'test_*.ipynb'
241
+ kernels: # Apply to specific notebook kernels
242
+ - 'python3'
243
+ - 'ir'
244
+ directories: # Apply to specific directories
245
+ - '/projects/ml'
246
+ ---
247
+ ```
248
+
249
+ #### Configuration
250
+
251
+ **Enable/Disable Rules System:**
252
+
253
+ Edit `~/.jupyter/nbi/config.json`:
254
+
255
+ ```json
256
+ {
257
+ "rules_enabled": true
258
+ }
259
+ ```
260
+
261
+ **Auto-Reload Configuration:**
262
+
263
+ Rules are automatically reloaded when changed (enabled by default). This behavior is controlled by the `NBI_RULES_AUTO_RELOAD` environment variable.
264
+
265
+ To disable auto-reload:
266
+
267
+ ```bash
268
+ export NBI_RULES_AUTO_RELOAD=false
269
+ jupyter lab
270
+ ```
271
+
272
+ Or to enable (default):
273
+
274
+ ```bash
275
+ export NBI_RULES_AUTO_RELOAD=true
276
+ jupyter lab
277
+ ```
278
+
279
+ #### Managing Rules
280
+
281
+ Rules are automatically discovered from:
282
+
283
+ - **Global rules**: `~/.jupyter/nbi/rules/*.md`
284
+ - **Mode-specific rules**: `~/.jupyter/nbi/rules/modes/{mode}/*.md` where `{mode}` can be:
285
+ - `ask` - For question/answer interactions
286
+ - `agent` - For autonomous agent operations
287
+ - `inline-chat` - For inline code generation
288
+
289
+ Rules are applied in priority order (lower numbers first) and can be toggled on/off without deleting the files.
290
+
178
291
  ### Developer documentation
179
292
 
180
293
  For building locally and contributing see the [developer documentatation](CONTRIBUTING.md).
package/lib/api.d.ts CHANGED
@@ -22,6 +22,9 @@ export declare class NBIConfig {
22
22
  get usingGitHubCopilotModel(): boolean;
23
23
  get storeGitHubAccessToken(): boolean;
24
24
  get toolConfig(): any;
25
+ get mcpServers(): any;
26
+ getMCPServer(serverId: string): any;
27
+ getMCPServerPrompt(serverId: string, promptName: string): any;
25
28
  get mcpServerSettings(): any;
26
29
  capabilities: any;
27
30
  chatParticipants: IChatParticipant[];
@@ -39,6 +42,9 @@ export declare class NBIAPI {
39
42
  static initializeWebsocket(): Promise<void>;
40
43
  static getLoginStatus(): GitHubCopilotLoginStatus;
41
44
  static getDeviceVerificationInfo(): IDeviceVerificationInfo;
45
+ static getGHLoginRequired(): boolean;
46
+ static getChatEnabled(): any;
47
+ static getInlineCompletionEnabled(): any;
42
48
  static loginToGitHub(): Promise<unknown>;
43
49
  static logoutFromGitHub(): Promise<unknown>;
44
50
  static updateGitHubLoginStatus(): Promise<void>;
package/lib/api.js CHANGED
@@ -53,6 +53,19 @@ export class NBIConfig {
53
53
  get toolConfig() {
54
54
  return this.capabilities.tool_config;
55
55
  }
56
+ get mcpServers() {
57
+ return this.toolConfig.mcpServers;
58
+ }
59
+ getMCPServer(serverId) {
60
+ return this.toolConfig.mcpServers.find((server) => server.id === serverId);
61
+ }
62
+ getMCPServerPrompt(serverId, promptName) {
63
+ const server = this.getMCPServer(serverId);
64
+ if (server) {
65
+ return server.prompts.find((prompt) => prompt.name === promptName);
66
+ }
67
+ return null;
68
+ }
56
69
  get mcpServerSettings() {
57
70
  return this.capabilities.mcp_server_settings;
58
71
  }
@@ -98,6 +111,21 @@ class NBIAPI {
98
111
  static getDeviceVerificationInfo() {
99
112
  return this._deviceVerificationInfo;
100
113
  }
114
+ static getGHLoginRequired() {
115
+ return (this.config.usingGitHubCopilotModel &&
116
+ NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn);
117
+ }
118
+ static getChatEnabled() {
119
+ return this.config.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
120
+ ? !this.getGHLoginRequired()
121
+ : this.config.llmProviders.find(provider => provider.id === this.config.chatModel.provider);
122
+ }
123
+ static getInlineCompletionEnabled() {
124
+ return this.config.inlineCompletionModel.provider ===
125
+ GITHUB_COPILOT_PROVIDER_ID
126
+ ? !this.getGHLoginRequired()
127
+ : this.config.llmProviders.find(provider => provider.id === this.config.inlineCompletionModel.provider);
128
+ }
101
129
  static async loginToGitHub() {
102
130
  this._loginStatus = GitHubCopilotLoginStatus.ActivatingDevice;
103
131
  return new Promise((resolve, reject) => {
@@ -42,6 +42,8 @@ export interface IInlinePromptWidgetOptions {
42
42
  existingCode: string;
43
43
  prefix: string;
44
44
  suffix: string;
45
+ language?: string;
46
+ filename?: string;
45
47
  onRequestSubmitted: (prompt: string) => void;
46
48
  onRequestCancelled: () => void;
47
49
  onContentStream: (content: string) => void;
@@ -73,3 +75,12 @@ export declare class GitHubCopilotLoginDialogBody extends ReactWidget {
73
75
  render(): JSX.Element;
74
76
  private _onLoggedIn;
75
77
  }
78
+ export declare class FormInputDialogBody extends ReactWidget {
79
+ constructor(options: {
80
+ fields: any;
81
+ onDone: (formData: any) => void;
82
+ });
83
+ render(): JSX.Element;
84
+ private _fields;
85
+ private _onDone;
86
+ }
@@ -4,7 +4,7 @@ import { ReactWidget } from '@jupyterlab/apputils';
4
4
  import { UUID } from '@lumino/coreutils';
5
5
  import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
6
6
  import { NBIAPI, GitHubCopilotLoginStatus } from './api';
7
- import { BackendMessageType, BuiltinToolsetType, ContextType, GITHUB_COPILOT_PROVIDER_ID, RequestDataType, ResponseStreamDataType, TelemetryEventType } from './tokens';
7
+ import { BackendMessageType, BuiltinToolsetType, ContextType, RequestDataType, ResponseStreamDataType, TelemetryEventType } from './tokens';
8
8
  import { MarkdownRenderer as OriginalMarkdownRenderer } from './markdown-renderer';
9
9
  const MarkdownRenderer = memo(OriginalMarkdownRenderer);
10
10
  import copySvgstr from '../style/icons/copy.svg';
@@ -613,29 +613,36 @@ function SidebarComponent(props) {
613
613
  setToolSelections(newConfig);
614
614
  };
615
615
  useEffect(() => {
616
+ var _a;
616
617
  const prefixes = [];
617
- if (chatMode !== 'ask') {
618
- prefixes.push('/clear');
619
- setOriginalPrefixes(prefixes);
620
- setPrefixSuggestions(prefixes);
621
- return;
622
- }
623
- const chatParticipants = NBIAPI.config.chatParticipants;
624
- for (const participant of chatParticipants) {
625
- const id = participant.id;
626
- const commands = participant.commands;
627
- const participantPrefix = id === 'default' ? '' : `@${id}`;
628
- if (participantPrefix !== '') {
629
- prefixes.push(participantPrefix);
618
+ prefixes.push('/clear');
619
+ if (chatMode === 'ask') {
620
+ const chatParticipants = NBIAPI.config.chatParticipants;
621
+ for (const participant of chatParticipants) {
622
+ const id = participant.id;
623
+ const commands = participant.commands;
624
+ const participantPrefix = id === 'default' ? '' : `@${id}`;
625
+ if (participantPrefix !== '') {
626
+ prefixes.push(participantPrefix);
627
+ }
628
+ const commandPrefix = participantPrefix === '' ? '' : `${participantPrefix} `;
629
+ for (const command of commands) {
630
+ prefixes.push(`${commandPrefix}/${command}`);
631
+ }
630
632
  }
631
- const commandPrefix = participantPrefix === '' ? '' : `${participantPrefix} `;
632
- for (const command of commands) {
633
- prefixes.push(`${commandPrefix}/${command}`);
633
+ }
634
+ const mcpServers = NBIAPI.config.toolConfig.mcpServers;
635
+ const mcpServerSettings = NBIAPI.config.mcpServerSettings;
636
+ for (const mcpServer of mcpServers) {
637
+ if (((_a = mcpServerSettings[mcpServer.id]) === null || _a === void 0 ? void 0 : _a.disabled) !== true) {
638
+ for (const prompt of mcpServer.prompts) {
639
+ prefixes.push(`/mcp:${mcpServer.id}:${prompt.name}`);
640
+ }
634
641
  }
635
642
  }
636
643
  setOriginalPrefixes(prefixes);
637
644
  setPrefixSuggestions(prefixes);
638
- }, [chatMode]);
645
+ }, [chatMode, renderCount]);
639
646
  useEffect(() => {
640
647
  const fetchData = () => {
641
648
  setGHLoginStatus(NBIAPI.getLoginStatus());
@@ -664,13 +671,37 @@ function SidebarComponent(props) {
664
671
  setShowPopover(false);
665
672
  }
666
673
  };
667
- const applyPrefixSuggestion = (prefix) => {
674
+ const applyPrefixSuggestion = async (prefix) => {
668
675
  var _a;
676
+ let mcpArguments = '';
677
+ if (prefix.startsWith('/mcp:')) {
678
+ mcpArguments = ':';
679
+ const serverId = prefix.split(':')[1];
680
+ const promptName = prefix.split(':')[2];
681
+ const promptConfig = NBIAPI.config.getMCPServerPrompt(serverId, promptName);
682
+ if (promptConfig &&
683
+ promptConfig.arguments &&
684
+ promptConfig.arguments.length > 0) {
685
+ const result = await props
686
+ .getApp()
687
+ .commands.execute('notebook-intelligence:show-form-input-dialog', {
688
+ title: 'Input Parameters',
689
+ fields: promptConfig.arguments
690
+ });
691
+ const argumentValues = [];
692
+ for (const argument of promptConfig.arguments) {
693
+ if (result[argument.name] !== undefined) {
694
+ argumentValues.push(`${argument.name}=${result[argument.name]}`);
695
+ }
696
+ }
697
+ mcpArguments = `(${argumentValues.join(', ')}):`;
698
+ }
699
+ }
669
700
  if (prefix.includes(prompt)) {
670
- setPrompt(`${prefix} `);
701
+ setPrompt(`${prefix}${mcpArguments} `);
671
702
  }
672
703
  else {
673
- setPrompt(`${prefix} ${prompt} `);
704
+ setPrompt(`${prefix} ${prompt}${mcpArguments} `);
674
705
  }
675
706
  setShowPopover(false);
676
707
  (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
@@ -721,7 +752,6 @@ function SidebarComponent(props) {
721
752
  }
722
753
  }
723
754
  }
724
- const promptPrefix = promptPrefixParts.length > 0 ? promptPrefixParts.join(' ') + ' ' : '';
725
755
  lastMessageId.current = UUID.uuid4();
726
756
  lastRequestTime.current = new Date();
727
757
  const newList = [
@@ -780,7 +810,7 @@ function SidebarComponent(props) {
780
810
  type: RunChatCompletionType.Chat,
781
811
  content: extractedPrompt,
782
812
  language: activeDocInfo.language,
783
- filename: activeDocInfo.filename,
813
+ filename: activeDocInfo.filePath,
784
814
  additionalContext,
785
815
  chatMode,
786
816
  toolSelections: toolSelections
@@ -863,7 +893,7 @@ function SidebarComponent(props) {
863
893
  ]);
864
894
  }
865
895
  });
866
- const newPrompt = prompt.startsWith('/settings') ? '' : promptPrefix;
896
+ const newPrompt = '';
867
897
  setPrompt(newPrompt);
868
898
  filterPrefixSuggestions(newPrompt);
869
899
  telemetryEmitter.emitTelemetryEvent({
@@ -1130,27 +1160,17 @@ function SidebarComponent(props) {
1130
1160
  }
1131
1161
  return `${activeDocumentInfo.filename}${cellAndLineIndicator}`;
1132
1162
  };
1133
- const nbiConfig = NBIAPI.config;
1134
- const getGHLoginRequired = () => {
1135
- return (nbiConfig.usingGitHubCopilotModel &&
1136
- NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn);
1137
- };
1138
- const getChatEnabled = () => {
1139
- return nbiConfig.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
1140
- ? !getGHLoginRequired()
1141
- : nbiConfig.llmProviders.find(provider => provider.id === nbiConfig.chatModel.provider);
1142
- };
1143
- const [ghLoginRequired, setGHLoginRequired] = useState(getGHLoginRequired());
1144
- const [chatEnabled, setChatEnabled] = useState(getChatEnabled());
1163
+ const [ghLoginRequired, setGHLoginRequired] = useState(NBIAPI.getGHLoginRequired());
1164
+ const [chatEnabled, setChatEnabled] = useState(NBIAPI.getChatEnabled());
1145
1165
  useEffect(() => {
1146
1166
  NBIAPI.configChanged.connect(() => {
1147
- setGHLoginRequired(getGHLoginRequired());
1148
- setChatEnabled(getChatEnabled());
1167
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
1168
+ setChatEnabled(NBIAPI.getChatEnabled());
1149
1169
  });
1150
1170
  }, []);
1151
1171
  useEffect(() => {
1152
- setGHLoginRequired(getGHLoginRequired());
1153
- setChatEnabled(getChatEnabled());
1172
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
1173
+ setChatEnabled(NBIAPI.getChatEnabled());
1154
1174
  }, [ghLoginStatus]);
1155
1175
  return (React.createElement("div", { className: "sidebar" },
1156
1176
  React.createElement("div", { className: "sidebar-header" },
@@ -1242,7 +1262,7 @@ function SidebarComponent(props) {
1242
1262
  } })))),
1243
1263
  renderCount > 0 &&
1244
1264
  mcpServerEnabledState.size > 0 &&
1245
- toolConfigRef.current.mcpServers.length > 0 && (React.createElement("div", { className: "mode-tools-group-header" }, "MCP Servers")),
1265
+ toolConfigRef.current.mcpServers.length > 0 && (React.createElement("div", { className: "mode-tools-group-header" }, "MCP Server Tools")),
1246
1266
  renderCount > 0 &&
1247
1267
  toolConfigRef.current.mcpServers
1248
1268
  .filter(mcpServer => mcpServerEnabledState.has(mcpServer.id))
@@ -1350,8 +1370,8 @@ function InlinePromptComponent(props) {
1350
1370
  chatId: UUID.uuid4(),
1351
1371
  type: RunChatCompletionType.GenerateCode,
1352
1372
  content: prompt,
1353
- language: undefined,
1354
- filename: undefined,
1373
+ language: props.language || 'python',
1374
+ filename: props.filename || 'Untitled.ipynb',
1355
1375
  prefix: props.prefix,
1356
1376
  suffix: props.suffix,
1357
1377
  existingCode: props.existingCode,
@@ -1508,3 +1528,31 @@ function GitHubCopilotLoginDialogBodyComponent(props) {
1508
1528
  React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleLogoutClick },
1509
1529
  React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Cancel activation"))))));
1510
1530
  }
1531
+ export class FormInputDialogBody extends ReactWidget {
1532
+ constructor(options) {
1533
+ super();
1534
+ this._fields = options.fields || [];
1535
+ this._onDone = options.onDone || (() => { });
1536
+ }
1537
+ render() {
1538
+ return (React.createElement(FormInputDialogBodyComponent, { fields: this._fields, onDone: this._onDone }));
1539
+ }
1540
+ }
1541
+ function FormInputDialogBodyComponent(props) {
1542
+ const [formData, setFormData] = useState({});
1543
+ const handleInputChange = (event) => {
1544
+ setFormData({ ...formData, [event.target.name]: event.target.value });
1545
+ };
1546
+ return (React.createElement("div", { className: "form-input-dialog-body" },
1547
+ React.createElement("div", { className: "form-input-dialog-body-content" },
1548
+ React.createElement("div", { className: "form-input-dialog-body-content-title" }, props.title),
1549
+ React.createElement("div", { className: "form-input-dialog-body-content-fields" }, props.fields.map((field) => (React.createElement("div", { className: "form-input-dialog-body-content-field", key: field.name },
1550
+ React.createElement("label", { className: "form-input-dialog-body-content-field-label jp-mod-styled", htmlFor: field.name },
1551
+ field.name,
1552
+ field.required ? ' (required)' : ''),
1553
+ React.createElement("input", { className: "form-input-dialog-body-content-field-input jp-mod-styled", type: field.type, id: field.name, name: field.name, onChange: handleInputChange, value: formData[field.name] || '' }))))),
1554
+ React.createElement("div", null,
1555
+ React.createElement("div", { style: { marginTop: '10px' } },
1556
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => props.onDone(formData) },
1557
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Done")))))));
1558
+ }
package/lib/index.js CHANGED
@@ -14,11 +14,12 @@ import { LabIcon } from '@jupyterlab/ui-components';
14
14
  import { Menu, Panel, Widget } from '@lumino/widgets';
15
15
  import { CommandRegistry } from '@lumino/commands';
16
16
  import { IStatusBar } from '@jupyterlab/statusbar';
17
- import { ChatSidebar, GitHubCopilotLoginDialogBody, GitHubCopilotStatusBarItem, InlinePromptWidget, RunChatCompletionType } from './chat-sidebar';
17
+ import { ChatSidebar, FormInputDialogBody, GitHubCopilotLoginDialogBody, GitHubCopilotStatusBarItem, InlinePromptWidget, RunChatCompletionType } from './chat-sidebar';
18
18
  import { NBIAPI, GitHubCopilotLoginStatus } from './api';
19
19
  import { BackendMessageType, GITHUB_COPILOT_PROVIDER_ID, INotebookIntelligence, RequestDataType, TelemetryEventType } from './tokens';
20
20
  import sparklesSvgstr from '../style/icons/sparkles.svg';
21
21
  import copilotSvgstr from '../style/icons/copilot.svg';
22
+ import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg';
22
23
  import { applyCodeToSelectionInEditor, cellOutputAsText, compareSelections, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
23
24
  import { UUID } from '@lumino/coreutils';
24
25
  import * as path from 'path';
@@ -52,6 +53,7 @@ var CommandIDs;
52
53
  CommandIDs.getCurrentFileContent = 'notebook-intelligence:get-current-file-content';
53
54
  CommandIDs.setCurrentFileContent = 'notebook-intelligence:set-current-file-content';
54
55
  CommandIDs.openMCPConfigEditor = 'notebook-intelligence:open-mcp-config-editor';
56
+ CommandIDs.showFormInputDialog = 'notebook-intelligence:show-form-input-dialog';
55
57
  })(CommandIDs || (CommandIDs = {}));
56
58
  const DOCUMENT_WATCH_INTERVAL = 1000;
57
59
  const MAX_TOKENS = 4096;
@@ -63,6 +65,10 @@ const sparkleIcon = new LabIcon({
63
65
  name: 'notebook-intelligence:sparkles-icon',
64
66
  svgstr: sparklesSvgstr
65
67
  });
68
+ const sparkleWarningIcon = new LabIcon({
69
+ name: 'notebook-intelligence:sparkles-warning-icon',
70
+ svgstr: sparklesWarningSvgstr
71
+ });
66
72
  const emptyNotebookContent = {
67
73
  cells: [],
68
74
  metadata: {},
@@ -499,7 +505,7 @@ const plugin = {
499
505
  panel.id = 'notebook-intelligence-tab';
500
506
  panel.title.caption = 'Notebook Intelligence';
501
507
  const sidebarIcon = new LabIcon({
502
- name: 'ui-components:palette',
508
+ name: 'notebook-intelligence:sidebar-icon',
503
509
  svgstr: sparklesSvgstr
504
510
  });
505
511
  panel.title.icon = sidebarIcon;
@@ -526,6 +532,23 @@ const plugin = {
526
532
  panel.addWidget(sidebar);
527
533
  app.shell.add(panel, 'left', { rank: 1000 });
528
534
  app.shell.activateById(panel.id);
535
+ const updateSidebarIcon = () => {
536
+ if (NBIAPI.getChatEnabled()) {
537
+ panel.title.icon = sidebarIcon;
538
+ }
539
+ else {
540
+ panel.title.icon = sparkleWarningIcon;
541
+ }
542
+ };
543
+ NBIAPI.githubLoginStatusChanged.connect((_, args) => {
544
+ updateSidebarIcon();
545
+ });
546
+ NBIAPI.configChanged.connect((_, args) => {
547
+ updateSidebarIcon();
548
+ });
549
+ setTimeout(() => {
550
+ updateSidebarIcon();
551
+ }, 2000);
529
552
  app.commands.addCommand(CommandIDs.chatuserInput, {
530
553
  execute: args => {
531
554
  NBIAPI.sendChatUserInput(args.id, args.data);
@@ -595,6 +618,36 @@ const plugin = {
595
618
  return newPyFile;
596
619
  }
597
620
  });
621
+ app.commands.addCommand(CommandIDs.showFormInputDialog, {
622
+ execute: async (args) => {
623
+ const title = args.title;
624
+ const fields = args.fields;
625
+ return new Promise((resolve, reject) => {
626
+ let dialog = null;
627
+ const dialogBody = new FormInputDialogBody({
628
+ fields: fields,
629
+ onDone: (formData) => {
630
+ dialog.dispose();
631
+ resolve(formData);
632
+ }
633
+ });
634
+ dialog = new Dialog({
635
+ title: title,
636
+ hasClose: true,
637
+ body: dialogBody,
638
+ buttons: []
639
+ });
640
+ dialog
641
+ .launch()
642
+ .then((result) => {
643
+ reject();
644
+ })
645
+ .catch(() => {
646
+ reject(new Error('Failed to show form input dialog'));
647
+ });
648
+ });
649
+ }
650
+ });
598
651
  app.commands.addCommand(CommandIDs.createNewNotebookFromPython, {
599
652
  execute: async (args) => {
600
653
  var _a;
@@ -1143,6 +1196,8 @@ const plugin = {
1143
1196
  existingCode,
1144
1197
  prefix: prefix,
1145
1198
  suffix: suffix,
1199
+ language: ActiveDocumentWatcher.activeDocumentInfo.language,
1200
+ filename: ActiveDocumentWatcher.activeDocumentInfo.filePath,
1146
1201
  onRequestSubmitted: (prompt) => {
1147
1202
  userPrompt = prompt;
1148
1203
  generatedContent = '';
package/lib/tokens.d.ts CHANGED
@@ -51,7 +51,9 @@ export declare enum MCPServerStatus {
51
51
  FailedToConnect = "failed-to-connect",
52
52
  Connected = "connected",
53
53
  UpdatingToolList = "updating-tool-list",
54
- UpdatedToolList = "updated-tool-list"
54
+ UpdatedToolList = "updated-tool-list",
55
+ UpdatingPromptList = "updating-prompt-list",
56
+ UpdatedPromptList = "updated-prompt-list"
55
57
  }
56
58
  export interface IContextItem {
57
59
  type: ContextType;
package/lib/tokens.js CHANGED
@@ -45,6 +45,8 @@ export var MCPServerStatus;
45
45
  MCPServerStatus["Connected"] = "connected";
46
46
  MCPServerStatus["UpdatingToolList"] = "updating-tool-list";
47
47
  MCPServerStatus["UpdatedToolList"] = "updated-tool-list";
48
+ MCPServerStatus["UpdatingPromptList"] = "updating-prompt-list";
49
+ MCPServerStatus["UpdatedPromptList"] = "updated-prompt-list";
48
50
  })(MCPServerStatus || (MCPServerStatus = {}));
49
51
  export var BuiltinToolsetType;
50
52
  (function (BuiltinToolsetType) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notebook-intelligence/notebook-intelligence",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "AI coding assistant for JupyterLab",
5
5
  "keywords": [
6
6
  "AI",
package/src/api.ts CHANGED
@@ -76,6 +76,24 @@ export class NBIConfig {
76
76
  return this.capabilities.tool_config;
77
77
  }
78
78
 
79
+ get mcpServers(): any {
80
+ return this.toolConfig.mcpServers;
81
+ }
82
+
83
+ getMCPServer(serverId: string): any {
84
+ return this.toolConfig.mcpServers.find(
85
+ (server: any) => server.id === serverId
86
+ );
87
+ }
88
+
89
+ getMCPServerPrompt(serverId: string, promptName: string): any {
90
+ const server = this.getMCPServer(serverId);
91
+ if (server) {
92
+ return server.prompts.find((prompt: any) => prompt.name === promptName);
93
+ }
94
+ return null;
95
+ }
96
+
79
97
  get mcpServerSettings(): any {
80
98
  return this.capabilities.mcp_server_settings;
81
99
  }
@@ -152,6 +170,30 @@ export class NBIAPI {
152
170
  return this._deviceVerificationInfo;
153
171
  }
154
172
 
173
+ static getGHLoginRequired() {
174
+ return (
175
+ this.config.usingGitHubCopilotModel &&
176
+ NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn
177
+ );
178
+ }
179
+
180
+ static getChatEnabled() {
181
+ return this.config.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
182
+ ? !this.getGHLoginRequired()
183
+ : this.config.llmProviders.find(
184
+ provider => provider.id === this.config.chatModel.provider
185
+ );
186
+ }
187
+
188
+ static getInlineCompletionEnabled() {
189
+ return this.config.inlineCompletionModel.provider ===
190
+ GITHUB_COPILOT_PROVIDER_ID
191
+ ? !this.getGHLoginRequired()
192
+ : this.config.llmProviders.find(
193
+ provider => provider.id === this.config.inlineCompletionModel.provider
194
+ );
195
+ }
196
+
155
197
  static async loginToGitHub() {
156
198
  this._loginStatus = GitHubCopilotLoginStatus.ActivatingDevice;
157
199
  return new Promise((resolve, reject) => {
@@ -20,7 +20,6 @@ import {
20
20
  BackendMessageType,
21
21
  BuiltinToolsetType,
22
22
  ContextType,
23
- GITHUB_COPILOT_PROVIDER_ID,
24
23
  IActiveDocumentInfo,
25
24
  ICellContents,
26
25
  IChatCompletionResponseEmitter,
@@ -118,6 +117,8 @@ export interface IInlinePromptWidgetOptions {
118
117
  existingCode: string;
119
118
  prefix: string;
120
119
  suffix: string;
120
+ language?: string;
121
+ filename?: string;
121
122
  onRequestSubmitted: (prompt: string) => void;
122
123
  onRequestCancelled: () => void;
123
124
  onContentStream: (content: string) => void;
@@ -1078,30 +1079,38 @@ function SidebarComponent(props: any) {
1078
1079
 
1079
1080
  useEffect(() => {
1080
1081
  const prefixes: string[] = [];
1081
- if (chatMode !== 'ask') {
1082
- prefixes.push('/clear');
1083
- setOriginalPrefixes(prefixes);
1084
- setPrefixSuggestions(prefixes);
1085
- return;
1082
+ prefixes.push('/clear');
1083
+
1084
+ if (chatMode === 'ask') {
1085
+ const chatParticipants = NBIAPI.config.chatParticipants;
1086
+ for (const participant of chatParticipants) {
1087
+ const id = participant.id;
1088
+ const commands = participant.commands;
1089
+ const participantPrefix = id === 'default' ? '' : `@${id}`;
1090
+ if (participantPrefix !== '') {
1091
+ prefixes.push(participantPrefix);
1092
+ }
1093
+ const commandPrefix =
1094
+ participantPrefix === '' ? '' : `${participantPrefix} `;
1095
+ for (const command of commands) {
1096
+ prefixes.push(`${commandPrefix}/${command}`);
1097
+ }
1098
+ }
1086
1099
  }
1087
1100
 
1088
- const chatParticipants = NBIAPI.config.chatParticipants;
1089
- for (const participant of chatParticipants) {
1090
- const id = participant.id;
1091
- const commands = participant.commands;
1092
- const participantPrefix = id === 'default' ? '' : `@${id}`;
1093
- if (participantPrefix !== '') {
1094
- prefixes.push(participantPrefix);
1095
- }
1096
- const commandPrefix =
1097
- participantPrefix === '' ? '' : `${participantPrefix} `;
1098
- for (const command of commands) {
1099
- prefixes.push(`${commandPrefix}/${command}`);
1101
+ const mcpServers = NBIAPI.config.toolConfig.mcpServers;
1102
+ const mcpServerSettings = NBIAPI.config.mcpServerSettings;
1103
+ for (const mcpServer of mcpServers) {
1104
+ if (mcpServerSettings[mcpServer.id]?.disabled !== true) {
1105
+ for (const prompt of mcpServer.prompts) {
1106
+ prefixes.push(`/mcp:${mcpServer.id}:${prompt.name}`);
1107
+ }
1100
1108
  }
1101
1109
  }
1110
+
1102
1111
  setOriginalPrefixes(prefixes);
1103
1112
  setPrefixSuggestions(prefixes);
1104
- }, [chatMode]);
1113
+ }, [chatMode, renderCount]);
1105
1114
 
1106
1115
  useEffect(() => {
1107
1116
  const fetchData = () => {
@@ -1137,11 +1146,41 @@ function SidebarComponent(props: any) {
1137
1146
  }
1138
1147
  };
1139
1148
 
1140
- const applyPrefixSuggestion = (prefix: string) => {
1149
+ const applyPrefixSuggestion = async (prefix: string) => {
1150
+ let mcpArguments = '';
1151
+ if (prefix.startsWith('/mcp:')) {
1152
+ mcpArguments = ':';
1153
+ const serverId = prefix.split(':')[1];
1154
+ const promptName = prefix.split(':')[2];
1155
+ const promptConfig = NBIAPI.config.getMCPServerPrompt(
1156
+ serverId,
1157
+ promptName
1158
+ );
1159
+ if (
1160
+ promptConfig &&
1161
+ promptConfig.arguments &&
1162
+ promptConfig.arguments.length > 0
1163
+ ) {
1164
+ const result = await props
1165
+ .getApp()
1166
+ .commands.execute('notebook-intelligence:show-form-input-dialog', {
1167
+ title: 'Input Parameters',
1168
+ fields: promptConfig.arguments
1169
+ });
1170
+ const argumentValues: string[] = [];
1171
+ for (const argument of promptConfig.arguments) {
1172
+ if (result[argument.name] !== undefined) {
1173
+ argumentValues.push(`${argument.name}=${result[argument.name]}`);
1174
+ }
1175
+ }
1176
+ mcpArguments = `(${argumentValues.join(', ')}):`;
1177
+ }
1178
+ }
1179
+
1141
1180
  if (prefix.includes(prompt)) {
1142
- setPrompt(`${prefix} `);
1181
+ setPrompt(`${prefix}${mcpArguments} `);
1143
1182
  } else {
1144
- setPrompt(`${prefix} ${prompt} `);
1183
+ setPrompt(`${prefix} ${prompt}${mcpArguments} `);
1145
1184
  }
1146
1185
  setShowPopover(false);
1147
1186
  promptInputRef.current?.focus();
@@ -1201,9 +1240,6 @@ function SidebarComponent(props: any) {
1201
1240
  }
1202
1241
  }
1203
1242
 
1204
- const promptPrefix =
1205
- promptPrefixParts.length > 0 ? promptPrefixParts.join(' ') + ' ' : '';
1206
-
1207
1243
  lastMessageId.current = UUID.uuid4();
1208
1244
  lastRequestTime.current = new Date();
1209
1245
 
@@ -1275,7 +1311,7 @@ function SidebarComponent(props: any) {
1275
1311
  type: RunChatCompletionType.Chat,
1276
1312
  content: extractedPrompt,
1277
1313
  language: activeDocInfo.language,
1278
- filename: activeDocInfo.filename,
1314
+ filename: activeDocInfo.filePath,
1279
1315
  additionalContext,
1280
1316
  chatMode,
1281
1317
  toolSelections: toolSelections
@@ -1368,7 +1404,7 @@ function SidebarComponent(props: any) {
1368
1404
  }
1369
1405
  );
1370
1406
 
1371
- const newPrompt = prompt.startsWith('/settings') ? '' : promptPrefix;
1407
+ const newPrompt = '';
1372
1408
 
1373
1409
  setPrompt(newPrompt);
1374
1410
  filterPrefixSuggestions(newPrompt);
@@ -1697,34 +1733,21 @@ function SidebarComponent(props: any) {
1697
1733
  return `${activeDocumentInfo.filename}${cellAndLineIndicator}`;
1698
1734
  };
1699
1735
 
1700
- const nbiConfig = NBIAPI.config;
1701
- const getGHLoginRequired = () => {
1702
- return (
1703
- nbiConfig.usingGitHubCopilotModel &&
1704
- NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn
1705
- );
1706
- };
1707
- const getChatEnabled = () => {
1708
- return nbiConfig.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
1709
- ? !getGHLoginRequired()
1710
- : nbiConfig.llmProviders.find(
1711
- provider => provider.id === nbiConfig.chatModel.provider
1712
- );
1713
- };
1714
-
1715
- const [ghLoginRequired, setGHLoginRequired] = useState(getGHLoginRequired());
1716
- const [chatEnabled, setChatEnabled] = useState(getChatEnabled());
1736
+ const [ghLoginRequired, setGHLoginRequired] = useState(
1737
+ NBIAPI.getGHLoginRequired()
1738
+ );
1739
+ const [chatEnabled, setChatEnabled] = useState(NBIAPI.getChatEnabled());
1717
1740
 
1718
1741
  useEffect(() => {
1719
1742
  NBIAPI.configChanged.connect(() => {
1720
- setGHLoginRequired(getGHLoginRequired());
1721
- setChatEnabled(getChatEnabled());
1743
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
1744
+ setChatEnabled(NBIAPI.getChatEnabled());
1722
1745
  });
1723
1746
  }, []);
1724
1747
 
1725
1748
  useEffect(() => {
1726
- setGHLoginRequired(getGHLoginRequired());
1727
- setChatEnabled(getChatEnabled());
1749
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
1750
+ setChatEnabled(NBIAPI.getChatEnabled());
1728
1751
  }, [ghLoginStatus]);
1729
1752
 
1730
1753
  return (
@@ -1986,7 +2009,9 @@ function SidebarComponent(props: any) {
1986
2009
  {renderCount > 0 &&
1987
2010
  mcpServerEnabledState.size > 0 &&
1988
2011
  toolConfigRef.current.mcpServers.length > 0 && (
1989
- <div className="mode-tools-group-header">MCP Servers</div>
2012
+ <div className="mode-tools-group-header">
2013
+ MCP Server Tools
2014
+ </div>
1990
2015
  )}
1991
2016
  {renderCount > 0 &&
1992
2017
  toolConfigRef.current.mcpServers
@@ -2236,8 +2261,8 @@ function InlinePromptComponent(props: any) {
2236
2261
  chatId: UUID.uuid4(),
2237
2262
  type: RunChatCompletionType.GenerateCode,
2238
2263
  content: prompt,
2239
- language: undefined,
2240
- filename: undefined,
2264
+ language: props.language || 'python',
2265
+ filename: props.filename || 'Untitled.ipynb',
2241
2266
  prefix: props.prefix,
2242
2267
  suffix: props.suffix,
2243
2268
  existingCode: props.existingCode,
@@ -2509,3 +2534,76 @@ function GitHubCopilotLoginDialogBodyComponent(props: any) {
2509
2534
  </div>
2510
2535
  );
2511
2536
  }
2537
+
2538
+ export class FormInputDialogBody extends ReactWidget {
2539
+ constructor(options: { fields: any; onDone: (formData: any) => void }) {
2540
+ super();
2541
+
2542
+ this._fields = options.fields || [];
2543
+ this._onDone = options.onDone || (() => {});
2544
+ }
2545
+
2546
+ render(): JSX.Element {
2547
+ return (
2548
+ <FormInputDialogBodyComponent
2549
+ fields={this._fields}
2550
+ onDone={this._onDone}
2551
+ />
2552
+ );
2553
+ }
2554
+
2555
+ private _fields: any;
2556
+ private _onDone: (formData: any) => void;
2557
+ }
2558
+
2559
+ function FormInputDialogBodyComponent(props: any) {
2560
+ const [formData, setFormData] = useState<any>({});
2561
+
2562
+ const handleInputChange = (event: any) => {
2563
+ setFormData({ ...formData, [event.target.name]: event.target.value });
2564
+ };
2565
+
2566
+ return (
2567
+ <div className="form-input-dialog-body">
2568
+ <div className="form-input-dialog-body-content">
2569
+ <div className="form-input-dialog-body-content-title">
2570
+ {props.title}
2571
+ </div>
2572
+ <div className="form-input-dialog-body-content-fields">
2573
+ {props.fields.map((field: any) => (
2574
+ <div
2575
+ className="form-input-dialog-body-content-field"
2576
+ key={field.name}
2577
+ >
2578
+ <label
2579
+ className="form-input-dialog-body-content-field-label jp-mod-styled"
2580
+ htmlFor={field.name}
2581
+ >
2582
+ {field.name}
2583
+ {field.required ? ' (required)' : ''}
2584
+ </label>
2585
+ <input
2586
+ className="form-input-dialog-body-content-field-input jp-mod-styled"
2587
+ type={field.type}
2588
+ id={field.name}
2589
+ name={field.name}
2590
+ onChange={handleInputChange}
2591
+ value={formData[field.name] || ''}
2592
+ />
2593
+ </div>
2594
+ ))}
2595
+ </div>
2596
+ <div>
2597
+ <div style={{ marginTop: '10px' }}>
2598
+ <button
2599
+ className="jp-Dialog-button jp-mod-accept jp-mod-styled"
2600
+ onClick={() => props.onDone(formData)}
2601
+ >
2602
+ <div className="jp-Dialog-buttonLabel">Done</div>
2603
+ </button>
2604
+ </div>
2605
+ </div>
2606
+ </div>
2607
+ </div>
2608
+ );
2609
+ }
package/src/index.ts CHANGED
@@ -44,6 +44,7 @@ import { IStatusBar } from '@jupyterlab/statusbar';
44
44
 
45
45
  import {
46
46
  ChatSidebar,
47
+ FormInputDialogBody,
47
48
  GitHubCopilotLoginDialogBody,
48
49
  GitHubCopilotStatusBarItem,
49
50
  InlinePromptWidget,
@@ -64,6 +65,7 @@ import {
64
65
  } from './tokens';
65
66
  import sparklesSvgstr from '../style/icons/sparkles.svg';
66
67
  import copilotSvgstr from '../style/icons/copilot.svg';
68
+ import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg';
67
69
 
68
70
  import {
69
71
  applyCodeToSelectionInEditor,
@@ -126,6 +128,8 @@ namespace CommandIDs {
126
128
  'notebook-intelligence:set-current-file-content';
127
129
  export const openMCPConfigEditor =
128
130
  'notebook-intelligence:open-mcp-config-editor';
131
+ export const showFormInputDialog =
132
+ 'notebook-intelligence:show-form-input-dialog';
129
133
  }
130
134
 
131
135
  const DOCUMENT_WATCH_INTERVAL = 1000;
@@ -140,6 +144,10 @@ const sparkleIcon = new LabIcon({
140
144
  svgstr: sparklesSvgstr
141
145
  });
142
146
 
147
+ const sparkleWarningIcon = new LabIcon({
148
+ name: 'notebook-intelligence:sparkles-warning-icon',
149
+ svgstr: sparklesWarningSvgstr
150
+ });
143
151
  const emptyNotebookContent: any = {
144
152
  cells: [],
145
153
  metadata: {},
@@ -711,7 +719,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
711
719
  panel.id = 'notebook-intelligence-tab';
712
720
  panel.title.caption = 'Notebook Intelligence';
713
721
  const sidebarIcon = new LabIcon({
714
- name: 'ui-components:palette',
722
+ name: 'notebook-intelligence:sidebar-icon',
715
723
  svgstr: sparklesSvgstr
716
724
  });
717
725
  panel.title.icon = sidebarIcon;
@@ -739,6 +747,26 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
739
747
  app.shell.add(panel, 'left', { rank: 1000 });
740
748
  app.shell.activateById(panel.id);
741
749
 
750
+ const updateSidebarIcon = () => {
751
+ if (NBIAPI.getChatEnabled()) {
752
+ panel.title.icon = sidebarIcon;
753
+ } else {
754
+ panel.title.icon = sparkleWarningIcon;
755
+ }
756
+ };
757
+
758
+ NBIAPI.githubLoginStatusChanged.connect((_, args) => {
759
+ updateSidebarIcon();
760
+ });
761
+
762
+ NBIAPI.configChanged.connect((_, args) => {
763
+ updateSidebarIcon();
764
+ });
765
+
766
+ setTimeout(() => {
767
+ updateSidebarIcon();
768
+ }, 2000);
769
+
742
770
  app.commands.addCommand(CommandIDs.chatuserInput, {
743
771
  execute: args => {
744
772
  NBIAPI.sendChatUserInput(args.id as string, args.data);
@@ -820,6 +848,39 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
820
848
  }
821
849
  });
822
850
 
851
+ app.commands.addCommand(CommandIDs.showFormInputDialog, {
852
+ execute: async args => {
853
+ const title = args.title as string;
854
+ const fields = args.fields;
855
+
856
+ return new Promise<any>((resolve, reject) => {
857
+ let dialog: Dialog<unknown> | null = null;
858
+ const dialogBody = new FormInputDialogBody({
859
+ fields: fields,
860
+ onDone: (formData: any) => {
861
+ dialog.dispose();
862
+ resolve(formData);
863
+ }
864
+ });
865
+ dialog = new Dialog({
866
+ title: title,
867
+ hasClose: true,
868
+ body: dialogBody,
869
+ buttons: []
870
+ });
871
+
872
+ dialog
873
+ .launch()
874
+ .then((result: any) => {
875
+ reject();
876
+ })
877
+ .catch(() => {
878
+ reject(new Error('Failed to show form input dialog'));
879
+ });
880
+ });
881
+ }
882
+ });
883
+
823
884
  app.commands.addCommand(CommandIDs.createNewNotebookFromPython, {
824
885
  execute: async args => {
825
886
  let pythonKernelSpec = null;
@@ -1501,6 +1562,8 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1501
1562
  existingCode,
1502
1563
  prefix: prefix,
1503
1564
  suffix: suffix,
1565
+ language: ActiveDocumentWatcher.activeDocumentInfo.language,
1566
+ filename: ActiveDocumentWatcher.activeDocumentInfo.filePath,
1504
1567
  onRequestSubmitted: (prompt: string) => {
1505
1568
  userPrompt = prompt;
1506
1569
  generatedContent = '';
package/src/tokens.ts CHANGED
@@ -60,7 +60,9 @@ export enum MCPServerStatus {
60
60
  FailedToConnect = 'failed-to-connect',
61
61
  Connected = 'connected',
62
62
  UpdatingToolList = 'updating-tool-list',
63
- UpdatedToolList = 'updated-tool-list'
63
+ UpdatedToolList = 'updated-tool-list',
64
+ UpdatingPromptList = 'updating-prompt-list',
65
+ UpdatedPromptList = 'updated-prompt-list'
64
66
  }
65
67
 
66
68
  export interface IContextItem {
package/style/base.css CHANGED
@@ -890,13 +890,15 @@ svg.access-token-warning {
890
890
  }
891
891
 
892
892
  .server-status-indicator.connected,
893
- .server-status-indicator.updated-tool-list {
893
+ .server-status-indicator.updated-tool-list,
894
+ .server-status-indicator.updated-prompt-list {
894
895
  background-color: var(--jp-success-color1);
895
896
  }
896
897
 
897
898
  .server-status-indicator.connecting,
898
899
  .server-status-indicator.disconnecting,
899
- .server-status-indicator.updating-tool-list {
900
+ .server-status-indicator.updating-tool-list,
901
+ .server-status-indicator.updating-prompt-list {
900
902
  background-color: var(--jp-warn-color1);
901
903
  }
902
904
 
@@ -904,3 +906,27 @@ svg.access-token-warning {
904
906
  .server-status-indicator.failed-to-connect {
905
907
  background-color: var(--jp-error-color1);
906
908
  }
909
+
910
+ .form-input-dialog-body {
911
+ width: 500px;
912
+ }
913
+
914
+ .form-input-dialog-body-content-fields {
915
+ display: flex;
916
+ flex-direction: column;
917
+ gap: 10px;
918
+ }
919
+
920
+ .form-input-dialog-body-content-field {
921
+ display: flex;
922
+ flex-direction: row;
923
+ align-items: center;
924
+ }
925
+
926
+ .form-input-dialog-body-content-field-label {
927
+ width: 50%;
928
+ }
929
+
930
+ .form-input-dialog-body-content-field-input {
931
+ width: 50%;
932
+ }
@@ -0,0 +1,5 @@
1
+ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="var(--jp-inverse-layout-color3)"><path d="M 5.398 10.807 C 5.574 10.931 5.785 10.998 6 10.997 C 6.216 10.998 6.427 10.93 6.602 10.804 C 6.78 10.674 6.915 10.494 6.989 10.286 L 7.436 8.913 C 7.551 8.569 7.744 8.256 8 7.999 C 8.257 7.743 8.569 7.549 8.913 7.434 L 10.304 6.983 C 10.456 6.929 10.594 6.84 10.706 6.724 C 10.817 6.608 10.901 6.467 10.949 6.313 C 10.998 6.159 11.01 5.996 10.985 5.837 C 10.96 5.677 10.898 5.526 10.804 5.394 C 10.67 5.208 10.479 5.071 10.26 5.003 L 8.885 4.556 C 8.541 4.442 8.228 4.249 7.971 3.993 C 7.714 3.736 7.52 3.424 7.405 3.079 L 6.953 1.691 C 6.881 1.489 6.748 1.314 6.571 1.191 C 6.439 1.098 6.286 1.036 6.125 1.012 C 5.965 0.987 5.801 1.001 5.646 1.051 C 5.492 1.101 5.351 1.187 5.236 1.301 C 5.12 1.415 5.033 1.555 4.98 1.708 L 4.523 3.108 C 4.409 3.443 4.22 3.748 3.97 3.999 C 3.721 4.25 3.418 4.441 3.083 4.557 L 1.692 5.005 C 1.541 5.06 1.404 5.149 1.292 5.265 C 1.18 5.381 1.097 5.521 1.048 5.675 C 1 5.829 0.988 5.992 1.013 6.151 C 1.038 6.31 1.099 6.462 1.192 6.593 C 1.32 6.773 1.501 6.908 1.709 6.979 L 3.083 7.424 C 3.524 7.571 3.91 7.845 4.193 8.212 C 4.356 8.423 4.481 8.66 4.564 8.912 L 5.016 10.303 C 5.088 10.507 5.222 10.683 5.398 10.807 Z M 11.535 14.849 C 11.671 14.946 11.834 14.997 12 14.997 C 12.165 14.997 12.326 14.946 12.461 14.851 C 12.601 14.753 12.706 14.613 12.761 14.451 L 13.009 13.689 C 13.063 13.531 13.152 13.387 13.269 13.268 C 13.387 13.15 13.531 13.061 13.689 13.009 L 14.461 12.757 C 14.619 12.703 14.756 12.6 14.852 12.464 C 14.926 12.361 14.974 12.242 14.992 12.117 C 15.011 11.992 14.999 11.865 14.959 11.745 C 14.918 11.625 14.85 11.516 14.76 11.428 C 14.669 11.34 14.559 11.274 14.438 11.236 L 13.674 10.987 C 13.516 10.935 13.372 10.846 13.254 10.729 C 13.136 10.611 13.047 10.467 12.994 10.309 L 12.742 9.536 C 12.689 9.379 12.586 9.242 12.449 9.146 C 12.347 9.073 12.23 9.025 12.106 9.006 C 11.982 8.987 11.855 8.998 11.736 9.037 C 11.616 9.076 11.508 9.142 11.419 9.231 C 11.33 9.319 11.264 9.427 11.224 9.546 L 10.977 10.308 C 10.925 10.466 10.838 10.61 10.721 10.728 C 10.607 10.845 10.467 10.934 10.312 10.987 L 9.539 11.239 C 9.38 11.293 9.242 11.396 9.145 11.533 C 9.047 11.669 8.995 11.833 8.996 12.001 C 8.997 12.169 9.051 12.333 9.15 12.468 C 9.249 12.604 9.388 12.705 9.547 12.757 L 10.31 13.004 C 10.469 13.058 10.614 13.147 10.732 13.265 C 10.851 13.384 10.939 13.528 10.99 13.687 L 11.243 14.461 C 11.298 14.618 11.4 14.753 11.535 14.849 Z"/>
2
+ <g class="layer">
3
+ <circle cx="12" cy="4" fill="#FF0000" r="4" stroke="#FFFFFF" stroke-width="0"/>
4
+ </g>
5
+ </svg>