@jupyterlite/ai 0.14.0 → 0.16.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 (64) hide show
  1. package/lib/agent.d.ts +33 -115
  2. package/lib/agent.js +192 -106
  3. package/lib/chat-model-handler.d.ts +9 -11
  4. package/lib/chat-model-handler.js +9 -4
  5. package/lib/chat-model.d.ts +84 -13
  6. package/lib/chat-model.js +214 -136
  7. package/lib/completion/completion-provider.d.ts +2 -3
  8. package/lib/components/completion-status.d.ts +2 -2
  9. package/lib/components/index.d.ts +1 -1
  10. package/lib/components/index.js +1 -1
  11. package/lib/components/model-select.d.ts +3 -3
  12. package/lib/components/save-button.d.ts +31 -0
  13. package/lib/components/save-button.js +41 -0
  14. package/lib/components/tool-select.d.ts +3 -4
  15. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +13 -14
  16. package/lib/components/usage-display.js +109 -0
  17. package/lib/diff-manager.d.ts +2 -3
  18. package/lib/index.d.ts +2 -4
  19. package/lib/index.js +186 -28
  20. package/lib/models/settings-model.d.ts +11 -53
  21. package/lib/models/settings-model.js +38 -22
  22. package/lib/providers/built-in-providers.js +22 -36
  23. package/lib/providers/generated-context-windows.d.ts +8 -0
  24. package/lib/providers/generated-context-windows.js +96 -0
  25. package/lib/providers/model-info.d.ts +3 -0
  26. package/lib/providers/model-info.js +58 -0
  27. package/lib/tokens.d.ts +361 -36
  28. package/lib/tokens.js +18 -13
  29. package/lib/tools/commands.d.ts +2 -3
  30. package/lib/widgets/ai-settings.d.ts +3 -5
  31. package/lib/widgets/ai-settings.js +12 -0
  32. package/lib/widgets/main-area-chat.d.ts +2 -3
  33. package/lib/widgets/main-area-chat.js +12 -12
  34. package/lib/widgets/provider-config-dialog.d.ts +1 -2
  35. package/lib/widgets/provider-config-dialog.js +34 -34
  36. package/package.json +17 -10
  37. package/schema/settings-model.json +18 -1
  38. package/src/agent.ts +275 -248
  39. package/src/chat-model-handler.ts +25 -21
  40. package/src/chat-model.ts +307 -196
  41. package/src/completion/completion-provider.ts +7 -4
  42. package/src/components/completion-status.tsx +3 -3
  43. package/src/components/index.ts +1 -1
  44. package/src/components/model-select.tsx +4 -3
  45. package/src/components/save-button.tsx +84 -0
  46. package/src/components/tool-select.tsx +10 -4
  47. package/src/components/usage-display.tsx +208 -0
  48. package/src/diff-manager.ts +4 -4
  49. package/src/index.ts +250 -58
  50. package/src/models/settings-model.ts +46 -88
  51. package/src/providers/built-in-providers.ts +22 -36
  52. package/src/providers/generated-context-windows.ts +102 -0
  53. package/src/providers/model-info.ts +88 -0
  54. package/src/tokens.ts +438 -58
  55. package/src/tools/commands.ts +2 -3
  56. package/src/widgets/ai-settings.tsx +69 -15
  57. package/src/widgets/main-area-chat.ts +18 -15
  58. package/src/widgets/provider-config-dialog.tsx +96 -61
  59. package/style/base.css +17 -195
  60. package/lib/approval-buttons.d.ts +0 -49
  61. package/lib/approval-buttons.js +0 -79
  62. package/lib/components/token-usage-display.js +0 -72
  63. package/src/approval-buttons.ts +0 -115
  64. package/src/components/token-usage-display.tsx +0 -138
@@ -45,17 +45,16 @@ import {
45
45
  createTheme
46
46
  } from '@mui/material';
47
47
  import React, { useEffect, useMemo, useState } from 'react';
