@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.
- package/lib/agent.d.ts +243 -0
- package/lib/agent.js +627 -0
- package/lib/chat-model.d.ts +195 -0
- package/lib/chat-model.js +591 -0
- package/lib/completion/completion-provider.d.ts +93 -0
- package/lib/completion/completion-provider.js +235 -0
- package/lib/completion/index.d.ts +1 -0
- package/lib/completion/index.js +1 -0
- package/lib/components/clear-button.d.ts +18 -0
- package/lib/components/clear-button.js +31 -0
- package/lib/components/index.d.ts +3 -0
- package/lib/components/index.js +3 -0
- package/lib/components/model-select.d.ts +19 -0
- package/lib/components/model-select.js +154 -0
- package/lib/components/stop-button.d.ts +3 -3
- package/lib/components/stop-button.js +8 -9
- package/lib/components/token-usage-display.d.ts +45 -0
- package/lib/components/token-usage-display.js +74 -0
- package/lib/components/tool-select.d.ts +27 -0
- package/lib/components/tool-select.js +130 -0
- package/lib/icons.d.ts +3 -1
- package/lib/icons.js +10 -13
- package/lib/index.d.ts +5 -5
- package/lib/index.js +341 -169
- package/lib/mcp/browser.d.ts +68 -0
- package/lib/mcp/browser.js +132 -0
- package/lib/models/settings-model.d.ts +70 -0
- package/lib/models/settings-model.js +296 -0
- package/lib/providers/built-in-providers.d.ts +9 -0
- package/lib/providers/built-in-providers.js +266 -0
- package/lib/providers/models.d.ts +37 -0
- package/lib/providers/models.js +28 -0
- package/lib/providers/provider-registry.d.ts +94 -0
- package/lib/providers/provider-registry.js +155 -0
- package/lib/tokens.d.ts +167 -86
- package/lib/tokens.js +25 -12
- package/lib/tools/commands.d.ts +11 -0
- package/lib/tools/commands.js +126 -0
- package/lib/tools/file.d.ts +27 -0
- package/lib/tools/file.js +262 -0
- package/lib/tools/notebook.d.ts +41 -0
- package/lib/tools/notebook.js +779 -0
- package/lib/tools/tool-registry.d.ts +35 -0
- package/lib/tools/tool-registry.js +55 -0
- package/lib/widgets/ai-settings.d.ts +49 -0
- package/lib/widgets/ai-settings.js +580 -0
- package/lib/widgets/chat-wrapper.d.ts +144 -0
- package/lib/widgets/chat-wrapper.js +390 -0
- package/lib/widgets/provider-config-dialog.d.ts +14 -0
- package/lib/widgets/provider-config-dialog.js +112 -0
- package/package.json +151 -40
- package/schema/settings-model.json +159 -0
- package/src/agent.ts +836 -0
- package/src/chat-model.ts +771 -0
- package/src/completion/completion-provider.ts +346 -0
- package/src/completion/index.ts +1 -0
- package/src/components/clear-button.tsx +56 -0
- package/src/components/index.ts +3 -0
- package/src/components/model-select.tsx +245 -0
- package/src/components/stop-button.tsx +11 -11
- package/src/components/token-usage-display.tsx +130 -0
- package/src/components/tool-select.tsx +218 -0
- package/src/icons.ts +12 -14
- package/src/index.ts +485 -232
- package/src/mcp/browser.ts +213 -0
- package/src/models/settings-model.ts +413 -0
- package/src/providers/built-in-providers.ts +294 -0
- package/src/providers/models.ts +79 -0
- package/src/providers/provider-registry.ts +189 -0
- package/src/tokens.ts +217 -90
- package/src/tools/commands.ts +151 -0
- package/src/tools/file.ts +307 -0
- package/src/tools/notebook.ts +987 -0
- package/src/tools/tool-registry.ts +63 -0
- package/src/types.d.ts +4 -0
- package/src/widgets/ai-settings.tsx +1233 -0
- package/src/widgets/chat-wrapper.tsx +543 -0
- package/src/widgets/provider-config-dialog.tsx +272 -0
- package/style/base.css +335 -14
- package/style/icons/jupyternaut-lite.svg +1 -1
- package/lib/base-completer.d.ts +0 -49
- package/lib/base-completer.js +0 -14
- package/lib/chat-handler.d.ts +0 -56
- package/lib/chat-handler.js +0 -201
- package/lib/completion-provider.d.ts +0 -34
- package/lib/completion-provider.js +0 -32
- package/lib/default-prompts.d.ts +0 -2
- package/lib/default-prompts.js +0 -31
- package/lib/default-providers/Anthropic/completer.d.ts +0 -12
- package/lib/default-providers/Anthropic/completer.js +0 -46
- package/lib/default-providers/Anthropic/settings-schema.json +0 -70
- package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
- package/lib/default-providers/ChromeAI/completer.js +0 -56
- package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
- package/lib/default-providers/ChromeAI/instructions.js +0 -42
- package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
- package/lib/default-providers/Gemini/completer.d.ts +0 -12
- package/lib/default-providers/Gemini/completer.js +0 -48
- package/lib/default-providers/Gemini/instructions.d.ts +0 -2
- package/lib/default-providers/Gemini/instructions.js +0 -9
- package/lib/default-providers/Gemini/settings-schema.json +0 -64
- package/lib/default-providers/MistralAI/completer.d.ts +0 -13
- package/lib/default-providers/MistralAI/completer.js +0 -52
- package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
- package/lib/default-providers/MistralAI/instructions.js +0 -18
- package/lib/default-providers/MistralAI/settings-schema.json +0 -75
- package/lib/default-providers/Ollama/completer.d.ts +0 -12
- package/lib/default-providers/Ollama/completer.js +0 -43
- package/lib/default-providers/Ollama/instructions.d.ts +0 -2
- package/lib/default-providers/Ollama/instructions.js +0 -70
- package/lib/default-providers/Ollama/settings-schema.json +0 -143
- package/lib/default-providers/OpenAI/completer.d.ts +0 -12
- package/lib/default-providers/OpenAI/completer.js +0 -43
- package/lib/default-providers/OpenAI/settings-schema.json +0 -628
- package/lib/default-providers/WebLLM/completer.d.ts +0 -21
- package/lib/default-providers/WebLLM/completer.js +0 -127
- package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
- package/lib/default-providers/WebLLM/instructions.js +0 -32
- package/lib/default-providers/WebLLM/settings-schema.json +0 -19
- package/lib/default-providers/index.d.ts +0 -2
- package/lib/default-providers/index.js +0 -179
- package/lib/provider.d.ts +0 -144
- package/lib/provider.js +0 -412
- package/lib/settings/base.json +0 -7
- package/lib/settings/index.d.ts +0 -3
- package/lib/settings/index.js +0 -3
- package/lib/settings/panel.d.ts +0 -226
- package/lib/settings/panel.js +0 -510
- package/lib/settings/textarea.d.ts +0 -2
- package/lib/settings/textarea.js +0 -18
- package/lib/settings/utils.d.ts +0 -2
- package/lib/settings/utils.js +0 -4
- package/lib/types/ai-model.d.ts +0 -24
- package/lib/types/ai-model.js +0 -5
- package/schema/chat.json +0 -28
- package/schema/provider-registry.json +0 -29
- package/schema/system-prompts.json +0 -22
- package/src/base-completer.ts +0 -75
- package/src/chat-handler.ts +0 -262
- package/src/completion-provider.ts +0 -64
- package/src/default-prompts.ts +0 -33
- package/src/default-providers/Anthropic/completer.ts +0 -59
- package/src/default-providers/ChromeAI/completer.ts +0 -73
- package/src/default-providers/ChromeAI/instructions.ts +0 -45
- package/src/default-providers/Gemini/completer.ts +0 -61
- package/src/default-providers/Gemini/instructions.ts +0 -9
- package/src/default-providers/MistralAI/completer.ts +0 -69
- package/src/default-providers/MistralAI/instructions.ts +0 -18
- package/src/default-providers/Ollama/completer.ts +0 -54
- package/src/default-providers/Ollama/instructions.ts +0 -70
- package/src/default-providers/OpenAI/completer.ts +0 -54
- package/src/default-providers/WebLLM/completer.ts +0 -151
- package/src/default-providers/WebLLM/instructions.ts +0 -33
- package/src/default-providers/index.ts +0 -211
- package/src/global.d.ts +0 -9
- package/src/provider.ts +0 -514
- package/src/settings/index.ts +0 -3
- package/src/settings/panel.tsx +0 -773
- package/src/settings/textarea.tsx +0 -33
- package/src/settings/utils.ts +0 -5
- package/src/types/ai-model.ts +0 -37
- 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
|
+
}
|