@jupyterlite/ai 0.15.0 → 0.17.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 (45) hide show
  1. package/lib/agent.d.ts +12 -2
  2. package/lib/agent.js +112 -17
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.js +4 -1
  5. package/lib/chat-model.d.ts +25 -24
  6. package/lib/chat-model.js +262 -132
  7. package/lib/components/clear-button.d.ts +1 -1
  8. package/lib/components/clear-button.js +1 -1
  9. package/lib/components/index.d.ts +1 -1
  10. package/lib/components/index.js +1 -1
  11. package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
  12. package/lib/components/usage-display.js +109 -0
  13. package/lib/index.js +205 -20
  14. package/lib/models/settings-model.js +1 -0
  15. package/lib/providers/built-in-providers.js +5 -0
  16. package/lib/providers/generated-context-windows.d.ts +8 -0
  17. package/lib/providers/generated-context-windows.js +96 -0
  18. package/lib/providers/model-info.d.ts +3 -0
  19. package/lib/providers/model-info.js +58 -0
  20. package/lib/tokens.d.ts +34 -3
  21. package/lib/tokens.js +8 -7
  22. package/lib/widgets/ai-settings.js +9 -0
  23. package/lib/widgets/main-area-chat.d.ts +1 -0
  24. package/lib/widgets/main-area-chat.js +10 -4
  25. package/lib/widgets/provider-config-dialog.js +18 -5
  26. package/package.json +3 -2
  27. package/schema/settings-model.json +11 -0
  28. package/src/agent.ts +151 -21
  29. package/src/chat-commands/clear.ts +1 -1
  30. package/src/chat-model-handler.ts +6 -1
  31. package/src/chat-model.ts +350 -175
  32. package/src/components/clear-button.tsx +3 -3
  33. package/src/components/index.ts +1 -1
  34. package/src/components/usage-display.tsx +208 -0
  35. package/src/index.ts +250 -26
  36. package/src/models/settings-model.ts +1 -0
  37. package/src/providers/built-in-providers.ts +5 -0
  38. package/src/providers/generated-context-windows.ts +102 -0
  39. package/src/providers/model-info.ts +88 -0
  40. package/src/tokens.ts +46 -10
  41. package/src/widgets/ai-settings.tsx +42 -0
  42. package/src/widgets/main-area-chat.ts +12 -4
  43. package/src/widgets/provider-config-dialog.tsx +45 -5
  44. package/lib/components/token-usage-display.js +0 -72
  45. package/src/components/token-usage-display.tsx +0 -137
@@ -16,7 +16,7 @@ export interface IClearButtonProps
16
16
  /**
17
17
  * The function to clear messages.
18
18
  */
19
- clearMessages: () => void;
19
+ clearMessages: () => Promise<void>;
20
20
  /**
21
21
  * The application language translator.
22
22
  */