48
- import { AgentManagerFactory } from '../agent';
48
+ import { getEffectiveContextWindow } from '../providers/model-info';
49
49
  import {
50
- AISettingsModel,
51
- IAIConfig,
52
- IMCPServerConfig,
53
- IProviderConfig
54
- } from '../models/settings-model';
55
- import {
56
- SECRETS_REPLACEMENT,
50
+ type IAgentManagerFactory,
51
+ type IAIConfig,
57
52
  type IAISecretsAccess,
58
- type IProviderRegistry
53
+ type IAISettingsModel,
54
+ type IMCPServerConfig,
55
+ type IProviderConfig,
56
+ type IProviderRegistry,
57
+ SECRETS_REPLACEMENT
59
58
  } from '../tokens';
60
59
  import { ProviderConfigDialog } from './provider-config-dialog';
61
60
 
@@ -122,8 +121,8 @@ export class AISettingsWidget extends ReactWidget {
122
121
  );
123
122
  }
124
123
 
125
- private _settingsModel: AISettingsModel;
126
- private _agentManagerFactory?: AgentManagerFactory;
124
+ private _settingsModel: IAISettingsModel;
125
+ private _agentManagerFactory?: IAgentManagerFactory;
127
126
  private _themeManager?: IThemeManager;
128
127
  private _providerRegistry: IProviderRegistry;
129
128
  private _secretsAccess?: IAISecretsAccess;
