@jupyterlite/ai 0.8.1 → 0.9.0-a1

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 (162) hide show
  1. package/lib/agent.d.ts +243 -0
  2. package/lib/agent.js +627 -0
  3. package/lib/chat-model.d.ts +195 -0
  4. package/lib/chat-model.js +591 -0
  5. package/lib/completion/completion-provider.d.ts +93 -0
  6. package/lib/completion/completion-provider.js +235 -0
  7. package/lib/completion/index.d.ts +1 -0
  8. package/lib/completion/index.js +1 -0
  9. package/lib/components/clear-button.d.ts +18 -0
  10. package/lib/components/clear-button.js +31 -0
  11. package/lib/components/index.d.ts +3 -0
  12. package/lib/components/index.js +3 -0
  13. package/lib/components/model-select.d.ts +19 -0
  14. package/lib/components/model-select.js +154 -0
  15. package/lib/components/stop-button.d.ts +3 -3
  16. package/lib/components/stop-button.js +8 -9
  17. package/lib/components/token-usage-display.d.ts +45 -0
  18. package/lib/components/token-usage-display.js +74 -0
  19. package/lib/components/tool-select.d.ts +27 -0
  20. package/lib/components/tool-select.js +130 -0
  21. package/lib/icons.d.ts +3 -1
  22. package/lib/icons.js +10 -13
  23. package/lib/index.d.ts +5 -5
  24. package/lib/index.js +341 -169
  25. package/lib/mcp/browser.d.ts +68 -0
  26. package/lib/mcp/browser.js +132 -0
  27. package/lib/models/settings-model.d.ts +70 -0
  28. package/lib/models/settings-model.js +296 -0
  29. package/lib/providers/built-in-providers.d.ts +9 -0
  30. package/lib/providers/built-in-providers.js +266 -0
  31. package/lib/providers/models.d.ts +37 -0
  32. package/lib/providers/models.js +28 -0
  33. package/lib/providers/provider-registry.d.ts +94 -0
  34. package/lib/providers/provider-registry.js +155 -0
  35. package/lib/tokens.d.ts +167 -86
  36. package/lib/tokens.js +25 -12
  37. package/lib/tools/commands.d.ts +11 -0
  38. package/lib/tools/commands.js +126 -0
  39. package/lib/tools/file.d.ts +27 -0
  40. package/lib/tools/file.js +262 -0
  41. package/lib/tools/notebook.d.ts +41 -0
  42. package/lib/tools/notebook.js +779 -0
  43. package/lib/tools/tool-registry.d.ts +35 -0
  44. package/lib/tools/tool-registry.js +55 -0
  45. package/lib/widgets/ai-settings.d.ts +49 -0
  46. package/lib/widgets/ai-settings.js +580 -0
  47. package/lib/widgets/chat-wrapper.d.ts +144 -0
  48. package/lib/widgets/chat-wrapper.js +390 -0
  49. package/lib/widgets/provider-config-dialog.d.ts +14 -0
  50. package/lib/widgets/provider-config-dialog.js +112 -0
  51. package/package.json +151 -40
  52. package/schema/settings-model.json +159 -0
  53. package/src/agent.ts +836 -0
  54. package/src/chat-model.ts +771 -0
  55. package/src/completion/completion-provider.ts +346 -0
  56. package/src/completion/index.ts +1 -0
  57. package/src/components/clear-button.tsx +56 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/components/model-select.tsx +245 -0
  60. package/src/components/stop-button.tsx +11 -11
  61. package/src/components/token-usage-display.tsx +130 -0
  62. package/src/components/tool-select.tsx +218 -0
  63. package/src/icons.ts +12 -14
  64. package/src/index.ts +485 -232
  65. package/src/mcp/browser.ts +213 -0
  66. package/src/models/settings-model.ts +413 -0
  67. package/src/providers/built-in-providers.ts +294 -0
  68. package/src/providers/models.ts +79 -0
  69. package/src/providers/provider-registry.ts +189 -0
  70. package/src/tokens.ts +217 -90
  71. package/src/tools/commands.ts +151 -0
  72. package/src/tools/file.ts +307 -0
  73. package/src/tools/notebook.ts +987 -0
  74. package/src/tools/tool-registry.ts +63 -0
  75. package/src/types.d.ts +4 -0
  76. package/src/widgets/ai-settings.tsx +1233 -0
  77. package/src/widgets/chat-wrapper.tsx +543 -0
  78. package/src/widgets/provider-config-dialog.tsx +272 -0
  79. package/style/base.css +335 -14
  80. package/style/icons/jupyternaut-lite.svg +1 -1
  81. package/lib/base-completer.d.ts +0 -49
  82. package/lib/base-completer.js +0 -14
  83. package/lib/chat-handler.d.ts +0 -56
  84. package/lib/chat-handler.js +0 -201
  85. package/lib/completion-provider.d.ts +0 -34
  86. package/lib/completion-provider.js +0 -32
  87. package/lib/default-prompts.d.ts +0 -2
  88. package/lib/default-prompts.js +0 -31
  89. package/lib/default-providers/Anthropic/completer.d.ts +0 -12
  90. package/lib/default-providers/Anthropic/completer.js +0 -46
  91. package/lib/default-providers/Anthropic/settings-schema.json +0 -70
  92. package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
  93. package/lib/default-providers/ChromeAI/completer.js +0 -56
  94. package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
  95. package/lib/default-providers/ChromeAI/instructions.js +0 -42
  96. package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
  97. package/lib/default-providers/Gemini/completer.d.ts +0 -12
  98. package/lib/default-providers/Gemini/completer.js +0 -48
  99. package/lib/default-providers/Gemini/instructions.d.ts +0 -2
  100. package/lib/default-providers/Gemini/instructions.js +0 -9
  101. package/lib/default-providers/Gemini/settings-schema.json +0 -64
  102. package/lib/default-providers/MistralAI/completer.d.ts +0 -13
  103. package/lib/default-providers/MistralAI/completer.js +0 -52
  104. package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
  105. package/lib/default-providers/MistralAI/instructions.js +0 -18
  106. package/lib/default-providers/MistralAI/settings-schema.json +0 -75
  107. package/lib/default-providers/Ollama/completer.d.ts +0 -12
  108. package/lib/default-providers/Ollama/completer.js +0 -43
  109. package/lib/default-providers/Ollama/instructions.d.ts +0 -2
  110. package/lib/default-providers/Ollama/instructions.js +0 -70
  111. package/lib/default-providers/Ollama/settings-schema.json +0 -143
  112. package/lib/default-providers/OpenAI/completer.d.ts +0 -12
  113. package/lib/default-providers/OpenAI/completer.js +0 -43
  114. package/lib/default-providers/OpenAI/settings-schema.json +0 -628
  115. package/lib/default-providers/WebLLM/completer.d.ts +0 -21
  116. package/lib/default-providers/WebLLM/completer.js +0 -127
  117. package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
  118. package/lib/default-providers/WebLLM/instructions.js +0 -32
  119. package/lib/default-providers/WebLLM/settings-schema.json +0 -19
  120. package/lib/default-providers/index.d.ts +0 -2
  121. package/lib/default-providers/index.js +0 -179
  122. package/lib/provider.d.ts +0 -144
  123. package/lib/provider.js +0 -412
  124. package/lib/settings/base.json +0 -7
  125. package/lib/settings/index.d.ts +0 -3
  126. package/lib/settings/index.js +0 -3
  127. package/lib/settings/panel.d.ts +0 -226
  128. package/lib/settings/panel.js +0 -510
  129. package/lib/settings/textarea.d.ts +0 -2
  130. package/lib/settings/textarea.js +0 -18
  131. package/lib/settings/utils.d.ts +0 -2
  132. package/lib/settings/utils.js +0 -4
  133. package/lib/types/ai-model.d.ts +0 -24
  134. package/lib/types/ai-model.js +0 -5
  135. package/schema/chat.json +0 -28
  136. package/schema/provider-registry.json +0 -29
  137. package/schema/system-prompts.json +0 -22
  138. package/src/base-completer.ts +0 -75
  139. package/src/chat-handler.ts +0 -262
  140. package/src/completion-provider.ts +0 -64
  141. package/src/default-prompts.ts +0 -33
  142. package/src/default-providers/Anthropic/completer.ts +0 -59
  143. package/src/default-providers/ChromeAI/completer.ts +0 -73
  144. package/src/default-providers/ChromeAI/instructions.ts +0 -45
  145. package/src/default-providers/Gemini/completer.ts +0 -61
  146. package/src/default-providers/Gemini/instructions.ts +0 -9
  147. package/src/default-providers/MistralAI/completer.ts +0 -69
  148. package/src/default-providers/MistralAI/instructions.ts +0 -18
  149. package/src/default-providers/Ollama/completer.ts +0 -54
  150. package/src/default-providers/Ollama/instructions.ts +0 -70
  151. package/src/default-providers/OpenAI/completer.ts +0 -54
  152. package/src/default-providers/WebLLM/completer.ts +0 -151
  153. package/src/default-providers/WebLLM/instructions.ts +0 -33
  154. package/src/default-providers/index.ts +0 -211
  155. package/src/global.d.ts +0 -9
  156. package/src/provider.ts +0 -514
  157. package/src/settings/index.ts +0 -3
  158. package/src/settings/panel.tsx +0 -773
  159. package/src/settings/textarea.tsx +0 -33
  160. package/src/settings/utils.ts +0 -5
  161. package/src/types/ai-model.ts +0 -37
  162. package/src/types/service-worker.d.ts +0 -6
