@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.
Files changed (49) hide show
  1. package/lib/chat-commands/mention.d.ts +9 -0
  2. package/lib/chat-commands/mention.js +30 -0
  3. package/lib/completion/completion-provider.d.ts +86 -0
  4. package/lib/completion/completion-provider.js +246 -0
  5. package/lib/completion/index.d.ts +2 -0
  6. package/lib/completion/index.js +1 -0
  7. package/lib/components/completion-status.d.ts +26 -0
  8. package/lib/components/completion-status.js +52 -0
  9. package/lib/components/index.d.ts +2 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/diff-manager.d.ts +25 -0
  12. package/lib/diff-manager.js +60 -0
  13. package/lib/index.d.ts +8 -0
  14. package/lib/index.js +522 -0
  15. package/lib/models/settings-model.d.ts +36 -0
  16. package/lib/models/settings-model.js +356 -0
  17. package/lib/persona-registry.d.ts +15 -0
  18. package/lib/persona-registry.js +29 -0
  19. package/lib/persona.d.ts +66 -0
  20. package/lib/persona.js +414 -0
  21. package/lib/process-attachments.d.ts +5 -0
  22. package/lib/process-attachments.js +287 -0
  23. package/lib/tokens.d.ts +101 -0
  24. package/lib/tokens.js +20 -0
  25. package/lib/widgets/ai-settings.d.ts +54 -0
  26. package/lib/widgets/ai-settings.js +572 -0
  27. package/lib/widgets/provider-config-dialog.d.ts +16 -0
  28. package/lib/widgets/provider-config-dialog.js +384 -0
  29. package/package.json +111 -7
  30. package/schema/settings-model.json +287 -0
  31. package/src/chat-commands/mention.tsx +46 -0
  32. package/src/completion/completion-provider.ts +350 -0
  33. package/src/completion/index.ts +1 -0
  34. package/src/components/completion-status.tsx +93 -0
  35. package/src/components/index.ts +1 -0
  36. package/src/diff-manager.ts +81 -0
  37. package/src/index.ts +710 -0
  38. package/src/models/settings-model.ts +415 -0
  39. package/src/persona-registry.ts +46 -0
  40. package/src/persona.ts +610 -0
  41. package/src/process-attachments.ts +369 -0
  42. package/src/tokens.ts +121 -0
  43. package/src/widgets/ai-settings.tsx +1308 -0
  44. package/src/widgets/provider-config-dialog.tsx +997 -0
  45. package/style/base.css +14 -0
  46. package/style/index.css +1 -0
  47. package/style/index.js +1 -0
  48. package/README.md +0 -3
  49. package/index.js +0 -1