@@ -134,8 +133,8 @@ export class AISettingsWidget extends ReactWidget {
134
133
  * Props interface for the AISettingsComponent
135
134
  */
136
135
  interface IAISettingsComponentProps {
137
- model: AISettingsModel;
138
- agentManagerFactory?: AgentManagerFactory;
136
+ model: IAISettingsModel;
137
+ agentManagerFactory?: IAgentManagerFactory;
139
138
  themeManager?: IThemeManager;
140
139
  providerRegistry: IProviderRegistry;
141
140
  secretsAccess?: IAISecretsAccess;
@@ -672,6 +671,10 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
672
671
  const providerToolCapabilities =
673
672
  providerInfo?.providerToolCapabilities;
674
673
  const params = provider.parameters;
674
+ const effectiveContextWindow = getEffectiveContextWindow(
675
+ provider,
676
+ providerRegistry
677
+ );
675
678
  const webSearchEnabled =
676
679
  !!providerToolCapabilities?.webSearch &&
677
680
  provider.customSettings?.webSearch?.enabled === true;
@@ -743,6 +746,7 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
743
746
  {(params?.temperature !== undefined ||
744
747
  params?.maxOutputTokens !== undefined ||
745
748
  params?.maxTurns !== undefined ||
749
+ effectiveContextWindow !== undefined ||
746
750
  webSearchEnabled ||
747
751
  webFetchEnabled) && (
748
752
  <Box
@@ -783,6 +787,16 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
783
787
  variant="outlined"
784
788
  />
785
789
  )}
790
+ {effectiveContextWindow !== undefined && (
791
+ <Chip
792
+ label={trans.__(
793
+ 'Context: %1',
794
+ effectiveContextWindow
795
+ )}
796
+ size="small"
797
+ variant="outlined"
798
+ />
799
+ )}
786
800
  {webSearchEnabled && (
787
801
  <Chip
788
802
  label={trans.__('Web Search')}
@@ -937,6 +951,32 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
937
951
  }
938
952
  />
939
953
 
954
+ <FormControlLabel
955
+ control={
956
+ <Switch
957
+ checked={config.showContextUsage}
958
+ onChange={e =>
959
+ handleConfigUpdate({
960
+ showContextUsage: e.target.checked
961
+ })
962
+ }
963
+ color="primary"
964
+ />
965
+ }
966
+ label={
967
+ <Box>
968
+ <Typography variant="body1">
969
+ {trans.__('Show Context Usage')}
970
+ </Typography>
971
+ <Typography variant="caption" color="text.secondary">
972
+ {trans.__(
973
+ 'Display estimated context usage in the chat toolbar'
974
+ )}
975
+ </Typography>
976
+ </Box>
977
+ }
978
+ />
979
+
940
980
  <FormControlLabel
941
981
  control={
942
982
  <Switch
@@ -1011,6 +1051,20 @@ const AISettingsComponent: React.FC<IAISettingsComponentProps> = ({
1011
1051
  }
1012
1052
  />
1013
1053
 
1054
+ <TextField
1055
+ fullWidth
1056
+ label={trans.__('Chat Backup Directory')}
1057
+ value={config.chatBackupDirectory}
1058
+ onChange={e =>
1059
+ handleConfigUpdate({
1060
+ chatBackupDirectory: e.target.value
1061
+ })
1062
+ }
1063
+ helperText={trans.__(
1064
+ 'Directory where chat history backups are saved (relative to the server root)'
1065
+ )}
1066
+ />
1067
+
1014
1068
  <Divider sx={{ my: 1 }} />
1015
1069
 
1016
1070
  <TextField
@@ -1678,8 +1732,8 @@ export namespace AISettingsWidget {
1678
1732
  * Options interface for constructing an AISettingsWidget
1679
1733
  */
1680
1734
  export interface IOptions {
1681
- settingsModel: AISettingsModel;
1682
- agentManagerFactory?: AgentManagerFactory;
1735
+ settingsModel: IAISettingsModel;
1736
+ agentManagerFactory?: IAgentManagerFactory;
1683
1737
  themeManager?: IThemeManager;
1684
1738
  providerRegistry: IProviderRegistry;
1685
1739
  /**
@@ -4,17 +4,16 @@ import { launchIcon } from '@jupyterlab/ui-components';
4
4
  import type { TranslationBundle } from '@jupyterlab/translation';
5
5
  import { CommandRegistry } from '@lumino/commands';
6
6
 
7
- import { ApprovalButtons } from '../approval-buttons';
8
7
  import { AIChatModel } from '../chat-model';
9
- import { TokenUsageWidget } from '../components/token-usage-display';
10
- import { AISettingsModel } from '../models/settings-model';
8
+ import { SaveComponentWidget } from '../components/save-button';
9
+ import { UsageWidget } from '../components/usage-display';
11
10
  import { RenderedMessageOutputAreaCompat } from '../rendered-message-outputarea';
12
- import { CommandIds } from '../tokens';
11
+ import { CommandIds, type IAISettingsModel } from '../tokens';
13
12
 
14
13
  export namespace MainAreaChat {
15
14
  export interface IOptions extends MainAreaWidget.IOptions<ChatWidget> {
16
15
  commands: CommandRegistry;
17
- settingsModel: AISettingsModel;
16
+ settingsModel: IAISettingsModel;
18
17
  trans: TranslationBundle;
19
18
  }
20
19
  }
@@ -29,7 +28,7 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
29
28
 
30
29
  const { trans } = options;
31
30
 
32
- // add the move to side button.
31
+ // Move to side button.
33
32
  this.toolbar.addItem(
34
33
  'moveToSide',
35
34
  new CommandToolbarButton({
@@ -43,20 +42,26 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
43
42
  })
44
43
  );
45
44
 
45
+ if (this.model.saveAvailable) {
46
+ // Save chat component
47
+ this.toolbar.addItem(
48
+ 'saveChat',
49
+ new SaveComponentWidget({
50
+ model: this.model,
51
+ translator: trans
52
+ })
53
+ );
54
+ }
55
+
46
56
  // Add the token usage button.
47
- const tokenUsageWidget = new TokenUsageWidget({
57
+ const usageWidget = new UsageWidget({
48
58
  tokenUsageChanged: this.model.tokenUsageChanged,
49
59
  settingsModel: options.settingsModel,
50
60
  initialTokenUsage: this.model.agentManager.tokenUsage,
51
61
  translator: trans
52
62
  });
53
- this.toolbar.addItem('token-usage', tokenUsageWidget);
63
+ this.toolbar.addItem('usage', usageWidget);
54
64
 
55
- // Add the approval button, tied to the chat widget.
56
- this._approvalButtons = new ApprovalButtons({
57
- chatPanel: this.content,
58
- agentManager: this.model.agentManager
59
- });
60
65
  // Temporary compat: keep output-area CSS context for MIME renderers
61
66
  // until jupyter-chat provides it natively.
62
67
  this._outputAreaCompat = new RenderedMessageOutputAreaCompat({
@@ -69,7 +74,6 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
69
74
  dispose(): void {
70
75
  super.dispose();
71
76
  // Dispose of the approval buttons widget when the chat is disposed.
72
- this._approvalButtons.dispose();
73
77
  this._outputAreaCompat.dispose();
74
78
  this.model.writersChanged.disconnect(this._writersChanged);
75
79
  }
@@ -103,6 +107,5 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
103
107
  }
104
108
  };
105
109
 
106
- private _approvalButtons: ApprovalButtons;
107
110
  private _outputAreaCompat: RenderedMessageOutputAreaCompat;
108
111
  }
@@ -31,8 +31,13 @@ import {
31
31
  } from '@mui/material';
32
32
  import type { TranslationBundle } from '@jupyterlab/translation';
33
33
  import React from 'react';
34
- import { IProviderConfig, IProviderParameters } from '../models/settings-model';
35
- import type { IProviderRegistry, IProviderToolCapabilities } from '../tokens';
34
+ import type {
35
+ IProviderConfig,
36
+ IProviderParameters,
37
+ IProviderRegistry,
38
+ IProviderToolCapabilities
39
+ } from '../tokens';
40
+ import { getProviderModelInfo } from '../providers/model-info';
36
41
 
37
42
  /**
38
43
  * Default parameter values for provider configuration
@@ -164,6 +169,10 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
164
169
  );
165
170
  const providerToolCapabilities =
166
171
  selectedProviderInfo?.providerToolCapabilities;
172
+ const selectedModelInfo = React.useMemo(
173
+ () => getProviderModelInfo(selectedProviderInfo, model),
174
+ [selectedProviderInfo, model]
175
+ );
167
176
  const webSearchImplementation =
168
177
  providerToolCapabilities?.webSearch?.implementation;
169
178
  const supportsWebSearch = !!providerToolCapabilities?.webSearch;
@@ -187,7 +196,6 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
187
196
  label: info.name,
188
197
  models: info.defaultModels,
189
198
  apiKeyRequirement: info.apiKeyRequirement,
190
- allowCustomModel: id === 'generic', // Generic allows custom models
191
199
  supportsBaseURL: info.supportsBaseURL,
192
200
  description: info.description,
193
201
  baseUrls: info.baseUrls
@@ -200,9 +208,14 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
200
208
  React.useEffect(() => {
201
209
  if (open) {
202
210
  // Reset form when dialog opens
211
+ const initialProvider = initialConfig?.provider || 'anthropic';
212
+ const initialProviderInfo =
213
+ providerRegistry.getProviderInfo(initialProvider);
203
214
  setName(initialConfig?.name || '');
204
- setProvider(initialConfig?.provider || 'anthropic');
205
- setModel(initialConfig?.model || '');
215
+ setProvider(initialProvider);
216
+ setModel(
217
+ initialConfig?.model || initialProviderInfo?.defaultModels[0] || ''
218
+ );
206
219
  setApiKey(initialConfig?.apiKey || '');
207
220
  setBaseURL(initialConfig?.baseURL || '');
208
221
  setParameters(initialConfig?.parameters || {});
@@ -215,14 +228,7 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
215
228
  setDomainInputs(createEmptyDomainInputs());
216
229
  setExpandedAdvanced(false);
217
230
  }
218
- }, [open, initialConfig]);
219
-
220
- React.useEffect(() => {
221
- // Auto-select first model when provider changes
222
- if (selectedProvider && selectedProvider.models.length > 0 && !model) {
223
- setModel(selectedProvider.models[0]);
224
- }
225
- }, [provider, selectedProvider, model]);
231
+ }, [open, initialConfig, providerRegistry]);
226
232
 
227
233
  const handleRef = React.useCallback(
228
234
  (node: HTMLInputElement | null) => {
@@ -233,6 +239,15 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
233
239
  [provider, handleSecretField, open]
234
240
  );
235
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
+
236
251
  const updateCustomSetting = React.useCallback(
237
252
  (section: 'webSearch' | 'webFetch', key: string, value: unknown) => {
238
253
  setCustomSettings(prev => {
@@ -450,7 +465,9 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
450
465
  value={provider}
451
466
  label={trans.__('Provider Type')}
452
467
  onChange={e =>
453
- setProvider(e.target.value as IProviderConfig['provider'])
468
+ handleProviderChange(
469
+ e.target.value as IProviderConfig['provider']
470
+ )
454
471
  }
455
472
  >
456
473
  {providerOptions.map(option => (
@@ -478,49 +495,32 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
478
495
  </Select>
479
496
  </FormControl>
480
497
 
481
- {selectedProvider?.allowCustomModel ? (
482
- <TextField
483
- fullWidth
484
- label={trans.__('Model')}
485
- value={model}
486
- onChange={e => setModel(e.target.value)}
487
- placeholder={trans.__('Enter model name')}
488
- helperText={trans.__('Enter any compatible model name')}
489
- required
490
- />
491
- ) : (
492
- <FormControl fullWidth required>
493
- <InputLabel>{trans.__('Model')}</InputLabel>
494
- <Select
495
- value={model}
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
496
514
  label={trans.__('Model')}
497
- onChange={e => setModel(e.target.value)}
498
- >
499
- {selectedProvider?.models.map(modelOption => (
500
- <MenuItem key={modelOption} value={modelOption}>
501
- <Box>
502
- <Typography variant="body1">{modelOption}</Typography>
503
- <Typography variant="caption" color="text.secondary">
504
- {modelOption.includes('sonnet')
505
- ? trans.__('Balanced performance')
506
- : modelOption.includes('opus')
507
- ? trans.__('Advanced reasoning')
508
- : modelOption.includes('haiku')
509
- ? trans.__('Fast and lightweight')
510
- : modelOption.includes('large')
511
- ? trans.__('Most capable model')
512
- : modelOption.includes('small')
513
- ? trans.__('Fast and efficient')
514
- : modelOption.includes('codestral')
515
- ? trans.__('Code-specialized')
516
- : trans.__('General purpose')}
517
- </Typography>
518
- </Box>
519
- </MenuItem>
520
- ))}
521
- </Select>
522
- </FormControl>
523
- )}
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
524
 
525
525
  {selectedProvider &&
526
526
  selectedProvider?.apiKeyRequirement !== 'none' && (
@@ -658,7 +658,7 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
658
658
  }
659
659
  placeholder={trans.__('Leave empty for provider default')}
660
660
  helperText={trans.__('Maximum length of AI responses')}
661
- inputProps={{ min: 1 }}
661
+ slotProps={{ htmlInput: { min: 1 } }}
662
662
  />
663
663
 
664
664
  <TextField
@@ -678,7 +678,42 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
678
678
  helperText={trans.__(
679
679
  'Maximum number of tool execution turns'
680
680
  )}
681
- inputProps={{ min: 1, max: 100 }}
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 } }}
682
717
  />
683
718
 
684
719
  <Typography
@@ -835,7 +870,7 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
835
870
  : undefined
836
871
  )
837
872
  }
838
- inputProps={{ min: 1 }}
873
+ slotProps={{ htmlInput: { min: 1 } }}
839
874
  />
840
875
  {renderDomainList(
841
876
  'webSearch.blockedDomains',
@@ -893,7 +928,7 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
893
928
  : undefined
894
929
  )
895
930
  }
896
- inputProps={{ min: 1 }}
931
+ slotProps={{ htmlInput: { min: 1 } }}
897
932
  />
898
933
  <TextField
899
934
  fullWidth
@@ -909,7 +944,7 @@ export const ProviderConfigDialog: React.FC<IProviderConfigDialogProps> = ({
909
944
  : undefined
910
945
  )
911
946
  }
912
- inputProps={{ min: 1 }}
947
+ slotProps={{ htmlInput: { min: 1 } }}
913
948
  />
914
949
  {renderDomainList(
915
950
  'webFetch.allowedDomains',