@jupyterlite/ai 0.9.0-a3 → 0.9.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 (50) hide show
  1. package/README.md +20 -89
  2. package/lib/agent.d.ts +10 -4
  3. package/lib/agent.js +30 -17
  4. package/lib/chat-model.d.ts +6 -0
  5. package/lib/chat-model.js +144 -17
  6. package/lib/completion/completion-provider.js +1 -13
  7. package/lib/components/completion-status.d.ts +20 -0
  8. package/lib/components/completion-status.js +51 -0
  9. package/lib/components/index.d.ts +1 -0
  10. package/lib/components/index.js +1 -0
  11. package/lib/components/model-select.js +1 -2
  12. package/lib/diff-manager.d.ts +25 -0
  13. package/lib/diff-manager.js +60 -0
  14. package/lib/icons.d.ts +0 -1
  15. package/lib/icons.js +2 -6
  16. package/lib/index.d.ts +2 -2
  17. package/lib/index.js +54 -23
  18. package/lib/models/settings-model.d.ts +4 -0
  19. package/lib/models/settings-model.js +24 -2
  20. package/lib/providers/built-in-providers.d.ts +0 -4
  21. package/lib/providers/built-in-providers.js +17 -23
  22. package/lib/tokens.d.ts +74 -0
  23. package/lib/tokens.js +4 -0
  24. package/lib/tools/commands.js +36 -35
  25. package/lib/tools/file.d.ts +10 -1
  26. package/lib/tools/file.js +235 -146
  27. package/lib/tools/notebook.d.ts +2 -3
  28. package/lib/tools/notebook.js +11 -11
  29. package/lib/widgets/ai-settings.js +78 -13
  30. package/lib/widgets/provider-config-dialog.js +15 -8
  31. package/package.json +5 -3
  32. package/schema/settings-model.json +25 -0
  33. package/src/agent.ts +35 -20
  34. package/src/chat-model.ts +182 -19
  35. package/src/completion/completion-provider.ts +1 -14
  36. package/src/components/completion-status.tsx +79 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/components/model-select.tsx +0 -3
  39. package/src/diff-manager.ts +81 -0
  40. package/src/icons.ts +2 -7
  41. package/src/index.ts +74 -24
  42. package/src/models/settings-model.ts +28 -2
  43. package/src/providers/built-in-providers.ts +17 -24
  44. package/src/tokens.ts +78 -0
  45. package/src/tools/commands.ts +45 -40
  46. package/src/tools/file.ts +295 -164
  47. package/src/tools/notebook.ts +13 -14
  48. package/src/widgets/ai-settings.tsx +184 -35
  49. package/src/widgets/provider-config-dialog.tsx +43 -16
  50. package/style/base.css +14 -0
@@ -1,5 +1,6 @@
1
1
  import { IThemeManager } from '@jupyterlab/apputils';
2
2
  import { ReactWidget } from '@jupyterlab/ui-components';
3
+ import { Debouncer } from '@lumino/polling';
3
4
  import Add from '@mui/icons-material/Add';
4
5
  import Cable from '@mui/icons-material/Cable';
5
6
  import CheckCircle from '@mui/icons-material/CheckCircle';
@@ -42,7 +43,7 @@ import {
42
43
  createTheme
43
44
  } from '@mui/material';
44
45
  import { ISecretsManager } from 'jupyter-secrets-manager';
45
- import React, { useEffect, useState } from 'react';
46
+ import React, { useEffect, useMemo, useState } from 'react';
46
47
  import { AgentManagerFactory } from '../agent';
