@plmbr/notebook-intelligence 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +412 -0
  3. package/lib/api.d.ts +288 -0
  4. package/lib/api.js +927 -0
  5. package/lib/cell-output-bundle.d.ts +25 -0
  6. package/lib/cell-output-bundle.js +129 -0
  7. package/lib/cell-output-toolbar.d.ts +26 -0
  8. package/lib/cell-output-toolbar.js +188 -0
  9. package/lib/chat-progress-feedback.d.ts +3 -0
  10. package/lib/chat-progress-feedback.js +27 -0
  11. package/lib/chat-sidebar.d.ts +92 -0
  12. package/lib/chat-sidebar.js +3452 -0
  13. package/lib/command-ids.d.ts +39 -0
  14. package/lib/command-ids.js +44 -0
  15. package/lib/components/ask-user-question.d.ts +2 -0
  16. package/lib/components/ask-user-question.js +85 -0
  17. package/lib/components/checkbox.d.ts +2 -0
  18. package/lib/components/checkbox.js +30 -0
  19. package/lib/components/claude-mcp-panel.d.ts +2 -0
  20. package/lib/components/claude-mcp-panel.js +275 -0
  21. package/lib/components/claude-mcp-paste.d.ts +7 -0
  22. package/lib/components/claude-mcp-paste.js +104 -0
  23. package/lib/components/claude-session-picker.d.ts +8 -0
  24. package/lib/components/claude-session-picker.js +127 -0
  25. package/lib/components/form-dialog.d.ts +25 -0
  26. package/lib/components/form-dialog.js +35 -0
  27. package/lib/components/launcher-picker.d.ts +6 -0
  28. package/lib/components/launcher-picker.js +135 -0
  29. package/lib/components/mcp-util.d.ts +2 -0
  30. package/lib/components/mcp-util.js +37 -0
  31. package/lib/components/notebook-generation-popover.d.ts +7 -0
  32. package/lib/components/notebook-generation-popover.js +60 -0
  33. package/lib/components/pill.d.ts +2 -0
  34. package/lib/components/pill.js +5 -0
  35. package/lib/components/plugins-panel.d.ts +3 -0
  36. package/lib/components/plugins-panel.js +466 -0
  37. package/lib/components/settings-panel.d.ts +11 -0
  38. package/lib/components/settings-panel.js +742 -0
  39. package/lib/components/skills-panel.d.ts +2 -0
  40. package/lib/components/skills-panel.js +1264 -0
  41. package/lib/handler.d.ts +8 -0
  42. package/lib/handler.js +36 -0
  43. package/lib/icons.d.ts +45 -0
  44. package/lib/icons.js +54 -0
  45. package/lib/index.d.ts +8 -0
  46. package/lib/index.js +2079 -0
  47. package/lib/markdown-renderer.d.ts +10 -0
  48. package/lib/markdown-renderer.js +64 -0
  49. package/lib/notebook-generation-toolbar.d.ts +16 -0
  50. package/lib/notebook-generation-toolbar.js +197 -0
  51. package/lib/notebook-generation.d.ts +8 -0
  52. package/lib/notebook-generation.js +12 -0
  53. package/lib/open-file-refresh-watcher-env.d.ts +4 -0
  54. package/lib/open-file-refresh-watcher-env.js +33 -0
  55. package/lib/open-file-refresh-watcher.d.ts +97 -0
  56. package/lib/open-file-refresh-watcher.js +190 -0
  57. package/lib/shell-utils.d.ts +6 -0
  58. package/lib/shell-utils.js +9 -0
  59. package/lib/task-target-notebook.d.ts +2 -0
  60. package/lib/task-target-notebook.js +28 -0
  61. package/lib/terminal-drag-format.d.ts +9 -0
  62. package/lib/terminal-drag-format.js +23 -0
  63. package/lib/terminal-drag.d.ts +12 -0
  64. package/lib/terminal-drag.js +268 -0
  65. package/lib/tokens.d.ts +149 -0
  66. package/lib/tokens.js +88 -0
  67. package/lib/tour/tour-anchors.d.ts +18 -0
  68. package/lib/tour/tour-anchors.js +18 -0
  69. package/lib/tour/tour-config.d.ts +66 -0
  70. package/lib/tour/tour-config.js +99 -0
  71. package/lib/tour/tour-defaults.json +58 -0
  72. package/lib/tour/tour-events.d.ts +19 -0
  73. package/lib/tour/tour-events.js +30 -0
  74. package/lib/tour/tour-overlay.d.ts +6 -0
  75. package/lib/tour/tour-overlay.js +350 -0
  76. package/lib/tour/tour-state.d.ts +20 -0
  77. package/lib/tour/tour-state.js +81 -0
  78. package/lib/tour/tour-steps.d.ts +33 -0
  79. package/lib/tour/tour-steps.js +216 -0
  80. package/lib/utils.d.ts +53 -0
  81. package/lib/utils.js +385 -0
  82. package/package.json +258 -0
  83. package/schema/plugin.json +42 -0
  84. package/src/api.ts +1424 -0
  85. package/src/cell-output-bundle.ts +176 -0
  86. package/src/cell-output-toolbar.ts +232 -0
  87. package/src/chat-progress-feedback.ts +35 -0
  88. package/src/chat-sidebar.tsx +5147 -0
  89. package/src/command-ids.ts +67 -0
  90. package/src/components/ask-user-question.tsx +151 -0
  91. package/src/components/checkbox.tsx +62 -0
  92. package/src/components/claude-mcp-panel.tsx +543 -0
  93. package/src/components/claude-mcp-paste.ts +132 -0
  94. package/src/components/claude-session-picker.tsx +214 -0
  95. package/src/components/form-dialog.tsx +75 -0
  96. package/src/components/launcher-picker.tsx +237 -0
  97. package/src/components/mcp-util.ts +53 -0
  98. package/src/components/notebook-generation-popover.tsx +127 -0
  99. package/src/components/pill.tsx +15 -0
  100. package/src/components/plugins-panel.tsx +774 -0
  101. package/src/components/settings-panel.tsx +1631 -0
  102. package/src/components/skills-panel.tsx +2084 -0
  103. package/src/handler.ts +51 -0
  104. package/src/icons.ts +71 -0
  105. package/src/index.ts +2583 -0
  106. package/src/markdown-renderer.tsx +153 -0
  107. package/src/notebook-generation-toolbar.tsx +281 -0
  108. package/src/notebook-generation.ts +23 -0
  109. package/src/open-file-refresh-watcher-env.ts +52 -0
  110. package/src/open-file-refresh-watcher.ts +260 -0
  111. package/src/shell-utils.ts +10 -0
  112. package/src/svg.d.ts +4 -0
  113. package/src/task-target-notebook.ts +37 -0
  114. package/src/terminal-drag-format.ts +29 -0
  115. package/src/terminal-drag.ts +382 -0
  116. package/src/tokens.ts +171 -0
  117. package/src/tour/tour-anchors.ts +21 -0
  118. package/src/tour/tour-config.ts +160 -0
  119. package/src/tour/tour-events.ts +34 -0
  120. package/src/tour/tour-overlay.tsx +474 -0
  121. package/src/tour/tour-state.ts +87 -0
  122. package/src/tour/tour-steps.ts +281 -0
  123. package/src/utils.ts +455 -0
  124. package/style/base.css +3238 -0
  125. package/style/icons/cell-toolbar-bug.svg +5 -0
  126. package/style/icons/cell-toolbar-chat.svg +5 -0
  127. package/style/icons/cell-toolbar-sparkle.svg +5 -0
  128. package/style/icons/claude.svg +1 -0
  129. package/style/icons/copilot-warning.svg +1 -0
  130. package/style/icons/copilot.svg +1 -0
  131. package/style/icons/copy.svg +1 -0
  132. package/style/icons/openai.svg +1 -0
  133. package/style/icons/opencode.svg +1 -0
  134. package/style/icons/sparkles-warning.svg +5 -0
  135. package/style/icons/sparkles.svg +1 -0
  136. package/style/index.css +1 -0
  137. package/style/index.js +1 -0