@@ -53,8 +53,8 @@ export function clearItem(
53
53
  return {
54
54
  element: (props: InputToolbarRegistry.IToolbarItemProps) => {
55
55
  const { model } = props;
56
- const clearMessages = () =>
57
- (model.chatContext as AIChatModel.IAIChatContext).clearMessages();
56
+ const clearMessages = async () =>
57
+ await (model.chatContext as AIChatModel.IAIChatContext).clearMessages();
58
58
  const clearProps: IClearButtonProps = {
59
59
  ...props,
60
60
  clearMessages,
@@ -2,5 +2,5 @@ export * from './clear-button';
2
2
  export * from './completion-status';
3
3
  export * from './model-select';
4
4
  export * from './stop-button';
5
- export * from './token-usage-display';
5
+ export * from './usage-display';
6
6
  export * from './tool-select';
@@ -0,0 +1,208 @@
1
+ import { ReactWidget, UseSignal } from '@jupyterlab/ui-components';
2
+ import type { TranslationBundle } from '@jupyterlab/translation';
3
+ import React from 'react';
4
+ import { ISignal } from '@lumino/signaling';
5
+ import type { IAISettingsModel, ITokenUsage } from '../tokens';
6
+
7
+ /**
8
+ * Props for the UsageDisplay component.
9
+ */
10
+ export interface IUsageDisplayProps {
11
+ /**
12
+ * The token usage changed signal
13
+ */
14
+ tokenUsageChanged: ISignal<any, ITokenUsage>;
15
+
16
+ /**
17
+ * The settings model instance for configuration options
18
+ */
19
+ settingsModel: IAISettingsModel;
20
+
21
+ /**
22
+ * Initial token usage.
23
+ */
24
+ initialTokenUsage?: ITokenUsage;
25
+
26
+ /**
27
+ * The application language translator.
28
+ */
29
+ translator: TranslationBundle;
30
+ }
31
+
32
+ /**
33
+ * React component that displays usage information.
34
+ * Shows input/output token counts and optional estimated context usage.
35
+ * Only renders when token or context usage display is enabled in settings.
36
+ */
37
+ export const UsageDisplay: React.FC<IUsageDisplayProps> = ({
38
+ tokenUsageChanged,
39
+ settingsModel,
40
+ initialTokenUsage,
41
+ translator: trans
42
+ }) => {
43
+ const formatContextPercent = (value: number): string => {
44
+ return Math.round(value).toLocaleString();
45
+ };
46
+
47
+ const badgeStyle: React.CSSProperties = {
48
+ display: 'flex',
49
+ alignItems: 'center',
50
+ gap: '6px',
51
+ fontSize: '12px',
52
+ color: 'var(--jp-ui-font-color2)',
53
+ padding: '4px 8px',
54
+ backgroundColor: 'var(--jp-layout-color1)',
55
+ border: '1px solid var(--jp-border-color1)',
56
+ borderRadius: '4px',
57
+ whiteSpace: 'nowrap'
58
+ };
59
+
60
+ return (
61
+ <UseSignal signal={settingsModel.stateChanged} initialArgs={undefined}>
62
+ {() => {
63
+ const config = settingsModel.config;
64
+ const showTokenUsage = config.showTokenUsage;
65
+ const showContextUsage = config.showContextUsage;
66
+ if (!showTokenUsage && !showContextUsage) {
67
+ return null;
68
+ }
69
+
70
+ return (
71
+ <UseSignal signal={tokenUsageChanged} initialArgs={initialTokenUsage}>
72
+ {(_, tokenUsage: ITokenUsage | null | undefined) => {
73
+ if (!tokenUsage) {
74
+ return null;
75
+ }
76
+
77
+ const total = tokenUsage.inputTokens + tokenUsage.outputTokens;
78
+ const hasKnownContextWindow =
79
+ showContextUsage && tokenUsage.contextWindow !== undefined;
80
+ const contextUsagePercent =
81
+ tokenUsage.lastRequestInputTokens !== undefined &&
82
+ tokenUsage.contextWindow !== undefined &&
83
+ tokenUsage.contextWindow > 0
84
+ ? Math.max(
85
+ 0,
86
+ Math.min(
87
+ 100,
88
+ (tokenUsage.lastRequestInputTokens /
89
+ tokenUsage.contextWindow) *
90
+ 100
91
+ )
92
+ )
93
+ : undefined;
94
+ const hasContextEstimate =
95
+ hasKnownContextWindow &&
96
+ contextUsagePercent !== undefined &&
97
+ tokenUsage.lastRequestInputTokens !== undefined;
98
+
99
+ const contextLabel = hasContextEstimate
100
+ ? `${formatContextPercent(contextUsagePercent)}%`
101
+ : hasKnownContextWindow
102
+ ? '0%'
103
+ : '?';
104
+
105
+ const contextTitle = hasContextEstimate
106
+ ? trans.__(
107
+ 'Context Usage (estimated): %1% (%2 / %3 tokens)',
108
+ formatContextPercent(contextUsagePercent),
109
+ tokenUsage.lastRequestInputTokens!.toLocaleString(),
110
+ tokenUsage.contextWindow!.toLocaleString()
111
+ )
112
+ : hasKnownContextWindow
113
+ ? trans.__(
114
+ 'Context usage estimate will appear after the next request. Showing 0% until then. Context window: %1 tokens',
115
+ tokenUsage.contextWindow!.toLocaleString()
116
+ )
117
+ : trans.__(
118
+ 'Context Usage unavailable. Configure a context window for the active provider/model to enable estimation.'
119
+ );
120
+
121
+ return (
122
+ <div
123
+ style={{
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ gap: '6px'
127
+ }}
128
+ >
129
+ {showTokenUsage && (
130
+ <span
131
+ style={badgeStyle}
132
+ title={trans.__(
133
+ 'Token Usage - Sent: %1, Received: %2, Total: %3',
134
+ tokenUsage.inputTokens.toLocaleString(),
135
+ tokenUsage.outputTokens.toLocaleString(),
136
+ total.toLocaleString()
137
+ )}
138
+ >
139
+ <span
140
+ style={{
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ gap: '2px'
144
+ }}
145
+ >
146
+ <span>↑</span>
147
+ <span>{tokenUsage.inputTokens.toLocaleString()}</span>
148
+ </span>
149
+ <span
150
+ style={{
151
+ display: 'flex',
152
+ alignItems: 'center',
153
+ gap: '2px'
154
+ }}
155
+ >
156
+ <span>↓</span>
157
+ <span>{tokenUsage.outputTokens.toLocaleString()}</span>
158
+ </span>
159
+ </span>
160
+ )}
161
+ {showContextUsage && (
162
+ <span style={badgeStyle} title={contextTitle}>
163
+ <span
164
+ style={{
165
+ display: 'flex',
166
+ alignItems: 'center',
167
+ gap: '2px'
168
+ }}
169
+ >
170
+ <span>ctx</span>
171
+ <span>{contextLabel}</span>
172
+ </span>
173
+ </span>
174
+ )}
175
+ </div>
176
+ );
177
+ }}
178
+ </UseSignal>
179
+ );
180
+ }}
181
+ </UseSignal>
182
+ );
183
+ };
184
+
185
+ /**
186
+ * JupyterLab widget wrapper for the UsageDisplay component.
187
+ * Extends ReactWidget to integrate with the JupyterLab widget system.
188
+ */
189
+ export class UsageWidget extends ReactWidget {
190
+ /**
191
+ * Creates a new UsageWidget instance.
192
+ * @param options - Configuration options containing required models
193
+ */
194
+ constructor(options: IUsageDisplayProps) {
195
+ super();
196
+ this._options = options;
197
+ }
198
+
199
+ /**
200
+ * Renders the React component within the widget.
201
+ * @returns The UsageDisplay React element
202
+ */
203
+ protected render(): React.ReactElement {
204
+ return <UsageDisplay {...this._options} />;
205
+ }
206
+
207
+ private _options: IUsageDisplayProps;
208
+ }
package/src/index.ts CHANGED
@@ -114,7 +114,7 @@ import {
114
114
  createToolSelectItem,
115
115
  stopItem,
116
116
  CompletionStatusWidget,
117
- TokenUsageWidget
117
+ UsageWidget
118
118
  } from './components';
119
119
 
120
120
  import { AISettingsModel } from './models/settings-model';
@@ -357,6 +357,27 @@ const chatModelHandler: JupyterFrontEndPlugin<IChatModelHandler> = {
357
357
  }
358
358
  };
359
359
 
360
+ /**
361
+ * The active cell manager plugin, to allow copying code from chat to notebook.
362
+ */
363
+ const activeCellManager: JupyterFrontEndPlugin<void> = {
364
+ id: '@jupyterlite/ai:activeCellManager',
365
+ description: 'Add the active cell manager to the model handler',
366
+ autoStart: true,
367
+ requires: [IChatModelHandler, INotebookTracker],
368
+ activate: (
369
+ app: JupyterFrontEnd,
370
+ modelHandler: IChatModelHandler,
371
+ notebookTracker: INotebookTracker
372
+ ) => {
373
+ const activeCellManager = new ActiveCellManager({
374
+ tracker: notebookTracker,
375
+ shell: app.shell
376
+ });
377
+ modelHandler.activeCellManager = activeCellManager;
378
+ }
379
+ };
380
+
360
381
  /**
361
382
  * Initialization data for the extension.
362
383
  */
@@ -376,7 +397,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
376
397
  IThemeManager,
377
398
  ILayoutRestorer,
378
399
  ILabShell,
379
- INotebookTracker,
380
400
  ITranslator,
381
401
  IComponentsRendererFactory,
382
402
  ICommandPalette,
@@ -392,7 +412,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
392
412
  themeManager?: IThemeManager,
393
413
  restorer?: ILayoutRestorer,
394
414
  labShell?: ILabShell,
395
- notebookTracker?: INotebookTracker,
396
415
  translator?: ITranslator,
397
416
  chatComponentsFactory?: IComponentsRendererFactory,
398
417
  palette?: ICommandPalette,
@@ -416,17 +435,6 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
416
435
  }
417
436
  };
418
437
 
419
- // Create ActiveCellManager if notebook tracker is available, and add it to the
420
- // model registry.
421
- let activeCellManager: ActiveCellManager | undefined;
422
- if (notebookTracker) {
423
- activeCellManager = new ActiveCellManager({
424
- tracker: notebookTracker,
425
- shell: app.shell
426
- });
427
- }
428
- modelHandler.activeCellManager = activeCellManager;
429
-
430
438
  // Creating the tracker for the chat widgets
431
439
  const namespace = 'ai-chat';
432
440
  const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
@@ -516,7 +524,7 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
516
524
  chatPanel.disposed.connect(disconnectSettingsButtonListener);
517
525
  }
