@jupyternaut/persona 0.0.0 → 0.20.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.
- package/lib/chat-commands/mention.d.ts +9 -0
- package/lib/chat-commands/mention.js +30 -0
- package/lib/completion/completion-provider.d.ts +86 -0
- package/lib/completion/completion-provider.js +246 -0
- package/lib/completion/index.d.ts +2 -0
- package/lib/completion/index.js +1 -0
- package/lib/components/completion-status.d.ts +26 -0
- package/lib/components/completion-status.js +52 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +1 -0
- package/lib/diff-manager.d.ts +25 -0
- package/lib/diff-manager.js +60 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +522 -0
- package/lib/models/settings-model.d.ts +36 -0
- package/lib/models/settings-model.js +356 -0
- package/lib/persona-registry.d.ts +15 -0
- package/lib/persona-registry.js +29 -0
- package/lib/persona.d.ts +66 -0
- package/lib/persona.js +414 -0
- package/lib/process-attachments.d.ts +5 -0
- package/lib/process-attachments.js +287 -0
- package/lib/tokens.d.ts +101 -0
- package/lib/tokens.js +20 -0
- package/lib/widgets/ai-settings.d.ts +54 -0
- package/lib/widgets/ai-settings.js +572 -0
- package/lib/widgets/provider-config-dialog.d.ts +16 -0
- package/lib/widgets/provider-config-dialog.js +384 -0
- package/package.json +111 -7
- package/schema/settings-model.json +287 -0
- package/src/chat-commands/mention.tsx +46 -0
- package/src/completion/completion-provider.ts +350 -0
- package/src/completion/index.ts +1 -0
- package/src/components/completion-status.tsx +93 -0
- package/src/components/index.ts +1 -0
- package/src/diff-manager.ts +81 -0
- package/src/index.ts +710 -0
- package/src/models/settings-model.ts +415 -0
- package/src/persona-registry.ts +46 -0
- package/src/persona.ts +610 -0
- package/src/process-attachments.ts +369 -0
- package/src/tokens.ts +121 -0
- package/src/widgets/ai-settings.tsx +1308 -0
- package/src/widgets/provider-config-dialog.tsx +997 -0
- package/style/base.css +14 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
- package/README.md +0 -3
- package/index.js +0 -1
|
@@ -0,0 +1,1308 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getEffectiveContextWindow,
|
|
3
|
+
SECRETS_REPLACEMENT
|
|
4
|
+
} from '@jupyternaut/agent';
|
|
5
|
+
import type {
|
|
6
|
+
IAgentManagerFactory,
|
|
7
|
+
IAIConfig,
|
|
8
|
+
IAISecretsAccess,
|
|
9
|
+
IAISettingsModel,
|
|
10
|
+
IProviderConfig,
|
|
11
|
+
IProviderRegistry
|
|
12
|
+
} from '@jupyternaut/agent';
|
|
13
|
+
import { IThemeManager } from '@jupyterlab/apputils';
|
|
14
|
+
import { ReactWidget } from '@jupyterlab/ui-components';
|
|
15
|
+
import type { TranslationBundle } from '@jupyterlab/translation';
|
|
16
|
+
import { Debouncer } from '@lumino/polling';
|
|
17
|
+
import Add from '@mui/icons-material/Add';
|
|
18
|
+
import CheckCircle from '@mui/icons-material/CheckCircle';
|
|
19
|
+
import Delete from '@mui/icons-material/Delete';
|
|
20
|
+
import Edit from '@mui/icons-material/Edit';
|
|
21
|
+
import Error from '@mui/icons-material/Error';
|
|
22
|
+
import InfoOutlined from '@mui/icons-material/InfoOutlined';
|
|
23
|
+
import MoreVert from '@mui/icons-material/MoreVert';
|
|
24
|
+
import Settings from '@mui/icons-material/Settings';
|
|
25
|
+
import {
|
|
26
|
+
Alert,
|
|
27
|
+
Box,
|
|
28
|
+
Button,
|
|
29
|
+
Card,
|
|
30
|
+
CardContent,
|
|
31
|
+
Chip,
|
|
32
|
+
Divider,
|
|
33
|
+
FormControl,
|
|
34
|
+
FormControlLabel,
|
|
35
|
+
IconButton,
|
|
36
|
+
InputLabel,
|
|
37
|
+
List,
|
|
38
|
+
ListItem,
|
|
39
|
+
ListItemText,
|
|
40
|
+
Menu,
|
|
41
|
+
MenuItem,
|
|
42
|
+
Select,
|
|
43
|
+
Switch,
|
|
44
|
+
Tab,
|
|
45
|
+
Tabs,
|
|
46
|
+
TextField,
|
|
47
|
+
ThemeProvider,
|
|
48
|
+
Tooltip,
|
|
49
|
+
Typography,
|
|
50
|
+
createTheme
|
|
51
|
+
} from '@mui/material';
|
|
52
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
53
|
+
|
|
54
|
+
import { ProviderConfigDialog } from './provider-config-dialog';
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a theme that uses IThemeManager to detect theme
|
|
58
|
+
* @param themeManager - Optional theme manager to detect theme
|
|
59
|
+
* @returns A Material-UI theme configured for the current JupyterLab theme
|
|
60
|
+
*/
|
|
61
|
+
const createJupyterLabTheme = (themeManager?: IThemeManager) => {
|
|
62
|
+
// Use IThemeManager if available, otherwise default to light theme
|
|
63
|
+
const isDark = themeManager?.theme
|
|
64
|
+
? !themeManager.isLight(themeManager.theme)
|
|
65
|
+
: false;
|
|
66
|
+
|
|
67
|
+
return createTheme({
|
|
68
|
+
palette: {
|
|
69
|
+
mode: isDark ? 'dark' : 'light'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A JupyterLab widget for AI settings configuration
|
|
76
|
+
*/
|
|
77
|
+
export class AISettingsWidget extends ReactWidget {
|
|
78
|
+
/**
|
|
79
|
+
* Construct a new AI settings widget
|
|
80
|
+
* @param options - The options for initializing the widget
|
|
81
|
+
*/
|
|
82
|
+
constructor(options: AISettingsWidget.IOptions) {
|
|
83
|
+
super();
|
|
84
|
+
this._settingsModel = options.settingsModel;
|
|
85
|
+
this._agentManagerFactory = options.agentManagerFactory;
|
|
86
|
+
this._themeManager = options.themeManager;
|
|
87
|
+
this._providerRegistry = options.providerRegistry;
|
|
88
|
+
this._secretsAccess = options.secretsAccess;
|
|
89
|
+
this._trans = options.trans;
|
|
90
|
+
this._mcpServerRenderer = options.mcpServerRenderer;
|
|
91
|
+
|
|
92
|
+
this.id = 'jupyternaut-persona-settings';
|
|
93
|
+
this.title.label = this._trans.__('Jupyternaut Settings');
|
|
94
|
+
this.title.caption = this._trans.__('Configure AI providers and behavior');
|
|
95
|
+
this.title.closable = true;
|
|
96
|
+
|
|
97
|
+
// Disable the secrets manager if the token is empty.
|
|
98
|
+
if (!options.secretsAccess.isAvailable) {
|
|
99
|
+
this._settingsModel.updateConfig({ useSecretsManager: false });
|
|
100
|
+
this._secretsAccess = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Render the AI settings component
|
|
106
|
+
* @returns A React element containing the AI settings interface
|
|
107
|
+
*/
|
|
108
|
+
protected render(): React.ReactElement {
|
|
109
|
+
return (
|
|
110
|
+
<AISettingsComponent
|
|
111
|
+
model={this._settingsModel}
|
|
112
|
+
agentManagerFactory={this._agentManagerFactory}
|
|
113
|
+
themeManager={this._themeManager}
|
|
114
|
+
providerRegistry={this._providerRegistry}
|
|
115
|
+
secretsAccess={this._secretsAccess}
|
|
116
|
+
trans={this._trans}
|
|
117
|
+
McpServerRenderer={this._mcpServerRenderer}
|
|
118
|
+
/>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private _settingsModel: IAISettingsModel;
|
|
123
|
+
private _agentManagerFactory?: IAgentManagerFactory;
|
|
124
|
+
private _themeManager?: IThemeManager;
|
|
125
|
+
private _providerRegistry: IProviderRegistry;
|
|
126
|
+
private _secretsAccess?: IAISecretsAccess;
|
|
127
|
+
private _trans: TranslationBundle;
|
|
128
|
+
private _mcpServerRenderer?: React.ComponentType<any>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Props interface for the AISettingsComponent
|
|
133
|
+
*/
|
|
134
|
+
interface IAISettingsComponentProps {
|
|
135
|
+
model: IAISettingsModel;
|
|
136
|
+
agentManagerFactory?: IAgentManagerFactory;
|
|
137
|
+
themeManager?: IThemeManager;
|
|
138
|
+
providerRegistry: IProviderRegistry;
|
|
139
|
+
secretsAccess?: IAISecretsAccess;
|
|
140
|
+
trans: TranslationBundle;
|
|
141
|
+
McpServerRenderer?: React.ComponentType<any>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The main AI settings component that provides configuration UI
|
|
146
|
+
* @param props - Component props containing models and theme manager
|
|
147
|
+
* @returns A React component for AI settings configuration
|
|
148
|
+
*/
|
|
149
|
+
const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
|
|
150
|
+
model,
|
|
151
|
+
agentManagerFactory,
|
|
152
|
+
themeManager,
|
|
153
|
+
providerRegistry,
|
|
154
|
+
secretsAccess,
|
|
155
|
+
trans,
|
|
156
|
+
McpServerRenderer
|
|
157
|
+
}) => {
|
|
158
|
+
if (!model) {
|
|
159
|
+
return <div>{trans.__('Settings model not available')}</div>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const [config, setConfig] = useState(model.config || {});
|
|
163
|
+
const [theme, setTheme] = useState(() => createJupyterLabTheme(themeManager));
|
|
164
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
165
|
+
const [dialogOpen, setDialogOpen] = useState(false);
|
|
166
|
+
const [editingProvider, setEditingProvider] = useState<
|
|
167
|
+
IProviderConfig | undefined
|
|
168
|
+
>();
|
|
169
|
+
const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
|
|
170
|
+
const [menuProviderId, setMenuProviderId] = useState<string>('');
|
|
171
|
+
const [systemPromptValue, setSystemPromptValue] = useState(
|
|
172
|
+
config.systemPrompt
|
|
173
|
+
);
|
|
174
|
+
const systemPromptValueRef = React.useRef(config.systemPrompt);
|
|
175
|
+
const [completionPromptValue, setCompletionPromptValue] = useState(
|
|
176
|
+
config.completionSystemPrompt
|
|
177
|
+
);
|
|
178
|
+
const completionPromptValueRef = React.useRef(config.completionSystemPrompt);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Effect to listen for model state changes and update config
|
|
182
|
+
*/
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
if (!model || !model.stateChanged) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const onStateChanged = () => {
|
|
189
|
+
setConfig(model.config || {});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
model.stateChanged.connect(onStateChanged);
|
|
193
|
+
return () => {
|
|
194
|
+
model.stateChanged.disconnect(onStateChanged);
|
|
195
|
+
};
|
|
196
|
+
}, [model]);
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Effect to listen for theme changes and update the Material-UI theme
|
|
200
|
+
*/
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (!themeManager) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const updateTheme = () => {
|
|
207
|
+
setTheme(createJupyterLabTheme(themeManager));
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
themeManager.themeChanged.connect(updateTheme);
|
|
211
|
+
return () => {
|
|
212
|
+
themeManager.themeChanged.disconnect(updateTheme);
|
|
213
|
+
};
|
|
214
|
+
}, [themeManager]);
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Effect to listen for MCP connection changes to re-render connection status
|
|
218
|
+
*/
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!agentManagerFactory) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const onMCPConnectionChanged = () => {
|
|
225
|
+
// Force a re-render by updating the config state
|
|
226
|
+
setConfig(prevConfig => ({ ...prevConfig }));
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
agentManagerFactory.mcpConnectionChanged.connect(onMCPConnectionChanged);
|
|
230
|
+
return () => {
|
|
231
|
+
agentManagerFactory.mcpConnectionChanged.disconnect(
|
|
232
|
+
onMCPConnectionChanged
|
|
233
|
+
);
|
|
234
|
+
};
|
|
235
|
+
}, [agentManagerFactory]);
|
|
236
|
+
|
|
237
|
+
// Sync local state when config changes externally
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
setSystemPromptValue(config.systemPrompt);
|
|
240
|
+
systemPromptValueRef.current = config.systemPrompt;
|
|
241
|
+
}, [config.systemPrompt]);
|
|
242
|
+
|
|
243
|
+
useEffect(() => {
|
|
244
|
+
setCompletionPromptValue(config.completionSystemPrompt);
|
|
245
|
+
completionPromptValueRef.current = config.completionSystemPrompt;
|
|
246
|
+
}, [config.completionSystemPrompt]);
|
|
247
|
+
|
|
248
|
+
const promptDebouncer = useMemo(
|
|
249
|
+
() =>
|
|
250
|
+
new Debouncer(async () => {
|
|
251
|
+
await handleConfigUpdate({
|
|
252
|
+
systemPrompt: systemPromptValueRef.current,
|
|
253
|
+
completionSystemPrompt: completionPromptValueRef.current
|
|
254
|
+
});
|
|
255
|
+
}, 1000),
|
|
256
|
+
[]
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
// Cleanup debouncer on unmount
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
return () => {
|
|
262
|
+
promptDebouncer.dispose();
|
|
263
|
+
};
|
|
264
|
+
}, [promptDebouncer]);
|
|
265
|
+
|
|
266
|
+
const handleSystemPromptChange = (value: string) => {
|
|
267
|
+
setSystemPromptValue(value);
|
|
268
|
+
systemPromptValueRef.current = value;
|
|
269
|
+
void promptDebouncer.invoke();
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const handleCompletionPromptChange = (value: string) => {
|
|
273
|
+
setCompletionPromptValue(value);
|
|
274
|
+
completionPromptValueRef.current = value;
|
|
275
|
+
void promptDebouncer.invoke();
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const getSecretFromManager = async (
|
|
279
|
+
provider: string,
|
|
280
|
+
fieldName: string
|
|
281
|
+
): Promise<string | undefined> => {
|
|
282
|
+
return secretsAccess?.get(`${provider}:${fieldName}`);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const setSecretToManager = async (
|
|
286
|
+
provider: string,
|
|
287
|
+
fieldName: string,
|
|
288
|
+
value: string
|
|
289
|
+
): Promise<void> => {
|
|
290
|
+
await secretsAccess?.set(`${provider}:${fieldName}`, value);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Attach a secrets field to the secrets manager.
|
|
295
|
+
* @param input - the DOm element to attach.
|
|
296
|
+
* @param provider - the name of the provider.
|
|
297
|
+
* @param fieldName - the name of the field.
|
|
298
|
+
*/
|
|
299
|
+
const handleSecretField = async (
|
|
300
|
+
input: HTMLInputElement,
|
|
301
|
+
provider: string,
|
|
302
|
+
fieldName: string
|
|
303
|
+
): Promise<void> => {
|
|
304
|
+
if (!(model.config.useSecretsManager && secretsAccess?.isAvailable)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await secretsAccess.attach(`${provider}:${fieldName}`, input);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Handle adding a new AI provider
|
|
312
|
+
* @param providerConfig - The provider configuration to add
|
|
313
|
+
*/
|
|
314
|
+
const handleAddProvider = async (
|
|
315
|
+
providerConfig: Omit<IProviderConfig, 'id'>
|
|
316
|
+
) => {
|
|
317
|
+
if (
|
|
318
|
+
model.config.useSecretsManager &&
|
|
319
|
+
secretsAccess?.isAvailable &&
|
|
320
|
+
providerConfig.apiKey
|
|
321
|
+
) {
|
|
322
|
+
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
323
|
+
}
|
|
324
|
+
await model.addProvider(providerConfig);
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Handle editing an existing AI provider
|
|
329
|
+
* @param providerConfig - The updated provider configuration
|
|
330
|
+
*/
|
|
331
|
+
const handleEditProvider = async (
|
|
332
|
+
providerConfig: Omit<IProviderConfig, 'id'>
|
|
333
|
+
) => {
|
|
334
|
+
if (editingProvider) {
|
|
335
|
+
if (
|
|
336
|
+
model.config.useSecretsManager &&
|
|
337
|
+
secretsAccess?.isAvailable &&
|
|
338
|
+
providerConfig.apiKey
|
|
339
|
+
) {
|
|
340
|
+
providerConfig.apiKey = SECRETS_REPLACEMENT;
|
|
341
|
+
}
|
|
342
|
+
await model.updateProvider(editingProvider.id, providerConfig);
|
|
343
|
+
setEditingProvider(undefined);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Handle deleting an AI provider
|
|
349
|
+
* @param id - The ID of the provider to delete
|
|
350
|
+
*/
|
|
351
|
+
const handleDeleteProvider = async (id: string) => {
|
|
352
|
+
await model.removeProvider(id);
|
|
353
|
+
setMenuAnchor(null);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Open the provider edit dialog
|
|
358
|
+
* @param provider - The provider to edit
|
|
359
|
+
*/
|
|
360
|
+
const openEditDialog = async (provider: IProviderConfig) => {
|
|
361
|
+
// Retrieve the API key from the secrets manager if necessary.
|
|
362
|
+
if (model.config.useSecretsManager && secretsAccess?.isAvailable) {
|
|
363
|
+
provider.apiKey =
|
|
364
|
+
(await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
|
|
365
|
+
}
|
|
366
|
+
setEditingProvider(provider);
|
|
367
|
+
setDialogOpen(true);
|
|
368
|
+
setMenuAnchor(null);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Open the provider add dialog
|
|
373
|
+
*/
|
|
374
|
+
const openAddDialog = () => {
|
|
375
|
+
setEditingProvider(undefined);
|
|
376
|
+
setDialogOpen(true);
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Handle provider menu click
|
|
381
|
+
* @param event - The click event
|
|
382
|
+
* @param providerId - The ID of the provider
|
|
383
|
+
*/
|
|
384
|
+
const handleMenuClick = (
|
|
385
|
+
event: React.MouseEvent<HTMLElement>,
|
|
386
|
+
providerId: string
|
|
387
|
+
) => {
|
|
388
|
+
setMenuAnchor(event.currentTarget);
|
|
389
|
+
setMenuProviderId(providerId);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Handle provider menu close
|
|
394
|
+
*/
|
|
395
|
+
const handleMenuClose = () => {
|
|
396
|
+
setMenuAnchor(null);
|
|
397
|
+
setMenuProviderId('');
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Handle updating AI configuration
|
|
402
|
+
* @param updates - Partial configuration updates to apply
|
|
403
|
+
*/
|
|
404
|
+
const handleConfigUpdate = async (updates: Partial<IAIConfig>) => {
|
|
405
|
+
if (updates.useSecretsManager !== undefined) {
|
|
406
|
+
if (updates.useSecretsManager) {
|
|
407
|
+
for (const provider of model.config.providers) {
|
|
408
|
+
const settingsApiKey = provider.apiKey;
|
|
409
|
+
// If the secrets manager doesn't have the current API key, set the current
|
|
410
|
+
// one from settings.
|
|
411
|
+
// Update the settings value with SECRETS_REPLACEMENT if a key exist in the
|
|
412
|
+
// secrets manager (was already there or a value was set in settings).
|
|
413
|
+
if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
|
|
414
|
+
if (settingsApiKey !== undefined) {
|
|
415
|
+
setSecretToManager(
|
|
416
|
+
provider.provider,
|
|
417
|
+
'apiKey',
|
|
418
|
+
settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : ''
|
|
419
|
+
);
|
|
420
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
421
|
+
await model.updateProvider(provider.id, provider);
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
provider.apiKey = SECRETS_REPLACEMENT;
|
|
425
|
+
await model.updateProvider(provider.id, provider);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
} else {
|
|
429
|
+
for (const provider of model.config.providers) {
|
|
430
|
+
const apiKey = await getSecretFromManager(
|
|
431
|
+
provider.provider,
|
|
432
|
+
'apiKey'
|
|
433
|
+
);
|
|
434
|
+
provider.apiKey = apiKey;
|
|
435
|
+
await model.updateProvider(provider.id, provider);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
await model.updateConfig(updates);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
<ThemeProvider theme={theme}>
|
|
444
|
+
<Box
|
|
445
|
+
sx={{
|
|
446
|
+
height: '100%',
|
|
447
|
+
maxHeight: '100vh',
|
|
448
|
+
overflow: 'auto',
|
|
449
|
+
p: 2,
|
|
450
|
+
pb: 4,
|
|
451
|
+
boxSizing: 'border-box',
|
|
452
|
+
fontSize: '0.9rem'
|
|
453
|
+
}}
|
|
454
|
+
>
|
|
455
|
+
{/* Header */}
|
|
456
|
+
<Box sx={{ mb: 2, display: 'flex', alignItems: 'center', gap: 2 }}>
|
|
457
|
+
<Settings color="primary" sx={{ fontSize: 24 }} />
|
|
458
|
+
<Typography variant="h5" component="h1" sx={{ fontWeight: 600 }}>
|
|
459
|
+
{trans.__('AI Settings')}
|
|
460
|
+
</Typography>
|
|
461
|
+
</Box>
|
|
462
|
+
{/* Tabs */}
|
|
463
|
+
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
|
464
|
+
<Tabs
|
|
465
|
+
value={activeTab}
|
|
466
|
+
onChange={(_, newValue) => setActiveTab(newValue)}
|
|
467
|
+
>
|
|
468
|
+
<Tab label={trans.__('Providers')} />
|
|
469
|
+
<Tab label={trans.__('Behavior')} />
|
|
470
|
+
{McpServerRenderer && <Tab label={trans.__('MCP Servers')} />}
|
|
471
|
+
</Tabs>
|
|
472
|
+
</Box>
|
|
473
|
+
|
|
474
|
+
{/* Tab Panels */}
|
|
475
|
+
{activeTab === 0 && (
|
|
476
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
477
|
+
{/* Default Provider Selection */}
|
|
478
|
+
{config.providers.length > 0 && (
|
|
479
|
+
<Card elevation={2}>
|
|
480
|
+
<CardContent>
|
|
481
|
+
<Typography variant="h6" component="h2" gutterBottom>
|
|
482
|
+
{trans.__('Default Providers')}
|
|
483
|
+
</Typography>
|
|
484
|
+
|
|
485
|
+
<Box
|
|
486
|
+
sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}
|
|
487
|
+
>
|
|
488
|
+
<FormControl fullWidth>
|
|
489
|
+
<InputLabel>{trans.__('Chat Provider')}</InputLabel>
|
|
490
|
+
<Select
|
|
491
|
+
value={config.defaultProvider}
|
|
492
|
+
label={trans.__('Chat Provider')}
|
|
493
|
+
onChange={e => model.setActiveProvider(e.target.value)}
|
|
494
|
+
>
|
|
495
|
+
{config.providers.map(provider => (
|
|
496
|
+
<MenuItem key={provider.id} value={provider.id}>
|
|
497
|
+
{provider.name}
|
|
498
|
+
</MenuItem>
|
|
499
|
+
))}
|
|
500
|
+
</Select>
|
|
501
|
+
</FormControl>
|
|
502
|
+
|
|
503
|
+
<FormControlLabel
|
|
504
|
+
control={
|
|
505
|
+
<Switch
|
|
506
|
+
checked={config.useSameProviderForChatAndCompleter}
|
|
507
|
+
onChange={e =>
|
|
508
|
+
handleConfigUpdate({
|
|
509
|
+
useSameProviderForChatAndCompleter:
|
|
510
|
+
e.target.checked
|
|
511
|
+
})
|
|
512
|
+
}
|
|
513
|
+
color="primary"
|
|
514
|
+
/>
|
|
515
|
+
}
|
|
516
|
+
label={trans.__(
|
|
517
|
+
'Use same provider for chat and completions'
|
|
518
|
+
)}
|
|
519
|
+
/>
|
|
520
|
+
|
|
521
|
+
{!config.useSameProviderForChatAndCompleter && (
|
|
522
|
+
<FormControl fullWidth>
|
|
523
|
+
<InputLabel>
|
|
524
|
+
{trans.__('Completion Provider')}
|
|
525
|
+
</InputLabel>
|
|
526
|
+
<Select
|
|
527
|
+
value={config.activeCompleterProvider || ''}
|
|
528
|
+
label={trans.__('Completion Provider')}
|
|
529
|
+
className="jp-ai-completion-provider-select"
|
|
530
|
+
onChange={e =>
|
|
531
|
+
model.setActiveCompleterProvider(
|
|
532
|
+
e.target.value || undefined
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
>
|
|
536
|
+
<MenuItem value="">
|
|
537
|
+
<em>{trans.__('No completion')}</em>
|
|
538
|
+
</MenuItem>
|
|
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
|
+
</Box>
|
|
548
|
+
</CardContent>
|
|
549
|
+
</Card>
|
|
550
|
+
)}
|
|
551
|
+
|
|
552
|
+
{/* Providers Card */}
|
|
553
|
+
<Card elevation={2}>
|
|
554
|
+
<CardContent>
|
|
555
|
+
<Box
|
|
556
|
+
sx={{
|
|
557
|
+
display: 'flex',
|
|
558
|
+
alignItems: 'center',
|
|
559
|
+
justifyContent: 'space-between',
|
|
560
|
+
mb: 2
|
|
561
|
+
}}
|
|
562
|
+
>
|
|
563
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
564
|
+
<Typography variant="h6" component="h2">
|
|
565
|
+
{trans.__('Configured Providers')}
|
|
566
|
+
</Typography>
|
|
567
|
+
</Box>
|
|
568
|
+
<Button
|
|
569
|
+
variant="contained"
|
|
570
|
+
startIcon={<Add />}
|
|
571
|
+
onClick={openAddDialog}
|
|
572
|
+
size="small"
|
|
573
|
+
>
|
|
574
|
+
{trans.__('Add Provider')}
|
|
575
|
+
</Button>
|
|
576
|
+
</Box>
|
|
577
|
+
|
|
578
|
+
{config.providers.length === 0 ? (
|
|
579
|
+
<Alert severity="info">
|
|
580
|
+
{trans.__(
|
|
581
|
+
'No providers configured yet. Click "Add Provider" to get started.'
|
|
582
|
+
)}
|
|
583
|
+
</Alert>
|
|
584
|
+
) : (
|
|
585
|
+
<List>
|
|
586
|
+
{config.providers.map(provider => {
|
|
587
|
+
const isActive = config.defaultProvider === provider.id;
|
|
588
|
+
const isActiveCompleter =
|
|
589
|
+
config.useSameProviderForChatAndCompleter
|
|
590
|
+
? isActive
|
|
591
|
+
: config.activeCompleterProvider === provider.id;
|
|
592
|
+
const providerInfo = providerRegistry.getProviderInfo(
|
|
593
|
+
provider.provider
|
|
594
|
+
);
|
|
595
|
+
const providerToolCapabilities =
|
|
596
|
+
providerInfo?.providerToolCapabilities;
|
|
597
|
+
const params = provider.parameters;
|
|
598
|
+
const effectiveContextWindow = getEffectiveContextWindow(
|
|
599
|
+
provider,
|
|
600
|
+
providerRegistry
|
|
601
|
+
);
|
|
602
|
+
const webSearchEnabled =
|
|
603
|
+
!!providerToolCapabilities?.webSearch &&
|
|
604
|
+
provider.customSettings?.webSearch?.enabled === true;
|
|
605
|
+
const webFetchEnabled =
|
|
606
|
+
!!providerToolCapabilities?.webFetch &&
|
|
607
|
+
provider.customSettings?.webFetch?.enabled === true;
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
<ListItem
|
|
611
|
+
key={provider.id}
|
|
612
|
+
sx={{
|
|
613
|
+
flexDirection: 'column',
|
|
614
|
+
alignItems: 'stretch',
|
|
615
|
+
py: 2
|
|
616
|
+
}}
|
|
617
|
+
>
|
|
618
|
+
<Box
|
|
619
|
+
sx={{
|
|
620
|
+
display: 'flex',
|
|
621
|
+
justifyContent: 'space-between',
|
|
622
|
+
alignItems: 'flex-start',
|
|
623
|
+
width: '100%',
|
|
624
|
+
mb: 1
|
|
625
|
+
}}
|
|
626
|
+
>
|
|
627
|
+
<Box sx={{ flex: 1 }}>
|
|
628
|
+
<Box
|
|
629
|
+
sx={{
|
|
630
|
+
display: 'flex',
|
|
631
|
+
alignItems: 'center',
|
|
632
|
+
gap: 1,
|
|
633
|
+
mb: 0.5
|
|
634
|
+
}}
|
|
635
|
+
>
|
|
636
|
+
<Typography
|
|
637
|
+
variant="subtitle1"
|
|
638
|
+
fontWeight="medium"
|
|
639
|
+
>
|
|
640
|
+
{provider.name}
|
|
641
|
+
</Typography>
|
|
642
|
+
{isActive && (
|
|
643
|
+
<Chip
|
|
644
|
+
label={trans.__('Chat')}
|
|
645
|
+
size="small"
|
|
646
|
+
color="primary"
|
|
647
|
+
icon={<CheckCircle />}
|
|
648
|
+
/>
|
|
649
|
+
)}
|
|
650
|
+
{isActiveCompleter && (
|
|
651
|
+
<Chip
|
|
652
|
+
label={trans.__('Completion')}
|
|
653
|
+
size="small"
|
|
654
|
+
color="secondary"
|
|
655
|
+
icon={<CheckCircle />}
|
|
656
|
+
/>
|
|
657
|
+
)}
|
|
658
|
+
</Box>
|
|
659
|
+
<Typography
|
|
660
|
+
variant="body2"
|
|
661
|
+
color="text.secondary"
|
|
662
|
+
gutterBottom
|
|
663
|
+
>
|
|
664
|
+
{provider.provider} • {provider.model}
|
|
665
|
+
{provider.description &&
|
|
666
|
+
` • ${provider.description}`}
|
|
667
|
+
</Typography>
|
|
668
|
+
|
|
669
|
+
{/* Display parameters if set */}
|
|
670
|
+
{(params?.temperature !== undefined ||
|
|
671
|
+
params?.maxOutputTokens !== undefined ||
|
|
672
|
+
params?.maxTurns !== undefined ||
|
|
673
|
+
effectiveContextWindow !== undefined ||
|
|
674
|
+
webSearchEnabled ||
|
|
675
|
+
webFetchEnabled) && (
|
|
676
|
+
<Box
|
|
677
|
+
sx={{
|
|
678
|
+
display: 'flex',
|
|
679
|
+
flexWrap: 'wrap',
|
|
680
|
+
gap: 1,
|
|
681
|
+
mt: 1
|
|
682
|
+
}}
|
|
683
|
+
>
|
|
684
|
+
{params?.temperature !== undefined && (
|
|
685
|
+
<Chip
|
|
686
|
+
label={trans.__(
|
|
687
|
+
'Temp: %1',
|
|
688
|
+
params.temperature
|
|
689
|
+
)}
|
|
690
|
+
size="small"
|
|
691
|
+
variant="outlined"
|
|
692
|
+
/>
|
|
693
|
+
)}
|
|
694
|
+
{params?.maxOutputTokens !== undefined && (
|
|
695
|
+
<Chip
|
|
696
|
+
label={trans.__(
|
|
697
|
+
'Tokens: %1',
|
|
698
|
+
params.maxOutputTokens
|
|
699
|
+
)}
|
|
700
|
+
size="small"
|
|
701
|
+
variant="outlined"
|
|
702
|
+
/>
|
|
703
|
+
)}
|
|
704
|
+
{params?.maxTurns !== undefined && (
|
|
705
|
+
<Chip
|
|
706
|
+
label={trans.__(
|
|
707
|
+
'Turns: %1',
|
|
708
|
+
params.maxTurns
|
|
709
|
+
)}
|
|
710
|
+
size="small"
|
|
711
|
+
variant="outlined"
|
|
712
|
+
/>
|
|
713
|
+
)}
|
|
714
|
+
{effectiveContextWindow !== undefined && (
|
|
715
|
+
<Chip
|
|
716
|
+
label={trans.__(
|
|
717
|
+
'Context: %1',
|
|
718
|
+
effectiveContextWindow
|
|
719
|
+
)}
|
|
720
|
+
size="small"
|
|
721
|
+
variant="outlined"
|
|
722
|
+
/>
|
|
723
|
+
)}
|
|
724
|
+
{webSearchEnabled && (
|
|
725
|
+
<Chip
|
|
726
|
+
label={trans.__('Web Search')}
|
|
727
|
+
size="small"
|
|
728
|
+
variant="outlined"
|
|
729
|
+
color="info"
|
|
730
|
+
/>
|
|
731
|
+
)}
|
|
732
|
+
{webFetchEnabled && (
|
|
733
|
+
<Chip
|
|
734
|
+
label={trans.__('Web Fetch')}
|
|
735
|
+
size="small"
|
|
736
|
+
variant="outlined"
|
|
737
|
+
color="info"
|
|
738
|
+
/>
|
|
739
|
+
)}
|
|
740
|
+
</Box>
|
|
741
|
+
)}
|
|
742
|
+
</Box>
|
|
743
|
+
<IconButton
|
|
744
|
+
onClick={e => handleMenuClick(e, provider.id)}
|
|
745
|
+
size="small"
|
|
746
|
+
>
|
|
747
|
+
<MoreVert />
|
|
748
|
+
</IconButton>
|
|
749
|
+
</Box>
|
|
750
|
+
</ListItem>
|
|
751
|
+
);
|
|
752
|
+
})}
|
|
753
|
+
</List>
|
|
754
|
+
)}
|
|
755
|
+
</CardContent>
|
|
756
|
+
</Card>
|
|
757
|
+
|
|
758
|
+
{/* Secrets Manager Settings */}
|
|
759
|
+
{secretsAccess?.isAvailable && (
|
|
760
|
+
<FormControlLabel
|
|
761
|
+
control={
|
|
762
|
+
<Switch
|
|
763
|
+
checked={config.useSecretsManager}
|
|
764
|
+
onChange={e =>
|
|
765
|
+
handleConfigUpdate({
|
|
766
|
+
useSecretsManager: e.target.checked
|
|
767
|
+
})
|
|
768
|
+
}
|
|
769
|
+
color="primary"
|
|
770
|
+
sx={{ alignSelf: 'flex-start' }}
|
|
771
|
+
/>
|
|
772
|
+
}
|
|
773
|
+
label={
|
|
774
|
+
<div>
|
|
775
|
+
<span>
|
|
776
|
+
{trans.__('Use the secrets manager to manage API keys')}
|
|
777
|
+
</span>
|
|
778
|
+
{!config.useSecretsManager && (
|
|
779
|
+
<Alert severity="warning" icon={<Error />} sx={{ mb: 2 }}>
|
|
780
|
+
{trans.__(
|
|
781
|
+
'The secrets are stored in plain text in settings'
|
|
782
|
+
)}
|
|
783
|
+
</Alert>
|
|
784
|
+
)}
|
|
785
|
+
</div>
|
|
786
|
+
}
|
|
787
|
+
/>
|
|
788
|
+
)}
|
|
789
|
+
</Box>
|
|
790
|
+
)}
|
|
791
|
+
|
|
792
|
+
{activeTab === 1 && (
|
|
793
|
+
<Card elevation={2}>
|
|
794
|
+
<CardContent>
|
|
795
|
+
<Typography variant="h6" component="h2" gutterBottom>
|
|
796
|
+
{trans.__('Behavior Settings')}
|
|
797
|
+
</Typography>
|
|
798
|
+
|
|
799
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
800
|
+
<FormControlLabel
|
|
801
|
+
control={
|
|
802
|
+
<Switch
|
|
803
|
+
checked={config.toolsEnabled}
|
|
804
|
+
onChange={e =>
|
|
805
|
+
handleConfigUpdate({
|
|
806
|
+
toolsEnabled: e.target.checked
|
|
807
|
+
})
|
|
808
|
+
}
|
|
809
|
+
color="primary"
|
|
810
|
+
/>
|
|
811
|
+
}
|
|
812
|
+
label={
|
|
813
|
+
<Box>
|
|
814
|
+
<Typography variant="body1">
|
|
815
|
+
{trans.__('Enable Tools')}
|
|
816
|
+
</Typography>
|
|
817
|
+
<Typography variant="caption" color="text.secondary">
|
|
818
|
+
{trans.__(
|
|
819
|
+
'Allow the AI to use tools like notebook operations, code execution, and file management'
|
|
820
|
+
)}
|
|
821
|
+
</Typography>
|
|
822
|
+
</Box>
|
|
823
|
+
}
|
|
824
|
+
/>
|
|
825
|
+
|
|
826
|
+
<FormControlLabel
|
|
827
|
+
control={
|
|
828
|
+
<Switch
|
|
829
|
+
checked={config.showCellDiff}
|
|
830
|
+
onChange={e =>
|
|
831
|
+
handleConfigUpdate({
|
|
832
|
+
showCellDiff: e.target.checked
|
|
833
|
+
})
|
|
834
|
+
}
|
|
835
|
+
color="primary"
|
|
836
|
+
/>
|
|
837
|
+
}
|
|
838
|
+
label={
|
|
839
|
+
<Box>
|
|
840
|
+
<Typography variant="body1">
|
|
841
|
+
{trans.__('Show Cell Diff')}
|
|
842
|
+
</Typography>
|
|
843
|
+
<Typography variant="caption" color="text.secondary">
|
|
844
|
+
{trans.__(
|
|
845
|
+
'Show diff view when AI modifies cell content'
|
|
846
|
+
)}
|
|
847
|
+
</Typography>
|
|
848
|
+
</Box>
|
|
849
|
+
}
|
|
850
|
+
/>
|
|
851
|
+
|
|
852
|
+
{config.showCellDiff && (
|
|
853
|
+
<FormControl sx={{ ml: 4 }}>
|
|
854
|
+
<InputLabel>{trans.__('Diff Display Mode')}</InputLabel>
|
|
855
|
+
<Select
|
|
856
|
+
value={config.diffDisplayMode}
|
|
857
|
+
label={trans.__('Diff Display Mode')}
|
|
858
|
+
onChange={e =>
|
|
859
|
+
handleConfigUpdate({
|
|
860
|
+
diffDisplayMode: e.target.value as 'split' | 'unified'
|
|
861
|
+
})
|
|
862
|
+
}
|
|
863
|
+
>
|
|
864
|
+
<MenuItem value="split">
|
|
865
|
+
{trans.__('Split View')}
|
|
866
|
+
</MenuItem>
|
|
867
|
+
<MenuItem value="unified">
|
|
868
|
+
{trans.__('Unified View')}
|
|
869
|
+
</MenuItem>
|
|
870
|
+
</Select>
|
|
871
|
+
</FormControl>
|
|
872
|
+
)}
|
|
873
|
+
|
|
874
|
+
<FormControlLabel
|
|
875
|
+
control={
|
|
876
|
+
<Switch
|
|
877
|
+
checked={config.showFileDiff}
|
|
878
|
+
onChange={e =>
|
|
879
|
+
handleConfigUpdate({
|
|
880
|
+
showFileDiff: e.target.checked
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
color="primary"
|
|
884
|
+
/>
|
|
885
|
+
}
|
|
886
|
+
label={
|
|
887
|
+
<Box>
|
|
888
|
+
<Typography variant="body1">
|
|
889
|
+
{trans.__('Show File Diff')}
|
|
890
|
+
</Typography>
|
|
891
|
+
<Typography variant="caption" color="text.secondary">
|
|
892
|
+
{trans.__(
|
|
893
|
+
'Show diff view when AI modifies file content'
|
|
894
|
+
)}
|
|
895
|
+
</Typography>
|
|
896
|
+
</Box>
|
|
897
|
+
}
|
|
898
|
+
/>
|
|
899
|
+
|
|
900
|
+
<Divider sx={{ my: 1 }} />
|
|
901
|
+
|
|
902
|
+
<TextField
|
|
903
|
+
fullWidth
|
|
904
|
+
multiline
|
|
905
|
+
rows={3}
|
|
906
|
+
label={trans.__('System Prompt')}
|
|
907
|
+
value={systemPromptValue}
|
|
908
|
+
onChange={e => handleSystemPromptChange(e.target.value)}
|
|
909
|
+
placeholder={trans.__(
|
|
910
|
+
"Define the AI's behavior and personality..."
|
|
911
|
+
)}
|
|
912
|
+
helperText={trans.__(
|
|
913
|
+
'Instructions that define how the AI should behave and respond'
|
|
914
|
+
)}
|
|
915
|
+
/>
|
|
916
|
+
|
|
917
|
+
<TextField
|
|
918
|
+
fullWidth
|
|
919
|
+
multiline
|
|
920
|
+
rows={3}
|
|
921
|
+
label={trans.__('Completion System Prompt')}
|
|
922
|
+
value={completionPromptValue}
|
|
923
|
+
onChange={e => handleCompletionPromptChange(e.target.value)}
|
|
924
|
+
placeholder={trans.__(
|
|
925
|
+
'Define how the AI should generate code completions...'
|
|
926
|
+
)}
|
|
927
|
+
helperText={trans.__(
|
|
928
|
+
'Instructions that define how the AI should generate code completions'
|
|
929
|
+
)}
|
|
930
|
+
/>
|
|
931
|
+
|
|
932
|
+
<Divider sx={{ my: 2 }} />
|
|
933
|
+
|
|
934
|
+
<Box>
|
|
935
|
+
<Typography
|
|
936
|
+
variant="body1"
|
|
937
|
+
gutterBottom
|
|
938
|
+
sx={{
|
|
939
|
+
display: 'inline-flex',
|
|
940
|
+
alignItems: 'center',
|
|
941
|
+
gap: 1
|
|
942
|
+
}}
|
|
943
|
+
>
|
|
944
|
+
{trans.__('Skills Paths')}
|
|
945
|
+
<Tooltip
|
|
946
|
+
title={trans.__(
|
|
947
|
+
'Directories containing agent skills, relative to the server root. Skills are loaded from all paths; the first occurrence of a skill name takes priority.'
|
|
948
|
+
)}
|
|
949
|
+
>
|
|
950
|
+
<InfoOutlined sx={{ fontSize: 16 }} />
|
|
951
|
+
</Tooltip>
|
|
952
|
+
</Typography>
|
|
953
|
+
|
|
954
|
+
<List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
|
|
955
|
+
{(config.skillsPaths ?? []).map((skillPath, index) => (
|
|
956
|
+
<ListItem
|
|
957
|
+
key={index}
|
|
958
|
+
divider
|
|
959
|
+
secondaryAction={
|
|
960
|
+
<IconButton
|
|
961
|
+
onClick={() => {
|
|
962
|
+
const newPaths = [...config.skillsPaths];
|
|
963
|
+
newPaths.splice(index, 1);
|
|
964
|
+
handleConfigUpdate({ skillsPaths: newPaths });
|
|
965
|
+
}}
|
|
966
|
+
size="small"
|
|
967
|
+
>
|
|
968
|
+
<Delete />
|
|
969
|
+
</IconButton>
|
|
970
|
+
}
|
|
971
|
+
>
|
|
972
|
+
<ListItemText primary={skillPath} />
|
|
973
|
+
</ListItem>
|
|
974
|
+
))}
|
|
975
|
+
</List>
|
|
976
|
+
|
|
977
|
+
<TextField
|
|
978
|
+
fullWidth
|
|
979
|
+
label={trans.__('Add Skills Path')}
|
|
980
|
+
placeholder={trans.__('e.g., .claude/skills')}
|
|
981
|
+
onKeyDown={e => {
|
|
982
|
+
if (e.key === 'Enter') {
|
|
983
|
+
const value = (
|
|
984
|
+
e.target as HTMLInputElement
|
|
985
|
+
).value.trim();
|
|
986
|
+
if (
|
|
987
|
+
value &&
|
|
988
|
+
!(config.skillsPaths ?? []).includes(value)
|
|
989
|
+
) {
|
|
990
|
+
const newPaths = [
|
|
991
|
+
...(config.skillsPaths ?? []),
|
|
992
|
+
value
|
|
993
|
+
];
|
|
994
|
+
handleConfigUpdate({ skillsPaths: newPaths });
|
|
995
|
+
(e.target as HTMLInputElement).value = '';
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}}
|
|
999
|
+
helperText={trans.__(
|
|
1000
|
+
'Press Enter to add a path. Defaults: .agents/skills, _agents/skills'
|
|
1001
|
+
)}
|
|
1002
|
+
/>
|
|
1003
|
+
</Box>
|
|
1004
|
+
|
|
1005
|
+
<Divider sx={{ my: 2 }} />
|
|
1006
|
+
|
|
1007
|
+
<Box>
|
|
1008
|
+
<Typography variant="body1" gutterBottom>
|
|
1009
|
+
{trans.__('Commands Requiring Approval')}
|
|
1010
|
+
</Typography>
|
|
1011
|
+
<Typography
|
|
1012
|
+
variant="caption"
|
|
1013
|
+
color="text.secondary"
|
|
1014
|
+
gutterBottom
|
|
1015
|
+
sx={{ display: 'block' }}
|
|
1016
|
+
>
|
|
1017
|
+
{trans.__(
|
|
1018
|
+
'Commands that require user approval before AI can execute them'
|
|
1019
|
+
)}
|
|
1020
|
+
</Typography>
|
|
1021
|
+
|
|
1022
|
+
<List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
|
|
1023
|
+
{config.commandsRequiringApproval.map((command, index) => (
|
|
1024
|
+
<ListItem
|
|
1025
|
+
key={index}
|
|
1026
|
+
divider
|
|
1027
|
+
secondaryAction={
|
|
1028
|
+
<IconButton
|
|
1029
|
+
onClick={() => {
|
|
1030
|
+
const newCommands = [
|
|
1031
|
+
...config.commandsRequiringApproval
|
|
1032
|
+
];
|
|
1033
|
+
newCommands.splice(index, 1);
|
|
1034
|
+
handleConfigUpdate({
|
|
1035
|
+
commandsRequiringApproval: newCommands
|
|
1036
|
+
});
|
|
1037
|
+
}}
|
|
1038
|
+
size="small"
|
|
1039
|
+
>
|
|
1040
|
+
<Delete />
|
|
1041
|
+
</IconButton>
|
|
1042
|
+
}
|
|
1043
|
+
>
|
|
1044
|
+
<ListItemText primary={command} />
|
|
1045
|
+
</ListItem>
|
|
1046
|
+
))}
|
|
1047
|
+
</List>
|
|
1048
|
+
|
|
1049
|
+
<TextField
|
|
1050
|
+
fullWidth
|
|
1051
|
+
label={trans.__('Add New Command')}
|
|
1052
|
+
placeholder={trans.__('e.g., notebook:run-cell')}
|
|
1053
|
+
onKeyDown={e => {
|
|
1054
|
+
if (e.key === 'Enter') {
|
|
1055
|
+
const value = (
|
|
1056
|
+
e.target as HTMLInputElement
|
|
1057
|
+
).value.trim();
|
|
1058
|
+
if (
|
|
1059
|
+
value &&
|
|
1060
|
+
!config.commandsRequiringApproval.includes(value)
|
|
1061
|
+
) {
|
|
1062
|
+
const newCommands = [
|
|
1063
|
+
...config.commandsRequiringApproval,
|
|
1064
|
+
value
|
|
1065
|
+
];
|
|
1066
|
+
handleConfigUpdate({
|
|
1067
|
+
commandsRequiringApproval: newCommands
|
|
1068
|
+
});
|
|
1069
|
+
(e.target as HTMLInputElement).value = '';
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}}
|
|
1073
|
+
helperText={trans.__(
|
|
1074
|
+
'Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code'
|
|
1075
|
+
)}
|
|
1076
|
+
/>
|
|
1077
|
+
</Box>
|
|
1078
|
+
|
|
1079
|
+
<Divider sx={{ my: 2 }} />
|
|
1080
|
+
|
|
1081
|
+
<Box>
|
|
1082
|
+
<Typography variant="body1" gutterBottom>
|
|
1083
|
+
{trans.__('Commands Auto-Rendering MIME Bundles')}
|
|
1084
|
+
</Typography>
|
|
1085
|
+
<Typography
|
|
1086
|
+
variant="caption"
|
|
1087
|
+
color="text.secondary"
|
|
1088
|
+
gutterBottom
|
|
1089
|
+
sx={{ display: 'block' }}
|
|
1090
|
+
>
|
|
1091
|
+
{trans.__(
|
|
1092
|
+
'Only these execute_command command IDs can auto-render MIME bundle outputs in chat'
|
|
1093
|
+
)}
|
|
1094
|
+
</Typography>
|
|
1095
|
+
|
|
1096
|
+
<List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
|
|
1097
|
+
{(config.commandsAutoRenderMimeBundles ?? []).map(
|
|
1098
|
+
(command, index) => (
|
|
1099
|
+
<ListItem
|
|
1100
|
+
key={index}
|
|
1101
|
+
divider
|
|
1102
|
+
secondaryAction={
|
|
1103
|
+
<IconButton
|
|
1104
|
+
onClick={() => {
|
|
1105
|
+
const newCommands = [
|
|
1106
|
+
...(config.commandsAutoRenderMimeBundles ??
|
|
1107
|
+
[])
|
|
1108
|
+
];
|
|
1109
|
+
newCommands.splice(index, 1);
|
|
1110
|
+
handleConfigUpdate({
|
|
1111
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
1112
|
+
});
|
|
1113
|
+
}}
|
|
1114
|
+
size="small"
|
|
1115
|
+
>
|
|
1116
|
+
<Delete />
|
|
1117
|
+
</IconButton>
|
|
1118
|
+
}
|
|
1119
|
+
>
|
|
1120
|
+
<ListItemText primary={command} />
|
|
1121
|
+
</ListItem>
|
|
1122
|
+
)
|
|
1123
|
+
)}
|
|
1124
|
+
</List>
|
|
1125
|
+
|
|
1126
|
+
<TextField
|
|
1127
|
+
fullWidth
|
|
1128
|
+
label={trans.__('Add Auto-Render Command')}
|
|
1129
|
+
placeholder={trans.__(
|
|
1130
|
+
'e.g., jupyterlab-ai-commands:execute-in-kernel'
|
|
1131
|
+
)}
|
|
1132
|
+
onKeyDown={e => {
|
|
1133
|
+
if (e.key === 'Enter') {
|
|
1134
|
+
const value = (
|
|
1135
|
+
e.target as HTMLInputElement
|
|
1136
|
+
).value.trim();
|
|
1137
|
+
const existingCommands =
|
|
1138
|
+
config.commandsAutoRenderMimeBundles ?? [];
|
|
1139
|
+
if (value && !existingCommands.includes(value)) {
|
|
1140
|
+
const newCommands = [...existingCommands, value];
|
|
1141
|
+
handleConfigUpdate({
|
|
1142
|
+
commandsAutoRenderMimeBundles: newCommands
|
|
1143
|
+
});
|
|
1144
|
+
(e.target as HTMLInputElement).value = '';
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
}}
|
|
1148
|
+
helperText={trans.__(
|
|
1149
|
+
'Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel'
|
|
1150
|
+
)}
|
|
1151
|
+
/>
|
|
1152
|
+
</Box>
|
|
1153
|
+
|
|
1154
|
+
<Divider sx={{ my: 2 }} />
|
|
1155
|
+
|
|
1156
|
+
<Box>
|
|
1157
|
+
<Typography variant="body1" gutterBottom>
|
|
1158
|
+
{trans.__('Trusted MIME Types for Auto-Render')}
|
|
1159
|
+
</Typography>
|
|
1160
|
+
<Typography
|
|
1161
|
+
variant="caption"
|
|
1162
|
+
color="text.secondary"
|
|
1163
|
+
gutterBottom
|
|
1164
|
+
sx={{ display: 'block' }}
|
|
1165
|
+
>
|
|
1166
|
+
{trans.__(
|
|
1167
|
+
'When auto-rendering command outputs, these MIME types are marked trusted in chat'
|
|
1168
|
+
)}
|
|
1169
|
+
</Typography>
|
|
1170
|
+
|
|
1171
|
+
<List sx={{ mb: 2, maxHeight: 200, overflow: 'auto' }}>
|
|
1172
|
+
{(config.trustedMimeTypesForAutoRender ?? []).map(
|
|
1173
|
+
(mimeType, index) => (
|
|
1174
|
+
<ListItem
|
|
1175
|
+
key={index}
|
|
1176
|
+
divider
|
|
1177
|
+
secondaryAction={
|
|
1178
|
+
<IconButton
|
|
1179
|
+
onClick={() => {
|
|
1180
|
+
const newMimeTypes = [
|
|
1181
|
+
...(config.trustedMimeTypesForAutoRender ??
|
|
1182
|
+
[])
|
|
1183
|
+
];
|
|
1184
|
+
newMimeTypes.splice(index, 1);
|
|
1185
|
+
handleConfigUpdate({
|
|
1186
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
1187
|
+
});
|
|
1188
|
+
}}
|
|
1189
|
+
size="small"
|
|
1190
|
+
>
|
|
1191
|
+
<Delete />
|
|
1192
|
+
</IconButton>
|
|
1193
|
+
}
|
|
1194
|
+
>
|
|
1195
|
+
<ListItemText primary={mimeType} />
|
|
1196
|
+
</ListItem>
|
|
1197
|
+
)
|
|
1198
|
+
)}
|
|
1199
|
+
</List>
|
|
1200
|
+
|
|
1201
|
+
<TextField
|
|
1202
|
+
fullWidth
|
|
1203
|
+
label={trans.__('Add Trusted MIME Type')}
|
|
1204
|
+
placeholder={trans.__('e.g., text/html')}
|
|
1205
|
+
onKeyDown={e => {
|
|
1206
|
+
if (e.key === 'Enter') {
|
|
1207
|
+
const value = (
|
|
1208
|
+
e.target as HTMLInputElement
|
|
1209
|
+
).value.trim();
|
|
1210
|
+
const existingMimeTypes =
|
|
1211
|
+
config.trustedMimeTypesForAutoRender ?? [];
|
|
1212
|
+
if (value && !existingMimeTypes.includes(value)) {
|
|
1213
|
+
const newMimeTypes = [...existingMimeTypes, value];
|
|
1214
|
+
handleConfigUpdate({
|
|
1215
|
+
trustedMimeTypesForAutoRender: newMimeTypes
|
|
1216
|
+
});
|
|
1217
|
+
(e.target as HTMLInputElement).value = '';
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}}
|
|
1221
|
+
helperText={trans.__(
|
|
1222
|
+
'Press Enter to add a MIME type. Default: text/html'
|
|
1223
|
+
)}
|
|
1224
|
+
/>
|
|
1225
|
+
</Box>
|
|
1226
|
+
</Box>
|
|
1227
|
+
</CardContent>
|
|
1228
|
+
</Card>
|
|
1229
|
+
)}
|
|
1230
|
+
|
|
1231
|
+
{activeTab === 2 && (
|
|
1232
|
+
<Card elevation={2}>
|
|
1233
|
+
<CardContent>
|
|
1234
|
+
{McpServerRenderer && <McpServerRenderer />}
|
|
1235
|
+
</CardContent>
|
|
1236
|
+
</Card>
|
|
1237
|
+
)}
|
|
1238
|
+
|
|
1239
|
+
{/* Provider Configuration Dialog */}
|
|
1240
|
+
<ProviderConfigDialog
|
|
1241
|
+
open={dialogOpen}
|
|
1242
|
+
onClose={() => setDialogOpen(false)}
|
|
1243
|
+
onSave={editingProvider ? handleEditProvider : handleAddProvider}
|
|
1244
|
+
initialConfig={editingProvider}
|
|
1245
|
+
mode={editingProvider ? 'edit' : 'add'}
|
|
1246
|
+
providerRegistry={providerRegistry}
|
|
1247
|
+
handleSecretField={handleSecretField}
|
|
1248
|
+
trans={trans}
|
|
1249
|
+
/>
|
|
1250
|
+
|
|
1251
|
+
{/* Provider Menu */}
|
|
1252
|
+
<Menu
|
|
1253
|
+
anchorEl={menuAnchor}
|
|
1254
|
+
open={Boolean(menuAnchor)}
|
|
1255
|
+
onClose={handleMenuClose}
|
|
1256
|
+
>
|
|
1257
|
+
<MenuItem
|
|
1258
|
+
onClick={() => {
|
|
1259
|
+
const provider = config.providers.find(
|
|
1260
|
+
p => p.id === menuProviderId
|
|
1261
|
+
);
|
|
1262
|
+
if (provider) {
|
|
1263
|
+
openEditDialog(provider);
|
|
1264
|
+
}
|
|
1265
|
+
}}
|
|
1266
|
+
>
|
|
1267
|
+
<Edit sx={{ mr: 1 }} />
|
|
1268
|
+
{trans.__('Edit')}
|
|
1269
|
+
</MenuItem>
|
|
1270
|
+
<MenuItem
|
|
1271
|
+
onClick={() => handleDeleteProvider(menuProviderId)}
|
|
1272
|
+
sx={{ color: 'error.main' }}
|
|
1273
|
+
>
|
|
1274
|
+
<Delete sx={{ mr: 1 }} />
|
|
1275
|
+
{trans.__('Delete')}
|
|
1276
|
+
</MenuItem>
|
|
1277
|
+
</Menu>
|
|
1278
|
+
</Box>
|
|
1279
|
+
</ThemeProvider>
|
|
1280
|
+
);
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Namespace for AISettingsWidget types and interfaces
|
|
1285
|
+
*/
|
|
1286
|
+
export namespace AISettingsWidget {
|
|
1287
|
+
/**
|
|
1288
|
+
* Options interface for constructing an AISettingsWidget
|
|
1289
|
+
*/
|
|
1290
|
+
export interface IOptions {
|
|
1291
|
+
settingsModel: IAISettingsModel;
|
|
1292
|
+
agentManagerFactory?: IAgentManagerFactory;
|
|
1293
|
+
themeManager?: IThemeManager;
|
|
1294
|
+
providerRegistry: IProviderRegistry;
|
|
1295
|
+
/**
|
|
1296
|
+
* Access to provider secrets in the shared namespace.
|
|
1297
|
+
*/
|
|
1298
|
+
secretsAccess: IAISecretsAccess;
|
|
1299
|
+
/**
|
|
1300
|
+
* The application language translation bundle.
|
|
1301
|
+
*/
|
|
1302
|
+
trans: TranslationBundle;
|
|
1303
|
+
/**
|
|
1304
|
+
* The renderer for the MCP settings.
|
|
1305
|
+
*/
|
|
1306
|
+
mcpServerRenderer?: React.ComponentType<any>;
|
|
1307
|
+
}
|
|
1308
|
+
}
|