@@ -0,0 +1,1631 @@
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 '../icons';
6
+ import * as path from 'path';
7
+
8
+ import copySvgstr from '../../style/icons/copy.svg';
9
+ import claudeSvgStr from '../../style/icons/claude.svg';
10
+ import {
11
+ ClaudeModelType,
12
+ ClaudeToolType,
13
+ ICellOutputFeatureFlag,
14
+ IClaudeModelInfo,
15
+ NBIAPI
16
+ } from '../api';
17
+ import { CheckBoxItem } from './checkbox';
18
+ import { PillItem } from './pill';
19
+ import { mcpServerSettingsToEnabledState } from './mcp-util';
20
+ import { SettingsPanelComponentSkills } from './skills-panel';
21
+ import { SettingsPanelComponentClaudeMCP } from './claude-mcp-panel';
22
+ import { SettingsPanelComponentPlugins } from './plugins-panel';
23
+ import { writeTextToClipboard } from '../utils';
24
+
25
+ const lockedTip = (locked: boolean): string =>
26
+ locked ? 'Locked by your administrator' : '';
27
+
28
+ // Stable id helper so the tab and its panel agree on aria-controls /
29
+ // aria-labelledby without scattering string concatenation through the
30
+ // component.
31
+ const tabId = (prefix: string, id: string): string => `${prefix}-${id}`;
32
+
33
+ type TablistOrientation = 'vertical' | 'horizontal';
34
+
35
+ // WAI-ARIA tablist arrow-key navigation. Same shape for both the
36
+ // vertical (Up/Down) main tabs and the horizontal (Left/Right) Claude
37
+ // subtabs — the orientation flag picks which keys move the cursor.
38
+ // Returns an ``onKeyDown`` for the tablist container; callers decide
39
+ // what to do with each id (typically: select + focus).
40
+ function useTablistArrowKeys<T extends { id: string }>(
41
+ tabs: T[],
42
+ activeId: string,
43
+ onSelect: (id: string) => void,
44
+ orientation: TablistOrientation,
45
+ domIdFor: (id: string) => string
46
+ ): (e: React.KeyboardEvent<HTMLDivElement>) => void {
47
+ return (e: React.KeyboardEvent<HTMLDivElement>) => {
48
+ const key = e.key;
49
+ const prevKey = orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
50
+ const nextKey = orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
51
+ if (key !== prevKey && key !== nextKey && key !== 'Home' && key !== 'End') {
52
+ return;
53
+ }
54
+ e.preventDefault();
55
+ const idx = tabs.findIndex(t => t.id === activeId);
56
+ let next = idx;
57
+ if (key === nextKey) {
58
+ next = (idx + 1) % tabs.length;
59
+ } else if (key === prevKey) {
60
+ next = (idx - 1 + tabs.length) % tabs.length;
61
+ } else if (key === 'Home') {
62
+ next = 0;
63
+ } else if (key === 'End') {
64
+ next = tabs.length - 1;
65
+ }
66
+ onSelect(tabs[next].id);
67
+ document.getElementById(domIdFor(tabs[next].id))?.focus();
68
+ };
69
+ }
70
+
71
+ // When a boolean policy is locked the panel shows the policy-resolved value;
72
+ // otherwise it shows the user's local toggle state.
73
+ const checkedValue = (
74
+ policy: ICellOutputFeatureFlag,
75
+ userValue: boolean
76
+ ): boolean => (policy.locked ? policy.enabled : userValue);
77
+
78
+ function useNbiPolicies() {
79
+ const [featurePolicies, setFeaturePolicies] = useState(
80
+ NBIAPI.config.featurePolicies
81
+ );
82
+ const [settingLocks, setSettingLocks] = useState(NBIAPI.config.settingLocks);
83
+ useEffect(() => {
84
+ const handler = () => {
85
+ setFeaturePolicies(NBIAPI.config.featurePolicies);
86
+ setSettingLocks(NBIAPI.config.settingLocks);
87
+ };
88
+ NBIAPI.configChanged.connect(handler);
89
+ return () => {
90
+ NBIAPI.configChanged.disconnect(handler);
91
+ };
92
+ }, []);
93
+ return { featurePolicies, settingLocks };
94
+ }
95
+
96
+ const OPENAI_COMPATIBLE_CHAT_MODEL_ID = 'openai-compatible-chat-model';
97
+ const LITELLM_COMPATIBLE_CHAT_MODEL_ID = 'litellm-compatible-chat-model';
98
+ const OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
99
+ 'openai-compatible-inline-completion-model';
100
+ const LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID =
101
+ 'litellm-compatible-inline-completion-model';
102
+
103
+ export class SettingsPanel extends ReactWidget {
104
+ constructor(options: {
105
+ onSave: () => void;
106
+ onEditMCPConfigClicked: () => void;
107
+ }) {
108
+ super();
109
+
110
+ this._onSave = options.onSave;
111
+ this._onEditMCPConfigClicked = options.onEditMCPConfigClicked;
112
+ }
113
+
114
+ render(): JSX.Element {
115
+ return (
116
+ <SettingsPanelComponent
117
+ onSave={this._onSave}
118
+ onEditMCPConfigClicked={this._onEditMCPConfigClicked}
119
+ />
120
+ );
121
+ }
122
+
123
+ private _onSave: () => void;
124
+ private _onEditMCPConfigClicked: () => void;
125
+ }
126
+
127
+ // Tab declaration. Adding a new tab is one entry here plus an icon
128
+ // (optional). The `visible` predicate runs against {featurePolicies,
129
+ // isInClaudeCodeMode, isClaudeCliAvailable} so policy / mode changes
130
+ // propagate through the registry without wiring extra props.
131
+ type TabSpec = {
132
+ id: string;
133
+ label: string;
134
+ icon?: () => JSX.Element;
135
+ visible: (ctx: TabVisibilityContext) => boolean;
136
+ render: (props: any) => JSX.Element;
137
+ };
138
+ type TabVisibilityContext = {
139
+ featurePolicies: import('../api').IFeaturePolicies;
140
+ isInClaudeCodeMode: boolean;
141
+ isClaudeCliAvailable: boolean;
142
+ };
143
+
144
+ const TABS: TabSpec[] = [
145
+ {
146
+ id: 'general',
147
+ label: 'General',
148
+ visible: () => true,
149
+ render: props => (
150
+ <SettingsPanelComponentGeneral
151
+ onSave={props.onSave}
152
+ onEditMCPConfigClicked={props.onEditMCPConfigClicked}
153
+ />
154
+ )
155
+ },
156
+ {
157
+ id: 'claude',
158
+ label: 'Claude',
159
+ icon: () => (
160
+ <span
161
+ className="claude-icon"
162
+ dangerouslySetInnerHTML={{ __html: claudeSvgStr }}
163
+ ></span>
164
+ ),
165
+ visible: () => true,
166
+ render: props => (
167
+ <SettingsPanelComponentClaude
168
+ onEditMCPConfigClicked={props.onEditMCPConfigClicked}
169
+ />
170
+ )
171
+ },
172
+ {
173
+ id: 'mcp-servers',
174
+ label: 'MCP Servers',
175
+ visible: ctx => !ctx.isInClaudeCodeMode,
176
+ render: props => (
177
+ <SettingsPanelComponentMCPServers
178
+ onEditMCPConfigClicked={props.onEditMCPConfigClicked}
179
+ />
180
+ )
181
+ },
182
+ {
183
+ id: 'claude-mcp',
184
+ label: 'Claude MCP',
185
+ visible: ctx =>
186
+ ctx.featurePolicies.claude_mcp_management.enabled &&
187
+ ctx.isInClaudeCodeMode &&
188
+ ctx.isClaudeCliAvailable,
189
+ render: () => <SettingsPanelComponentClaudeMCP />
190
+ },
191
+ {
192
+ id: 'plugins',
193
+ label: 'Plugins',
194
+ visible: ctx =>
195
+ ctx.featurePolicies.claude_plugins_management.enabled &&
196
+ ctx.isInClaudeCodeMode &&
197
+ ctx.isClaudeCliAvailable,
198
+ render: () => <SettingsPanelComponentPlugins />
199
+ },
200
+ {
201
+ id: 'skills',
202
+ label: 'Skills',
203
+ visible: ctx => ctx.featurePolicies.skills_management.enabled,
204
+ render: () => <SettingsPanelComponentSkills />
205
+ }
206
+ ];
207
+
208
+ function SettingsPanelComponent(props: any) {
209
+ const [activeTab, setActiveTab] = useState('general');
210
+ const { featurePolicies } = useNbiPolicies();
211
+ const [isInClaudeCodeMode, setIsInClaudeCodeMode] = useState(
212
+ NBIAPI.config.isInClaudeCodeMode
213
+ );
214
+ const [isClaudeCliAvailable, setIsClaudeCliAvailable] = useState(
215
+ NBIAPI.config.isClaudeCliAvailable
216
+ );
217
+
218
+ useEffect(() => {
219
+ const handler = () => {
220
+ setIsInClaudeCodeMode(NBIAPI.config.isInClaudeCodeMode);
221
+ setIsClaudeCliAvailable(NBIAPI.config.isClaudeCliAvailable);
222
+ };
223
+ NBIAPI.configChanged.connect(handler);
224
+ return () => {
225
+ NBIAPI.configChanged.disconnect(handler);
226
+ };
227
+ }, []);
228
+
229
+ const ctx: TabVisibilityContext = {
230
+ featurePolicies,
231
+ isInClaudeCodeMode,
232
+ isClaudeCliAvailable
233
+ };
234
+ const visibleTabs = TABS.filter(t => t.visible(ctx));
235
+ const activeTabSpec = visibleTabs.find(t => t.id === activeTab);
236
+
237
+ // Bounce off a tab that just disappeared (admin policy flip, mode toggle).
238
+ useEffect(() => {
239
+ if (!activeTabSpec) {
240
+ setActiveTab('general');
241
+ }
242
+ }, [activeTabSpec]);
243
+
244
+ return (
245
+ <div className="nbi-settings-panel">
246
+ <SettingsPanelTabsComponent
247
+ tabs={visibleTabs}
248
+ activeTab={activeTab}
249
+ onTabSelected={setActiveTab}
250
+ />
251
+ <div
252
+ className="nbi-settings-panel-tab-content"
253
+ role="tabpanel"
254
+ id={tabId('nbi-settings-tabpanel', activeTab)}
255
+ aria-labelledby={tabId('nbi-settings-tab', activeTab)}
256
+ >
257
+ {activeTabSpec && activeTabSpec.render(props)}
258
+ </div>
259
+ </div>
260
+ );
261
+ }
262
+
263
+ function SettingsPanelTabsComponent(props: {
264
+ tabs: TabSpec[];
265
+ activeTab: string;
266
+ onTabSelected: (tab: string) => void;
267
+ }) {
268
+ const onKeyDown = useTablistArrowKeys(
269
+ props.tabs,
270
+ props.activeTab,
271
+ props.onTabSelected,
272
+ 'vertical',
273
+ id => tabId('nbi-settings-tab', id)
274
+ );
275
+
276
+ return (
277
+ <div
278
+ className="nbi-settings-panel-tabs"
279
+ role="tablist"
280
+ aria-orientation="vertical"
281
+ aria-label="Settings sections"
282
+ onKeyDown={onKeyDown}
283
+ >
284
+ {props.tabs.map(tab => {
285
+ const selected = tab.id === props.activeTab;
286
+ return (
287
+ <button
288
+ type="button"
289
+ key={tab.id}
290
+ id={tabId('nbi-settings-tab', tab.id)}
291
+ className={`nbi-settings-panel-tab ${selected ? 'active' : ''}`}
292
+ role="tab"
293
+ aria-selected={selected}
294
+ aria-controls={tabId('nbi-settings-tabpanel', tab.id)}
295
+ tabIndex={selected ? 0 : -1}
296
+ onClick={() => props.onTabSelected(tab.id)}
297
+ >
298
+ {tab.icon && tab.icon()}
299
+ {tab.label}
300
+ </button>
301
+ );
302
+ })}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ function SettingsPanelComponentGeneral(props: any) {
308
+ const nbiConfig = NBIAPI.config;
309
+ const llmProviders = nbiConfig.llmProviders;
310
+ const [chatModels, setChatModels] = useState([]);
311
+ const [inlineCompletionModels, setInlineCompletionModels] = useState([]);
312
+ const isInClaudeCodeMode = nbiConfig.isInClaudeCodeMode;
313
+
314
+ const handleSaveSettings = async () => {
315
+ const config: any = {
316
+ default_chat_mode: defaultChatMode,
317
+ chat_model: {
318
+ provider: chatModelProvider,
319
+ model: chatModel,
320
+ properties: chatModelProperties
321
+ },
322
+ inline_completion_model: {
323
+ provider: inlineCompletionModelProvider,
324
+ model: inlineCompletionModel,
325
+ properties: inlineCompletionModelProperties
326
+ },
327
+ inline_completion_debouncer_delay: inlineCompletionDebouncerDelay
328
+ };
329
+
330
+ if (
331
+ chatModelProvider === 'github-copilot' ||
332
+ inlineCompletionModelProvider === 'github-copilot'
333
+ ) {
334
+ config.store_github_access_token = storeGitHubAccessToken;
335
+ }
336
+
337
+ await NBIAPI.setConfig(config);
338
+
339
+ props.onSave();
340
+ };
341
+
342
+ const handleRefreshOllamaModelListClick = async () => {
343
+ await NBIAPI.updateOllamaModelList();
344
+ updateModelOptionsForProvider(chatModelProvider, 'chat');
345
+ };
346
+
347
+ const [chatModelProvider, setChatModelProvider] = useState(
348
+ nbiConfig.chatModel.provider || 'none'
349
+ );
350
+ const [inlineCompletionModelProvider, setInlineCompletionModelProvider] =
351
+ useState(nbiConfig.inlineCompletionModel.provider || 'none');
352
+ const [defaultChatMode, setDefaultChatMode] = useState<string>(
353
+ nbiConfig.defaultChatMode
354
+ );
355
+ const [chatModel, setChatModel] = useState<string>(nbiConfig.chatModel.model);
356
+ const [chatModelProperties, setChatModelProperties] = useState<any[]>([]);
357
+ const [inlineCompletionModelProperties, setInlineCompletionModelProperties] =
358
+ useState<any[]>([]);
359
+ const [inlineCompletionModel, setInlineCompletionModel] = useState(
360
+ nbiConfig.inlineCompletionModel.model
361
+ );
362
+ const [storeGitHubAccessToken, setStoreGitHubAccessToken] = useState(
363
+ nbiConfig.storeGitHubAccessToken
364
+ );
365
+ const [inlineCompletionDebouncerDelay, setInlineCompletionDebouncerDelay] =
366
+ useState(nbiConfig.inlineCompletionDebouncerDelay);
367
+ const { featurePolicies, settingLocks } = useNbiPolicies();
368
+
369
+ const toggleExplainError = () => {
370
+ NBIAPI.setConfig({
371
+ enable_explain_error: !featurePolicies.explain_error.enabled
372
+ });
373
+ };
374
+
375
+ const toggleOutputFollowup = () => {
376
+ NBIAPI.setConfig({
377
+ enable_output_followup: !featurePolicies.output_followup.enabled
378
+ });
379
+ };
380
+
381
+ const toggleOutputToolbar = () => {
382
+ NBIAPI.setConfig({
383
+ enable_output_toolbar: !featurePolicies.output_toolbar.enabled
384
+ });
385
+ };
386
+
387
+ const toggleRefreshOpenFilesOnDiskChange = () => {
388
+ NBIAPI.setConfig({
389
+ refresh_open_files_on_disk_change:
390
+ !featurePolicies.refresh_open_files_on_disk_change.enabled
391
+ });
392
+ };
393
+
394
+ const updateModelOptionsForProvider = (
395
+ providerId: string,
396
+ modelType: 'chat' | 'inline-completion'
397
+ ) => {
398
+ if (modelType === 'chat') {
399
+ setChatModelProvider(providerId);
400
+ } else {
401
+ setInlineCompletionModelProvider(providerId);
402
+ }
403
+ const models =
404
+ modelType === 'chat'
405
+ ? nbiConfig.chatModels
406
+ : nbiConfig.inlineCompletionModels;
407
+ const selectedModelId =
408
+ modelType === 'chat'
409
+ ? nbiConfig.chatModel.model
410
+ : nbiConfig.inlineCompletionModel.model;
411
+
412
+ const providerModels = models.filter(
413
+ (model: any) => model.provider === providerId
414
+ );
415
+ if (modelType === 'chat') {
416
+ setChatModels(providerModels);
417
+ } else {
418
+ setInlineCompletionModels(providerModels);
419
+ }
420
+ let selectedModel = providerModels.find(
421
+ (model: any) => model.id === selectedModelId
422
+ );
423
+ if (!selectedModel) {
424
+ selectedModel = providerModels?.[0];
425
+ }
426
+ if (selectedModel) {
427
+ if (modelType === 'chat') {
428
+ setChatModel(selectedModel.id);
429
+ setChatModelProperties(selectedModel.properties);
430
+ } else {
431
+ setInlineCompletionModel(selectedModel.id);
432
+ setInlineCompletionModelProperties(selectedModel.properties);
433
+ }
434
+ } else {
435
+ if (modelType === 'chat') {
436
+ setChatModelProperties([]);
437
+ } else {
438
+ setInlineCompletionModelProperties([]);
439
+ }
440
+ }
441
+ };
442
+
443
+ const onModelPropertyChange = (
444
+ modelType: 'chat' | 'inline-completion',
445
+ propertyId: string,
446
+ value: string
447
+ ) => {
448
+ const modelProperties =
449
+ modelType === 'chat'
450
+ ? chatModelProperties
451
+ : inlineCompletionModelProperties;
452
+ const updatedProperties = modelProperties.map((property: any) => {
453
+ if (property.id === propertyId) {
454
+ return { ...property, value };
455
+ }
456
+ return property;
457
+ });
458
+ if (modelType === 'chat') {
459
+ setChatModelProperties(updatedProperties);
460
+ } else {
461
+ setInlineCompletionModelProperties(updatedProperties);
462
+ }
463
+ };
464
+
465
+ useEffect(() => {
466
+ updateModelOptionsForProvider(chatModelProvider, 'chat');
467
+ updateModelOptionsForProvider(
468
+ inlineCompletionModelProvider,
469
+ 'inline-completion'
470
+ );
471
+ }, []);
472
+
473
+ useEffect(() => {
474
+ handleSaveSettings();
475
+ }, [
476
+ defaultChatMode,
477
+ chatModelProvider,
478
+ chatModel,
479
+ chatModelProperties,
480
+ inlineCompletionModelProvider,
481
+ inlineCompletionModel,
482
+ inlineCompletionModelProperties,
483
+ storeGitHubAccessToken,
484
+ inlineCompletionDebouncerDelay
485
+ ]);
486
+
487
+ return (
488
+ <div className="config-dialog">
489
+ <div className="config-dialog-body">
490
+ {!isInClaudeCodeMode && (
491
+ <div className="model-config-section">
492
+ <div className="model-config-section-header">Default chat mode</div>
493
+ <div className="model-config-section-body">
494
+ <div className="model-config-section-row">
495
+ <div className="model-config-section-column">
496
+ <div>
497
+ <select
498
+ className="jp-mod-styled"
499
+ value={defaultChatMode}
500
+ onChange={event => setDefaultChatMode(event.target.value)}
501
+ >
502
+ <option value="ask">Ask</option>
503
+ <option value="agent">Agent</option>
504
+ </select>
505
+ </div>
506
+ </div>
507
+ <div className="model-config-section-column"> </div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ )}
512
+
513
+ {!isInClaudeCodeMode && (
514
+ <div className="model-config-section">
515
+ <div className="model-config-section-header">Chat model</div>
516
+ <div className="model-config-section-body">
517
+ <div className="model-config-section-row">
518
+ <div className="model-config-section-column">
519
+ <div>Provider</div>
520
+ <div
521
+ title={lockedTip(settingLocks.chat_model_provider.locked)}
522
+ >
523
+ <select
524
+ className="jp-mod-styled"
525
+ disabled={settingLocks.chat_model_provider.locked}
526
+ onChange={event =>
527
+ updateModelOptionsForProvider(
528
+ event.target.value,
529
+ 'chat'
530
+ )
531
+ }
532
+ >
533
+ {llmProviders.map((provider: any, index: number) => (
534
+ <option
535
+ key={index}
536
+ value={provider.id}
537
+ selected={provider.id === chatModelProvider}
538
+ >
539
+ {provider.name}
540
+ </option>
541
+ ))}
542
+ <option
543
+ key={-1}
544
+ value="none"
545
+ selected={
546
+ chatModelProvider === 'none' ||
547
+ !llmProviders.find(
548
+ provider => provider.id === chatModelProvider
549
+ )
550
+ }
551
+ >
552
+ None
553
+ </option>
554
+ </select>
555
+ </div>
556
+ </div>
557
+ {!['openai-compatible', 'litellm-compatible', 'none'].includes(
558
+ chatModelProvider
559
+ ) &&
560
+ chatModels.length > 0 && (
561
+ <div className="model-config-section-column">
562
+ <div>Model</div>
563
+ {![
564
+ OPENAI_COMPATIBLE_CHAT_MODEL_ID,
565
+ LITELLM_COMPATIBLE_CHAT_MODEL_ID
566
+ ].includes(chatModel) &&
567
+ chatModels.length > 0 && (
568
+ <div
569
+ title={lockedTip(settingLocks.chat_model_id.locked)}
570
+ >
571
+ <select
572
+ className="jp-mod-styled"
573
+ disabled={settingLocks.chat_model_id.locked}
574
+ onChange={event =>
575
+ setChatModel(event.target.value)
576
+ }
577
+ >
578
+ {chatModels.map((model: any, index: number) => (
579
+ <option
580
+ key={index}
581
+ value={model.id}
582
+ selected={model.id === chatModel}
583
+ >
584
+ {model.name}
585
+ </option>
586
+ ))}
587
+ </select>
588
+ </div>
589
+ )}
590
+ </div>
591
+ )}
592
+ </div>
593
+
594
+ <div className="model-config-section-row">
595
+ <div className="model-config-section-column">
596
+ {chatModelProvider === 'ollama' &&
597
+ chatModels.length === 0 && (
598
+ <div className="ollama-warning-message">
599
+ No Ollama models found! Make sure{' '}
600
+ <a href="https://ollama.com/" target="_blank">
601
+ Ollama
602
+ </a>{' '}
603
+ is running and models are downloaded to your computer.{' '}
604
+ <button
605
+ type="button"
606
+ className="link-button"
607
+ onClick={handleRefreshOllamaModelListClick}
608
+ >
609
+ Try again
610
+ </button>{' '}
611
+ once ready.
612
+ </div>
613
+ )}
614
+ </div>
615
+ </div>
616
+
617
+ <div className="model-config-section-row">
618
+ <div className="model-config-section-column">
619
+ {chatModelProperties.map((property: any, index: number) => (
620
+ <div className="form-field-row" key={index}>
621
+ <div className="form-field-description">
622
+ {property.name} {property.optional ? '(optional)' : ''}
623
+ </div>
624
+ <input
625
+ name="chat-model-id-input"
626
+ placeholder={property.description}
627
+ className="jp-mod-styled"
628
+ spellCheck={false}
629
+ value={property.value}
630
+ onChange={event =>
631
+ onModelPropertyChange(
632
+ 'chat',
633
+ property.id,
634
+ event.target.value
635
+ )
636
+ }
637
+ />
638
+ </div>
639
+ ))}
640
+ </div>
641
+ </div>
642
+ </div>
643
+ </div>
644
+ )}
645
+
646
+ <div className="model-config-section">
647
+ <div className="model-config-section-header">Auto-complete model</div>
648
+ <div className="model-config-section-body">
649
+ <div className="model-config-section-row">
650
+ <div className="model-config-section-column">
651
+ <div>Provider</div>
652
+ <div
653
+ title={lockedTip(
654
+ settingLocks.inline_completion_model_provider.locked
655
+ )}
656
+ >
657
+ <select
658
+ className="jp-mod-styled"
659
+ disabled={
660
+ settingLocks.inline_completion_model_provider.locked
661
+ }
662
+ onChange={event =>
663
+ updateModelOptionsForProvider(
664
+ event.target.value,
665
+ 'inline-completion'
666
+ )
667
+ }
668
+ >
669
+ {llmProviders.map((provider: any, index: number) => (
670
+ <option
671
+ key={index}
672
+ value={provider.id}
673
+ selected={provider.id === inlineCompletionModelProvider}
674
+ >
675
+ {provider.name}
676
+ </option>
677
+ ))}
678
+ <option
679
+ key={-1}
680
+ value="none"
681
+ selected={
682
+ inlineCompletionModelProvider === 'none' ||
683
+ !llmProviders.find(
684
+ provider =>
685
+ provider.id === inlineCompletionModelProvider
686
+ )
687
+ }
688
+ >
689
+ None
690
+ </option>
691
+ </select>
692
+ </div>
693
+ </div>
694
+ {!['openai-compatible', 'litellm-compatible', 'none'].includes(
695
+ inlineCompletionModelProvider
696
+ ) && (
697
+ <div className="model-config-section-column">
698
+ <div>Model</div>
699
+ {![
700
+ OPENAI_COMPATIBLE_INLINE_COMPLETION_MODEL_ID,
701
+ LITELLM_COMPATIBLE_INLINE_COMPLETION_MODEL_ID
702
+ ].includes(inlineCompletionModel) && (
703
+ <div
704
+ title={lockedTip(
705
+ settingLocks.inline_completion_model_id.locked
706
+ )}
707
+ >
708
+ <select
709
+ className="jp-mod-styled"
710
+ disabled={
711
+ settingLocks.inline_completion_model_id.locked
712
+ }
713
+ onChange={event =>
714
+ setInlineCompletionModel(event.target.value)
715
+ }
716
+ >
717
+ {inlineCompletionModels.map(
718
+ (model: any, index: number) => (
719
+ <option
720
+ key={index}
721
+ value={model.id}
722
+ selected={model.id === inlineCompletionModel}
723
+ >
724
+ {model.name}
725
+ </option>
726
+ )
727
+ )}
728
+ </select>
729
+ </div>
730
+ )}
731
+ </div>
732
+ )}
733
+ </div>
734
+
735
+ <div className="model-config-section-row">
736
+ <div className="model-config-section-column">
737
+ {inlineCompletionModelProperties.map(
738
+ (property: any, index: number) => (
739
+ <div className="form-field-row" key={index}>
740
+ <div className="form-field-description">
741
+ {property.name} {property.optional ? '(optional)' : ''}
742
+ </div>
743
+ <input
744
+ name="inline-completion-model-id-input"
745
+ placeholder={property.description}
746
+ className="jp-mod-styled"
747
+ spellCheck={false}
748
+ value={property.value}
749
+ onChange={event =>
750
+ onModelPropertyChange(
751
+ 'inline-completion',
752
+ property.id,
753
+ event.target.value
754
+ )
755
+ }
756
+ />
757
+ </div>
758
+ )
759
+ )}
760
+ </div>
761
+ </div>
762
+ </div>
763
+ </div>
764
+
765
+ <div className="model-config-section-row" style={{ width: '50%' }}>
766
+ <div className="model-config-section-column">
767
+ <div className="form-field-row" style={{ paddingLeft: '10px' }}>
768
+ <div className="form-field-description">
769
+ Auto-complete debouncer delay (ms)
770
+ </div>
771
+ <input
772
+ name="inline-completion-debouncer-delay-input"
773
+ placeholder="Auto-complete debouncer delay (milliseconds)"
774
+ className="jp-mod-styled"
775
+ spellCheck={false}
776
+ value={inlineCompletionDebouncerDelay}
777
+ type="number"
778
+ onChange={event =>
779
+ setInlineCompletionDebouncerDelay(Number(event.target.value))
780
+ }
781
+ />
782
+ </div>
783
+ </div>
784
+ </div>
785
+
786
+ {!isInClaudeCodeMode &&
787
+ (chatModelProvider === 'github-copilot' ||
788
+ inlineCompletionModelProvider === 'github-copilot') && (
789
+ <div className="model-config-section">
790
+ <div className="model-config-section-header access-token-config-header">
791
+ GitHub Copilot login{' '}
792
+ <a
793
+ href="https://github.com/plmbr/notebook-intelligence/blob/main/README.md#remembering-github-copilot-login"
794
+ target="_blank"
795
+ >
796
+ {' '}
797
+ <VscWarning
798
+ className="access-token-warning"
799
+ title="Click to learn more about security implications"
800
+ />
801
+ </a>
802
+ </div>
803
+ <div className="model-config-section-body">
804
+ <div className="model-config-section-row">
805
+ <div className="model-config-section-column">
806
+ <label
807
+ title={lockedTip(
808
+ featurePolicies.store_github_access_token.locked
809
+ )}
810
+ >
811
+ <input
812
+ type="checkbox"
813
+ checked={checkedValue(
814
+ featurePolicies.store_github_access_token,
815
+ storeGitHubAccessToken
816
+ )}
817
+ disabled={
818
+ featurePolicies.store_github_access_token.locked
819
+ }
820
+ onChange={event => {
821
+ setStoreGitHubAccessToken(event.target.checked);
822
+ }}
823
+ />
824
+ Remember my GitHub Copilot access token
825
+ </label>
826
+ </div>
827
+ </div>
828
+ </div>
829
+ </div>
830
+ )}
831
+
832
+ <div className="model-config-section">
833
+ <div className="model-config-section-header">
834
+ Cell output features
835
+ </div>
836
+ <div className="model-config-section-body">
837
+ <div className="model-config-section-row">
838
+ <div className="model-config-section-column">
839
+ <CheckBoxItem
840
+ label="Explain cell errors"
841
+ title="Show a 'Troubleshoot errors in output' context-menu item on failed cells"
842
+ checked={featurePolicies.explain_error.enabled}
843
+ disabled={featurePolicies.explain_error.locked}
844
+ tooltip={lockedTip(featurePolicies.explain_error.locked)}
845
+ onClick={toggleExplainError}
846
+ />
847
+ </div>
848
+ </div>
849
+ <div className="model-config-section-row">
850
+ <div className="model-config-section-column">
851
+ <CheckBoxItem
852
+ label="Ask about cell outputs"
853
+ title="Right-click a cell output to attach it to the chat"
854
+ checked={featurePolicies.output_followup.enabled}
855
+ disabled={featurePolicies.output_followup.locked}
856
+ tooltip={lockedTip(featurePolicies.output_followup.locked)}
857
+ onClick={toggleOutputFollowup}
858
+ />
859
+ </div>
860
+ </div>
861
+ <div className="model-config-section-row">
862
+ <div className="model-config-section-column">
863
+ <CheckBoxItem
864
+ label="Show output toolbar"
865
+ title="Show a hover toolbar over cell outputs with Explain / Ask / Troubleshoot buttons"
866
+ checked={featurePolicies.output_toolbar.enabled}
867
+ disabled={featurePolicies.output_toolbar.locked}
868
+ tooltip={lockedTip(featurePolicies.output_toolbar.locked)}
869
+ onClick={toggleOutputToolbar}
870
+ />
871
+ </div>
872
+ </div>
873
+ </div>
874
+ </div>
875
+
876
+ <div className="model-config-section">
877
+ <div className="model-config-section-header">External changes</div>
878
+ <div className="model-config-section-body">
879
+ <div className="model-config-section-row">
880
+ <div className="model-config-section-column">
881
+ <CheckBoxItem
882
+ label="Refresh open files when changed on disk"
883
+ title="Automatically reload notebook and file editor tabs when an external process (terminal command, sync client, or AI agent) edits the file. Skipped when the tab has unsaved local edits."
884
+ checked={
885
+ featurePolicies.refresh_open_files_on_disk_change.enabled
886
+ }
887
+ disabled={
888
+ featurePolicies.refresh_open_files_on_disk_change.locked
889
+ }
890
+ tooltip={lockedTip(
891
+ featurePolicies.refresh_open_files_on_disk_change.locked
892
+ )}
893
+ onClick={toggleRefreshOpenFilesOnDiskChange}
894
+ />
895
+ </div>
896
+ </div>
897
+ </div>
898
+ </div>
899
+
900
+ <div className="model-config-section">
901
+ <div className="model-config-section-header">Config file path</div>
902
+ <div className="model-config-section-body">
903
+ <div className="model-config-section-row">
904
+ <div className="model-config-section-column">
905
+ <span
906
+ className="user-code-span"
907
+ onClick={() => {
908
+ void writeTextToClipboard(
909
+ path.join(NBIAPI.config.userConfigDir, 'config.json')
910
+ );
911
+ return true;
912
+ }}
913
+ >
914
+ {path.join(NBIAPI.config.userConfigDir, 'config.json')}{' '}
915
+ <span
916
+ className="copy-icon"
917
+ dangerouslySetInnerHTML={{ __html: copySvgstr }}
918
+ ></span>
919
+ </span>
920
+ </div>
921
+ </div>
922
+ </div>
923
+ </div>
924
+ </div>
925
+ </div>
926
+ );
927
+ }
928
+
929
+ function SettingsPanelComponentMCPServers(props: any) {
930
+ const nbiConfig = NBIAPI.config;
931
+ const mcpServersRef = useRef<any>(nbiConfig.toolConfig.mcpServers);
932
+ const mcpServerSettingsRef = useRef<any>(nbiConfig.mcpServerSettings);
933
+ const [renderCount, setRenderCount] = useState(1);
934
+
935
+ const [mcpServerEnabledState, setMCPServerEnabledState] = useState(
936
+ new Map<string, Set<string>>(
937
+ mcpServerSettingsToEnabledState(
938
+ mcpServersRef.current,
939
+ mcpServerSettingsRef.current
940
+ )
941
+ )
942
+ );
943
+
944
+ const mcpServerEnabledStateToMcpServerSettings = () => {
945
+ const mcpServerSettings: any = {};
946
+ for (const mcpServer of mcpServersRef.current) {
947
+ if (mcpServerEnabledState.has(mcpServer.id)) {
948
+ const disabledTools = [];
949
+ for (const tool of mcpServer.tools) {
950
+ if (!mcpServerEnabledState.get(mcpServer.id).has(tool.name)) {
951
+ disabledTools.push(tool.name);
952
+ }
953
+ }
954
+ mcpServerSettings[mcpServer.id] = {
955
+ disabled: false,
956
+ disabled_tools: disabledTools
957
+ };
958
+ } else {
959
+ mcpServerSettings[mcpServer.id] = { disabled: true };
960
+ }
961
+ }
962
+ return mcpServerSettings;
963
+ };
964
+
965
+ const syncSettingsToServerState = () => {
966
+ NBIAPI.setConfig({
967
+ mcp_server_settings: mcpServerSettingsRef.current
968
+ });
969
+ };
970
+
971
+ const handleReloadMCPServersClick = async () => {
972
+ await NBIAPI.reloadMCPServers();
973
+ };
974
+
975
+ useEffect(() => {
976
+ syncSettingsToServerState();
977
+ }, [mcpServerSettingsRef.current]);
978
+
979
+ useEffect(() => {
980
+ mcpServerSettingsRef.current = mcpServerEnabledStateToMcpServerSettings();
981
+ setRenderCount(renderCount => renderCount + 1);
982
+ }, [mcpServerEnabledState]);
983
+
984
+ const setMCPServerEnabled = (serverId: string, enabled: boolean) => {
985
+ const currentState = new Map(mcpServerEnabledState);
986
+ if (enabled) {
987
+ if (!(serverId in currentState)) {
988
+ currentState.set(
989
+ serverId,
990
+ new Set<string>(
991
+ mcpServersRef.current
992
+ .find((server: any) => server.id === serverId)
993
+ ?.tools.map((tool: any) => tool.name)
994
+ )
995
+ );
996
+ }
997
+ } else {
998
+ currentState.delete(serverId);
999
+ }
1000
+
1001
+ setMCPServerEnabledState(currentState);
1002
+ };
1003
+
1004
+ const getMCPServerEnabled = (serverId: string) => {
1005
+ return mcpServerEnabledState.has(serverId);
1006
+ };
1007
+
1008
+ const getMCPServerToolEnabled = (serverId: string, toolName: string) => {
1009
+ return (
1010
+ mcpServerEnabledState.has(serverId) &&
1011
+ mcpServerEnabledState.get(serverId).has(toolName)
1012
+ );
1013
+ };
1014
+
1015
+ const setMCPServerToolEnabled = (
1016
+ serverId: string,
1017
+ toolName: string,
1018
+ enabled: boolean
1019
+ ) => {
1020
+ const currentState = new Map(mcpServerEnabledState);
1021
+ const serverState = currentState.get(serverId);
1022
+ if (enabled) {
1023
+ serverState.add(toolName);
1024
+ } else {
1025
+ serverState.delete(toolName);
1026
+ }
1027
+
1028
+ setMCPServerEnabledState(currentState);
1029
+ };
1030
+
1031
+ useEffect(() => {
1032
+ const handler = () => {
1033
+ mcpServersRef.current = nbiConfig.toolConfig.mcpServers;
1034
+ mcpServerSettingsRef.current = nbiConfig.mcpServerSettings;
1035
+ setRenderCount(renderCount => renderCount + 1);
1036
+ };
1037
+ NBIAPI.configChanged.connect(handler);
1038
+ return () => {
1039
+ NBIAPI.configChanged.disconnect(handler);
1040
+ };
1041
+ }, []);
1042
+
1043
+ return (
1044
+ <div className="config-dialog">
1045
+ <div className="config-dialog-body">
1046
+ <div className="model-config-section">
1047
+ <div
1048
+ className="model-config-section-header"
1049
+ style={{ display: 'flex' }}
1050
+ >
1051
+ <div style={{ flexGrow: 1 }}>MCP Servers</div>
1052
+ <div>
1053
+ <button
1054
+ className="jp-toast-button jp-mod-small jp-Button"
1055
+ onClick={handleReloadMCPServersClick}
1056
+ >
1057
+ <div className="jp-Dialog-buttonLabel">Reload</div>
1058
+ </button>
1059
+ </div>
1060
+ </div>
1061
+ <div className="model-config-section-body">
1062
+ {mcpServersRef.current.length === 0 && renderCount > 0 && (
1063
+ <div className="model-config-section-row">
1064
+ <div className="model-config-section-column">
1065
+ <div>
1066
+ No MCP servers found. Add MCP servers in the configuration
1067
+ file.
1068
+ </div>
1069
+ </div>
1070
+ </div>
1071
+ )}
1072
+ {mcpServersRef.current.length > 0 && renderCount > 0 && (
1073
+ <div className="model-config-section-row">
1074
+ <div className="model-config-section-column">
1075
+ {mcpServersRef.current.map((server: any) => (
1076
+ <div key={server.id}>
1077
+ <div style={{ display: 'flex', alignItems: 'center' }}>
1078
+ <CheckBoxItem
1079
+ header={true}
1080
+ label={server.id}
1081
+ checked={getMCPServerEnabled(server.id)}
1082
+ onClick={() => {
1083
+ setMCPServerEnabled(
1084
+ server.id,
1085
+ !getMCPServerEnabled(server.id)
1086
+ );
1087
+ }}
1088
+ ></CheckBoxItem>
1089
+ <div
1090
+ className={`server-status-indicator ${server.status}`}
1091
+ title={server.status}
1092
+ ></div>
1093
+ </div>
1094
+ {getMCPServerEnabled(server.id) && (
1095
+ <div>
1096
+ {server.tools.length > 0 && (
1097
+ <div className="mcp-server-tools">
1098
+ <div className="mcp-server-tools-header">
1099
+ Tools
1100
+ </div>
1101
+ <div>
1102
+ {server.tools.map((tool: any) => (
1103
+ <PillItem
1104
+ label={tool.name}
1105
+ title={tool.description}
1106
+ checked={getMCPServerToolEnabled(
1107
+ server.id,
1108
+ tool.name
1109
+ )}
1110
+ onClick={() => {
1111
+ setMCPServerToolEnabled(
1112
+ server.id,
1113
+ tool.name,
1114
+ !getMCPServerToolEnabled(
1115
+ server.id,
1116
+ tool.name
1117
+ )
1118
+ );
1119
+ }}
1120
+ ></PillItem>
1121
+ ))}
1122
+ </div>
1123
+ </div>
1124
+ )}
1125
+ {server.prompts.length > 0 && (
1126
+ <div className="mcp-server-prompts">
1127
+ <div className="mcp-server-prompts-header">
1128
+ Prompts
1129
+ </div>
1130
+ <div>
1131
+ {server.prompts.map((prompt: any) => (
1132
+ <PillItem
1133
+ label={prompt.name}
1134
+ title={prompt.description}
1135
+ checked={true}
1136
+ ></PillItem>
1137
+ ))}
1138
+ </div>
1139
+ </div>
1140
+ )}
1141
+ </div>
1142
+ )}
1143
+ </div>
1144
+ ))}
1145
+ </div>
1146
+ </div>
1147
+ )}
1148
+ <div className="model-config-section-row">
1149
+ <div
1150
+ className="model-config-section-column"
1151
+ style={{ flexGrow: 'initial' }}
1152
+ >
1153
+ <button
1154
+ className="jp-Dialog-button jp-mod-accept jp-mod-styled"
1155
+ style={{ width: 'max-content' }}
1156
+ onClick={props.onEditMCPConfigClicked}
1157
+ >
1158
+ <div className="jp-Dialog-buttonLabel">Add / Edit</div>
1159
+ </button>
1160
+ </div>
1161
+ </div>
1162
+ </div>
1163
+ </div>
1164
+ </div>
1165
+ </div>
1166
+ );
1167
+ }
1168
+
1169
+ function SettingsPanelComponentClaude(props: any) {
1170
+ const nbiConfig = NBIAPI.config;
1171
+ const claudeSettingsRef = useRef<any>(nbiConfig.claudeSettings);
1172
+ const [_renderCount, setRenderCount] = useState(1);
1173
+ const [claudeEnabled, setClaudeEnabled] = useState(
1174
+ nbiConfig.isInClaudeCodeMode
1175
+ );
1176
+ const [chatModel, setChatModel] = useState(
1177
+ nbiConfig.claudeSettings.chat_model ?? ClaudeModelType.Default
1178
+ );
1179
+ const [inlineCompletionModel, setInlineCompletionModel] = useState(
1180
+ nbiConfig.claudeSettings.inline_completion_model ?? ClaudeModelType.Default
1181
+ );
1182
+ const [apiKey, setApiKey] = useState(nbiConfig.claudeSettings.api_key ?? '');
1183
+ const [baseUrl, setBaseUrl] = useState(
1184
+ nbiConfig.claudeSettings.base_url ?? ''
1185
+ );
1186
+ const [settingSources, setSettingSources] = useState(
1187
+ nbiConfig.claudeSettings.setting_sources ?? []
1188
+ );
1189
+ const [tools, setTools] = useState(
1190
+ nbiConfig.claudeSettings.tools ?? [
1191
+ ClaudeToolType.ClaudeCodeTools,
1192
+ ClaudeToolType.JupyterUITools
1193
+ ]
1194
+ );
1195
+ const [continueConversation, setContinueConversation] = useState(
1196
+ nbiConfig.claudeSettings.continue_conversation ?? false
1197
+ );
1198
+ const [claudeModels, setClaudeModels] = useState<IClaudeModelInfo[]>(
1199
+ nbiConfig.claudeModels
1200
+ );
1201
+ const [loadingModels, setLoadingModels] = useState(false);
1202
+ const { featurePolicies, settingLocks } = useNbiPolicies();
1203
+
1204
+ useEffect(() => {
1205
+ const handler = () => {
1206
+ claudeSettingsRef.current = nbiConfig.claudeSettings;
1207
+ setClaudeModels(nbiConfig.claudeModels);
1208
+ setRenderCount(renderCount => renderCount + 1);
1209
+ };
1210
+ NBIAPI.configChanged.connect(handler);
1211
+ return () => {
1212
+ NBIAPI.configChanged.disconnect(handler);
1213
+ };
1214
+ }, []);
1215
+
1216
+ const refreshClaudeModels = async () => {
1217
+ setLoadingModels(true);
1218
+ try {
1219
+ await NBIAPI.updateClaudeModelList();
1220
+ const models = nbiConfig.claudeModels;
1221
+ console.log('claude_models after refresh:', models);
1222
+ setClaudeModels(models);
1223
+ } finally {
1224
+ setLoadingModels(false);
1225
+ }
1226
+ };
1227
+
1228
+ const syncSettingsToServerState = () => {
1229
+ NBIAPI.setConfig({
1230
+ claude_settings: {
1231
+ enabled: claudeEnabled,
1232
+ chat_model: chatModel,
1233
+ inline_completion_model: inlineCompletionModel,
1234
+ api_key: apiKey,
1235
+ base_url: baseUrl,
1236
+ setting_sources: settingSources,
1237
+ tools: tools,
1238
+ continue_conversation: continueConversation
1239
+ }
1240
+ });
1241
+ };
1242
+
1243
+ useEffect(() => {
1244
+ syncSettingsToServerState();
1245
+ }, [
1246
+ claudeEnabled,
1247
+ chatModel,
1248
+ inlineCompletionModel,
1249
+ apiKey,
1250
+ baseUrl,
1251
+ settingSources,
1252
+ tools,
1253
+ continueConversation
1254
+ ]);
1255
+
1256
+ return (
1257
+ <div className="config-dialog claude-mode-config-dialog">
1258
+ <div className="config-dialog-body">
1259
+ <div className="model-config-section">
1260
+ <div className="model-config-section-header">Enable Claude mode</div>
1261
+ <div className="model-config-section-body">
1262
+ <div className="model-config-section-row">
1263
+ <span>
1264
+ This requires a{' '}
1265
+ <a href="https://claude.ai" target="_blank">
1266
+ Claude
1267
+ </a>{' '}
1268
+ account and{' '}
1269
+ <a href="https://code.claude.com/" target="_blank">
1270
+ Claude Code
1271
+ </a>{' '}
1272
+ installed in your system.
1273
+ </span>
1274
+ </div>
1275
+ <div className="model-config-section-row">
1276
+ <div className="model-config-section-column">
1277
+ <div>
1278
+ <CheckBoxItem
1279
+ header={true}
1280
+ label="Enable Claude mode"
1281
+ checked={checkedValue(
1282
+ featurePolicies.claude_mode,
1283
+ claudeEnabled
1284
+ )}
1285
+ disabled={featurePolicies.claude_mode.locked}
1286
+ tooltip={lockedTip(featurePolicies.claude_mode.locked)}
1287
+ onClick={() => {
1288
+ setClaudeEnabled(!claudeEnabled);
1289
+ }}
1290
+ ></CheckBoxItem>
1291
+ </div>
1292
+ </div>
1293
+ </div>
1294
+ </div>
1295
+ </div>
1296
+
1297
+ <div className="model-config-section">
1298
+ <div
1299
+ className="model-config-section-header"
1300
+ style={{ display: 'flex' }}
1301
+ >
1302
+ <div style={{ flexGrow: 1 }}>Models</div>
1303
+ <div>
1304
+ <button
1305
+ className="jp-toast-button jp-mod-small jp-Button"
1306
+ onClick={refreshClaudeModels}
1307
+ disabled={loadingModels}
1308
+ >
1309
+ <div className="jp-Dialog-buttonLabel">
1310
+ {loadingModels ? 'Loading...' : 'Refresh'}
1311
+ </div>
1312
+ </button>
1313
+ </div>
1314
+ </div>
1315
+ <div className="model-config-section-body">
1316
+ <div className="model-config-section-row">
1317
+ <div className="model-config-section-column">
1318
+ <div id="nbi-claude-chat-model-label">Chat model</div>
1319
+ <div title={lockedTip(settingLocks.claude_chat_model.locked)}>
1320
+ <select
1321
+ className="jp-mod-styled"
1322
+ aria-labelledby="nbi-claude-chat-model-label"
1323
+ aria-describedby={
1324
+ settingLocks.claude_chat_model.locked
1325
+ ? 'nbi-claude-chat-model-lock-reason'
1326
+ : undefined
1327
+ }
1328
+ disabled={settingLocks.claude_chat_model.locked}
1329
+ value={chatModel}
1330
+ onChange={event => setChatModel(event.target.value)}
1331
+ >
1332
+ <option value={ClaudeModelType.Default}>
1333
+ Default (recommended)
1334
+ </option>
1335
+ {/* Placeholder for a persisted model id that hasn't
1336
+ landed in `claudeModels` yet (empty cache, no api
1337
+ key, mid-fetch). Rendered just after Default so
1338
+ the active value sits near the top of the list. */}
1339
+ {chatModel !== ClaudeModelType.Default &&
1340
+ !claudeModels.some(m => m.id === chatModel) && (
1341
+ <option key={chatModel} value={chatModel}>
1342
+ {chatModel}
1343
+ </option>
1344
+ )}
1345
+ {claudeModels.map(model => (
1346
+ <option key={model.id} value={model.id}>
1347
+ {model.name}
1348
+ </option>
1349
+ ))}
1350
+ </select>
1351
+ {settingLocks.claude_chat_model.locked && (
1352
+ <span
1353
+ id="nbi-claude-chat-model-lock-reason"
1354
+ className="nbi-sr-only"
1355
+ >
1356
+ Locked by your administrator
1357
+ </span>
1358
+ )}
1359
+ </div>
1360
+ </div>
1361
+ <div className="model-config-section-column">
1362
+ <div id="nbi-claude-inline-model-label">
1363
+ Auto-complete model
1364
+ </div>
1365
+ <div
1366
+ title={lockedTip(
1367
+ settingLocks.claude_inline_completion_model.locked
1368
+ )}
1369
+ >
1370
+ <select
1371
+ className="jp-mod-styled"
1372
+ aria-labelledby="nbi-claude-inline-model-label"
1373
+ aria-describedby={
1374
+ settingLocks.claude_inline_completion_model.locked
1375
+ ? 'nbi-claude-inline-model-lock-reason'
1376
+ : undefined
1377
+ }
1378
+ disabled={
1379
+ settingLocks.claude_inline_completion_model.locked
1380
+ }
1381
+ value={inlineCompletionModel}
1382
+ onChange={event =>
1383
+ setInlineCompletionModel(event.target.value)
1384
+ }
1385
+ >
1386
+ <option value={ClaudeModelType.None}>None</option>
1387
+ <option value={ClaudeModelType.Inherit}>
1388
+ Inherit from general settings
1389
+ </option>
1390
+ <option value={ClaudeModelType.Default}>
1391
+ Default (recommended)
1392
+ </option>
1393
+ {![
1394
+ ClaudeModelType.None,
1395
+ ClaudeModelType.Inherit,
1396
+ ClaudeModelType.Default
1397
+ ].includes(inlineCompletionModel as ClaudeModelType) &&
1398
+ !claudeModels.some(
1399
+ m => m.id === inlineCompletionModel
1400
+ ) && (
1401
+ <option
1402
+ key={inlineCompletionModel}
1403
+ value={inlineCompletionModel}
1404
+ >
1405
+ {inlineCompletionModel}
1406
+ </option>
1407
+ )}
1408
+ {claudeModels.map(model => (
1409
+ <option key={model.id} value={model.id}>
1410
+ {model.name}
1411
+ </option>
1412
+ ))}
1413
+ </select>
1414
+ {settingLocks.claude_inline_completion_model.locked && (
1415
+ <span
1416
+ id="nbi-claude-inline-model-lock-reason"
1417
+ className="nbi-sr-only"
1418
+ >
1419
+ Locked by your administrator
1420
+ </span>
1421
+ )}
1422
+ </div>
1423
+ </div>
1424
+ </div>
1425
+ </div>
1426
+ </div>
1427
+
1428
+ <div className="model-config-section">
1429
+ <div className="model-config-section-header">
1430
+ Chat Agent setting sources
1431
+ </div>
1432
+ <div className="model-config-section-body">
1433
+ <div className="model-config-section-row">
1434
+ <div className="model-config-section-column">
1435
+ <div>
1436
+ <CheckBoxItem
1437
+ header={true}
1438
+ label="User"
1439
+ checked={checkedValue(
1440
+ featurePolicies.claude_setting_source_user,
1441
+ settingSources.includes('user')
1442
+ )}
1443
+ disabled={featurePolicies.claude_setting_source_user.locked}
1444
+ tooltip={lockedTip(
1445
+ featurePolicies.claude_setting_source_user.locked
1446
+ )}
1447
+ onClick={() => {
1448
+ setSettingSources(
1449
+ settingSources.includes('user')
1450
+ ? settingSources.filter(
1451
+ (source: string) => source !== 'user'
1452
+ )
1453
+ : [...settingSources, 'user']
1454
+ );
1455
+ }}
1456
+ ></CheckBoxItem>
1457
+ </div>
1458
+ </div>
1459
+ <div className="model-config-section-column">
1460
+ <div>
1461
+ <CheckBoxItem
1462
+ header={true}
1463
+ label="Project (Jupyter root directory)"
1464
+ checked={checkedValue(
1465
+ featurePolicies.claude_setting_source_project,
1466
+ settingSources.includes('project')
1467
+ )}
1468
+ disabled={
1469
+ featurePolicies.claude_setting_source_project.locked
1470
+ }
1471
+ tooltip={lockedTip(
1472
+ featurePolicies.claude_setting_source_project.locked
1473
+ )}
1474
+ onClick={() => {
1475
+ setSettingSources(
1476
+ settingSources.includes('project')
1477
+ ? settingSources.filter(
1478
+ (source: string) => source !== 'project'
1479
+ )
1480
+ : [...settingSources, 'project']
1481
+ );
1482
+ }}
1483
+ ></CheckBoxItem>
1484
+ </div>
1485
+ </div>
1486
+ </div>
1487
+ </div>
1488
+ </div>
1489
+
1490
+ <div className="model-config-section">
1491
+ <div className="model-config-section-header">Chat Agent tools</div>
1492
+ <div className="model-config-section-body">
1493
+ <div className="model-config-section-row">
1494
+ <div className="model-config-section-column">
1495
+ <div>
1496
+ <CheckBoxItem
1497
+ header={true}
1498
+ label="Claude Code tools"
1499
+ checked={checkedValue(
1500
+ featurePolicies.claude_code_tools,
1501
+ tools.includes(ClaudeToolType.ClaudeCodeTools)
1502
+ )}
1503
+ disabled={true}
1504
+ tooltip={lockedTip(
1505
+ featurePolicies.claude_code_tools.locked
1506
+ )}
1507
+ onClick={() => {
1508
+ setTools(
1509
+ tools.includes(ClaudeToolType.ClaudeCodeTools)
1510
+ ? tools.filter(
1511
+ (tool: string) =>
1512
+ tool !== ClaudeToolType.ClaudeCodeTools
1513
+ )
1514
+ : [...tools, ClaudeToolType.ClaudeCodeTools]
1515
+ );
1516
+ }}
1517
+ ></CheckBoxItem>
1518
+ </div>
1519
+ </div>
1520
+ <div className="model-config-section-column">
1521
+ <div>
1522
+ <CheckBoxItem
1523
+ header={true}
1524
+ label="Jupyter UI tools"
1525
+ checked={checkedValue(
1526
+ featurePolicies.claude_jupyter_ui_tools,
1527
+ tools.includes(ClaudeToolType.JupyterUITools)
1528
+ )}
1529
+ disabled={featurePolicies.claude_jupyter_ui_tools.locked}
1530
+ tooltip={lockedTip(
1531
+ featurePolicies.claude_jupyter_ui_tools.locked
1532
+ )}
1533
+ onClick={() => {
1534
+ setTools(
1535
+ tools.includes(ClaudeToolType.JupyterUITools)
1536
+ ? tools.filter(
1537
+ (tool: string) =>
1538
+ tool !== ClaudeToolType.JupyterUITools
1539
+ )
1540
+ : [...tools, ClaudeToolType.JupyterUITools]
1541
+ );
1542
+ }}
1543
+ ></CheckBoxItem>
1544
+ </div>
1545
+ </div>
1546
+ </div>
1547
+ </div>
1548
+ </div>
1549
+
1550
+ <div className="model-config-section">
1551
+ <div className="model-config-section-header">
1552
+ Conversation History
1553
+ </div>
1554
+ <div className="model-config-section-body">
1555
+ <div className="model-config-section-row">
1556
+ <div className="model-config-section-column">
1557
+ <div>
1558
+ <CheckBoxItem
1559
+ header={true}
1560
+ label="Remember conversation history"
1561
+ checked={checkedValue(
1562
+ featurePolicies.claude_continue_conversation,
1563
+ continueConversation
1564
+ )}
1565
+ disabled={
1566
+ featurePolicies.claude_continue_conversation.locked
1567
+ }
1568
+ tooltip={lockedTip(
1569
+ featurePolicies.claude_continue_conversation.locked
1570
+ )}
1571
+ onClick={() => {
1572
+ setContinueConversation(!continueConversation);
1573
+ }}
1574
+ ></CheckBoxItem>
1575
+ </div>
1576
+ </div>
1577
+ </div>
1578
+ </div>
1579
+ </div>
1580
+
1581
+ <div className="model-config-section">
1582
+ <div className="model-config-section-header">Claude account</div>
1583
+ <div className="model-config-section-body">
1584
+ <div className="model-config-section-row">
1585
+ <div className="model-config-section-column">
1586
+ <div className="form-field-row">
1587
+ <div className="form-field-description">
1588
+ API Key (optional)
1589
+ </div>
1590
+ <input
1591
+ name="chat-model-id-input"
1592
+ placeholder={
1593
+ settingLocks.claude_api_key.locked
1594
+ ? 'Locked by ANTHROPIC_API_KEY'
1595
+ : 'API Key'
1596
+ }
1597
+ className="jp-mod-styled"
1598
+ spellCheck={false}
1599
+ value={settingLocks.claude_api_key.locked ? '' : apiKey}
1600
+ disabled={settingLocks.claude_api_key.locked}
1601
+ title={lockedTip(settingLocks.claude_api_key.locked)}
1602
+ onChange={event => setApiKey(event.target.value)}
1603
+ />
1604
+ </div>
1605
+ <div className="form-field-row">
1606
+ <div className="form-field-description">
1607
+ Base URL (optional)
1608
+ </div>
1609
+ <input
1610
+ name="chat-model-id-input"
1611
+ placeholder={
1612
+ settingLocks.claude_base_url.locked
1613
+ ? 'Locked by ANTHROPIC_BASE_URL'
1614
+ : 'https://api.anthropic.com'
1615
+ }
1616
+ className="jp-mod-styled"
1617
+ spellCheck={false}
1618
+ value={baseUrl}
1619
+ disabled={settingLocks.claude_base_url.locked}
1620
+ title={lockedTip(settingLocks.claude_base_url.locked)}
1621
+ onChange={event => setBaseUrl(event.target.value)}
1622
+ />
1623
+ </div>
1624
+ </div>
1625
+ </div>
1626
+ </div>
1627
+ </div>
1628
+ </div>
1629
+ </div>
1630
+ );
1631
+ }