518
526
 
519
- let tokenUsageWidget: TokenUsageWidget | null = null;
527
+ let usageWidget: UsageWidget | null = null;
520
528
  chatPanel.chatOpened.connect((_, widget) => {
521
529
  const model = widget.model as AIChatModel;
522
530
 
@@ -527,6 +535,18 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
527
535
  tracker.save(widget);
528
536
  }
529
537
 
538
+ function updateToolbarTitleOverlay() {
539
+ const titleNode = chatPanel.current?.toolbar.node
540
+ .getElementsByClassName('jp-chat-sidepanel-widget-title')
541
+ .item(0);
542
+ if (titleNode) {
543
+ titleNode.setAttribute('title', model.title ?? model.name);
544
+ }
545
+ }
546
+
547
+ model.titleChanged.connect(updateToolbarTitleOverlay);
548
+ updateToolbarTitleOverlay();
549
+
530
550
  // Update the tracker if the model name changed.
531
551
  model.nameChanged.connect(saveTracker);
532
552
 
@@ -534,19 +554,15 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
534
554
  model.agentManager.activeProviderChanged.connect(saveTracker);
535
555
 
536
556
  // Update the token usage widget.
537
- tokenUsageWidget?.dispose();
557
+ usageWidget?.dispose();
538
558
 
539
- tokenUsageWidget = new TokenUsageWidget({
559
+ usageWidget = new UsageWidget({
540
560
  tokenUsageChanged: model.tokenUsageChanged,
541
561
  settingsModel,
542
562
  initialTokenUsage: model.agentManager.tokenUsage,
543
563
  translator: trans
544
564
  });
545
- chatPanel.current?.toolbar.insertBefore(
546
- 'markRead',
547
- 'token-usage',
548
- tokenUsageWidget
549
- );
565
+ chatPanel.current?.toolbar.insertBefore('markRead', 'usage', usageWidget);
550
566
 
551
567
  if (model.saveAvailable) {
552
568
  const saveChatButton = new SaveComponentWidget({
@@ -586,6 +602,7 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
586
602
  });
587
603
 
588
604
  widget.disposed.connect(() => {
605
+ model.titleChanged.disconnect(updateToolbarTitleOverlay);
589
606
  model.nameChanged.disconnect(saveTracker);
590
607
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
591
608
  model.writersChanged?.disconnect(writersChanged);
@@ -738,7 +755,7 @@ function registerCommands(
738
755
  }
739
756
  });
740
757
 
741
- const openInMain = (model: AIChatModel) => {
758
+ const openInMain = (model: AIChatModel): MainAreaChat => {
742
759
  const content = new ChatWidget({
743
760
  model,
744
761
  rmRegistry,
@@ -771,6 +788,64 @@ function registerCommands(
771
788
  model.nameChanged.disconnect(saveTracker);
772
789
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
773
790
  });
791
+
792
+ return widget;
793
+ };
794
+
795
+ const focusOnChat = (
796
+ area: 'main' | 'side',
797
+ widget?: ChatWidget | MainAreaChat
798
+ ) => {
799
+ if (area === 'main' && widget) {
800
+ app.shell.activateById(widget.id);
801
+ } else {
802
+ app.shell.activateById(chatPanel.id);
803
+ }
804
+ };
805
+
806
+ const applyInputArgs = (model: IChatModel, args: any) => {
807
+ const input = typeof args.input === 'string' ? args.input : undefined;
808
+ const autoSend = args.autoSend === true;
809
+ const shouldFocus = args.focus !== false;
810
+
811
+ if (input !== undefined) {
812
+ model.input.value = input;
813
+ }
814
+ if (autoSend && input !== undefined) {
815
+ model.input.send(model.input.value);
816
+ }
817
+ if (shouldFocus) {
818
+ model.input.focus();
819
+ }
820
+ };
821
+
822
+ const findChatWidget = (
823
+ name?: string,
824
+ provider?: string
825
+ ): ChatWidget | MainAreaChat | undefined => {
826
+ if (!name && !provider) {
827
+ return;
828
+ }
829
+ return tracker.find(widget => {
830
+ const model = widget.model as AIChatModel;
831
+ return (
832
+ (!name || widget.model.name === name) &&
833
+ (!provider || model.agentManager.activeProvider === provider)
834
+ );
835
+ });
836
+ };
837
+
838
+ const disposeSideChatModel = (model: IChatModel): boolean => {
839
+ const loadedName = chatPanel
840
+ .getLoadedModelNames()
841
+ .find(name => chatPanel.getLoadedModel(name) === model);
842
+
843
+ if (!loadedName) {
844
+ return false;
845
+ }
846
+
847
+ chatPanel.disposeLoadedModel(loadedName);
848
+ return true;
774
849
  };
775
850
 
776
851
  commands.addCommand(CommandIds.openChat, {
@@ -805,11 +880,18 @@ function registerCommands(
805
880
  return false;
806
881
  }
807
882
 
883
+ const shouldFocus = args.focus === true;
884
+ let widget: ChatWidget | MainAreaChat | undefined;
808
885
  if (area === 'main') {
809
- openInMain(model);
886
+ widget = openInMain(model);
810
887
  } else {
811
- chatPanel.open({ model });
888
+ widget = chatPanel.open({ model });
889
+ }
890
+ if (shouldFocus) {
891
+ focusOnChat(area, widget);
812
892
  }
893
+ applyInputArgs(model, { ...args, focus: shouldFocus });
894
+
813
895
  return true;
814
896
  },
815
897
  describedBy: {
@@ -828,6 +910,137 @@ function registerCommands(
828
910
  provider: {
829
911
  type: 'string',
830
912
  description: trans.__('The provider/model to use with this chat')
913
+ },
914
+ input: {
915
+ type: 'string',
916
+ description: trans.__('The input text to prefill in the chat')
917
+ },
918
+ focus: {
919
+ type: 'boolean',
920
+ description: trans.__(
921
+ 'Whether to focus the chat input after opening it'
922
+ )
923
+ },
924
+ autoSend: {
925
+ type: 'boolean',
926
+ description: trans.__(
927
+ 'Whether to auto-send the provided input after opening the chat'
928
+ )
929
+ }
930
+ }
931
+ }
932
+ }
933
+ });
934
+
935
+ commands.addCommand(CommandIds.openOrRevealChat, {
936
+ label: trans.__('Open or reveal the chat panel'),
937
+ execute: async (args): Promise<boolean> => {
938
+ const area = (args.area as string) === 'main' ? 'main' : 'side';
939
+ const provider = (args.provider as string) ?? undefined;
940
+ const name = (args.name as string) ?? undefined;
941
+ const shouldFocus = args.focus === true;
942
+
943
+ let existingWidget = findChatWidget(name, provider);
944
+ if (!existingWidget && !name) {
945
+ const providerConfig = provider
946
+ ? settingsModel.getProvider(provider)
947
+ : settingsModel.getDefaultProvider();
948
+ existingWidget = findChatWidget(undefined, providerConfig?.id);
949
+ }
950
+
951
+ // If the side chat model is loaded but not currently displayed, reveal it first.
952
+ if (!existingWidget && name) {
953
+ const loadedModel = chatPanel.getLoadedModel(name);
954
+ if (loadedModel) {
955
+ existingWidget = chatPanel.open({ model: loadedModel });
956
+ }
957
+ }
958
+
959
+ if (!existingWidget) {
960
+ return commands.execute(CommandIds.openChat, {
961
+ ...args,
962
+ focus: shouldFocus
963
+ }) as Promise<boolean>;
964
+ }
965
+
966
+ const currentArea =
967
+ existingWidget instanceof MainAreaChat ? 'main' : 'side';
968
+ if (currentArea !== area) {
969
+ const targetName = existingWidget.model.name;
970
+ const moved = (await commands.execute(CommandIds.moveChat, {
971
+ name: targetName,
972
+ area
973
+ })) as boolean;
974
+ if (!moved) {
975
+ return false;
976
+ }
977
+
978
+ const movedWidget = findChatWidget(targetName);
979
+ if (!movedWidget) {
980
+ return false;
981
+ }
982
+
983
+ if (area === 'side') {
984
+ chatPanel.open({ model: movedWidget.model });
985
+ }
986
+ if (shouldFocus) {
987
+ focusOnChat(area, movedWidget);
988
+ }
989
+ applyInputArgs(movedWidget.model, {
990
+ ...args,
991
+ focus: shouldFocus
992
+ });
993
+
994
+ return true;
995
+ }
996
+
997
+ if (area === 'side') {
998
+ chatPanel.open({ model: existingWidget.model });
999
+ }
1000
+ if (shouldFocus) {
1001
+ focusOnChat(area, existingWidget);
1002
+ }
1003
+ applyInputArgs(existingWidget.model, {
1004
+ ...args,
1005
+ focus: shouldFocus
1006
+ });
1007
+
1008
+ return true;
1009
+ },
1010
+ describedBy: {
1011
+ args: {
1012
+ type: 'object',
1013
+ properties: {
1014
+ area: {
1015
+ type: 'string',
1016
+ enum: ['main', 'side'],
1017
+ description: trans.__(
1018
+ 'The name of the area to open or reveal the chat in'
1019
+ )
1020
+ },
1021
+ name: {
1022
+ type: 'string',
1023
+ description: trans.__('The name of the chat')
1024
+ },
1025
+ provider: {
1026
+ type: 'string',
1027
+ description: trans.__('The provider/model to use with this chat')
1028
+ },
1029
+ input: {
1030
+ type: 'string',
1031
+ description: trans.__('The input text to prefill in the chat')
1032
+ },
1033
+ focus: {
1034
+ type: 'boolean',
1035
+ description: trans.__(
1036
+ 'Whether to focus the chat input after opening it'
1037
+ )
1038
+ },
1039
+ autoSend: {
1040
+ type: 'boolean',
1041
+ description: trans.__(
1042
+ 'Whether to auto-send the provided input after opening the chat'
1043
+ )
831
1044
  }
832
1045
  }
833
1046
  }
@@ -887,7 +1100,8 @@ function registerCommands(
887
1100
  activeProvider: previousModel.agentManager.activeProvider,
888
1101
  tokenUsage: previousModel.agentManager.tokenUsage,
889
1102
  messages: previousModel.messages,
890
- autosave: previousModel.autosave
1103
+ autosave: previousModel.autosave,
1104
+ title: previousModel.title
891
1105
  });
892
1106
 
893
1107
  // Wait (with timeout) for the tracker to have updated the previous widget.
@@ -907,6 +1121,15 @@ function registerCommands(
907
1121
 
908
1122
  if (area === 'main') {
909
1123
  openInMain(model);
1124
+
1125
+ if (previousWidget instanceof ChatWidget) {
1126
+ // Clean up the side-panel model entry before disposing the previous
1127
+ // widget/model state.
1128
+ if (!disposeSideChatModel(previousModel)) {
1129
+ previousWidget.dispose();
1130
+ previousModel.dispose();
1131
+ }
1132
+ }
910
1133
  } else {
911
1134
  previousWidget?.dispose();
912
1135
  previousModel.dispose();
@@ -1516,6 +1739,7 @@ export default [
1516
1739
  skillRegistryPlugin,
1517
1740
  skillsCommandPlugin,
1518
1741
  chatModelHandler,
1742
+ activeCellManager,
1519
1743
  plugin,
1520
1744
  toolRegistry,
1521
1745
  agentManagerFactory,
@@ -23,6 +23,7 @@ export class AISettingsModel extends VDomModel implements IAISettingsModel {
23
23
  toolsEnabled: true,
24
24
  sendWithShiftEnter: false,
25
25
  showTokenUsage: false,
26
+ showContextUsage: false,
26
27
  showCellDiff: true,
27
28
  showFileDiff: true,
28
29
  diffDisplayMode: 'split',
@@ -4,6 +4,7 @@ import { createMistral } from '@ai-sdk/mistral';
4
4
  import { createOpenAI } from '@ai-sdk/openai';
5
5
  import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
6
6
 
7
+ import { BUILT_IN_PROVIDER_MODEL_INFO } from './generated-context-windows';
7
8
  import type { IProviderInfo } from '../tokens';
8
9
  import type { IModelOptions } from './models';
9
10
 
@@ -30,6 +31,7 @@ export const anthropicProvider: IProviderInfo = {
30
31
  'claude-sonnet-4-0',
31
32
  'claude-sonnet-4-20250514'
32
33
  ],
34
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.anthropic,
33
35
  supportsBaseURL: true,
34
36
  supportsHeaders: true,
35
37
  providerToolCapabilities: {
@@ -77,6 +79,7 @@ export const googleProvider: IProviderInfo = {
77
79
  'gemini-flash-latest',
78
80
  'gemini-flash-lite-latest'
79
81
  ],
82
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.google,
80
83
  supportsBaseURL: true,
81
84
  factory: (options: IModelOptions) => {
82
85
  if (!options.apiKey) {
@@ -113,6 +116,7 @@ export const mistralProvider: IProviderInfo = {
113
116
  'codestral-latest',
114
117
  'devstral-latest'
115
118
  ],
119
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.mistral,
116
120
  supportsBaseURL: true,
117
121
  factory: (options: IModelOptions) => {
118
122
  if (!options.apiKey) {
@@ -182,6 +186,7 @@ export const openaiProvider: IProviderInfo = {
182
186
  'gpt-3.5-turbo',
183
187
  'gpt-3.5-turbo-0125'
184
188
  ],
189
+ modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.openai,
185
190
  supportsBaseURL: true,
186
191
  supportsHeaders: true,
187
192
  providerToolCapabilities: {