@@ -0,0 +1,572 @@
1
+ import { getEffectiveContextWindow, SECRETS_REPLACEMENT } from '@jupyternaut/agent';
2
+ import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import { Debouncer } from '@lumino/polling';
4
+ import Add from '@mui/icons-material/Add';
5
+ import CheckCircle from '@mui/icons-material/CheckCircle';
6
+ import Delete from '@mui/icons-material/Delete';
7
+ import Edit from '@mui/icons-material/Edit';
8
+ import Error from '@mui/icons-material/Error';
9
+ import InfoOutlined from '@mui/icons-material/InfoOutlined';
10
+ import MoreVert from '@mui/icons-material/MoreVert';
11
+ import Settings from '@mui/icons-material/Settings';
12
+ import { Alert, Box, Button, Card, CardContent, Chip, Divider, FormControl, FormControlLabel, IconButton, InputLabel, List, ListItem, ListItemText, Menu, MenuItem, Select, Switch, Tab, Tabs, TextField, ThemeProvider, Tooltip, Typography, createTheme } from '@mui/material';
13
+ import React, { useEffect, useMemo, useState } from 'react';
14
+ import { ProviderConfigDialog } from './provider-config-dialog';
15
+ /**
16
+ * Create a theme that uses IThemeManager to detect theme
17
+ * @param themeManager - Optional theme manager to detect theme
18
+ * @returns A Material-UI theme configured for the current JupyterLab theme
19
+ */
20
+ const createJupyterLabTheme = (themeManager) => {
21
+ // Use IThemeManager if available, otherwise default to light theme
22
+ const isDark = themeManager?.theme
23
+ ? !themeManager.isLight(themeManager.theme)
24
+ : false;
25
+ return createTheme({
26
+ palette: {
27
+ mode: isDark ? 'dark' : 'light'
28
+ }
29
+ });
30
+ };
31
+ /**
32
+ * A JupyterLab widget for AI settings configuration
33
+ */
34
+ export class AISettingsWidget extends ReactWidget {
35
+ /**
36
+ * Construct a new AI settings widget
37
+ * @param options - The options for initializing the widget
38
+ */
39
+ constructor(options) {
40
+ super();
41
+ this._settingsModel = options.settingsModel;
42
+ this._agentManagerFactory = options.agentManagerFactory;
43
+ this._themeManager = options.themeManager;
44
+ this._providerRegistry = options.providerRegistry;
45
+ this._secretsAccess = options.secretsAccess;
46
+ this._trans = options.trans;
47
+ this._mcpServerRenderer = options.mcpServerRenderer;
48
+ this.id = 'jupyternaut-persona-settings';
49
+ this.title.label = this._trans.__('Jupyternaut Settings');
50
+ this.title.caption = this._trans.__('Configure AI providers and behavior');
51
+ this.title.closable = true;
52
+ // Disable the secrets manager if the token is empty.
53
+ if (!options.secretsAccess.isAvailable) {
54
+ this._settingsModel.updateConfig({ useSecretsManager: false });
55
+ this._secretsAccess = undefined;
56
+ }
57
+ }
58
+ /**
59
+ * Render the AI settings component
60
+ * @returns A React element containing the AI settings interface
61
+ */
62
+ render() {
63
+ return (React.createElement(AISettingsComponent, { model: this._settingsModel, agentManagerFactory: this._agentManagerFactory, themeManager: this._themeManager, providerRegistry: this._providerRegistry, secretsAccess: this._secretsAccess, trans: this._trans, McpServerRenderer: this._mcpServerRenderer }));
64
+ }
65
+ _settingsModel;
66
+ _agentManagerFactory;
67
+ _themeManager;
68
+ _providerRegistry;
69
+ _secretsAccess;
70
+ _trans;
71
+ _mcpServerRenderer;
72
+ }
73
+ /**
74
+ * The main AI settings component that provides configuration UI
75
+ * @param props - Component props containing models and theme manager
76
+ * @returns A React component for AI settings configuration
77
+ */
78
+ const AISettingsComponent = ({ model, agentManagerFactory, themeManager, providerRegistry, secretsAccess, trans, McpServerRenderer }) => {
79
+ if (!model) {
80
+ return React.createElement("div", null, trans.__('Settings model not available'));
81
+ }
82
+ const [config, setConfig] = useState(model.config || {});
83
+ const [theme, setTheme] = useState(() => createJupyterLabTheme(themeManager));
84
+ const [activeTab, setActiveTab] = useState(0);
85
+ const [dialogOpen, setDialogOpen] = useState(false);
86
+ const [editingProvider, setEditingProvider] = useState();
87
+ const [menuAnchor, setMenuAnchor] = useState(null);
88
+ const [menuProviderId, setMenuProviderId] = useState('');
89
+ const [systemPromptValue, setSystemPromptValue] = useState(config.systemPrompt);
90
+ const systemPromptValueRef = React.useRef(config.systemPrompt);
91
+ const [completionPromptValue, setCompletionPromptValue] = useState(config.completionSystemPrompt);
92
+ const completionPromptValueRef = React.useRef(config.completionSystemPrompt);
93
+ /**
94
+ * Effect to listen for model state changes and update config
95
+ */
96
+ useEffect(() => {
97
+ if (!model || !model.stateChanged) {
98
+ return;
99
+ }
100
+ const onStateChanged = () => {
101
+ setConfig(model.config || {});
102
+ };
103
+ model.stateChanged.connect(onStateChanged);
104
+ return () => {
105
+ model.stateChanged.disconnect(onStateChanged);
106
+ };
107
+ }, [model]);
108
+ /**
109
+ * Effect to listen for theme changes and update the Material-UI theme
110
+ */
111
+ useEffect(() => {
112
+ if (!themeManager) {
113
+ return;
114
+ }
115
+ const updateTheme = () => {
116
+ setTheme(createJupyterLabTheme(themeManager));
117
+ };
118
+ themeManager.themeChanged.connect(updateTheme);
119
+ return () => {
120
+ themeManager.themeChanged.disconnect(updateTheme);
121
+ };
122
+ }, [themeManager]);
123
+ /**
124
+ * Effect to listen for MCP connection changes to re-render connection status
125
+ */
126
+ useEffect(() => {
127
+ if (!agentManagerFactory) {
128
+ return;
129
+ }
130
+ const onMCPConnectionChanged = () => {
131
+ // Force a re-render by updating the config state
132
+ setConfig(prevConfig => ({ ...prevConfig }));
133
+ };
134
+ agentManagerFactory.mcpConnectionChanged.connect(onMCPConnectionChanged);
135
+ return () => {
136
+ agentManagerFactory.mcpConnectionChanged.disconnect(onMCPConnectionChanged);
137
+ };
138
+ }, [agentManagerFactory]);
139
+ // Sync local state when config changes externally
140
+ useEffect(() => {
141
+ setSystemPromptValue(config.systemPrompt);
142
+ systemPromptValueRef.current = config.systemPrompt;
143
+ }, [config.systemPrompt]);
144
+ useEffect(() => {
145
+ setCompletionPromptValue(config.completionSystemPrompt);
146
+ completionPromptValueRef.current = config.completionSystemPrompt;
147
+ }, [config.completionSystemPrompt]);
148
+ const promptDebouncer = useMemo(() => new Debouncer(async () => {
149
+ await handleConfigUpdate({
150
+ systemPrompt: systemPromptValueRef.current,
151
+ completionSystemPrompt: completionPromptValueRef.current
152
+ });
153
+ }, 1000), []);
154
+ // Cleanup debouncer on unmount
155
+ useEffect(() => {
156
+ return () => {
157
+ promptDebouncer.dispose();
158
+ };
159
+ }, [promptDebouncer]);
160
+ const handleSystemPromptChange = (value) => {
161
+ setSystemPromptValue(value);
162
+ systemPromptValueRef.current = value;
163
+ void promptDebouncer.invoke();
164
+ };
165
+ const handleCompletionPromptChange = (value) => {
166
+ setCompletionPromptValue(value);
167
+ completionPromptValueRef.current = value;
168
+ void promptDebouncer.invoke();
169
+ };
170
+ const getSecretFromManager = async (provider, fieldName) => {
171
+ return secretsAccess?.get(`${provider}:${fieldName}`);
172
+ };
173
+ const setSecretToManager = async (provider, fieldName, value) => {
174
+ await secretsAccess?.set(`${provider}:${fieldName}`, value);
175
+ };
176
+ /**
177
+ * Attach a secrets field to the secrets manager.
178
+ * @param input - the DOm element to attach.
179
+ * @param provider - the name of the provider.
180
+ * @param fieldName - the name of the field.
181
+ */
182
+ const handleSecretField = async (input, provider, fieldName) => {
183
+ if (!(model.config.useSecretsManager && secretsAccess?.isAvailable)) {
184
+ return;
185
+ }
186
+ await secretsAccess.attach(`${provider}:${fieldName}`, input);
187
+ };
188
+ /**
189
+ * Handle adding a new AI provider
190
+ * @param providerConfig - The provider configuration to add
191
+ */
192
+ const handleAddProvider = async (providerConfig) => {
193
+ if (model.config.useSecretsManager &&
194
+ secretsAccess?.isAvailable &&
195
+ providerConfig.apiKey) {
196
+ providerConfig.apiKey = SECRETS_REPLACEMENT;
197
+ }
198
+ await model.addProvider(providerConfig);
199
+ };
200
+ /**
201
+ * Handle editing an existing AI provider
202
+ * @param providerConfig - The updated provider configuration
203
+ */
204
+ const handleEditProvider = async (providerConfig) => {
205
+ if (editingProvider) {
206
+ if (model.config.useSecretsManager &&
207
+ secretsAccess?.isAvailable &&
208
+ providerConfig.apiKey) {
209
+ providerConfig.apiKey = SECRETS_REPLACEMENT;
210
+ }
211
+ await model.updateProvider(editingProvider.id, providerConfig);
212
+ setEditingProvider(undefined);
213
+ }
214
+ };
215
+ /**
216
+ * Handle deleting an AI provider
217
+ * @param id - The ID of the provider to delete
218
+ */
219
+ const handleDeleteProvider = async (id) => {
220
+ await model.removeProvider(id);
221
+ setMenuAnchor(null);
222
+ };
223
+ /**
224
+ * Open the provider edit dialog
225
+ * @param provider - The provider to edit
226
+ */
227
+ const openEditDialog = async (provider) => {
228
+ // Retrieve the API key from the secrets manager if necessary.
229
+ if (model.config.useSecretsManager && secretsAccess?.isAvailable) {
230
+ provider.apiKey =
231
+ (await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
232
+ }
233
+ setEditingProvider(provider);
234
+ setDialogOpen(true);
235
+ setMenuAnchor(null);
236
+ };
237
+ /**
238
+ * Open the provider add dialog
239
+ */
240
+ const openAddDialog = () => {
241
+ setEditingProvider(undefined);
242
+ setDialogOpen(true);
243
+ };
244
+ /**
245
+ * Handle provider menu click
246
+ * @param event - The click event
247
+ * @param providerId - The ID of the provider
248
+ */
249
+ const handleMenuClick = (event, providerId) => {
250
+ setMenuAnchor(event.currentTarget);
251
+ setMenuProviderId(providerId);
252
+ };
253
+ /**
254
+ * Handle provider menu close
255
+ */
256
+ const handleMenuClose = () => {
257
+ setMenuAnchor(null);
258
+ setMenuProviderId('');
259
+ };
260
+ /**
261
+ * Handle updating AI configuration
262
+ * @param updates - Partial configuration updates to apply
263
+ */
264
+ const handleConfigUpdate = async (updates) => {
265
+ if (updates.useSecretsManager !== undefined) {
266
+ if (updates.useSecretsManager) {
267
+ for (const provider of model.config.providers) {
268
+ const settingsApiKey = provider.apiKey;
269
+ // If the secrets manager doesn't have the current API key, set the current
270
+ // one from settings.
271
+ // Update the settings value with SECRETS_REPLACEMENT if a key exist in the
272
+ // secrets manager (was already there or a value was set in settings).
273
+ if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
274
+ if (settingsApiKey !== undefined) {
275
+ setSecretToManager(provider.provider, 'apiKey', settingsApiKey !== SECRETS_REPLACEMENT ? settingsApiKey : '');
276
+ provider.apiKey = SECRETS_REPLACEMENT;
277
+ await model.updateProvider(provider.id, provider);
278
+ }
279
+ }
280
+ else {
281
+ provider.apiKey = SECRETS_REPLACEMENT;
282
+ await model.updateProvider(provider.id, provider);
283
+ }
284
+ }
285
+ }
286
+ else {
287
+ for (const provider of model.config.providers) {
288
+ const apiKey = await getSecretFromManager(provider.provider, 'apiKey');
289
+ provider.apiKey = apiKey;
290
+ await model.updateProvider(provider.id, provider);
291
+ }
292
+ }
293
+ }
294
+ await model.updateConfig(updates);
295
+ };
296
+ return (React.createElement(ThemeProvider, { theme: theme },
297
+ React.createElement(Box, { sx: {
298
+ height: '100%',
299
+ maxHeight: '100vh',
300
+ overflow: 'auto',
301
+ p: 2,
302
+ pb: 4,
303
+ boxSizing: 'border-box',
304
+ fontSize: '0.9rem'
305
+ } },
306
+ React.createElement(Box, { sx: { mb: 2, display: 'flex', alignItems: 'center', gap: 2 } },
307
+ React.createElement(Settings, { color: "primary", sx: { fontSize: 24 } }),
308
+ React.createElement(Typography, { variant: "h5", component: "h1", sx: { fontWeight: 600 } }, trans.__('AI Settings'))),
309
+ React.createElement(Box, { sx: { borderBottom: 1, borderColor: 'divider', mb: 2 } },
310
+ React.createElement(Tabs, { value: activeTab, onChange: (_, newValue) => setActiveTab(newValue) },
311
+ React.createElement(Tab, { label: trans.__('Providers') }),
312
+ React.createElement(Tab, { label: trans.__('Behavior') }),
313
+ McpServerRenderer && React.createElement(Tab, { label: trans.__('MCP Servers') }))),
314
+ activeTab === 0 && (React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 } },
315
+ config.providers.length > 0 && (React.createElement(Card, { elevation: 2 },
316
+ React.createElement(CardContent, null,
317
+ React.createElement(Typography, { variant: "h6", component: "h2", gutterBottom: true }, trans.__('Default Providers')),
318
+ React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 } },
319
+ React.createElement(FormControl, { fullWidth: true },
320
+ React.createElement(InputLabel, null, trans.__('Chat Provider')),
321
+ React.createElement(Select, { value: config.defaultProvider, label: trans.__('Chat Provider'), onChange: e => model.setActiveProvider(e.target.value) }, config.providers.map(provider => (React.createElement(MenuItem, { key: provider.id, value: provider.id }, provider.name))))),
322
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSameProviderForChatAndCompleter, onChange: e => handleConfigUpdate({
323
+ useSameProviderForChatAndCompleter: e.target.checked
324
+ }), color: "primary" }), label: trans.__('Use same provider for chat and completions') }),
325
+ !config.useSameProviderForChatAndCompleter && (React.createElement(FormControl, { fullWidth: true },
326
+ React.createElement(InputLabel, null, trans.__('Completion Provider')),
327
+ React.createElement(Select, { value: config.activeCompleterProvider || '', label: trans.__('Completion Provider'), className: "jp-ai-completion-provider-select", onChange: e => model.setActiveCompleterProvider(e.target.value || undefined) },
328
+ React.createElement(MenuItem, { value: "" },
329
+ React.createElement("em", null, trans.__('No completion'))),
330
+ config.providers.map(provider => (React.createElement(MenuItem, { key: provider.id, value: provider.id }, provider.name)))))))))),
331
+ React.createElement(Card, { elevation: 2 },
332
+ React.createElement(CardContent, null,
333
+ React.createElement(Box, { sx: {
334
+ display: 'flex',
335
+ alignItems: 'center',
336
+ justifyContent: 'space-between',
337
+ mb: 2
338
+ } },
339
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
340
+ React.createElement(Typography, { variant: "h6", component: "h2" }, trans.__('Configured Providers'))),
341
+ React.createElement(Button, { variant: "contained", startIcon: React.createElement(Add, null), onClick: openAddDialog, size: "small" }, trans.__('Add Provider'))),
342
+ config.providers.length === 0 ? (React.createElement(Alert, { severity: "info" }, trans.__('No providers configured yet. Click "Add Provider" to get started.'))) : (React.createElement(List, null, config.providers.map(provider => {
343
+ const isActive = config.defaultProvider === provider.id;
344
+ const isActiveCompleter = config.useSameProviderForChatAndCompleter
345
+ ? isActive
346
+ : config.activeCompleterProvider === provider.id;
347
+ const providerInfo = providerRegistry.getProviderInfo(provider.provider);
348
+ const providerToolCapabilities = providerInfo?.providerToolCapabilities;
349
+ const params = provider.parameters;
350
+ const effectiveContextWindow = getEffectiveContextWindow(provider, providerRegistry);
351
+ const webSearchEnabled = !!providerToolCapabilities?.webSearch &&
352
+ provider.customSettings?.webSearch?.enabled === true;
353
+ const webFetchEnabled = !!providerToolCapabilities?.webFetch &&
354
+ provider.customSettings?.webFetch?.enabled === true;
355
+ return (React.createElement(ListItem, { key: provider.id, sx: {
356
+ flexDirection: 'column',
357
+ alignItems: 'stretch',
358
+ py: 2
359
+ } },
360
+ React.createElement(Box, { sx: {
361
+ display: 'flex',
362
+ justifyContent: 'space-between',
363
+ alignItems: 'flex-start',
364
+ width: '100%',
365
+ mb: 1
366
+ } },
367
+ React.createElement(Box, { sx: { flex: 1 } },
368
+ React.createElement(Box, { sx: {
369
+ display: 'flex',
370
+ alignItems: 'center',
371
+ gap: 1,
372
+ mb: 0.5
373
+ } },
374
+ React.createElement(Typography, { variant: "subtitle1", fontWeight: "medium" }, provider.name),
375
+ isActive && (React.createElement(Chip, { label: trans.__('Chat'), size: "small", color: "primary", icon: React.createElement(CheckCircle, null) })),
376
+ isActiveCompleter && (React.createElement(Chip, { label: trans.__('Completion'), size: "small", color: "secondary", icon: React.createElement(CheckCircle, null) }))),
377
+ React.createElement(Typography, { variant: "body2", color: "text.secondary", gutterBottom: true },
378
+ provider.provider,
379
+ " \u2022 ",
380
+ provider.model,
381
+ provider.description &&
382
+ ` • ${provider.description}`),
383
+ (params?.temperature !== undefined ||
384
+ params?.maxOutputTokens !== undefined ||
385
+ params?.maxTurns !== undefined ||
386
+ effectiveContextWindow !== undefined ||
387
+ webSearchEnabled ||
388
+ webFetchEnabled) && (React.createElement(Box, { sx: {
389
+ display: 'flex',
390
+ flexWrap: 'wrap',
391
+ gap: 1,
392
+ mt: 1
393
+ } },
394
+ params?.temperature !== undefined && (React.createElement(Chip, { label: trans.__('Temp: %1', params.temperature), size: "small", variant: "outlined" })),
395
+ params?.maxOutputTokens !== undefined && (React.createElement(Chip, { label: trans.__('Tokens: %1', params.maxOutputTokens), size: "small", variant: "outlined" })),
396
+ params?.maxTurns !== undefined && (React.createElement(Chip, { label: trans.__('Turns: %1', params.maxTurns), size: "small", variant: "outlined" })),
397
+ effectiveContextWindow !== undefined && (React.createElement(Chip, { label: trans.__('Context: %1', effectiveContextWindow), size: "small", variant: "outlined" })),
398
+ webSearchEnabled && (React.createElement(Chip, { label: trans.__('Web Search'), size: "small", variant: "outlined", color: "info" })),
399
+ webFetchEnabled && (React.createElement(Chip, { label: trans.__('Web Fetch'), size: "small", variant: "outlined", color: "info" }))))),
400
+ React.createElement(IconButton, { onClick: e => handleMenuClick(e, provider.id), size: "small" },
401
+ React.createElement(MoreVert, null)))));
402
+ }))))),
403
+ secretsAccess?.isAvailable && (React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.useSecretsManager, onChange: e => handleConfigUpdate({
404
+ useSecretsManager: e.target.checked
405
+ }), color: "primary", sx: { alignSelf: 'flex-start' } }), label: React.createElement("div", null,
406
+ React.createElement("span", null, trans.__('Use the secrets manager to manage API keys')),
407
+ !config.useSecretsManager && (React.createElement(Alert, { severity: "warning", icon: React.createElement(Error, null), sx: { mb: 2 } }, trans.__('The secrets are stored in plain text in settings')))) })))),
408
+ activeTab === 1 && (React.createElement(Card, { elevation: 2 },
409
+ React.createElement(CardContent, null,
410
+ React.createElement(Typography, { variant: "h6", component: "h2", gutterBottom: true }, trans.__('Behavior Settings')),
411
+ React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2 } },
412
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.toolsEnabled, onChange: e => handleConfigUpdate({
413
+ toolsEnabled: e.target.checked
414
+ }), color: "primary" }), label: React.createElement(Box, null,
415
+ React.createElement(Typography, { variant: "body1" }, trans.__('Enable Tools')),
416
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Allow the AI to use tools like notebook operations, code execution, and file management'))) }),
417
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showCellDiff, onChange: e => handleConfigUpdate({
418
+ showCellDiff: e.target.checked
419
+ }), color: "primary" }), label: React.createElement(Box, null,
420
+ React.createElement(Typography, { variant: "body1" }, trans.__('Show Cell Diff')),
421
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Show diff view when AI modifies cell content'))) }),
422
+ config.showCellDiff && (React.createElement(FormControl, { sx: { ml: 4 } },
423
+ React.createElement(InputLabel, null, trans.__('Diff Display Mode')),
424
+ React.createElement(Select, { value: config.diffDisplayMode, label: trans.__('Diff Display Mode'), onChange: e => handleConfigUpdate({
425
+ diffDisplayMode: e.target.value
426
+ }) },
427
+ React.createElement(MenuItem, { value: "split" }, trans.__('Split View')),
428
+ React.createElement(MenuItem, { value: "unified" }, trans.__('Unified View'))))),
429
+ React.createElement(FormControlLabel, { control: React.createElement(Switch, { checked: config.showFileDiff, onChange: e => handleConfigUpdate({
430
+ showFileDiff: e.target.checked
431
+ }), color: "primary" }), label: React.createElement(Box, null,
432
+ React.createElement(Typography, { variant: "body1" }, trans.__('Show File Diff')),
433
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, trans.__('Show diff view when AI modifies file content'))) }),
434
+ React.createElement(Divider, { sx: { my: 1 } }),
435
+ React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('System Prompt'), value: systemPromptValue, onChange: e => handleSystemPromptChange(e.target.value), placeholder: trans.__("Define the AI's behavior and personality..."), helperText: trans.__('Instructions that define how the AI should behave and respond') }),
436
+ React.createElement(TextField, { fullWidth: true, multiline: true, rows: 3, label: trans.__('Completion System Prompt'), value: completionPromptValue, onChange: e => handleCompletionPromptChange(e.target.value), placeholder: trans.__('Define how the AI should generate code completions...'), helperText: trans.__('Instructions that define how the AI should generate code completions') }),
437
+ React.createElement(Divider, { sx: { my: 2 } }),
438
+ React.createElement(Box, null,
439
+ React.createElement(Typography, { variant: "body1", gutterBottom: true, sx: {
440
+ display: 'inline-flex',
441
+ alignItems: 'center',
442
+ gap: 1
443
+ } },
444
+ trans.__('Skills Paths'),
445
+ React.createElement(Tooltip, { title: trans.__('Directories containing agent skills, relative to the server root. Skills are loaded from all paths; the first occurrence of a skill name takes priority.') },
446
+ React.createElement(InfoOutlined, { sx: { fontSize: 16 } }))),
447
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.skillsPaths ?? []).map((skillPath, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
448
+ const newPaths = [...config.skillsPaths];
449
+ newPaths.splice(index, 1);
450
+ handleConfigUpdate({ skillsPaths: newPaths });
451
+ }, size: "small" },
452
+ React.createElement(Delete, null)) },
453
+ React.createElement(ListItemText, { primary: skillPath }))))),
454
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Skills Path'), placeholder: trans.__('e.g., .claude/skills'), onKeyDown: e => {
455
+ if (e.key === 'Enter') {
456
+ const value = e.target.value.trim();
457
+ if (value &&
458
+ !(config.skillsPaths ?? []).includes(value)) {
459
+ const newPaths = [
460
+ ...(config.skillsPaths ?? []),
461
+ value
462
+ ];
463
+ handleConfigUpdate({ skillsPaths: newPaths });
464
+ e.target.value = '';
465
+ }
466
+ }
467
+ }, helperText: trans.__('Press Enter to add a path. Defaults: .agents/skills, _agents/skills') })),
468
+ React.createElement(Divider, { sx: { my: 2 } }),
469
+ React.createElement(Box, null,
470
+ React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Requiring Approval')),
471
+ React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Commands that require user approval before AI can execute them')),
472
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, config.commandsRequiringApproval.map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
473
+ const newCommands = [
474
+ ...config.commandsRequiringApproval
475
+ ];
476
+ newCommands.splice(index, 1);
477
+ handleConfigUpdate({
478
+ commandsRequiringApproval: newCommands
479
+ });
480
+ }, size: "small" },
481
+ React.createElement(Delete, null)) },
482
+ React.createElement(ListItemText, { primary: command }))))),
483
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add New Command'), placeholder: trans.__('e.g., notebook:run-cell'), onKeyDown: e => {
484
+ if (e.key === 'Enter') {
485
+ const value = e.target.value.trim();
486
+ if (value &&
487
+ !config.commandsRequiringApproval.includes(value)) {
488
+ const newCommands = [
489
+ ...config.commandsRequiringApproval,
490
+ value
491
+ ];
492
+ handleConfigUpdate({
493
+ commandsRequiringApproval: newCommands
494
+ });
495
+ e.target.value = '';
496
+ }
497
+ }
498
+ }, helperText: trans.__('Press Enter to add a command. Common commands: notebook:run-cell, console:execute, fileeditor:run-code') })),
499
+ React.createElement(Divider, { sx: { my: 2 } }),
500
+ React.createElement(Box, null,
501
+ React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Commands Auto-Rendering MIME Bundles')),
502
+ React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('Only these execute_command command IDs can auto-render MIME bundle outputs in chat')),
503
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.commandsAutoRenderMimeBundles ?? []).map((command, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
504
+ const newCommands = [
505
+ ...(config.commandsAutoRenderMimeBundles ??
506
+ [])
507
+ ];
508
+ newCommands.splice(index, 1);
509
+ handleConfigUpdate({
510
+ commandsAutoRenderMimeBundles: newCommands
511
+ });
512
+ }, size: "small" },
513
+ React.createElement(Delete, null)) },
514
+ React.createElement(ListItemText, { primary: command }))))),
515
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Auto-Render Command'), placeholder: trans.__('e.g., jupyterlab-ai-commands:execute-in-kernel'), onKeyDown: e => {
516
+ if (e.key === 'Enter') {
517
+ const value = e.target.value.trim();
518
+ const existingCommands = config.commandsAutoRenderMimeBundles ?? [];
519
+ if (value && !existingCommands.includes(value)) {
520
+ const newCommands = [...existingCommands, value];
521
+ handleConfigUpdate({
522
+ commandsAutoRenderMimeBundles: newCommands
523
+ });
524
+ e.target.value = '';
525
+ }
526
+ }
527
+ }, helperText: trans.__('Press Enter to add a command. Default: jupyterlab-ai-commands:execute-in-kernel') })),
528
+ React.createElement(Divider, { sx: { my: 2 } }),
529
+ React.createElement(Box, null,
530
+ React.createElement(Typography, { variant: "body1", gutterBottom: true }, trans.__('Trusted MIME Types for Auto-Render')),
531
+ React.createElement(Typography, { variant: "caption", color: "text.secondary", gutterBottom: true, sx: { display: 'block' } }, trans.__('When auto-rendering command outputs, these MIME types are marked trusted in chat')),
532
+ React.createElement(List, { sx: { mb: 2, maxHeight: 200, overflow: 'auto' } }, (config.trustedMimeTypesForAutoRender ?? []).map((mimeType, index) => (React.createElement(ListItem, { key: index, divider: true, secondaryAction: React.createElement(IconButton, { onClick: () => {
533
+ const newMimeTypes = [
534
+ ...(config.trustedMimeTypesForAutoRender ??
535
+ [])
536
+ ];
537
+ newMimeTypes.splice(index, 1);
538
+ handleConfigUpdate({
539
+ trustedMimeTypesForAutoRender: newMimeTypes
540
+ });
541
+ }, size: "small" },
542
+ React.createElement(Delete, null)) },
543
+ React.createElement(ListItemText, { primary: mimeType }))))),
544
+ React.createElement(TextField, { fullWidth: true, label: trans.__('Add Trusted MIME Type'), placeholder: trans.__('e.g., text/html'), onKeyDown: e => {
545
+ if (e.key === 'Enter') {
546
+ const value = e.target.value.trim();
547
+ const existingMimeTypes = config.trustedMimeTypesForAutoRender ?? [];
548
+ if (value && !existingMimeTypes.includes(value)) {
549
+ const newMimeTypes = [...existingMimeTypes, value];
550
+ handleConfigUpdate({
551
+ trustedMimeTypesForAutoRender: newMimeTypes
552
+ });
553
+ e.target.value = '';
554
+ }
555
+ }
556
+ }, helperText: trans.__('Press Enter to add a MIME type. Default: text/html') })))))),
557
+ activeTab === 2 && (React.createElement(Card, { elevation: 2 },
558
+ React.createElement(CardContent, null, McpServerRenderer && React.createElement(McpServerRenderer, null)))),
559
+ React.createElement(ProviderConfigDialog, { open: dialogOpen, onClose: () => setDialogOpen(false), onSave: editingProvider ? handleEditProvider : handleAddProvider, initialConfig: editingProvider, mode: editingProvider ? 'edit' : 'add', providerRegistry: providerRegistry, handleSecretField: handleSecretField, trans: trans }),
560
+ React.createElement(Menu, { anchorEl: menuAnchor, open: Boolean(menuAnchor), onClose: handleMenuClose },
561
+ React.createElement(MenuItem, { onClick: () => {
562
+ const provider = config.providers.find(p => p.id === menuProviderId);
563
+ if (provider) {
564
+ openEditDialog(provider);
565
+ }
566
+ } },
567
+ React.createElement(Edit, { sx: { mr: 1 } }),
568
+ trans.__('Edit')),
569
+ React.createElement(MenuItem, { onClick: () => handleDeleteProvider(menuProviderId), sx: { color: 'error.main' } },
570
+ React.createElement(Delete, { sx: { mr: 1 } }),
571
+ trans.__('Delete'))))));
572
+ };
@@ -0,0 +1,16 @@
1
+ import type { IProviderConfig, IProviderRegistry } from '@jupyternaut/agent';
2
+ import type { TranslationBundle } from '@jupyterlab/translation';
3
+ import React from 'react';
4
+ interface IProviderConfigDialogProps {
5
+ open: boolean;
6
+ onClose: () => void;
7
+ onSave: (config: Omit<IProviderConfig, 'id'>) => void;
8
+ initialConfig?: IProviderConfig;
9
+ mode: 'add' | 'edit';
10
+ providerRegistry: IProviderRegistry;
11
+ handleSecretField: (input: HTMLInputElement, provider: string, fieldName: string) => Promise<void>;
12
+ trans: TranslationBundle;
13
+ }
14
+ export declare const ProviderConfigDialog: React.FC<IProviderConfigDialogProps>;
15
+ export {};
16
+ //# sourceMappingURL=provider-config-dialog.d.ts.map