@notebook-intelligence/notebook-intelligence 2.4.1 → 2.5.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.
@@ -0,0 +1,770 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+
3
+ import React, { useEffect, useRef, useState } from 'react';
4
+ import { ReactWidget } from '@jupyterlab/apputils';
5
+ import { VscWarning } from 'react-icons/vsc';
6
+ import * as path from 'path';
7
+
8
+ import copySvgstr from '../../style/icons/copy.svg';
9
+ import { NBIAPI } from '../api';
10
+ import { CheckBoxItem } from './checkbox';
11
+ import { PillItem } from './pill';
12
+ import { mcpServerSettingsToEnabledState } from './mcp-util';
13
+
14
+ const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
15
+ const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
16
+ const OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
17
+ 'openai-compatible-inline-completion-model';
18
+ const LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
19
+ 'litellm-compatible-inline-completion-model';
20
+
21
+ export class SettingsPanel extends ReactWidget {
22
+ constructor(options: {
23
+ onSave: () => void;
24
+ onEditMCPConfigClicked: () => void;
25
+ }) {
26
+ super();
27
+
28
+ this._onSave = options.onSave;
29
+ this._onEditMCPConfigClicked = options.onEditMCPConfigClicked;
30
+ }
31
+
32
+ render(): JSX.Element {
33
+ return (
34
+ <SettingsPanelComponent
35
+ onSave={this._onSave}
36
+ onEditMCPConfigClicked={this._onEditMCPConfigClicked}
37
+ />
38
+ );
39
+ }
40
+
41
+ private _onSave: () => void;
42
+ private _onEditMCPConfigClicked: () => void;
43
+ }
44
+
45
+ function SettingsPanelComponent(props: any) {
46
+ const [activeTab, setActiveTab] = useState('general');
47
+
48
+ const onTabSelected = (tab: string) => {
49
+ setActiveTab(tab);
50
+ };
51
+
52
+ return (
53
+ <div className="nbi-settings-panel">
54
+ <div className="nbi-settings-panel-tabs">
55
+ <SettingsPanelTabsComponent
56
+ onTabSelected={onTabSelected}
57
+ activeTab={activeTab}
58
+ />
59
+ </div>
60
+ <div className="nbi-settings-panel-tab-content">
61
+ {activeTab === 'general' && (
62
+ <SettingsPanelComponentGeneral
63
+ onSave={props.onSave}
64
+ onEditMCPConfigClicked={props.onEditMCPConfigClicked}
65
+ />
66
+ )}
67
+ {activeTab === 'mcp-servers' && (
68
+ <SettingsPanelComponentMCPServers
69
+ onEditMCPConfigClicked={props.onEditMCPConfigClicked}
70
+ />
71
+ )}
72
+ </div>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ function SettingsPanelTabsComponent(props: any) {
78
+ const [activeTab, setActiveTab] = useState(props.activeTab);
79
+
80
+ return (
81
+ <div>
82
+ <div
83
+ className={`nbi-settings-panel-tab ${activeTab === 'general' ? 'active' : ''}`}
84
+ onClick={() => {
85
+ setActiveTab('general');
86
+ props.onTabSelected('general');
87
+ }}
88
+ >
89
+ General
90
+ </div>
91
+ <div
92
+ className={`nbi-settings-panel-tab ${activeTab === 'mcp-servers' ? 'active' : ''}`}
93
+ onClick={() => {
94
+ setActiveTab('mcp-servers');
95
+ props.onTabSelected('mcp-servers');
96
+ }}
97
+ >
98
+ MCP Servers
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function SettingsPanelComponentGeneral(props: any) {
105
+ const nbiConfig = NBIAPI.config;
106
+ const llmProviders = nbiConfig.llmProviders;
107
+ const [chatModels, setChatModels] = useState([]);
108
+ const [inlineCompletionModels, setInlineCompletionModels] = useState([]);
109
+
110
+ const handleSaveSettings = async () => {
111
+ const config: any = {
112
+ default_chat_mode: defaultChatMode,
113
+ chat_model: {
114
+ provider: chatModelProvider,
115
+ model: chatModel,
116
+ properties: chatModelProperties
117
+ },
118
+ inline_completion_model: {
119
+ provider: inlineCompletionModelProvider,
120
+ model: inlineCompletionModel,
121
+ properties: inlineCompletionModelProperties
122
+ }
123
+ };
124
+
125
+ if (
126
+ chatModelProvider === 'github-copilot' ||
127
+ inlineCompletionModelProvider === 'github-copilot'
128
+ ) {
129
+ config.store_github_access_token = storeGitHubAccessToken;
130
+ }
131
+
132
+ await NBIAPI.setConfig(config);
133
+
134
+ props.onSave();
135
+ };
136
+
137
+ const handleRefreshOllamaModelListClick = async () => {
138
+ await NBIAPI.updateOllamaModelList();
139
+ updateModelOptionsForProvider(chatModelProvider, 'chat');
140
+ };
141
+
142
+ const [chatModelProvider, setChatModelProvider] = useState(
143
+ nbiConfig.chatModel.provider || 'none'
144
+ );
145
+ const [inlineCompletionModelProvider, setInlineCompletionModelProvider] =
146
+ useState(nbiConfig.inlineCompletionModel.provider || 'none');
147
+ const [defaultChatMode, setDefaultChatMode] = useState<string>(
148
+ nbiConfig.defaultChatMode
149
+ );
150
+ const [chatModel, setChatModel] = useState<string>(nbiConfig.chatModel.model);
151
+ const [chatModelProperties, setChatModelProperties] = useState<any[]>([]);
152
+ const [inlineCompletionModelProperties, setInlineCompletionModelProperties] =
153
+ useState<any[]>([]);
154
+ const [inlineCompletionModel, setInlineCompletionModel] = useState(
155
+ nbiConfig.inlineCompletionModel.model
156
+ );
157
+ const [storeGitHubAccessToken, setStoreGitHubAccessToken] = useState(
158
+ nbiConfig.storeGitHubAccessToken
159
+ );
160
+
161
+ const updateModelOptionsForProvider = (
162
+ providerId: string,
163
+ modelType: 'chat' | 'inline-completion'
164
+ ) => {
165
+ if (modelType === 'chat') {
166
+ setChatModelProvider(providerId);
167
+ } else {
168
+ setInlineCompletionModelProvider(providerId);
169
+ }
170
+ const models =
171
+ modelType === 'chat'
172
+ ? nbiConfig.chatModels
173
+ : nbiConfig.inlineCompletionModels;
174
+ const selectedModelId =
175
+ modelType === 'chat'
176
+ ? nbiConfig.chatModel.model
177
+ : nbiConfig.inlineCompletionModel.model;
178
+
179
+ const providerModels = models.filter(
180
+ (model: any) => model.provider === providerId
181
+ );
182
+ if (modelType === 'chat') {
183
+ setChatModels(providerModels);
184
+ } else {
185
+ setInlineCompletionModels(providerModels);
186
+ }
187
+ let selectedModel = providerModels.find(
188
+ (model: any) => model.id === selectedModelId
189
+ );
190
+ if (!selectedModel) {
191
+ selectedModel = providerModels?.[0];
192
+ }
193
+ if (selectedModel) {
194
+ if (modelType === 'chat') {
195
+ setChatModel(selectedModel.id);
196
+ setChatModelProperties(selectedModel.properties);
197
+ } else {
198
+ setInlineCompletionModel(selectedModel.id);
199
+ setInlineCompletionModelProperties(selectedModel.properties);
200
+ }
201
+ } else {
202
+ if (modelType === 'chat') {
203
+ setChatModelProperties([]);
204
+ } else {
205
+ setInlineCompletionModelProperties([]);
206
+ }
207
+ }
208
+ };
209
+
210
+ const onModelPropertyChange = (
211
+ modelType: 'chat' | 'inline-completion',
212
+ propertyId: string,
213
+ value: string
214
+ ) => {
215
+ const modelProperties =
216
+ modelType === 'chat'
217
+ ? chatModelProperties
218
+ : inlineCompletionModelProperties;
219
+ const updatedProperties = modelProperties.map((property: any) => {
220
+ if (property.id === propertyId) {
221
+ return { ...property, value };
222
+ }
223
+ return property;
224
+ });
225
+ if (modelType === 'chat') {
226
+ setChatModelProperties(updatedProperties);
227
+ } else {
228
+ setInlineCompletionModelProperties(updatedProperties);
229
+ }
230
+ };
231
+
232
+ useEffect(() => {
233
+ updateModelOptionsForProvider(chatModelProvider, 'chat');
234
+ updateModelOptionsForProvider(
235
+ inlineCompletionModelProvider,
236
+ 'inline-completion'
237
+ );
238
+ }, []);
239
+
240
+ useEffect(() => {
241
+ handleSaveSettings();
242
+ }, [
243
+ defaultChatMode,
244
+ chatModelProvider,
245
+ chatModel,
246
+ chatModelProperties,
247
+ inlineCompletionModelProvider,
248
+ inlineCompletionModel,
249
+ inlineCompletionModelProperties,
250
+ storeGitHubAccessToken
251
+ ]);
252
+
253
+ return (
254
+ <div className="config-dialog">
255
+ <div className="config-dialog-body">
256
+ <div className="model-config-section">
257
+ <div className="model-config-section-header">Default chat mode</div>
258
+ <div className="model-config-section-body">
259
+ <div className="model-config-section-row">
260
+ <div className="model-config-section-column">
261
+ <div>
262
+ <select
263
+ className="jp-mod-styled"
264
+ value={defaultChatMode}
265
+ onChange={event => setDefaultChatMode(event.target.value)}
266
+ >
267
+ <option value="ask">Ask</option>
268
+ <option value="agent">Agent</option>
269
+ </select>
270
+ </div>
271
+ </div>
272
+ <div className="model-config-section-column"> </div>
273
+ </div>
274
+ </div>
275
+ </div>
276
+
277
+ <div className="model-config-section">
278
+ <div className="model-config-section-header">Chat model</div>
279
+ <div className="model-config-section-body">
280
+ <div className="model-config-section-row">
281
+ <div className="model-config-section-column">
282
+ <div>Provider</div>
283
+ <div>
284
+ <select
285
+ className="jp-mod-styled"
286
+ onChange={event =>
287
+ updateModelOptionsForProvider(event.target.value, 'chat')
288
+ }
289
+ >
290
+ {llmProviders.map((provider: any, index: number) => (
291
+ <option
292
+ key={index}
293
+ value={provider.id}
294
+ selected={provider.id === chatModelProvider}
295
+ >
296
+ {provider.name}
297
+ </option>
298
+ ))}
299
+ <option
300
+ key={-1}
301
+ value="none"
302
+ selected={
303
+ chatModelProvider === 'none' ||
304
+ !llmProviders.find(
305
+ provider => provider.id === chatModelProvider
306
+ )
307
+ }
308
+ >
309
+ None
310
+ </option>
311
+ </select>
312
+ </div>
313
+ </div>
314
+ {!['openai-compatible', 'litellm-compatible', 'none'].includes(
315
+ chatModelProvider
316
+ ) &&
317
+ chatModels.length > 0 && (
318
+ <div className="model-config-section-column">
319
+ <div>Model</div>
320
+ {![
321
+ OPENAI_COMPATIBLE_CHAT_MODEL_ID,
322
+ LITELLM_COMPATIBLE_CHAT_MODEL_ID
323
+ ].includes(chatModel) &&
324
+ chatModels.length > 0 && (
325
+ <div>
326
+ <select
327
+ className="jp-mod-styled"
328
+ onChange={event => setChatModel(event.target.value)}
329
+ >
330
+ {chatModels.map((model: any, index: number) => (
331
+ <option
332
+ key={index}
333
+ value={model.id}
334
+ selected={model.id === chatModel}
335
+ >
336
+ {model.name}
337
+ </option>
338
+ ))}
339
+ </select>
340
+ </div>
341
+ )}
342
+ </div>
343
+ )}
344
+ </div>
345
+
346
+ <div className="model-config-section-row">
347
+ <div className="model-config-section-column">
348
+ {chatModelProvider === 'ollama' && chatModels.length === 0 && (
349
+ <div className="ollama-warning-message">
350
+ No Ollama models found! Make sure{' '}
351
+ <a href="https://ollama.com/" target="_blank">
352
+ Ollama
353
+ </a>{' '}
354
+ is running and models are downloaded to your computer.{' '}
355
+ <a
356
+ href="javascript:void(0)"
357
+ onClick={handleRefreshOllamaModelListClick}
358
+ >
359
+ Try again
360
+ </a>{' '}
361
+ once ready.
362
+ </div>
363
+ )}
364
+ </div>
365
+ </div>
366
+
367
+ <div className="model-config-section-row">
368
+ <div className="model-config-section-column">
369
+ {chatModelProperties.map((property: any, index: number) => (
370
+ <div className="form-field-row" key={index}>
371
+ <div className="form-field-description">
372
+ {property.name} {property.optional ? '(optional)' : ''}
373
+ </div>
374
+ <input
375
+ name="chat-model-id-input"
376
+ placeholder={property.description}
377
+ className="jp-mod-styled"
378
+ spellCheck={false}
379
+ value={property.value}
380
+ onChange={event =>
381
+ onModelPropertyChange(
382
+ 'chat',
383
+ property.id,
384
+ event.target.value
385
+ )
386
+ }
387
+ />
388
+ </div>
389
+ ))}
390
+ </div>
391
+ </div>
392
+ </div>
393
+ </div>
394
+
395
+ <div className="model-config-section">
396
+ <div className="model-config-section-header">Auto-complete model</div>
397
+ <div className="model-config-section-body">
398
+ <div className="model-config-section-row">
399
+ <div className="model-config-section-column">
400
+ <div>Provider</div>
401
+ <div>
402
+ <select
403
+ className="jp-mod-styled"
404
+ onChange={event =>
405
+ updateModelOptionsForProvider(
406
+ event.target.value,
407
+ 'inline-completion'
408
+ )
409
+ }
410
+ >
411
+ {llmProviders.map((provider: any, index: number) => (
412
+ <option
413
+ key={index}
414
+ value={provider.id}
415
+ selected={provider.id === inlineCompletionModelProvider}
416
+ >
417
+ {provider.name}
418
+ </option>
419
+ ))}
420
+ <option
421
+ key={-1}
422
+ value="none"
423
+ selected={
424
+ inlineCompletionModelProvider === 'none' ||
425
+ !llmProviders.find(
426
+ provider =>
427
+ provider.id === inlineCompletionModelProvider
428
+ )
429
+ }
430
+ >
431
+ None
432
+ </option>
433
+ </select>
434
+ </div>
435
+ </div>
436
+ {!['openai-compatible', 'litellm-compatible', 'none'].includes(
437
+ inlineCompletionModelProvider
438
+ ) && (
439
+ <div className="model-config-section-column">
440
+ <div>Model</div>
441
+ {![
442
+ OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID,
443
+ LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID
444
+ ].includes(inlineCompletionModel) && (
445
+ <div>
446
+ <select
447
+ className="jp-mod-styled"
448
+ onChange={event =>
449
+ setInlineCompletionModel(event.target.value)
450
+ }
451
+ >
452
+ {inlineCompletionModels.map(
453
+ (model: any, index: number) => (
454
+ <option
455
+ key={index}
456
+ value={model.id}
457
+ selected={model.id === inlineCompletionModel}
458
+ >
459
+ {model.name}
460
+ </option>
461
+ )
462
+ )}
463
+ </select>
464
+ </div>
465
+ )}
466
+ </div>
467
+ )}
468
+ </div>
469
+
470
+ <div className="model-config-section-row">
471
+ <div className="model-config-section-column">
472
+ {inlineCompletionModelProperties.map(
473
+ (property: any, index: number) => (
474
+ <div className="form-field-row" key={index}>
475
+ <div className="form-field-description">
476
+ {property.name} {property.optional ? '(optional)' : ''}
477
+ </div>
478
+ <input
479
+ name="inline-completion-model-id-input"
480
+ placeholder={property.description}
481
+ className="jp-mod-styled"
482
+ spellCheck={false}
483
+ value={property.value}
484
+ onChange={event =>
485
+ onModelPropertyChange(
486
+ 'inline-completion',
487
+ property.id,
488
+ event.target.value
489
+ )
490
+ }
491
+ />
492
+ </div>
493
+ )
494
+ )}
495
+ </div>
496
+ </div>
497
+ </div>
498
+ </div>
499
+
500
+ {(chatModelProvider === 'github-copilot' ||
501
+ inlineCompletionModelProvider === 'github-copilot') && (
502
+ <div className="model-config-section">
503
+ <div className="model-config-section-header access-token-config-header">
504
+ GitHub Copilot login{' '}
505
+ <a
506
+ href="https://github.com/notebook-intelligence/notebook-intelligence/blob/main/README.md#remembering-github-copilot-login"
507
+ target="_blank"
508
+ >
509
+ {' '}
510
+ <VscWarning
511
+ className="access-token-warning"
512
+ title="Click to learn more about security implications"
513
+ />
514
+ </a>
515
+ </div>
516
+ <div className="model-config-section-body">
517
+ <div className="model-config-section-row">
518
+ <div className="model-config-section-column">
519
+ <label>
520
+ <input
521
+ type="checkbox"
522
+ checked={storeGitHubAccessToken}
523
+ onChange={event => {
524
+ setStoreGitHubAccessToken(event.target.checked);
525
+ }}
526
+ />
527
+ Remember my GitHub Copilot access token
528
+ </label>
529
+ </div>
530
+ </div>
531
+ </div>
532
+ </div>
533
+ )}
534
+
535
+ <div className="model-config-section">
536
+ <div className="model-config-section-header">Config file path</div>
537
+ <div className="model-config-section-body">
538
+ <div className="model-config-section-row">
539
+ <div className="model-config-section-column">
540
+ <span
541
+ className="user-code-span"
542
+ onClick={() => {
543
+ navigator.clipboard.writeText(
544
+ path.join(NBIAPI.config.userConfigDir, 'config.json')
545
+ );
546
+ return true;
547
+ }}
548
+ >
549
+ {path.join(NBIAPI.config.userConfigDir, 'config.json')}{' '}
550
+ <span
551
+ className="copy-icon"
552
+ dangerouslySetInnerHTML={{ __html: copySvgstr }}
553
+ ></span>
554
+ </span>
555
+ </div>
556
+ </div>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+ );
562
+ }
563
+
564
+ function SettingsPanelComponentMCPServers(props: any) {
565
+ const nbiConfig = NBIAPI.config;
566
+ const mcpServersRef = useRef<any>(nbiConfig.toolConfig.mcpServers);
567
+ const mcpServerSettingsRef = useRef<any>(nbiConfig.mcpServerSettings);
568
+ const [renderCount, setRenderCount] = useState(1);
569
+
570
+ const [mcpServerEnabledState, setMCPServerEnabledState] = useState(
571
+ new Map<string, Set<string>>(
572
+ mcpServerSettingsToEnabledState(
573
+ mcpServersRef.current,
574
+ mcpServerSettingsRef.current
575
+ )
576
+ )
577
+ );
578
+
579
+ const mcpServerEnabledStateToMcpServerSettings = () => {
580
+ const mcpServerSettings: any = {};
581
+ for (const mcpServer of mcpServersRef.current) {
582
+ if (mcpServerEnabledState.has(mcpServer.id)) {
583
+ const disabledTools = [];
584
+ for (const tool of mcpServer.tools) {
585
+ if (!mcpServerEnabledState.get(mcpServer.id).has(tool.name)) {
586
+ disabledTools.push(tool.name);
587
+ }
588
+ }
589
+ mcpServerSettings[mcpServer.id] = {
590
+ disabled: false,
591
+ disabled_tools: disabledTools
592
+ };
593
+ } else {
594
+ mcpServerSettings[mcpServer.id] = { disabled: true };
595
+ }
596
+ }
597
+ return mcpServerSettings;
598
+ };
599
+
600
+ const syncSettingsToServerState = () => {
601
+ NBIAPI.setConfig({
602
+ mcp_server_settings: mcpServerSettingsRef.current
603
+ });
604
+ };
605
+
606
+ const handleReloadMCPServersClick = async () => {
607
+ await NBIAPI.reloadMCPServers();
608
+ };
609
+
610
+ useEffect(() => {
611
+ syncSettingsToServerState();
612
+ }, [mcpServerSettingsRef.current]);
613
+
614
+ useEffect(() => {
615
+ mcpServerSettingsRef.current = mcpServerEnabledStateToMcpServerSettings();
616
+ setRenderCount(renderCount => renderCount + 1);
617
+ }, [mcpServerEnabledState]);
618
+
619
+ const setMCPServerEnabled = (serverId: string, enabled: boolean) => {
620
+ const currentState = new Map(mcpServerEnabledState);
621
+ if (enabled) {
622
+ if (!(serverId in currentState)) {
623
+ currentState.set(
624
+ serverId,
625
+ new Set<string>(
626
+ mcpServersRef.current
627
+ .find((server: any) => server.id === serverId)
628
+ ?.tools.map((tool: any) => tool.name)
629
+ )
630
+ );
631
+ }
632
+ } else {
633
+ currentState.delete(serverId);
634
+ }
635
+
636
+ setMCPServerEnabledState(currentState);
637
+ };
638
+
639
+ const getMCPServerEnabled = (serverId: string) => {
640
+ return mcpServerEnabledState.has(serverId);
641
+ };
642
+
643
+ const getMCPServerToolEnabled = (serverId: string, toolName: string) => {
644
+ return (
645
+ mcpServerEnabledState.has(serverId) &&
646
+ mcpServerEnabledState.get(serverId).has(toolName)
647
+ );
648
+ };
649
+
650
+ const setMCPServerToolEnabled = (
651
+ serverId: string,
652
+ toolName: string,
653
+ enabled: boolean
654
+ ) => {
655
+ const currentState = new Map(mcpServerEnabledState);
656
+ const serverState = currentState.get(serverId);
657
+ if (enabled) {
658
+ serverState.add(toolName);
659
+ } else {
660
+ serverState.delete(toolName);
661
+ }
662
+
663
+ setMCPServerEnabledState(currentState);
664
+ };
665
+
666
+ useEffect(() => {
667
+ NBIAPI.configChanged.connect(() => {
668
+ mcpServersRef.current = nbiConfig.toolConfig.mcpServers;
669
+ mcpServerSettingsRef.current = nbiConfig.mcpServerSettings;
670
+ setRenderCount(renderCount => renderCount + 1);
671
+ });
672
+ }, []);
673
+
674
+ return (
675
+ <div className="config-dialog">
676
+ <div className="config-dialog-body">
677
+ <div className="model-config-section">
678
+ <div
679
+ className="model-config-section-header"
680
+ style={{ display: 'flex' }}
681
+ >
682
+ <div style={{ flexGrow: 1 }}>MCP Servers</div>
683
+ <div>
684
+ <button
685
+ className="jp-toast-button jp-mod-small jp-Button"
686
+ onClick={handleReloadMCPServersClick}
687
+ >
688
+ <div className="jp-Dialog-buttonLabel">Reload</div>
689
+ </button>
690
+ </div>
691
+ </div>
692
+ <div className="model-config-section-body">
693
+ {mcpServersRef.current.length === 0 && renderCount > 0 && (
694
+ <div className="model-config-section-row">
695
+ <div className="model-config-section-column">
696
+ <div>
697
+ No MCP servers found. Add MCP servers in the configuration
698
+ file.
699
+ </div>
700
+ </div>
701
+ </div>
702
+ )}
703
+ {mcpServersRef.current.length > 0 && renderCount > 0 && (
704
+ <div className="model-config-section-row">
705
+ <div className="model-config-section-column">
706
+ {mcpServersRef.current.map((server: any) => (
707
+ <div key={server.id}>
708
+ <div style={{ display: 'flex', alignItems: 'center' }}>
709
+ <CheckBoxItem
710
+ header={true}
711
+ label={server.id}
712
+ checked={getMCPServerEnabled(server.id)}
713
+ onClick={() => {
714
+ setMCPServerEnabled(
715
+ server.id,
716
+ !getMCPServerEnabled(server.id)
717
+ );
718
+ }}
719
+ ></CheckBoxItem>
720
+ <div
721
+ className={`server-status-indicator ${server.status}`}
722
+ title={server.status}
723
+ ></div>
724
+ </div>
725
+ {getMCPServerEnabled(server.id) && (
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
+ ))}
744
+ </div>
745
+ )}
746
+ </div>
747
+ ))}
748
+ </div>
749
+ </div>
750
+ )}
751
+ <div className="model-config-section-row">
752
+ <div
753
+ className="model-config-section-column"
754
+ style={{ flexGrow: 'initial' }}
755
+ >
756
+ <button
757
+ className="jp-Dialog-button jp-mod-accept jp-mod-styled"
758
+ style={{ width: 'max-content' }}
759
+ onClick={props.onEditMCPConfigClicked}
760
+ >
761
+ <div className="jp-Dialog-buttonLabel">Add / Edit</div>
762
+ </button>
763
+ </div>
764
+ </div>
765
+ </div>
766
+ </div>
767
+ </div>
768
+ </div>
769
+ );
770
+ }