47
48
  import {
48
49
  AISettingsModel,
@@ -162,6 +163,14 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
162
163
  >();
163
164
  const [mcpMenuAnchor, setMcpMenuAnchor] = useState<null | HTMLElement>(null);
164
165
  const [mcpMenuServerId, setMcpMenuServerId] = useState<string>('');
166
+ const [systemPromptValue, setSystemPromptValue] = useState(
167
+ config.systemPrompt
168
+ );
169
+ const systemPromptValueRef = React.useRef(config.systemPrompt);
170
+ const [completionPromptValue, setCompletionPromptValue] = useState(
171
+ config.completionSystemPrompt
172
+ );
173
+ const completionPromptValueRef = React.useRef(config.completionSystemPrompt);
165
174
 
166
175
  /**
167
176
  * Effect to listen for model state changes and update config
@@ -220,6 +229,47 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
220
229
  };
221
230
  }, [agentManagerFactory]);
222
231
 
232
+ // Sync local state when config changes externally
233
+ useEffect(() => {
234
+ setSystemPromptValue(config.systemPrompt);
235
+ systemPromptValueRef.current = config.systemPrompt;
236
+ }, [config.systemPrompt]);
237
+
238
+ useEffect(() => {
239
+ setCompletionPromptValue(config.completionSystemPrompt);
240
+ completionPromptValueRef.current = config.completionSystemPrompt;
241
+ }, [config.completionSystemPrompt]);
242
+
243
+ const promptDebouncer = useMemo(
244
+ () =>
245
+ new Debouncer(async () => {
246
+ await handleConfigUpdate({
247
+ systemPrompt: systemPromptValueRef.current,
248
+ completionSystemPrompt: completionPromptValueRef.current
249
+ });
250
+ }, 1000),
251
+ []
252
+ );
253
+
254
+ // Cleanup debouncer on unmount
255
+ useEffect(() => {
256
+ return () => {
257
+ promptDebouncer.dispose();
258
+ };
259
+ }, [promptDebouncer]);
260
+
261
+ const handleSystemPromptChange = (value: string) => {
262
+ setSystemPromptValue(value);
263
+ systemPromptValueRef.current = value;
264
+ void promptDebouncer.invoke();
265
+ };
266
+
267
+ const handleCompletionPromptChange = (value: string) => {
268
+ setCompletionPromptValue(value);
269
+ completionPromptValueRef.current = value;
270
+ void promptDebouncer.invoke();
271
+ };
272
+
223
273
  const getSecretFromManager = async (
224
274
  provider: string,
225
275
  fieldName: string
@@ -232,6 +282,23 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
232
282
  return secret?.value;
233
283
  };
234
284
 
285
+ const setSecretToManager = async (
286
+ provider: string,
287
+ fieldName: string,
288
+ value: string
289
+ ): Promise<void> => {
290
+ await secretsManager?.set(
291
+ Private.getToken(),
292
+ SECRETS_NAMESPACE,
293
+ `${provider}:${fieldName}`,
294
+ {
295
+ namespace: SECRETS_NAMESPACE,
296
+ id: `${provider}:${fieldName}`,
297
+ value
298
+ }
299
+ );
300
+ };
301
+
235
302
  /**
236
303
  * Attach a secrets field to the secrets manager.
237
304
  * @param input - the DOm element to attach.
@@ -308,9 +375,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
308
375
  // Retrieve the API key from the secrets manager if necessary.
309
376
  if (model.config.useSecretsManager && secretsManager) {
310
377
  provider.apiKey =
311
- (await getSecretFromManager(provider.provider, 'apiKey')) ??
312
- provider.apiKey ??
313
- '';
378
+ (await getSecretFromManager(provider.provider, 'apiKey')) ?? '';
314
379
  }
315
380
  setEditingProvider(provider);
316
381
  setDialogOpen(true);
@@ -354,6 +419,15 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
354
419
  if (updates.useSecretsManager !== undefined) {
355
420
  if (updates.useSecretsManager) {
356
421
  for (const provider of model.config.providers) {
422
+ // if the secrets manager doesn't have the current API key, copy the current
423
+ // one from settings.
424
+ if (!(await getSecretFromManager(provider.provider, 'apiKey'))) {
425
+ setSecretToManager(
426
+ provider.provider,
427
+ 'apiKey',
428
+ provider.apiKey ?? ''
429
+ );
430
+ }
357
431
  provider.apiKey = SECRETS_REPLACEMENT;
358
432
  await model.updateProvider(provider.id, provider);
359
433
  }
@@ -453,6 +527,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
453
527
  overflow: 'auto',
454
528
  p: 2,
455
529
  pb: 4,
530
+ boxSizing: 'border-box',
456
531
  fontSize: '0.9rem'
457
532
  }}
458
533
  >
@@ -478,32 +553,6 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
478
553
  {/* Tab Panels */}