@@ -0,0 +1,1233 @@
1
+ import { IThemeManager } from '@jupyterlab/apputils';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import Add from '@mui/icons-material/Add';
4
+ import Cable from '@mui/icons-material/Cable';
5
+ import CheckCircle from '@mui/icons-material/CheckCircle';
6
+ import CheckCircleOutline from '@mui/icons-material/CheckCircleOutline';
7
+ import Delete from '@mui/icons-material/Delete';
8
+ import Edit from '@mui/icons-material/Edit';
9
+ import Error from '@mui/icons-material/Error';
10
+ import ErrorOutline from '@mui/icons-material/ErrorOutline';
11
+ import MoreVert from '@mui/icons-material/MoreVert';
12
+ import Psychology from '@mui/icons-material/Psychology';
13
+ import Settings from '@mui/icons-material/Settings';
14
+ import {
15
+ Alert,
16
+ Box,
17
+ Button,
18
+ Card,
19
+ CardContent,
20
+ Dialog,
21
+ DialogActions,
22
+ DialogContent,
23
+ DialogTitle,
24
+ Divider,
25
+ FormControl,
26
+ FormControlLabel,
27
+ IconButton,
28
+ InputLabel,
29
+ List,
30
+ ListItem,
31
+ ListItemSecondaryAction,
32
+ ListItemText,
33
+ Menu,
34
+ MenuItem,
35
+ Select,
36
+ Slider,
37
+ Switch,
38
+ Tab,
39
+ Tabs,
40
+ TextField,
41
+ ThemeProvider,
42
+ Typography,
43
+ createTheme
44
+ } from '@mui/material';
45
+ import { ISecretsManager } from 'jupyter-secrets-manager';
46
+ import React, { useEffect, useState } from 'react';
47
+ import { AgentManager } from '../agent';
48
+ import {
49
+ AISettingsModel,
50
+ IAIConfig,
51
+ IMCPServerConfig,
52
+ IProviderConfig
53
+ } from '../models/settings-model';
54
+ import {
55
+ SECRETS_NAMESPACE,
56
+ SECRETS_REPLACEMENT,
57
+ type IChatProviderRegistry
58
+ } from '../tokens';
59
+ import { ProviderConfigDialog } from './provider-config-dialog';
60
+
61
+ /**
62
+ * Create a theme that uses IThemeManager to detect theme
63
+ * @param themeManager - Optional theme manager to detect theme
64
+ * @returns A Material-UI theme configured for the current JupyterLab theme
65
+ */
66
+ const createJupyterLabTheme = (themeManager?: IThemeManager) => {
67
+ // Use IThemeManager if available, otherwise default to light theme
68
+ const isDark = themeManager?.theme
69
+ ? !themeManager.isLight(themeManager.theme)
70
+ : false;
71
+
72
+ return createTheme({
73
+ palette: {
74
+ mode: isDark ? 'dark' : 'light'
75
+ }
76
+ });
77
+ };
78
+
79
+ /**
80
+ * A JupyterLab widget for AI settings configuration
81
+ */
82
+ export class AISettingsWidget extends ReactWidget {
83
+ /**
84
+ * Construct a new AI settings widget
85
+ * @param options - The options for initializing the widget
86
+ */
87
+ constructor(options: AISettingsWidget.IOptions) {
88
+ super();
89
+ Private.setToken(options.token);
90
+ this._settingsModel = options.settingsModel;
91
+ this._agentManager = options.agentManager;
92
+ this._themeManager = options.themeManager;
93
+ this._chatProviderRegistry = options.chatProviderRegistry;
94
+ this._secretsManager = options.secretsManager;
95
+ this.id = 'jupyterlite-ai-settings';
96
+ this.title.label = 'AI Settings';
97
+ this.title.caption = 'Configure AI providers and behavior';
98
+ this.title.closable = true;
99
+ }
100
+
101
+ /**
102
+ * Render the AI settings component
103
+ * @returns A React element containing the AI settings interface
104
+ */
105
+ protected render(): React.ReactElement {
106
+ return (
107
+ <AISettingsComponent
108
+ model={this._settingsModel}
109
+ agentManager={this._agentManager}
110
+ themeManager={this._themeManager}
111
+ chatProviderRegistry={this._chatProviderRegistry}
112
+ secretsManager={this._secretsManager}
113
+ />
114
+ );
115
+ }
116
+
117
+ private _settingsModel: AISettingsModel;
118
+ private _agentManager?: AgentManager;
119
+ private _themeManager?: IThemeManager;
120
+ private _chatProviderRegistry: IChatProviderRegistry;
121
+ private _secretsManager?: ISecretsManager;
122
+ }
123
+
124
+ /**
125
+ * Props interface for the AISettingsComponent
126
+ */
127
+ interface IAISettingsComponentProps {
128
+ model: AISettingsModel;
129
+ agentManager?: AgentManager;
130
+ themeManager?: IThemeManager;
131
+ chatProviderRegistry: IChatProviderRegistry;
132
+ secretsManager?: ISecretsManager;
133
+ }
134
+
135
+ /**
136
+ * The main AI settings component that provides configuration UI
137
+ * @param props - Component props containing models and theme manager
138
+ * @returns A React component for AI settings configuration
139
+ */
140
+ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
141
+ model,
142
+ agentManager,
143
+ themeManager,
144
+ chatProviderRegistry,
145
+ secretsManager
146
+ }) => {
147
+ if (!model) {
148
+ return <div>Settings model not available</div>;
149
+ }
150
+
151
+ const [config, setConfig] = useState(model.config || {});
152
+ const [theme, setTheme] = useState(() => createJupyterLabTheme(themeManager));
153
+ const [activeTab, setActiveTab] = useState(0);
154
+ const [dialogOpen, setDialogOpen] = useState(false);
155
+ const [editingProvider, setEditingProvider] = useState<
156
+ IProviderConfig | undefined
157
+ >();
158
+ const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
159
+ const [menuProviderId, setMenuProviderId] = useState<string>('');
160
+ const [mcpDialogOpen, setMcpDialogOpen] = useState(false);
161
+ const [editingMCPServer, setEditingMCPServer] = useState<
162
+ IMCPServerConfig | undefined
163
+ >();
164
+ const [mcpMenuAnchor, setMcpMenuAnchor] = useState<null | HTMLElement>(null);
165
+ const [mcpMenuServerId, setMcpMenuServerId] = useState<string>('');
166
+
167
+ /**
168
+ * Effect to listen for model state changes and update config
169
+ */
170
+ useEffect(() => {
171
+ if (!model || !model.stateChanged) {
172
+ return;
173
+ }
174
+
175
+ const onStateChanged = () => {
176
+ setConfig(model.config || {});
177
+ };
178
+
179
+ model.stateChanged.connect(onStateChanged);
180
+ return () => {
181
+ model.stateChanged.disconnect(onStateChanged);
182
+ };
183
+ }, [model]);
184
+
185
+ /**
186
+ * Effect to listen for theme changes and update the Material-UI theme
187
+ */
188
+ useEffect(() => {
189
+ if (!themeManager) {
190
+ return;
191
+ }
192
+
193
+ const updateTheme = () => {
194
+ setTheme(createJupyterLabTheme(themeManager));
195
+ };
196
+
197
+ themeManager.themeChanged.connect(updateTheme);
198
+ return () => {
199
+ themeManager.themeChanged.disconnect(updateTheme);
200
+ };
201
+ }, [themeManager]);
202
+
203
+ /**
204
+ * Effect to listen for MCP connection changes to re-render connection status
205
+ */
206
+ useEffect(() => {
207
+ if (!agentManager) {
208
+ return;
209
+ }
210
+
211
+ const onMCPConnectionChanged = () => {
212
+ // Force a re-render by updating the config state
213
+ setConfig(prevConfig => ({ ...prevConfig }));
214
+ };
215
+
216
+ agentManager.mcpConnectionChanged.connect(onMCPConnectionChanged);
217
+ return () => {
218
+ agentManager.mcpConnectionChanged.disconnect(onMCPConnectionChanged);
219
+ };
220
+ }, [agentManager]);
221
+
222
+ const getSecretFromManager = async (
223
+ provider: string,
224
+ fieldName: string
225
+ ): Promise<string | undefined> => {
226
+ const secret = await secretsManager?.get(
227
+ Private.getToken(),
228
+ SECRETS_NAMESPACE,
229
+ `${provider}:${fieldName}`
230
+ );
231
+ return secret?.value;
232
+ };
233
+
234
+ /**
235
+ * Attach a secrets field to the secrets manager.
236
+ * @param input - the DOm element to attach.
237
+ * @param provider - the name of the provider.
238
+ * @param fieldName - the name of the field.
239
+ */
240
+ const handleSecretField = async (
241
+ input: HTMLInputElement,
242
+ provider: string,
243
+ fieldName: string
244
+ ): Promise<void> => {
245
+ if (!(model.config.useSecretsManager && secretsManager)) {
246
+ return;
247
+ }
248
+ await secretsManager?.attach(
249
+ Private.getToken(),
250
+ SECRETS_NAMESPACE,
251
+ `${provider}:${fieldName}`,
252
+ input
253
+ );
254
+ };
255
+
256
+ /**
257
+ * Handle adding a new AI provider
258
+ * @param providerConfig - The provider configuration to add
259
+ */
260
+ const handleAddProvider = async (
261
+ providerConfig: Omit<IProviderConfig, 'id'>
262
+ ) => {
263
+ if (
264
+ model.config.useSecretsManager &&
265
+ secretsManager &&
266
+ providerConfig.apiKey
267
+ ) {
268
+ providerConfig.apiKey = SECRETS_REPLACEMENT;
269
+ }
270
+ await model.addProvider(providerConfig);
271
+ };
272
+
273
+ /**
274
+ * Handle editing an existing AI provider
275
+ * @param providerConfig - The updated provider configuration
276
+ */
277
+ const handleEditProvider = async (
278
+ providerConfig: Omit<IProviderConfig, 'id'>
279
+ ) => {
280
+ if (editingProvider) {
281
+ if (
282
+ model.config.useSecretsManager &&
283
+ secretsManager &&
284
+ providerConfig.apiKey
285
+ ) {
286
+ providerConfig.apiKey = SECRETS_REPLACEMENT;
287
+ }
288
+ await model.updateProvider(editingProvider.id, providerConfig);
289
+ setEditingProvider(undefined);
290
+ }
291
+ };
292
+
293
+ /**
294
+ * Handle deleting an AI provider
295
+ * @param id - The ID of the provider to delete
296
+ */
297
+ const handleDeleteProvider = async (id: string) => {
298
+ await model.removeProvider(id);
299
+ setMenuAnchor(null);
300
+ };
301
+
302
+ /**
303
+ * Open the provider edit dialog
304
+ * @param provider - The provider to edit
305
+ */
306
+ const openEditDialog = async (provider: IProviderConfig) => {
307
+ // Retrieve the API key from the secrets manager if necessary.
308
+ if (model.config.useSecretsManager && secretsManager) {
309
+ provider.apiKey =
310
+ (await getSecretFromManager(provider.provider, 'apiKey')) ??
311
+ provider.apiKey ??
312
+ '';
313
+ }
314
+ setEditingProvider(provider);
315
+ setDialogOpen(true);
316
+ setMenuAnchor(null);
317
+ };
318
+
319
+ /**
320
+ * Open the provider add dialog
321
+ */
322
+ const openAddDialog = () => {
323
+ setEditingProvider(undefined);
324
+ setDialogOpen(true);
325
+ };
326
+
327
+ /**
328
+ * Handle provider menu click
329
+ * @param event - The click event
330
+ * @param providerId - The ID of the provider
331
+ */
332
+ const handleMenuClick = (
333
+ event: React.MouseEvent<HTMLElement>,
334
+ providerId: string
335
+ ) => {
336
+ setMenuAnchor(event.currentTarget);
337
+ setMenuProviderId(providerId);
338
+ };
339
+
340
+ /**
341
+ * Handle provider menu close
342
+ */
343
+ const handleMenuClose = () => {
344
+ setMenuAnchor(null);
345
+ setMenuProviderId('');
346
+ };
347
+
348
+ /**
349
+ * Handle updating AI configuration
350
+ * @param updates - Partial configuration updates to apply
351
+ */
352
+ const handleConfigUpdate = async (updates: Partial<IAIConfig>) => {
353
+ if (updates.useSecretsManager !== undefined) {
354
+ if (updates.useSecretsManager) {
355
+ for (const provider of model.config.providers) {
356
+ provider.apiKey = SECRETS_REPLACEMENT;
357
+ await model.updateProvider(provider.id, provider);
358
+ }
359
+ } else {
360
+ for (const provider of model.config.providers) {
361
+ const apiKey = await getSecretFromManager(
362
+ provider.provider,
363
+ 'apiKey'
364
+ );
365
+ if (apiKey) {
366
+ provider.apiKey = apiKey;
367
+ await model.updateProvider(provider.id, provider);
368
+ }
369
+ }
370
+ }
371
+ }
372
+ await model.updateConfig(updates);
373
+ };
374
+
375
+ /**
376
+ * Handle adding a new MCP server
377
+ * @param serverConfig - The MCP server configuration to add
378
+ */
379
+ const handleAddMCPServer = async (
380
+ serverConfig: Omit<IMCPServerConfig, 'id'>
381
+ ) => {
382
+ await model.addMCPServer(serverConfig);
383
+ };
384
+
385
+ /**
386
+ * Handle editing an existing MCP server
387
+ * @param serverConfig - The updated MCP server configuration
388
+ */
389
+ const handleEditMCPServer = async (
390
+ serverConfig: Omit<IMCPServerConfig, 'id'>
391
+ ) => {
392
+ if (editingMCPServer) {
393
+ await model.updateMCPServer(editingMCPServer.id, serverConfig);
394
+ setEditingMCPServer(undefined);
395
+ }
396
+ };
397
+
398
+ /**
399
+ * Handle deleting an MCP server
400
+ * @param id - The ID of the MCP server to delete
401
+ */
402
+ const handleDeleteMCPServer = async (id: string) => {
403
+ await model.removeMCPServer(id);
404
+ setMcpMenuAnchor(null);
405
+ };
406
+
407
+ /**
408
+ * Open the MCP server edit dialog
409
+ * @param server - The MCP server to edit
410
+ */
411
+ const openEditMCPDialog = (server: IMCPServerConfig) => {
412
+ setEditingMCPServer(server);
413
+ setMcpDialogOpen(true);
414
+ setMcpMenuAnchor(null);
415
+ };
416
+
417
+ /**
418
+ * Open the MCP server add dialog
419
+ */
420
+ const openAddMCPDialog = () => {
421
+ setEditingMCPServer(undefined);
422
+ setMcpDialogOpen(true);
423
+ };
424
+
425
+ /**
426
+ * Handle MCP server menu click
427
+ * @param event - The click event
428
+ * @param serverId - The ID of the MCP server
429
+ */
430
+ const handleMCPMenuClick = (
431
+ event: React.MouseEvent<HTMLElement>,
432
+ serverId: string
433
+ ) => {
434
+ setMcpMenuAnchor(event.currentTarget);
435
+ setMcpMenuServerId(serverId);
436
+ };
437
+
438
+ /**
439
+ * Handle MCP server menu close
440
+ */
441
+ const handleMCPMenuClose = () => {
442
+ setMcpMenuAnchor(null);
443
+ setMcpMenuServerId('');
444
+ };
445
+
446
+ const isValidConfig = agentManager?.hasValidConfig() ?? false;
447
+
448
+ return (
449
+ <ThemeProvider theme={theme}>
450
+ <Box
451
+ sx={{
452
+ height: '100%',
453
+ maxHeight: '100vh',
454
+ overflow: 'auto',
455
+ p: 2,
456
+ pb: 4,
457
+ fontSize: '0.9rem'
458
+ }}
459
+ >
460
+ {/* Header */}
461
+ <Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
462
+ <Settings color="primary" sx={{ fontSize: 24 }} />
463
+ <Typography variant="h5" component="h1" sx={{ fontWeight: 600 }}>
464
+ AI Settings
465
+ </Typography>
466
+ </Box>
467
+
468
+ {/* Status Alert */}
469
+ <Alert
470
+ severity={isValidConfig ? 'success' : 'warning'}
471
+ icon={isValidConfig ? <CheckCircle /> : <Error />}
472
+ sx={{ mb: 2 }}
473
+ >
474
+ {isValidConfig
475
+ ? 'Configuration is valid and ready to use'
476
+ : 'Please add and configure a provider to get started'}
477
+ </Alert>
478
+
479
+ {/* Tabs */}
480
+ <Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
481
+ <Tabs
482
+ value={activeTab}
483
+ onChange={(_, newValue) => setActiveTab(newValue)}
484
+ >
485
+ <Tab label="Providers" />
486
+ <Tab label="Model Parameters" />
487
+ <Tab label="Behavior" />
488
+ <Tab label="MCP Servers" />
489
+ </Tabs>
490
+ </Box>
491
+
492
+ {/* Tab Panels */}
493
+ {activeTab === 0 && (
494
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
495
+ {secretsManager !== undefined && (
496
+ <FormControlLabel
497
+ control={
498
+ <Switch
499
+ checked={config.useSecretsManager}
500
+ onChange={e =>
501
+ handleConfigUpdate({
502
+ useSecretsManager: e.target.checked
503
+ })
504
+ }
505
+ color="primary"
506
+ sx={{ alignSelf: 'flex-start' }}
507
+ />
508
+ }
509
+ label={
510
+ <div>
511
+ <span>Use the secrets manager to manage API keys</span>
512
+ {!config.useSecretsManager && (
513
+ <Alert severity="warning" icon={<Error />} sx={{ mb: 2 }}>
514
+ The secrets will be stored in plain text in settings
515
+ </Alert>
516
+ )}
517
+ </div>
518
+ }
519
+ />
520
+ )}
521
+ {/* Active Provider Selection */}
522
+ {config.providers.length > 0 && (
523
+ <Card elevation={2}>
524
+ <CardContent>
525
+ <Typography variant="h6" component="h2" gutterBottom>
526
+ Active Providers
527
+ </Typography>
528
+
529
+ <Box
530
+ sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
531
+ >
532
+ <FormControl fullWidth>
533
+ <InputLabel>Chat Provider</InputLabel>
534
+ <Select
535
+ value={config.activeProvider}
536
+ label="Chat Provider"
537
+ onChange={e => model.setActiveProvider(e.target.value)}
538
+ >
539
+ {config.providers.map(provider => (
540
+ <MenuItem key={provider.id} value={provider.id}>
541
+ {provider.name}
542
+ </MenuItem>
543
+ ))}
544
+ </Select>
545
+ </FormControl>
546
+
547
+ <FormControlLabel
548
+ control={
549
+ <Switch
550
+ checked={config.useSameProviderForChatAndCompleter}
551
+ onChange={e =>
552
+ handleConfigUpdate({
553
+ useSameProviderForChatAndCompleter:
554
+ e.target.checked
555
+ })
556
+ }
557
+ color="primary"
558
+ />
559
+ }
560
+ label="Use same provider for chat and completions"
561
+ />
562
+
563
+ {!config.useSameProviderForChatAndCompleter && (
564
+ <FormControl fullWidth>
565
+ <InputLabel>Completion Provider</InputLabel>
566
+ <Select
567
+ value={config.activeCompleterProvider || ''}
568
+ label="Completion Provider"
569
+ onChange={e =>
570
+ model.setActiveCompleterProvider(
571
+ e.target.value || undefined
572
+ )
573
+ }
574
+ >
575
+ <MenuItem value="">
576
+ <em>Use chat provider</em>
577
+ </MenuItem>
578
+ {config.providers.map(provider => (
579
+ <MenuItem key={provider.id} value={provider.id}>
580
+ {provider.name}
581
+ </MenuItem>
582
+ ))}
583
+ </Select>
584
+ </FormControl>
585
+ )}
586
+ </Box>
587
+ </CardContent>
588
+ </Card>
589
+ )}
590
+
591
+ {/* Providers Card */}
592
+ <Card elevation={2}>
593
+ <CardContent>
594
+ <Box
595
+ sx={{
596
+ display: 'flex',
597
+ alignItems: 'center',
598
+ justifyContent: 'space-between',
599
+ mb: 2
600
+ }}
601
+ >
602
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
603
+ <Typography variant="h6" component="h2">
604
+ Configured Providers
605
+ </Typography>
606
+ </Box>
607
+ <Button
608
+ variant="contained"
609
+ startIcon={<Add />}
610
+ onClick={openAddDialog}
611
+ size="small"
612
+ >
613
+ Add Provider
614
+ </Button>
615
+ </Box>
616
+
617
+ {config.providers.length === 0 ? (
618
+ <Alert severity="info">
619
+ No providers configured yet. Click "Add Provider" to get
620
+ started.
621
+ </Alert>
622
+ ) : (
623
+ <List>
624
+ {config.providers.map(provider => (
625
+ <ListItem key={provider.id} divider>
626
+ <ListItemText
627
+ primary={provider.name}
628
+ secondary={
629
+ <Typography variant="body2" color="text.secondary">
630
+ {provider.provider} • {provider.model}
631
+ </Typography>
632
+ }
633
+ />
634
+ <ListItemSecondaryAction>
635
+ <IconButton
636
+ onClick={e => handleMenuClick(e, provider.id)}
637
+ size="small"
638
+ >
639
+ <MoreVert />
640
+ </IconButton>
641
+ </ListItemSecondaryAction>
642
+ </ListItem>
643
+ ))}
644
+ </List>
645
+ )}
646
+ </CardContent>
647
+ </Card>
648
+ </Box>
649
+ )}
650
+
651
+ {activeTab === 1 && (
652
+ <Card elevation={2}>
653
+ <CardContent>
654
+ <Box
655
+ sx={{ display: 'flex', alignItems: 'center', mb: 1.5, gap: 1 }}
656
+ >
657
+ <Psychology color="primary" />
658
+ <Typography variant="h6" component="h2">
659
+ Model Parameters
660
+ </Typography>
661
+ </Box>
662
+
663
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
664
+ <Box>
665
+ <Typography variant="body1" gutterBottom>
666
+ Temperature: {config.temperature}
667
+ </Typography>
668
+ <Slider
669
+ value={config.temperature}
670
+ onChange={(_, value) =>
671
+ handleConfigUpdate({ temperature: value as number })
672
+ }
673
+ min={0}
674
+ max={2}
675
+ step={0.1}
676
+ valueLabelDisplay="auto"
677
+ />
678
+ <Typography variant="caption" color="text.secondary">
679
+ Controls response creativity: low value = deterministic and
680
+ focused, middle value = balanced, high value = creative and
681
+ varied
682
+ </Typography>
683
+ </Box>
684
+
685
+ <TextField
686
+ fullWidth
687
+ label="Max Tokens"
688
+ type="number"
689
+ value={config.maxTokens || ''}
690
+ onChange={e =>
691
+ handleConfigUpdate({
692
+ maxTokens: e.target.value
693
+ ? parseInt(e.target.value)
694
+ : undefined
695
+ })
696
+ }
697
+ inputProps={{ min: 1, max: 8192 }}
698
+ helperText="Maximum length of AI responses (leave empty for provider default)"
699
+ />
700
+
701
+ <TextField
702
+ fullWidth
703
+ label="Max Turns"
704
+ type="number"
705
+ value={config.maxTurns}
706
+ onChange={e =>
707
+ handleConfigUpdate({
708
+ maxTurns: parseInt(e.target.value)
709
+ })
710
+ }
711
+ inputProps={{ min: 1, max: 100 }}
712
+ helperText="Maximum number of tool execution turns (when using tools)"
713
+ />
714
+ </Box>
715
+ </CardContent>
716
+ </Card>
717
+ )}
718
+
719
+ {activeTab === 2 && (
720
+ <Card elevation={2}>
721
+ <CardContent>
722
+ <Typography variant="h6" component="h2" gutterBottom>
723
+ Behavior Settings
724
+ </Typography>
725
+
726
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
727
+ <FormControlLabel
728
+ control={
729
+ <Switch
730
+ checked={config.toolsEnabled}
731
+ onChange={e =>
732
+ handleConfigUpdate({
733
+ toolsEnabled: e.target.checked
734
+ })
735
+ }
736
+ color="primary"
737
+ />
738
+ }
739
+ label={
740
+ <Box>
741
+ <Typography variant="body1">Enable Tools</Typography>
742
+ <Typography variant="caption" color="text.secondary">
743
+ Allow the AI to use tools like notebook operations, code
744
+ execution, and file management
745
+ </Typography>
746
+ </Box>
747
+ }
748
+ />
749
+
750
+ <FormControlLabel
751
+ control={
752
+ <Switch
753
+ checked={config.sendWithShiftEnter}
754
+ onChange={e =>
755
+ handleConfigUpdate({
756
+ sendWithShiftEnter: e.target.checked
757
+ })
758
+ }
759
+ color="primary"
760
+ />
761
+ }
762
+ label={
763
+ <Box>
764
+ <Typography variant="body1">
765
+ Send with Shift+Enter
766
+ </Typography>
767
+ <Typography variant="caption" color="text.secondary">
768
+ Use Shift+Enter to send messages (Enter creates new
769
+ line)
770
+ </Typography>
771
+ </Box>
772
+ }
773
+ />
774
+
775
+ <FormControlLabel
776
+ control={
777
+ <Switch
778
+ checked={config.showTokenUsage}
779
+ onChange={e =>
780
+ handleConfigUpdate({
781
+ showTokenUsage: e.target.checked
782
+ })
783
+ }
784
+ color="primary"
785
+ />
786
+ }
787
+ label={
788
+ <Box>
789
+ <Typography variant="body1">Show Token Usage</Typography>
790
+ <Typography variant="caption" color="text.secondary">
791
+ Display token usage information in the chat toolbar
792
+ </Typography>
793
+ </Box>
794
+ }
795
+ />
796
+
797
+ <Divider sx={{ my: 1 }} />
798
+
799
+ <TextField
800
+ fullWidth
801
+ multiline
802
+ rows={3}
803
+ label="System Prompt"
804
+ value={config.systemPrompt}
805
+ onChange={e =>
806
+ handleConfigUpdate({ systemPrompt: e.target.value })
807
+ }
808
+ placeholder="Define the AI's behavior and personality..."
809
+ helperText="Instructions that define how the AI should behave and respond"
810
+ />
811
+
812
+ <Divider sx={{ my: 2 }} />
813
+
814
+ <Box>
815
+ <Typography variant="body1" gutterBottom>
816
+ Commands Requiring Approval
817
+ </Typography>
818
+ <Typography
819
+ variant="caption"
820
+ color="text.secondary"
821
+ gutterBottom
822
+ sx={{ display: 'block' }}
823
+ >
824
+ Commands that require user approval before AI can execute
825
+ them
826
+ </Typography>
827
+
828
+ <List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
829
+ {config.commandsRequiringApproval.map((command, index) => (
830
+ <ListItem key={index} divider>
831
+ <ListItemText primary={command} />
832
+ <ListItemSecondaryAction>
833
+ <IconButton
834
+ onClick={() => {
835
+ const newCommands = [
836
+ ...config.commandsRequiringApproval
837
+ ];
838
+ newCommands.splice(index, 1);
839
+ handleConfigUpdate({
840
+ commandsRequiringApproval: newCommands
841
+ });
842
+ }}
843
+ size="small"
844
+ >
845
+ <Delete />
846
+ </IconButton>
847
+ </ListItemSecondaryAction>
848
+ </ListItem>
849
+ ))}
850
+ </List>
851
+
852
+ <TextField
853
+ fullWidth
854
+ label="Add New Command"
855
+ placeholder="e.g., notebook:run-cell"
856
+ onKeyDown={e => {
857
+ if (e.key === 'Enter') {
858
+ const value = (
859
+ e.target as HTMLInputElement
860
+ ).value.trim();
861
+ if (
862
+ value &&
863
+ !config.commandsRequiringApproval.includes(value)
864
+ ) {
865
+ const newCommands = [
866
+ ...config.commandsRequiringApproval,
867
+ value
868
+ ];
869
+ handleConfigUpdate({
870
+ commandsRequiringApproval: newCommands
871
+ });
872
+ (e.target as HTMLInputElement).value = '';
873
+ }
874
+ }
875
+ }}
876
+ helperText="Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code"
877
+ />
878
+ </Box>
879
+ </Box>
880
+ </CardContent>
881
+ </Card>
882
+ )}
883
+
884
+ {activeTab === 3 && (
885
+ <Card elevation={2}>
886
+ <CardContent>
887
+ <Box
888
+ sx={{
889
+ display: 'flex',
890
+ alignItems: 'center',
891
+ justifyContent: 'space-between',
892
+ mb: 2
893
+ }}
894
+ >
895
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
896
+ <Cable color="primary" />
897
+ <Typography variant="h6" component="h2">
898
+ Remote MCP Servers
899
+ </Typography>
900
+ </Box>
901
+ <Button
902
+ variant="contained"
903
+ startIcon={<Add />}
904
+ onClick={openAddMCPDialog}
905
+ size="small"
906
+ >
907
+ Add Server
908
+ </Button>
909
+ </Box>
910
+
911
+ <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
912
+ Configure remote Model Context Protocol (MCP) servers to extend
913
+ the AI's capabilities with external tools and data sources.
914
+ </Typography>
915
+
916
+ {config.mcpServers.length === 0 ? (
917
+ <Alert severity="info">
918
+ No MCP servers configured yet. Click "Add Server" to connect
919
+ to remote MCP services.
920
+ </Alert>
921
+ ) : (
922
+ <List>
923
+ {config.mcpServers.map(server => (
924
+ <ListItem key={server.id} divider>
925
+ <ListItemText
926
+ primary={
927
+ <Box
928
+ sx={{
929
+ display: 'flex',
930
+ alignItems: 'center',
931
+ gap: 1
932
+ }}
933
+ >
934
+ <Typography variant="body1">
935
+ {server.name}
936
+ </Typography>
937
+ {server.enabled &&
938
+ agentManager?.isMCPServerConnected(
939
+ server.name
940
+ ) && (
941
+ <CheckCircleOutline
942
+ sx={{ color: 'success.main', fontSize: 16 }}
943
+ />
944
+ )}
945
+ {server.enabled &&
946
+ !agentManager?.isMCPServerConnected(
947
+ server.name
948
+ ) && (
949
+ <ErrorOutline
950
+ sx={{ color: 'error.main', fontSize: 16 }}
951
+ />
952
+ )}
953
+ <Switch
954
+ checked={server.enabled}
955
+ onChange={e =>
956
+ model.updateMCPServer(server.id, {
957
+ enabled: e.target.checked
958
+ })
959
+ }
960
+ size="small"
961
+ color="primary"
962
+ />
963
+ </Box>
964
+ }
965
+ secondary={
966
+ <Box>
967
+ <Typography variant="body2" color="text.secondary">
968
+ {server.url}
969
+ </Typography>
970
+ {server.enabled &&
971
+ server.connected !== undefined && (
972
+ <Typography
973
+ variant="caption"
974
+ color="text.secondary"
975
+ >
976
+ Status:{' '}
977
+ {server.connected
978
+ ? 'Connected'
979
+ : 'Connection failed'}
980
+ </Typography>
981
+ )}
982
+ </Box>
983
+ }
984
+ />
985
+ <ListItemSecondaryAction>
986
+ <IconButton
987
+ onClick={e => handleMCPMenuClick(e, server.id)}
988
+ size="small"
989
+ >
990
+ <MoreVert />
991
+ </IconButton>
992
+ </ListItemSecondaryAction>
993
+ </ListItem>
994
+ ))}
995
+ </List>
996
+ )}
997
+ </CardContent>
998
+ </Card>
999
+ )}
1000
+
1001
+ {/* Provider Configuration Dialog */}
1002
+ <ProviderConfigDialog
1003
+ open={dialogOpen}
1004
+ onClose={() => setDialogOpen(false)}
1005
+ onSave={editingProvider ? handleEditProvider : handleAddProvider}
1006
+ initialConfig={editingProvider}
1007
+ mode={editingProvider ? 'edit' : 'add'}
1008
+ chatProviderRegistry={chatProviderRegistry}
1009
+ handleSecretField={handleSecretField}
1010
+ />
1011
+
1012
+ {/* Provider Menu */}
1013
+ <Menu
1014
+ anchorEl={menuAnchor}
1015
+ open={Boolean(menuAnchor)}
1016
+ onClose={handleMenuClose}
1017
+ >
1018
+ <MenuItem
1019
+ onClick={() => {
1020
+ const provider = config.providers.find(
1021
+ p => p.id === menuProviderId
1022
+ );
1023
+ if (provider) {
1024
+ openEditDialog(provider);
1025
+ }
1026
+ }}
1027
+ >
1028
+ <Edit sx={{ mr: 1 }} />
1029
+ Edit
1030
+ </MenuItem>
1031
+ <MenuItem
1032
+ onClick={() => handleDeleteProvider(menuProviderId)}
1033
+ sx={{ color: 'error.main' }}
1034
+ >
1035
+ <Delete sx={{ mr: 1 }} />
1036
+ Delete
1037
+ </MenuItem>
1038
+ </Menu>
1039
+
1040
+ {/* MCP Server Configuration Dialog */}
1041
+ <MCPServerDialog
1042
+ open={mcpDialogOpen}
1043
+ onClose={() => setMcpDialogOpen(false)}
1044
+ onSave={editingMCPServer ? handleEditMCPServer : handleAddMCPServer}
1045
+ initialConfig={editingMCPServer}
1046
+ mode={editingMCPServer ? 'edit' : 'add'}
1047
+ />
1048
+
1049
+ {/* MCP Server Menu */}
1050
+ <Menu
1051
+ anchorEl={mcpMenuAnchor}
1052
+ open={Boolean(mcpMenuAnchor)}
1053
+ onClose={handleMCPMenuClose}
1054
+ >
1055
+ <MenuItem
1056
+ onClick={() => {
1057
+ const server = config.mcpServers.find(
1058
+ s => s.id === mcpMenuServerId
1059
+ );
1060
+ if (server) {
1061
+ openEditMCPDialog(server);
1062
+ }
1063
+ }}
1064
+ >
1065
+ <Edit sx={{ mr: 1 }} />
1066
+ Edit
1067
+ </MenuItem>
1068
+ <MenuItem
1069
+ onClick={() => handleDeleteMCPServer(mcpMenuServerId)}
1070
+ sx={{ color: 'error.main' }}
1071
+ >
1072
+ <Delete sx={{ mr: 1 }} />
1073
+ Delete
1074
+ </MenuItem>
1075
+ </Menu>
1076
+ </Box>
1077
+ </ThemeProvider>
1078
+ );
1079
+ };
1080
+
1081
+ /**
1082
+ * Props interface for the MCPServerDialog component
1083
+ */
1084
+ interface IMCPServerDialogProps {
1085
+ open: boolean;
1086
+ onClose: () => void;
1087
+ onSave: (config: Omit<IMCPServerConfig, 'id'>) => void;
1088
+ initialConfig?: IMCPServerConfig;
1089
+ mode: 'add' | 'edit';
1090
+ }
1091
+
1092
+ /**
1093
+ * Dialog component for adding/editing MCP server configurations
1094
+ * @param props - Component props for the MCP server dialog
1095
+ * @returns A React component for MCP server configuration
1096
+ */
1097
+ const MCPServerDialog: React.FC<IMCPServerDialogProps> = ({
1098
+ open,
1099
+ onClose,
1100
+ onSave,
1101
+ initialConfig,
1102
+ mode
1103
+ }) => {
1104
+ const [name, setName] = useState(initialConfig?.name || '');
1105
+ const [url, setUrl] = useState(initialConfig?.url || '');
1106
+ const [enabled, setEnabled] = useState(initialConfig?.enabled ?? true);
1107
+
1108
+ /**
1109
+ * Effect to reset dialog state when opened with new config
1110
+ */
1111
+ useEffect(() => {
1112
+ if (open) {
1113
+ setName(initialConfig?.name || '');
1114
+ setUrl(initialConfig?.url || '');
1115
+ setEnabled(initialConfig?.enabled ?? true);
1116
+ }
1117
+ }, [open, initialConfig]);
1118
+
1119
+ /**
1120
+ * Handle saving the MCP server configuration
1121
+ */
1122
+ const handleSave = () => {
1123
+ if (!name.trim() || !url.trim()) {
1124
+ return;
1125
+ }
1126
+
1127
+ onSave({
1128
+ name: name.trim(),
1129
+ url: url.trim(),
1130
+ enabled,
1131
+ connected: false
1132
+ });
1133
+ onClose();
1134
+ };
1135
+
1136
+ /**
1137
+ * Check if a URL is valid
1138
+ * @param url - The URL to validate
1139
+ * @returns true if the URL is valid
1140
+ */
1141
+ const _isValidUrl = (url: string): boolean => {
1142
+ try {
1143
+ new URL(url);
1144
+ return true;
1145
+ } catch {
1146
+ return false;
1147
+ }
1148
+ };
1149
+
1150
+ const canSave = name.trim() && url.trim() && _isValidUrl(url.trim());
1151
+
1152
+ return (
1153
+ <Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
1154
+ <DialogTitle>
1155
+ {mode === 'add' ? 'Add MCP Server' : 'Edit MCP Server'}
1156
+ </DialogTitle>
1157
+ <DialogContent>
1158
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
1159
+ <TextField
1160
+ autoFocus
1161
+ fullWidth
1162
+ label="Server Name"
1163
+ value={name}
1164
+ onChange={e => setName(e.target.value)}
1165
+ placeholder="My MCP Server"
1166
+ helperText="A friendly name to identify this MCP server"
1167
+ />
1168
+ <TextField
1169
+ fullWidth
1170
+ label="Server URL"
1171
+ value={url}
1172
+ onChange={e => setUrl(e.target.value)}
1173
+ placeholder="https://example.com/mcp"
1174
+ helperText="The HTTP/HTTPS URL of the MCP server"
1175
+ error={Boolean(url.trim() && !_isValidUrl(url.trim()))}
1176
+ />
1177
+ <FormControlLabel
1178
+ control={
1179
+ <Switch
1180
+ checked={enabled}
1181
+ onChange={e => setEnabled(e.target.checked)}
1182
+ color="primary"
1183
+ />
1184
+ }
1185
+ label="Enable this server"
1186
+ />
1187
+ </Box>
1188
+ </DialogContent>
1189
+ <DialogActions>
1190
+ <Button onClick={onClose}>Cancel</Button>
1191
+ <Button onClick={handleSave} variant="contained" disabled={!canSave}>
1192
+ {mode === 'add' ? 'Add' : 'Save'}
1193
+ </Button>
1194
+ </DialogActions>
1195
+ </Dialog>
1196
+ );
1197
+ };
1198
+
1199
+ /**
1200
+ * Namespace for AISettingsWidget types and interfaces
1201
+ */
1202
+ export namespace AISettingsWidget {
1203
+ /**
1204
+ * Options interface for constructing an AISettingsWidget
1205
+ */
1206
+ export interface IOptions {
1207
+ settingsModel: AISettingsModel;
1208
+ agentManager?: AgentManager;
1209
+ themeManager?: IThemeManager;
1210
+ chatProviderRegistry: IChatProviderRegistry;
1211
+ /**
1212
+ * The secrets manager.
1213
+ */
1214
+ secretsManager?: ISecretsManager;
1215
+ /**
1216
+ * The token used to request the secrets manager.
1217
+ */
1218
+ token: symbol;
1219
+ }
1220
+ }
1221
+
1222
+ namespace Private {
1223
+ /**
1224
+ * The token to use with the secrets manager, setter and getter.
1225
+ */
1226
+ let secretsToken: symbol;
1227
+ export function setToken(value: symbol): void {
1228
+ secretsToken = value;
1229
+ }
1230
+ export function getToken(): symbol {
1231
+ return secretsToken;
1232
+ }
1233
+ }