@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,997 @@
1
+ import { getProviderModelInfo } from '@jupyternaut/agent';
2
+ import type {
3
+ IProviderConfig,
4
+ IProviderParameters,
5
+ IProviderRegistry,
6
+ IProviderToolCapabilities
7
+ } from '@jupyternaut/agent';
8
+ import type { TranslationBundle } from '@jupyterlab/translation';
9
+ import ExpandMore from '@mui/icons-material/ExpandMore';
10
+ import Delete from '@mui/icons-material/Delete';
11
+ import Visibility from '@mui/icons-material/Visibility';
12
+ import VisibilityOff from '@mui/icons-material/VisibilityOff';
13
+ import {
14
+ Accordion,
15
+ AccordionDetails,
16
+ AccordionSummary,
17
+ Autocomplete,
18
+ Box,
19
+ Button,
20
+ Chip,
21
+ Dialog,
22
+ DialogActions,
23
+ DialogContent,
24
+ DialogTitle,
25
+ FormControl,
26
+ FormControlLabel,
27
+ IconButton,
28
+ InputAdornment,
29
+ InputLabel,
30
+ List,
31
+ ListItem,
32
+ ListItemText,
33
+ MenuItem,
34
+ Select,
35
+ Slider,
36
+ Switch,
37
+ TextField,
38
+ Typography
39
+ } from '@mui/material';
40
+ import React from 'react';
41
+
42
+ /**
43
+ * Default parameter values for provider configuration
44
+ */
45
+ const DEFAULT_TEMPERATURE = 0.7;
46
+ const DEFAULT_MAX_TURNS = 25;
47
+ type DomainSection = 'webSearch' | 'webFetch';
48
+ type DomainKey = 'allowedDomains' | 'blockedDomains';
49
+ type DomainFieldId = `${DomainSection}.${DomainKey}`;
50
+
51
+ const DOMAIN_FIELD_MAP: Record<
52
+ DomainFieldId,
53
+ { section: DomainSection; key: DomainKey }
54
+ > = {
55
+ 'webSearch.allowedDomains': {
56
+ section: 'webSearch',
57
+ key: 'allowedDomains'
58
+ },
59
+ 'webSearch.blockedDomains': {
60
+ section: 'webSearch',
61
+ key: 'blockedDomains'
62
+ },
63
+ 'webFetch.allowedDomains': {
64
+ section: 'webFetch',
65
+ key: 'allowedDomains'
66
+ },
67
+ 'webFetch.blockedDomains': {
68
+ section: 'webFetch',
69
+ key: 'blockedDomains'
70
+ }
71
+ };
72
+
73
+ function createEmptyDomainInputs(): Record<DomainFieldId, string> {
74
+ return {
75
+ 'webSearch.allowedDomains': '',
76
+ 'webSearch.blockedDomains': '',
77
+ 'webFetch.allowedDomains': '',
78
+ 'webFetch.blockedDomains': ''
79
+ };
80
+ }
81
+
82
+ function toRecord(value: unknown): Record<string, any> {
83
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
84
+ return value as Record<string, any>;
85
+ }
86
+ return {};
87
+ }
88
+
89
+ function toStringArray(value: unknown): string[] {
90
+ if (!Array.isArray(value)) {
91
+ return [];
92
+ }
93
+ return value.filter((item): item is string => typeof item === 'string');
94
+ }
95
+
96
+ function sanitizeCustomSettingsForProvider(
97
+ customSettings: Record<string, any>,
98
+ capabilities?: IProviderToolCapabilities
99
+ ): Record<string, any> {
100
+ const result: Record<string, any> = { ...customSettings };
101
+ const webSearch = toRecord(customSettings.webSearch);
102
+ const webFetch = toRecord(customSettings.webFetch);
103
+ const supportsWebSearch = !!capabilities?.webSearch;
104
+ const supportsWebFetch = !!capabilities?.webFetch;
105
+
106
+ if (supportsWebSearch && webSearch.enabled === true) {
107
+ result.webSearch = webSearch;
108
+ } else {
109
+ delete result.webSearch;
110
+ }
111
+
112
+ if (supportsWebFetch && webFetch.enabled === true) {
113
+ result.webFetch = webFetch;
114
+ } else {
115
+ delete result.webFetch;
116
+ }
117
+
118
+ return result;
119
+ }
120
+
121
+ interface IProviderConfigDialogProps {
122
+ open: boolean;
123
+ onClose: () => void;
124
+ onSave: (config: Omit<IProviderConfig, 'id'>) => void;
125
+ initialConfig?: IProviderConfig;
126
+ mode: 'add' | 'edit';
127
+ providerRegistry: IProviderRegistry;
128
+ handleSecretField: (
129
+ input: HTMLInputElement,
130
+ provider: string,
131
+ fieldName: string
132
+ ) => Promise<void>;
133
+ trans: TranslationBundle;
134
+ }
135
+
136
+ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
137
+ open,
138
+ onClose,
139
+ onSave,
140
+ initialConfig,
141
+ mode,
142
+ providerRegistry,
143
+ handleSecretField,
144
+ trans
145
+ }) => {
146
+ const [name, setName] = React.useState(initialConfig?.name || '');
147
+ const [provider, setProvider] = React.useState(
148
+ initialConfig?.provider || 'anthropic'
149
+ );
150
+ const [model, setModel] = React.useState(initialConfig?.model || '');
151
+ const [apiKey, setApiKey] = React.useState(initialConfig?.apiKey || '');
152
+ const [baseURL, setBaseURL] = React.useState(initialConfig?.baseURL || '');
153
+ const [showApiKey, setShowApiKey] = React.useState(false);
154
+ const [customSettings, setCustomSettings] = React.useState<
155
+ Record<string, any>
156
+ >(initialConfig?.customSettings || {});
157
+ const [domainInputs, setDomainInputs] = React.useState<
158
+ Record<DomainFieldId, string>
159
+ >(createEmptyDomainInputs());
160
+
161
+ const [parameters, setParameters] = React.useState<IProviderParameters>(
162
+ initialConfig?.parameters || {}
163
+ );
164
+
165
+ const [expandedAdvanced, setExpandedAdvanced] = React.useState(false);
166
+ const selectedProviderInfo = React.useMemo(
167
+ () => providerRegistry.getProviderInfo(provider),
168
+ [providerRegistry, provider]
169
+ );
170
+ const providerToolCapabilities =
171
+ selectedProviderInfo?.providerToolCapabilities;
172
+ const selectedModelInfo = React.useMemo(
173
+ () => getProviderModelInfo(selectedProviderInfo, model),
174
+ [selectedProviderInfo, model]
175
+ );
176
+ const webSearchImplementation =
177
+ providerToolCapabilities?.webSearch?.implementation;
178
+ const supportsWebSearch = !!providerToolCapabilities?.webSearch;
179
+ const supportsWebFetch = !!providerToolCapabilities?.webFetch;
180
+ const webSearchSettings = React.useMemo(
181
+ () => toRecord(customSettings.webSearch),
182
+ [customSettings]
183
+ );
184
+ const webFetchSettings = React.useMemo(
185
+ () => toRecord(customSettings.webFetch),
186
+ [customSettings]
187
+ );
188
+
189
+ // Get provider options from registry
190
+ const providerOptions = React.useMemo(() => {
191
+ const providers = providerRegistry.providers;
192
+ return Object.keys(providers).map(id => {
193
+ const info = providers[id];
194
+ return {
195
+ value: id,
196
+ label: info.name,
197
+ models: info.defaultModels,
198
+ apiKeyRequirement: info.apiKeyRequirement,
199
+ supportsBaseURL: info.supportsBaseURL,
200
+ description: info.description,
201
+ baseUrls: info.baseUrls
202
+ };
203
+ });
204
+ }, [providerRegistry]);
205
+
206
+ const selectedProvider = providerOptions.find(p => p.value === provider);
207
+
208
+ React.useEffect(() => {
209
+ if (open) {
210
+ // Reset form when dialog opens
211
+ const initialProvider = initialConfig?.provider || 'anthropic';
212
+ const initialProviderInfo =
213
+ providerRegistry.getProviderInfo(initialProvider);
214
+ setName(initialConfig?.name || '');
215
+ setProvider(initialProvider);
216
+ setModel(
217
+ initialConfig?.model || initialProviderInfo?.defaultModels[0] || ''
218
+ );
219
+ setApiKey(initialConfig?.apiKey || '');
220
+ setBaseURL(initialConfig?.baseURL || '');
221
+ setParameters(initialConfig?.parameters || {});
222
+ setCustomSettings(initialConfig?.customSettings || {});
223
+ setDomainInputs(createEmptyDomainInputs());
224
+ setShowApiKey(false);
225
+ setExpandedAdvanced(false);
226
+ } else {
227
+ // Reset expanded state when dialog closes
228
+ setDomainInputs(createEmptyDomainInputs());
229
+ setExpandedAdvanced(false);
230
+ }
231
+ }, [open, initialConfig, providerRegistry]);
232
+
233
+ const handleRef = React.useCallback(
234
+ (node: HTMLInputElement | null) => {
235
+ if (open && node) {
236
+ handleSecretField(node, provider, 'apiKey');
237
+ }
238
+ },
239
+ [provider, handleSecretField, open]
240
+ );
241
+
242
+ const handleProviderChange = React.useCallback(
243
+ (newProvider: IProviderConfig['provider']) => {
244
+ const newProviderInfo = providerRegistry.getProviderInfo(newProvider);
245
+ setProvider(newProvider);
246
+ setModel(newProviderInfo?.defaultModels[0] || '');
247
+ },
248
+ [providerRegistry]
249
+ );
250
+
251
+ const updateCustomSetting = React.useCallback(
252
+ (section: 'webSearch' | 'webFetch', key: string, value: unknown) => {
253
+ setCustomSettings(prev => {
254
+ const next = { ...prev };
255
+ const sectionSettings = { ...toRecord(next[section]) };
256
+ const shouldDelete =
257
+ value === undefined ||
258
+ value === null ||
259
+ value === '' ||
260
+ (Array.isArray(value) && value.length === 0);
261
+
262
+ if (shouldDelete) {
263
+ delete sectionSettings[key];
264
+ } else {
265
+ sectionSettings[key] = value;
266
+ }
267
+
268
+ if (Object.keys(sectionSettings).length === 0) {
269
+ delete next[section];
270
+ } else {
271
+ next[section] = sectionSettings;
272
+ }
273
+
274
+ return next;
275
+ });
276
+ },
277
+ []
278
+ );
279
+
280
+ const addDomainValue = React.useCallback(
281
+ (fieldId: DomainFieldId) => {
282
+ const valueToAdd = domainInputs[fieldId].trim();
283
+ if (!valueToAdd) {
284
+ return;
285
+ }
286
+
287
+ const { section, key } = DOMAIN_FIELD_MAP[fieldId];
288
+ const currentValues = toStringArray(
289
+ toRecord(customSettings[section])[key]
290
+ );
291
+ if (currentValues.includes(valueToAdd)) {
292
+ setDomainInputs(prev => ({
293
+ ...prev,
294
+ [fieldId]: ''
295
+ }));
296
+ return;
297
+ }
298
+ const nextValues = [...currentValues, valueToAdd];
299
+
300
+ updateCustomSetting(section, key, nextValues);
301
+ setDomainInputs(prev => ({
302
+ ...prev,
303
+ [fieldId]: ''
304
+ }));
305
+ },
306
+ [customSettings, domainInputs, updateCustomSetting]
307
+ );
308
+
309
+ const removeDomainValue = React.useCallback(
310
+ (fieldId: DomainFieldId, valueToRemove: string) => {
311
+ const { section, key } = DOMAIN_FIELD_MAP[fieldId];
312
+ const currentValues = toStringArray(
313
+ toRecord(customSettings[section])[key]
314
+ );
315
+ const nextValues = currentValues.filter(value => value !== valueToRemove);
316
+ updateCustomSetting(
317
+ section,
318
+ key,
319
+ nextValues.length > 0 ? nextValues : undefined
320
+ );
321
+ },
322
+ [customSettings, updateCustomSetting]
323
+ );
324
+
325
+ const renderDomainList = React.useCallback(
326
+ (
327
+ fieldId: DomainFieldId,
328
+ label: string,
329
+ placeholder: string,
330
+ values: unknown
331
+ ) => {
332
+ const domainValues = toStringArray(values);
333
+
334
+ return (
335
+ <Box>
336
+ <Typography variant="body2" gutterBottom>
337
+ {label}
338
+ </Typography>
339
+ <List
340
+ dense
341
+ sx={{
342
+ mb: 1,
343
+ maxHeight: 160,
344
+ overflow: 'auto',
345
+ border: 1,
346
+ borderColor: 'divider',
347
+ borderRadius: 1
348
+ }}
349
+ >
350
+ {domainValues.length === 0 ? (
351
+ <ListItem>
352
+ <ListItemText
353
+ secondary={trans.__('No domains added.')}
354
+ slotProps={{
355
+ secondary: {
356
+ color: 'text.secondary'
357
+ }
358
+ }}
359
+ />
360
+ </ListItem>
361
+ ) : (
362
+ domainValues.map(value => (
363
+ <ListItem
364
+ key={value}
365
+ secondaryAction={
366
+ <IconButton
367
+ onClick={() => removeDomainValue(fieldId, value)}
368
+ size="small"
369
+ >
370
+ <Delete fontSize="small" />
371
+ </IconButton>
372
+ }
373
+ >
374
+ <ListItemText primary={value} />
375
+ </ListItem>
376
+ ))
377
+ )}
378
+ </List>
379
+ <TextField
380
+ fullWidth
381
+ size="small"
382
+ label={trans.__('Add Domain')}
383
+ value={domainInputs[fieldId]}
384
+ onChange={e =>
385
+ setDomainInputs(prev => ({
386
+ ...prev,
387
+ [fieldId]: e.target.value
388
+ }))
389
+ }
390
+ onKeyDown={e => {
391
+ if (e.key === 'Enter') {
392
+ e.preventDefault();
393
+ addDomainValue(fieldId);
394
+ }
395
+ }}
396
+ placeholder={placeholder}
397
+ helperText={trans.__('Press Enter to add one domain.')}
398
+ />
399
+ </Box>
400
+ );
401
+ },
402
+ [addDomainValue, domainInputs, removeDomainValue, trans]
403
+ );
404
+
405
+ const handleSave = () => {
406
+ if (!name.trim() || !provider || !model) {
407
+ return;
408
+ }
409
+
410
+ // Only include parameters if at least one is set
411
+ const hasParameters = Object.keys(parameters).some(
412
+ key => parameters[key as keyof IProviderParameters] !== undefined
413
+ );
414
+ const sanitizedCustomSettings = sanitizeCustomSettingsForProvider(
415
+ customSettings,
416
+ providerToolCapabilities
417
+ );
418
+
419
+ const config: Omit<IProviderConfig, 'id'> = {
420
+ name: name.trim(),
421
+ provider: provider as IProviderConfig['provider'],
422
+ model,
423
+ ...(apiKey && { apiKey }),
424
+ ...(baseURL && { baseURL }),
425
+ ...(hasParameters && { parameters }),
426
+ ...(Object.keys(sanitizedCustomSettings).length > 0 && {
427
+ customSettings: sanitizedCustomSettings
428
+ })
429
+ };
430
+
431
+ onSave(config);
432
+ onClose();
433
+ };
434
+
435
+ const isValid =
436
+ name.trim() &&
437
+ provider &&
438
+ model &&
439
+ (selectedProvider?.apiKeyRequirement !== 'required' || apiKey);
440
+
441
+ return (
442
+ <Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
443
+ <DialogTitle>
444
+ {mode === 'add'
445
+ ? trans.__('Add New Provider')
446
+ : trans.__('Edit Provider')}
447
+ </DialogTitle>
448
+ <DialogContent>
449
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, pt: 1 }}>
450
+ <TextField
451
+ fullWidth
452
+ label={trans.__('Provider Name')}
453
+ value={name}
454
+ onChange={e => setName(e.target.value)}
455
+ placeholder={trans.__('e.g., My Anthropic Config, Work Provider')}
456
+ helperText={trans.__(
457
+ 'A friendly name to identify this provider configuration'
458
+ )}
459
+ required
460
+ />
461
+
462
+ <FormControl fullWidth required>
463
+ <InputLabel>{trans.__('Provider Type')}</InputLabel>
464
+ <Select
465
+ value={provider}
466
+ label={trans.__('Provider Type')}
467
+ onChange={e =>
468
+ handleProviderChange(
469
+ e.target.value as IProviderConfig['provider']
470
+ )
471
+ }
472
+ >
473
+ {providerOptions.map(option => (
474
+ <MenuItem key={option.value} value={option.value}>
475
+ <Box>
476
+ <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
477
+ {option.label}
478
+ {option.apiKeyRequirement === 'required' && (
479
+ <Chip
480
+ size="small"
481
+ label={trans.__('API Key')}
482
+ color="default"
483
+ variant="outlined"
484
+ />
485
+ )}
486
+ </Box>
487
+ {option.description && (
488
+ <Typography variant="caption" color="text.secondary">
489
+ {option.description}
490
+ </Typography>
491
+ )}
492
+ </Box>
493
+ </MenuItem>
494
+ ))}
495
+ </Select>
496
+ </FormControl>
497
+
498
+ <Autocomplete
499
+ freeSolo
500
+ fullWidth
501
+ options={selectedProvider?.models ?? []}
502
+ value={model}
503
+ onChange={(_, value) => {
504
+ setModel(typeof value === 'string' ? value : '');
505
+ }}
506
+ inputValue={model}
507
+ onInputChange={(_, value) => {
508
+ setModel(value);
509
+ }}
510
+ renderInput={params => (
511
+ <TextField
512
+ {...params}
513
+ fullWidth
514
+ label={trans.__('Model')}
515
+ placeholder={trans.__('Select or type a model ID')}
516
+ required
517
+ helperText={trans.__(
518
+ 'Choose from the list or enter a custom model ID'
519
+ )}
520
+ />
521
+ )}
522
+ clearOnBlur={false}
523
+ />
524
+
525
+ {selectedProvider &&
526
+ selectedProvider?.apiKeyRequirement !== 'none' && (
527
+ <TextField
528
+ fullWidth
529
+ inputRef={handleRef}
530
+ label={
531
+ selectedProvider?.apiKeyRequirement === 'required'
532
+ ? trans.__('API Key')
533
+ : trans.__('API Key (Optional)')
534
+ }
535
+ type={showApiKey ? 'text' : 'password'}
536
+ value={apiKey}
537
+ onChange={e => setApiKey(e.target.value)}
538
+ placeholder={trans.__('Enter your API key...')}
539
+ required={selectedProvider?.apiKeyRequirement === 'required'}
540
+ InputProps={{
541
+ endAdornment: (
542
+ <InputAdornment position="end">
543
+ <IconButton
544
+ onClick={() => setShowApiKey(!showApiKey)}
545
+ edge="end"
546
+ >
547
+ {showApiKey ? <VisibilityOff /> : <Visibility />}
548
+ </IconButton>
549
+ </InputAdornment>
550
+ )
551
+ }}
552
+ />
553
+ )}
554
+
555
+ {selectedProvider?.supportsBaseURL && (
556
+ <Autocomplete
557
+ freeSolo
558
+ fullWidth
559
+ options={(selectedProvider.baseUrls ?? []).map(
560
+ option => option.url
561
+ )}
562
+ value={baseURL || ''}
563
+ onChange={(_, value) => {
564
+ if (value && typeof value === 'string') {
565
+ setBaseURL(value);
566
+ }
567
+ }}
568
+ inputValue={baseURL || ''}
569
+ renderOption={(props, option) => {
570
+ const urlOption = (selectedProvider.baseUrls ?? []).find(
571
+ u => u.url === option
572
+ );
573
+ return (
574
+ <Box component="li" {...props} key={option}>
575
+ <Box>
576
+ <Typography variant="body2">{option}</Typography>
577
+ {urlOption?.description && (
578
+ <Typography variant="caption" color="text.secondary">
579
+ {urlOption.description}
580
+ </Typography>
581
+ )}
582
+ </Box>
583
+ </Box>
584
+ );
585
+ }}
586
+ renderInput={params => (
587
+ <TextField
588
+ {...params}
589
+ fullWidth
590
+ label={trans.__('Base URL')}
591
+ placeholder="https://api.example.com/v1"
592
+ onChange={e => setBaseURL(e.target.value)}
593
+ />
594
+ )}
595
+ clearOnBlur={false}
596
+ />
597
+ )}
598
+
599
+ {/* Advanced Settings Section */}
600
+ <Accordion
601
+ expanded={expandedAdvanced}
602
+ onChange={(_, isExpanded) => setExpandedAdvanced(isExpanded)}
603
+ sx={{
604
+ mt: 2,
605
+ bgcolor: 'transparent',
606
+ boxShadow: 'none',
607
+ border: 1,
608
+ borderColor: 'divider',
609
+ borderRadius: 1
610
+ }}
611
+ >
612
+ <AccordionSummary expandIcon={<ExpandMore />}>
613
+ <Typography variant="subtitle1" fontWeight="medium">
614
+ {trans.__('Advanced Settings')}
615
+ </Typography>
616
+ </AccordionSummary>
617
+ <AccordionDetails sx={{ bgcolor: 'transparent' }}>
618
+ <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
619
+ <Box>
620
+ <Typography gutterBottom>
621
+ {trans.__(
622
+ 'Temperature: %1',
623
+ parameters.temperature ?? trans.__('Default')
624
+ )}
625
+ </Typography>
626
+ <Slider
627
+ value={parameters.temperature ?? DEFAULT_TEMPERATURE}
628
+ onChange={(_, value) =>
629
+ setParameters({
630
+ ...parameters,
631
+ temperature: value as number
632
+ })
633
+ }
634
+ min={0}
635
+ max={2}
636
+ step={0.1}
637
+ valueLabelDisplay="auto"
638
+ />
639
+ <Typography variant="caption" color="text.secondary">
640
+ {trans.__(
641
+ 'Temperature for the model (lower values are more deterministic)'
642
+ )}
643
+ </Typography>
644
+ </Box>
645
+
646
+ <TextField
647
+ fullWidth
648
+ label={trans.__('Max Tokens (Optional)')}
649
+ type="number"
650
+ value={parameters.maxOutputTokens ?? ''}
651
+ onChange={e =>
652
+ setParameters({
653
+ ...parameters,
654
+ maxOutputTokens: e.target.value
655
+ ? Number(e.target.value)
656
+ : undefined
657
+ })
658
+ }
659
+ placeholder={trans.__('Leave empty for provider default')}
660
+ helperText={trans.__('Maximum length of AI responses')}
661
+ slotProps={{ htmlInput: { min: 1 } }}
662
+ />
663
+
664
+ <TextField
665
+ fullWidth
666
+ label={trans.__('Max Turns (Optional)')}
667
+ type="number"
668
+ value={parameters.maxTurns ?? ''}
669
+ onChange={e =>
670
+ setParameters({
671
+ ...parameters,
672
+ maxTurns: e.target.value
673
+ ? Number(e.target.value)
674
+ : undefined
675
+ })
676
+ }
677
+ placeholder={trans.__('Default: %1', DEFAULT_MAX_TURNS)}
678
+ helperText={trans.__(
679
+ 'Maximum number of tool execution turns'
680
+ )}
681
+ slotProps={{ htmlInput: { min: 1, max: 100 } }}
682
+ />
683
+
684
+ <TextField
685
+ fullWidth
686
+ label={trans.__('Context Window (Optional)')}
687
+ type="number"
688
+ value={parameters.contextWindow ?? ''}
689
+ onChange={e =>
690
+ setParameters({
691
+ ...parameters,
692
+ contextWindow: e.target.value
693
+ ? Number(e.target.value)
694
+ : undefined
695
+ })
696
+ }
697
+ placeholder={
698
+ selectedModelInfo?.contextWindow !== undefined
699
+ ? trans.__(
700
+ 'Default: %1',
701
+ selectedModelInfo.contextWindow.toLocaleString()
702
+ )
703
+ : trans.__('e.g., 128000')
704
+ }
705
+ helperText={
706
+ selectedModelInfo?.contextWindow !== undefined &&
707
+ parameters.contextWindow === undefined
708
+ ? trans.__(
709
+ 'Using provider metadata default of %1 tokens for this model unless you override it here.',
710
+ selectedModelInfo.contextWindow.toLocaleString()
711
+ )
712
+ : trans.__(
713
+ 'Model context window size in tokens (used for context usage estimation)'
714
+ )
715
+ }
716
+ slotProps={{ htmlInput: { min: 1 } }}
717
+ />
718
+
719
+ <Typography
720
+ variant="body2"
721
+ color="text.secondary"
722
+ sx={{ mt: 2, mb: 1 }}
723
+ >
724
+ {trans.__('Completion Options')}
725
+ </Typography>
726
+
727
+ <FormControlLabel
728
+ control={
729
+ <Switch
730
+ checked={parameters.supportsFillInMiddle ?? false}
731
+ onChange={e =>
732
+ setParameters({
733
+ ...parameters,
734
+ supportsFillInMiddle: e.target.checked
735
+ })
736
+ }
737
+ />
738
+ }
739
+ label={trans.__('Fill-in-the-middle support')}
740
+ />
741
+
742
+ <FormControlLabel
743
+ control={
744
+ <Switch
745
+ checked={parameters.useFilterText ?? false}
746
+ onChange={e =>
747
+ setParameters({
748
+ ...parameters,
749
+ useFilterText: e.target.checked
750
+ })
751
+ }
752
+ />
753
+ }
754
+ label={trans.__('Use filter text')}
755
+ />
756
+
757
+ {(supportsWebSearch || supportsWebFetch) && (
758
+ <>
759
+ <Typography
760
+ variant="body2"
761
+ color="text.secondary"
762
+ sx={{ mt: 2, mb: 1 }}
763
+ >
764
+ {trans.__('Provider Web Tools')}
765
+ </Typography>
766
+
767
+ {supportsWebSearch && (
768
+ <>
769
+ <FormControlLabel
770
+ control={
771
+ <Switch
772
+ checked={webSearchSettings.enabled === true}
773
+ onChange={e =>
774
+ updateCustomSetting(
775
+ 'webSearch',
776
+ 'enabled',
777
+ e.target.checked
778
+ )
779
+ }
780
+ />
781
+ }
782
+ label={trans.__('Enable Web Search')}
783
+ />
784
+
785
+ {webSearchSettings.enabled === true && (
786
+ <Box
787
+ sx={{
788
+ pl: 2,
789
+ borderLeft: 2,
790
+ borderColor: 'divider',
791
+ display: 'flex',
792
+ flexDirection: 'column',
793
+ gap: 1.5
794
+ }}
795
+ >
796
+ {(webSearchImplementation === 'openai' ||
797
+ webSearchImplementation === 'anthropic') &&
798
+ renderDomainList(
799
+ 'webSearch.allowedDomains',
800
+ trans.__('Allowed Domains'),
801
+ trans.__('example.com'),
802
+ webSearchSettings.allowedDomains
803
+ )}
804
+
805
+ {webSearchImplementation === 'openai' && (
806
+ <>
807
+ <FormControl fullWidth>
808
+ <InputLabel>
809
+ {trans.__('Search Context Size')}
810
+ </InputLabel>
811
+ <Select
812
+ value={
813
+ webSearchSettings.searchContextSize ??
814
+ 'medium'
815
+ }
816
+ label={trans.__('Search Context Size')}
817
+ onChange={e =>
818
+ updateCustomSetting(
819
+ 'webSearch',
820
+ 'searchContextSize',
821
+ e.target.value
822
+ )
823
+ }
824
+ >
825
+ <MenuItem value="low">
826
+ {trans.__('Low')}
827
+ </MenuItem>
828
+ <MenuItem value="medium">
829
+ {trans.__('Medium')}
830
+ </MenuItem>
831
+ <MenuItem value="high">
832
+ {trans.__('High')}
833
+ </MenuItem>
834
+ </Select>
835
+ </FormControl>
836
+ <FormControlLabel
837
+ control={
838
+ <Switch
839
+ checked={
840
+ webSearchSettings.externalWebAccess !==
841
+ false
842
+ }
843
+ onChange={e =>
844
+ updateCustomSetting(
845
+ 'webSearch',
846
+ 'externalWebAccess',
847
+ e.target.checked
848
+ )
849
+ }
850
+ />
851
+ }
852
+ label={trans.__('Use External Web Access')}
853
+ />
854
+ </>
855
+ )}
856
+
857
+ {webSearchImplementation === 'anthropic' && (
858
+ <>
859
+ <TextField
860
+ fullWidth
861
+ label={trans.__('Web Search Max Uses')}
862
+ type="number"
863
+ value={webSearchSettings.maxUses ?? ''}
864
+ onChange={e =>
865
+ updateCustomSetting(
866
+ 'webSearch',
867
+ 'maxUses',
868
+ e.target.value
869
+ ? Number(e.target.value)
870
+ : undefined
871
+ )
872
+ }
873
+ slotProps={{ htmlInput: { min: 1 } }}
874
+ />
875
+ {renderDomainList(
876
+ 'webSearch.blockedDomains',
877
+ trans.__('Blocked Domains'),
878
+ trans.__('spam.example.com'),
879
+ webSearchSettings.blockedDomains
880
+ )}
881
+ </>
882
+ )}
883
+ </Box>
884
+ )}
885
+ </>
886
+ )}
887
+
888
+ {supportsWebFetch && (
889
+ <>
890
+ <FormControlLabel
891
+ control={
892
+ <Switch
893
+ checked={webFetchSettings.enabled === true}
894
+ onChange={e =>
895
+ updateCustomSetting(
896
+ 'webFetch',
897
+ 'enabled',
898
+ e.target.checked
899
+ )
900
+ }
901
+ />
902
+ }
903
+ label={trans.__('Enable Web Fetch')}
904
+ />
905
+
906
+ {webFetchSettings.enabled === true && (
907
+ <Box
908
+ sx={{
909
+ pl: 2,
910
+ borderLeft: 2,
911
+ borderColor: 'divider',
912
+ display: 'flex',
913
+ flexDirection: 'column',
914
+ gap: 1.5
915
+ }}
916
+ >
917
+ <TextField
918
+ fullWidth
919
+ label={trans.__('Web Fetch Max Uses')}
920
+ type="number"
921
+ value={webFetchSettings.maxUses ?? ''}
922
+ onChange={e =>
923
+ updateCustomSetting(
924
+ 'webFetch',
925
+ 'maxUses',
926
+ e.target.value
927
+ ? Number(e.target.value)
928
+ : undefined
929
+ )
930
+ }
931
+ slotProps={{ htmlInput: { min: 1 } }}
932
+ />
933
+ <TextField
934
+ fullWidth
935
+ label={trans.__('Web Fetch Max Content Tokens')}
936
+ type="number"
937
+ value={webFetchSettings.maxContentTokens ?? ''}
938
+ onChange={e =>
939
+ updateCustomSetting(
940
+ 'webFetch',
941
+ 'maxContentTokens',
942
+ e.target.value
943
+ ? Number(e.target.value)
944
+ : undefined
945
+ )
946
+ }
947
+ slotProps={{ htmlInput: { min: 1 } }}
948
+ />
949
+ {renderDomainList(
950
+ 'webFetch.allowedDomains',
951
+ trans.__('Allowed Domains'),
952
+ trans.__('docs.example.com'),
953
+ webFetchSettings.allowedDomains
954
+ )}
955
+ {renderDomainList(
956
+ 'webFetch.blockedDomains',
957
+ trans.__('Blocked Domains'),
958
+ trans.__('spam.example.com'),
959
+ webFetchSettings.blockedDomains
960
+ )}
961
+ <FormControlLabel
962
+ control={
963
+ <Switch
964
+ checked={
965
+ webFetchSettings.citationsEnabled === true
966
+ }
967
+ onChange={e =>
968
+ updateCustomSetting(
969
+ 'webFetch',
970
+ 'citationsEnabled',
971
+ e.target.checked
972
+ )
973
+ }
974
+ />
975
+ }
976
+ label={trans.__('Enable Citations')}
977
+ />
978
+ </Box>
979
+ )}
980
+ </>
981
+ )}
982
+ </>
983
+ )}
984
+ </Box>
985
+ </AccordionDetails>
986
+ </Accordion>
987
+ </Box>
988
+ </DialogContent>
989
+ <DialogActions>
990
+ <Button onClick={onClose}>{trans.__('Cancel')}</Button>
991
+ <Button onClick={handleSave} variant="contained" disabled={!isValid}>
992
+ {mode === 'add' ? trans.__('Add Provider') : trans.__('Save Changes')}
993
+ </Button>
994
+ </DialogActions>
995
+ </Dialog>
996
+ );
997
+ };