479
554
  {activeTab === 0 && (
480
555
  <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
481
- {secretsManager !== undefined && (
482
- <FormControlLabel
483
- control={
484
- <Switch
485
- checked={config.useSecretsManager}
486
- onChange={e =>
487
- handleConfigUpdate({
488
- useSecretsManager: e.target.checked
489
- })
490
- }
491
- color="primary"
492
- sx={{ alignSelf: 'flex-start' }}
493
- />
494
- }
495
- label={
496
- <div>
497
- <span>Use the secrets manager to manage API keys</span>
498
- {!config.useSecretsManager && (
499
- <Alert severity="warning" icon={<Error />} sx={{ mb: 2 }}>
500
- The secrets will be stored in plain text in settings
501
- </Alert>
502
- )}
503
- </div>
504
- }
505
- />
506
- )}
507
556
  {/* Default Provider Selection */}
508
557
  {config.providers.length > 0 && (
509
558
  <Card elevation={2}>
@@ -552,6 +601,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
552
601
  <Select
553
602
  value={config.activeCompleterProvider || ''}
554
603
  label="Completion Provider"
604
+ className="jp-ai-completion-provider-select"
555
605
  onChange={e =>
556
606
  model.setActiveCompleterProvider(
557
607
  e.target.value || undefined
@@ -559,7 +609,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
559
609
  }
560
610
  >
561
611
  <MenuItem value="">
562
- <em>Use chat provider</em>
612
+ <em>No completion</em>
563
613
  </MenuItem>
564
614
  {config.providers.map(provider => (
565
615
  <MenuItem key={provider.id} value={provider.id}>
@@ -726,6 +776,34 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
726
776
  )}
727
777
  </CardContent>
728
778
  </Card>
779
+
780
+ {/* Secrets Manager Settings */}
781
+ {secretsManager !== undefined && (
782
+ <FormControlLabel
783
+ control={
784
+ <Switch
785
+ checked={config.useSecretsManager}
786
+ onChange={e =>
787
+ handleConfigUpdate({
788
+ useSecretsManager: e.target.checked
789
+ })
790
+ }
791
+ color="primary"
792
+ sx={{ alignSelf: 'flex-start' }}
793
+ />
794
+ }
795
+ label={
796
+ <div>
797
+ <span>Use the secrets manager to manage API keys</span>
798
+ {!config.useSecretsManager && (
799
+ <Alert severity="warning" icon={<Error />} sx={{ mb: 2 }}>
800
+ The secrets are stored in plain text in settings
801
+ </Alert>
802
+ )}
803
+ </div>
804
+ }
805
+ />
806
+ )}
729
807
  </Box>
730
808
  )}
731
809
 
@@ -807,6 +885,68 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
807
885
  }
808
886
  />
809
887
 
888
+ <FormControlLabel
889
+ control={
890
+ <Switch
891
+ checked={config.showCellDiff}
892
+ onChange={e =>
893
+ handleConfigUpdate({
894
+ showCellDiff: e.target.checked
895
+ })
896
+ }
897
+ color="primary"
898
+ />
899
+ }
900
+ label={
901
+ <Box>
902
+ <Typography variant="body1">Show Cell Diff</Typography>
903
+ <Typography variant="caption" color="text.secondary">
904
+ Show diff view when AI modifies cell content
905
+ </Typography>
906
+ </Box>
907
+ }
908
+ />
909
+
910
+ {config.showCellDiff && (
911
+ <FormControl sx={{ ml: 4 }}>
912
+ <InputLabel>Diff Display Mode</InputLabel>
913
+ <Select
914
+ value={config.diffDisplayMode}
915
+ label="Diff Display Mode"
916
+ onChange={e =>
917
+ handleConfigUpdate({
918
+ diffDisplayMode: e.target.value as 'split' | 'unified'
919
+ })
920
+ }
921
+ >
922
+ <MenuItem value="split">Split View</MenuItem>
923
+ <MenuItem value="unified">Unified View</MenuItem>
924
+ </Select>
925
+ </FormControl>
926
+ )}
927
+
928
+ <FormControlLabel
929
+ control={
930
+ <Switch
931
+ checked={config.showFileDiff}
932
+ onChange={e =>
933
+ handleConfigUpdate({
934
+ showFileDiff: e.target.checked
935
+ })
936
+ }
937
+ color="primary"
938
+ />
939
+ }
940
+ label={
941
+ <Box>
942
+ <Typography variant="body1">Show File Diff</Typography>
943
+ <Typography variant="caption" color="text.secondary">
944
+ Show diff view when AI modifies file content
945
+ </Typography>
946
+ </Box>
947
+ }
948
+ />
949
+
810
950
  <Divider sx={{ my: 1 }} />
811
951
 
812
952
  <TextField
@@ -814,14 +954,23 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
814
954
  multiline
815
955
  rows={3}
816
956
  label="System Prompt"
817
- value={config.systemPrompt}
818
- onChange={e =>
819
- handleConfigUpdate({ systemPrompt: e.target.value })
820
- }
957
+ value={systemPromptValue}
958
+ onChange={e => handleSystemPromptChange(e.target.value)}
821
959
  placeholder="Define the AI's behavior and personality..."
822
960
  helperText="Instructions that define how the AI should behave and respond"
823
961
  />
824
962
 
963
+ <TextField
964
+ fullWidth
965
+ multiline
966
+ rows={3}
967
+ label="Completion System Prompt"
968
+ value={completionPromptValue}
969
+ onChange={e => handleCompletionPromptChange(e.target.value)}
970
+ placeholder="Define how the AI should generate code completions..."
971
+ helperText="Instructions that define how the AI should generate code completions"
972
+ />
973
+
825
974
  <Divider sx={{ my: 2 }} />
826
975
 
827
976
  <Box>
@@ -5,6 +5,7 @@ import {
5
5
  Accordion,
6
6
  AccordionDetails,
7
7
  AccordionSummary,
8
+ Autocomplete,
8
9
  Box,
9
10
  Button,
10
11
  Chip,
@@ -83,9 +84,10 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
83
84
  label: info.name,
84
85
  models: info.defaultModels,
85
86
  apiKeyRequirement: info.apiKeyRequirement,
86
- allowCustomModel: id === 'ollama' || id === 'generic', // Ollama and Generic allow custom models
87
+ allowCustomModel: id === 'generic', // Generic allows custom models
87
88
  supportsBaseURL: info.supportsBaseURL,
88
- description: info.description
89
+ description: info.description,
90
+ baseUrls: info.baseUrls
89
91
  };
90
92
  });
91
93
  }, [providerRegistry]);
@@ -279,21 +281,46 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
279
281
  )}
280
282
 
281
283
  {selectedProvider?.supportsBaseURL && (
282
- <TextField
284
+ <Autocomplete
285
+ freeSolo
283
286
  fullWidth
284
- label="Base URL (Optional)"
285
- value={baseURL}
286
- onChange={e => setBaseURL(e.target.value)}
287
- placeholder={
288
- provider === 'ollama'
289
- ? 'http://localhost:11434/api'
290
- : 'Custom API endpoint'
291
- }
292
- helperText={
293
- provider === 'ollama'
294
- ? 'Ollama server endpoint'
295
- : 'Custom API base URL (e.g., for LiteLLM proxy). Leave empty to use default provider endpoint.'
296
- }
287
+ options={(selectedProvider.baseUrls ?? []).map(
288
+ option => option.url
289
+ )}
290
+ value={baseURL || ''}
291
+ onChange={(_, value) => {
292
+ if (value && typeof value === 'string') {
293
+ setBaseURL(value);
294
+ }
295
+ }}
296
+ inputValue={baseURL || ''}
297
+ renderOption={(props, option) => {
298
+ const urlOption = (selectedProvider.baseUrls ?? []).find(
299
+ u => u.url === option
300
+ );
301
+ return (
302
+ <Box component="li" {...props} key={option}>
303
+ <Box>
304
+ <Typography variant="body2">{option}</Typography>
305
+ {urlOption?.description && (
306
+ <Typography variant="caption" color="text.secondary">
307
+ {urlOption.description}
308
+ </Typography>
309
+ )}
310
+ </Box>
311
+ </Box>
312
+ );
313
+ }}
314
+ renderInput={params => (
315
+ <TextField
316
+ {...params}
317
+ fullWidth
318
+ label="Base URL"
319
+ placeholder="https://api.example.com/v1"
320
+ onChange={e => setBaseURL(e.target.value)}
321
+ />
322
+ )}
323
+ clearOnBlur={false}
297
324
  />
298
325
  )}
299
326
 
package/style/base.css CHANGED
@@ -371,6 +371,11 @@
371
371
  transform: rotate(180deg);
372
372
  }
373
373
 
374
+ .jp-ai-settings-icon {
375
+ align-items: center;
376
+ display: flex;
377
+ }
378
+
374
379
  .jp-chat-sidepanel .jp-chat-add span.jp-ToolbarButtonComponent-label {
375
380
  display: none;
376
381
  }
@@ -379,3 +384,12 @@
379
384
  stroke: var(--jp-inverse-layout-color3);
380
385
  stroke-width: 2;
381
386
  }
387
+
388
+ /* Disabled color for the completion status */
389
+ .jp-ai-completion-status .jp-ai-completion-disabled circle {
390
+ fill: var(--jp-layout-color3);
391
+ }
392
+
393
+ .jp-ai-completion-status .jp-ai-completion-disabled path {
394
+ fill: var(--jp-layout-color2);
395
+ }