@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.
- package/lib/agent.d.ts +12 -2
- package/lib/agent.js +112 -17
- package/lib/chat-commands/clear.js +1 -1
- package/lib/chat-model-handler.js +4 -1
- package/lib/chat-model.d.ts +25 -24
- package/lib/chat-model.js +262 -132
- package/lib/components/clear-button.d.ts +1 -1
- package/lib/components/clear-button.js +1 -1
- package/lib/components/index.d.ts +1 -1
- package/lib/components/index.js +1 -1
- package/lib/components/{token-usage-display.d.ts → usage-display.d.ts} +11 -11
- package/lib/components/usage-display.js +109 -0
- package/lib/index.js +205 -20
- package/lib/models/settings-model.js +1 -0
- package/lib/providers/built-in-providers.js +5 -0
- package/lib/providers/generated-context-windows.d.ts +8 -0
- package/lib/providers/generated-context-windows.js +96 -0
- package/lib/providers/model-info.d.ts +3 -0
- package/lib/providers/model-info.js +58 -0
- package/lib/tokens.d.ts +34 -3
- package/lib/tokens.js +8 -7
- package/lib/widgets/ai-settings.js +9 -0
- package/lib/widgets/main-area-chat.d.ts +1 -0
- package/lib/widgets/main-area-chat.js +10 -4
- package/lib/widgets/provider-config-dialog.js +18 -5
- package/package.json +3 -2
- package/schema/settings-model.json +11 -0
- package/src/agent.ts +151 -21
- package/src/chat-commands/clear.ts +1 -1
- package/src/chat-model-handler.ts +6 -1
- package/src/chat-model.ts +350 -175
- package/src/components/clear-button.tsx +3 -3
- package/src/components/index.ts +1 -1
- package/src/components/usage-display.tsx +208 -0
- package/src/index.ts +250 -26
- package/src/models/settings-model.ts +1 -0
- package/src/providers/built-in-providers.ts +5 -0
- package/src/providers/generated-context-windows.ts +102 -0
- package/src/providers/model-info.ts +88 -0
- package/src/tokens.ts +46 -10
- package/src/widgets/ai-settings.tsx +42 -0
- package/src/widgets/main-area-chat.ts +12 -4
- package/src/widgets/provider-config-dialog.tsx +45 -5
- package/lib/components/token-usage-display.js +0 -72
- package/src/components/token-usage-display.tsx +0 -137
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ReactWidget, UseSignal } from '@jupyterlab/ui-components';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* React component that displays usage information.
|
|
5
|
+
* Shows input/output token counts and optional estimated context usage.
|
|
6
|
+
* Only renders when token or context usage display is enabled in settings.
|
|
7
|
+
*/
|
|
8
|
+
export const UsageDisplay = ({ tokenUsageChanged, settingsModel, initialTokenUsage, translator: trans }) => {
|
|
9
|
+
const formatContextPercent = (value) => {
|
|
10
|
+
return Math.round(value).toLocaleString();
|
|
11
|
+
};
|
|
12
|
+
const badgeStyle = {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
gap: '6px',
|
|
16
|
+
fontSize: '12px',
|
|
17
|
+
color: 'var(--jp-ui-font-color2)',
|
|
18
|
+
padding: '4px 8px',
|
|
19
|
+
backgroundColor: 'var(--jp-layout-color1)',
|
|
20
|
+
border: '1px solid var(--jp-border-color1)',
|
|
21
|
+
borderRadius: '4px',
|
|
22
|
+
whiteSpace: 'nowrap'
|
|
23
|
+
};
|
|
24
|
+
return (React.createElement(UseSignal, { signal: settingsModel.stateChanged, initialArgs: undefined }, () => {
|
|
25
|
+
const config = settingsModel.config;
|
|
26
|
+
const showTokenUsage = config.showTokenUsage;
|
|
27
|
+
const showContextUsage = config.showContextUsage;
|
|
28
|
+
if (!showTokenUsage && !showContextUsage) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return (React.createElement(UseSignal, { signal: tokenUsageChanged, initialArgs: initialTokenUsage }, (_, tokenUsage) => {
|
|
32
|
+
if (!tokenUsage) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const total = tokenUsage.inputTokens + tokenUsage.outputTokens;
|
|
36
|
+
const hasKnownContextWindow = showContextUsage && tokenUsage.contextWindow !== undefined;
|
|
37
|
+
const contextUsagePercent = tokenUsage.lastRequestInputTokens !== undefined &&
|
|
38
|
+
tokenUsage.contextWindow !== undefined &&
|
|
39
|
+
tokenUsage.contextWindow > 0
|
|
40
|
+
? Math.max(0, Math.min(100, (tokenUsage.lastRequestInputTokens /
|
|
41
|
+
tokenUsage.contextWindow) *
|
|
42
|
+
100))
|
|
43
|
+
: undefined;
|
|
44
|
+
const hasContextEstimate = hasKnownContextWindow &&
|
|
45
|
+
contextUsagePercent !== undefined &&
|
|
46
|
+
tokenUsage.lastRequestInputTokens !== undefined;
|
|
47
|
+
const contextLabel = hasContextEstimate
|
|
48
|
+
? `${formatContextPercent(contextUsagePercent)}%`
|
|
49
|
+
: hasKnownContextWindow
|
|
50
|
+
? '0%'
|
|
51
|
+
: '?';
|
|
52
|
+
const contextTitle = hasContextEstimate
|
|
53
|
+
? trans.__('Context Usage (estimated): %1% (%2 / %3 tokens)', formatContextPercent(contextUsagePercent), tokenUsage.lastRequestInputTokens.toLocaleString(), tokenUsage.contextWindow.toLocaleString())
|
|
54
|
+
: hasKnownContextWindow
|
|
55
|
+
? trans.__('Context usage estimate will appear after the next request. Showing 0% until then. Context window: %1 tokens', tokenUsage.contextWindow.toLocaleString())
|
|
56
|
+
: trans.__('Context Usage unavailable. Configure a context window for the active provider/model to enable estimation.');
|
|
57
|
+
return (React.createElement("div", { style: {
|
|
58
|
+
display: 'flex',
|
|
59
|
+
alignItems: 'center',
|
|
60
|
+
gap: '6px'
|
|
61
|
+
} },
|
|
62
|
+
showTokenUsage && (React.createElement("span", { style: badgeStyle, title: trans.__('Token Usage - Sent: %1, Received: %2, Total: %3', tokenUsage.inputTokens.toLocaleString(), tokenUsage.outputTokens.toLocaleString(), total.toLocaleString()) },
|
|
63
|
+
React.createElement("span", { style: {
|
|
64
|
+
display: 'flex',
|
|
65
|
+
alignItems: 'center',
|
|
66
|
+
gap: '2px'
|
|
67
|
+
} },
|
|
68
|
+
React.createElement("span", null, "\u2191"),
|
|
69
|
+
React.createElement("span", null, tokenUsage.inputTokens.toLocaleString())),
|
|
70
|
+
React.createElement("span", { style: {
|
|
71
|
+
display: 'flex',
|
|
72
|
+
alignItems: 'center',
|
|
73
|
+
gap: '2px'
|
|
74
|
+
} },
|
|
75
|
+
React.createElement("span", null, "\u2193"),
|
|
76
|
+
React.createElement("span", null, tokenUsage.outputTokens.toLocaleString())))),
|
|
77
|
+
showContextUsage && (React.createElement("span", { style: badgeStyle, title: contextTitle },
|
|
78
|
+
React.createElement("span", { style: {
|
|
79
|
+
display: 'flex',
|
|
80
|
+
alignItems: 'center',
|
|
81
|
+
gap: '2px'
|
|
82
|
+
} },
|
|
83
|
+
React.createElement("span", null, "ctx"),
|
|
84
|
+
React.createElement("span", null, contextLabel))))));
|
|
85
|
+
}));
|
|
86
|
+
}));
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* JupyterLab widget wrapper for the UsageDisplay component.
|
|
90
|
+
* Extends ReactWidget to integrate with the JupyterLab widget system.
|
|
91
|
+
*/
|
|
92
|
+
export class UsageWidget extends ReactWidget {
|
|
93
|
+
/**
|
|
94
|
+
* Creates a new UsageWidget instance.
|
|
95
|
+
* @param options - Configuration options containing required models
|
|
96
|
+
*/
|
|
97
|
+
constructor(options) {
|
|
98
|
+
super();
|
|
99
|
+
this._options = options;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Renders the React component within the widget.
|
|
103
|
+
* @returns The UsageDisplay React element
|
|
104
|
+
*/
|
|
105
|
+
render() {
|
|
106
|
+
return React.createElement(UsageDisplay, { ...this._options });
|
|
107
|
+
}
|
|
108
|
+
_options;
|
|
109
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -25,7 +25,7 @@ import { ChatModelHandler } from './chat-model-handler';
|
|
|
25
25
|
import { CommandIds, IAgentManagerFactory, IAISettingsModel, IChatModelHandler, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE } from './tokens';
|
|
26
26
|
import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
|
|
27
27
|
import { AICompletionProvider } from './completion';
|
|
28
|
-
import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget,
|
|
28
|
+
import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, UsageWidget } from './components';
|
|
29
29
|
import { AISettingsModel } from './models/settings-model';
|
|
30
30
|
import { loadSkillsFromPaths, SkillRegistry } from './skills';
|
|
31
31
|
import { DiffManager } from './diff-manager';
|
|
@@ -213,6 +213,22 @@ const chatModelHandler = {
|
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
};
|
|
216
|
+
/**
|
|
217
|
+
* The active cell manager plugin, to allow copying code from chat to notebook.
|
|
218
|
+
*/
|
|
219
|
+
const activeCellManager = {
|
|
220
|
+
id: '@jupyterlite/ai:activeCellManager',
|
|
221
|
+
description: 'Add the active cell manager to the model handler',
|
|
222
|
+
autoStart: true,
|
|
223
|
+
requires: [IChatModelHandler, INotebookTracker],
|
|
224
|
+
activate: (app, modelHandler, notebookTracker) => {
|
|
225
|
+
const activeCellManager = new ActiveCellManager({
|
|
226
|
+
tracker: notebookTracker,
|
|
227
|
+
shell: app.shell
|
|
228
|
+
});
|
|
229
|
+
modelHandler.activeCellManager = activeCellManager;
|
|
230
|
+
}
|
|
231
|
+
};
|
|
216
232
|
/**
|
|
217
233
|
* Initialization data for the extension.
|
|
218
234
|
*/
|
|
@@ -232,13 +248,12 @@ const plugin = {
|
|
|
232
248
|
IThemeManager,
|
|
233
249
|
ILayoutRestorer,
|
|
234
250
|
ILabShell,
|
|
235
|
-
INotebookTracker,
|
|
236
251
|
ITranslator,
|
|
237
252
|
IComponentsRendererFactory,
|
|
238
253
|
ICommandPalette,
|
|
239
254
|
IDocumentManager
|
|
240
255
|
],
|
|
241
|
-
activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell,
|
|
256
|
+
activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, translator, chatComponentsFactory, palette, documentManager) => {
|
|
242
257
|
const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
|
|
243
258
|
// Create attachment opener registry to handle file attachments
|
|
244
259
|
const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
|
|
@@ -253,16 +268,6 @@ const plugin = {
|
|
|
253
268
|
void app.commands.execute(CommandIds.openSettings);
|
|
254
269
|
}
|
|
255
270
|
};
|
|
256
|
-
// Create ActiveCellManager if notebook tracker is available, and add it to the
|
|
257
|
-
// model registry.
|
|
258
|
-
let activeCellManager;
|
|
259
|
-
if (notebookTracker) {
|
|
260
|
-
activeCellManager = new ActiveCellManager({
|
|
261
|
-
tracker: notebookTracker,
|
|
262
|
-
shell: app.shell
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
modelHandler.activeCellManager = activeCellManager;
|
|
266
271
|
// Creating the tracker for the chat widgets
|
|
267
272
|
const namespace = 'ai-chat';
|
|
268
273
|
const tracker = new WidgetTracker({ namespace });
|
|
@@ -338,7 +343,7 @@ const plugin = {
|
|
|
338
343
|
app.commands.commandChanged.connect(onCommandChanged);
|
|
339
344
|
chatPanel.disposed.connect(disconnectSettingsButtonListener);
|
|
340
345
|
}
|
|
341
|
-
let
|
|
346
|
+
let usageWidget = null;
|
|
342
347
|
chatPanel.chatOpened.connect((_, widget) => {
|
|
343
348
|
const model = widget.model;
|
|
344
349
|
// Add the widget to the tracker.
|
|
@@ -346,19 +351,29 @@ const plugin = {
|
|
|
346
351
|
function saveTracker() {
|
|
347
352
|
tracker.save(widget);
|
|
348
353
|
}
|
|
354
|
+
function updateToolbarTitleOverlay() {
|
|
355
|
+
const titleNode = chatPanel.current?.toolbar.node
|
|
356
|
+
.getElementsByClassName('jp-chat-sidepanel-widget-title')
|
|
357
|
+
.item(0);
|
|
358
|
+
if (titleNode) {
|
|
359
|
+
titleNode.setAttribute('title', model.title ?? model.name);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
model.titleChanged.connect(updateToolbarTitleOverlay);
|
|
363
|
+
updateToolbarTitleOverlay();
|
|
349
364
|
// Update the tracker if the model name changed.
|
|
350
365
|
model.nameChanged.connect(saveTracker);
|
|
351
366
|
// Update the tracker if the active provider changed.
|
|
352
367
|
model.agentManager.activeProviderChanged.connect(saveTracker);
|
|
353
368
|
// Update the token usage widget.
|
|
354
|
-
|
|
355
|
-
|
|
369
|
+
usageWidget?.dispose();
|
|
370
|
+
usageWidget = new UsageWidget({
|
|
356
371
|
tokenUsageChanged: model.tokenUsageChanged,
|
|
357
372
|
settingsModel,
|
|
358
373
|
initialTokenUsage: model.agentManager.tokenUsage,
|
|
359
374
|
translator: trans
|
|
360
375
|
});
|
|
361
|
-
chatPanel.current?.toolbar.insertBefore('markRead', '
|
|
376
|
+
chatPanel.current?.toolbar.insertBefore('markRead', 'usage', usageWidget);
|
|
362
377
|
if (model.saveAvailable) {
|
|
363
378
|
const saveChatButton = new SaveComponentWidget({
|
|
364
379
|
model,
|
|
@@ -386,6 +401,7 @@ const plugin = {
|
|
|
386
401
|
chatPanel: widget
|
|
387
402
|
});
|
|
388
403
|
widget.disposed.connect(() => {
|
|
404
|
+
model.titleChanged.disconnect(updateToolbarTitleOverlay);
|
|
389
405
|
model.nameChanged.disconnect(saveTracker);
|
|
390
406
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
391
407
|
model.writersChanged?.disconnect(writersChanged);
|
|
@@ -514,6 +530,49 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
514
530
|
model.nameChanged.disconnect(saveTracker);
|
|
515
531
|
model.agentManager.activeProviderChanged.disconnect(saveTracker);
|
|
516
532
|
});
|
|
533
|
+
return widget;
|
|
534
|
+
};
|
|
535
|
+
const focusOnChat = (area, widget) => {
|
|
536
|
+
if (area === 'main' && widget) {
|
|
537
|
+
app.shell.activateById(widget.id);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
app.shell.activateById(chatPanel.id);
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
const applyInputArgs = (model, args) => {
|
|
544
|
+
const input = typeof args.input === 'string' ? args.input : undefined;
|
|
545
|
+
const autoSend = args.autoSend === true;
|
|
546
|
+
const shouldFocus = args.focus !== false;
|
|
547
|
+
if (input !== undefined) {
|
|
548
|
+
model.input.value = input;
|
|
549
|
+
}
|
|
550
|
+
if (autoSend && input !== undefined) {
|
|
551
|
+
model.input.send(model.input.value);
|
|
552
|
+
}
|
|
553
|
+
if (shouldFocus) {
|
|
554
|
+
model.input.focus();
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
const findChatWidget = (name, provider) => {
|
|
558
|
+
if (!name && !provider) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
return tracker.find(widget => {
|
|
562
|
+
const model = widget.model;
|
|
563
|
+
return ((!name || widget.model.name === name) &&
|
|
564
|
+
(!provider || model.agentManager.activeProvider === provider));
|
|
565
|
+
});
|
|
566
|
+
};
|
|
567
|
+
const disposeSideChatModel = (model) => {
|
|
568
|
+
const loadedName = chatPanel
|
|
569
|
+
.getLoadedModelNames()
|
|
570
|
+
.find(name => chatPanel.getLoadedModel(name) === model);
|
|
571
|
+
if (!loadedName) {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
chatPanel.disposeLoadedModel(loadedName);
|
|
575
|
+
return true;
|
|
517
576
|
};
|
|
518
577
|
commands.addCommand(CommandIds.openChat, {
|
|
519
578
|
label: trans.__('Open a chat'),
|
|
@@ -543,12 +602,18 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
543
602
|
if (!model) {
|
|
544
603
|
return false;
|
|
545
604
|
}
|
|
605
|
+
const shouldFocus = args.focus === true;
|
|
606
|
+
let widget;
|
|
546
607
|
if (area === 'main') {
|
|
547
|
-
openInMain(model);
|
|
608
|
+
widget = openInMain(model);
|
|
548
609
|
}
|
|
549
610
|
else {
|
|
550
|
-
chatPanel.open({ model });
|
|
611
|
+
widget = chatPanel.open({ model });
|
|
612
|
+
}
|
|
613
|
+
if (shouldFocus) {
|
|
614
|
+
focusOnChat(area, widget);
|
|
551
615
|
}
|
|
616
|
+
applyInputArgs(model, { ...args, focus: shouldFocus });
|
|
552
617
|
return true;
|
|
553
618
|
},
|
|
554
619
|
describedBy: {
|
|
@@ -567,6 +632,116 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
567
632
|
provider: {
|
|
568
633
|
type: 'string',
|
|
569
634
|
description: trans.__('The provider/model to use with this chat')
|
|
635
|
+
},
|
|
636
|
+
input: {
|
|
637
|
+
type: 'string',
|
|
638
|
+
description: trans.__('The input text to prefill in the chat')
|
|
639
|
+
},
|
|
640
|
+
focus: {
|
|
641
|
+
type: 'boolean',
|
|
642
|
+
description: trans.__('Whether to focus the chat input after opening it')
|
|
643
|
+
},
|
|
644
|
+
autoSend: {
|
|
645
|
+
type: 'boolean',
|
|
646
|
+
description: trans.__('Whether to auto-send the provided input after opening the chat')
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
commands.addCommand(CommandIds.openOrRevealChat, {
|
|
653
|
+
label: trans.__('Open or reveal the chat panel'),
|
|
654
|
+
execute: async (args) => {
|
|
655
|
+
const area = args.area === 'main' ? 'main' : 'side';
|
|
656
|
+
const provider = args.provider ?? undefined;
|
|
657
|
+
const name = args.name ?? undefined;
|
|
658
|
+
const shouldFocus = args.focus === true;
|
|
659
|
+
let existingWidget = findChatWidget(name, provider);
|
|
660
|
+
if (!existingWidget && !name) {
|
|
661
|
+
const providerConfig = provider
|
|
662
|
+
? settingsModel.getProvider(provider)
|
|
663
|
+
: settingsModel.getDefaultProvider();
|
|
664
|
+
existingWidget = findChatWidget(undefined, providerConfig?.id);
|
|
665
|
+
}
|
|
666
|
+
// If the side chat model is loaded but not currently displayed, reveal it first.
|
|
667
|
+
if (!existingWidget && name) {
|
|
668
|
+
const loadedModel = chatPanel.getLoadedModel(name);
|
|
669
|
+
if (loadedModel) {
|
|
670
|
+
existingWidget = chatPanel.open({ model: loadedModel });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!existingWidget) {
|
|
674
|
+
return commands.execute(CommandIds.openChat, {
|
|
675
|
+
...args,
|
|
676
|
+
focus: shouldFocus
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const currentArea = existingWidget instanceof MainAreaChat ? 'main' : 'side';
|
|
680
|
+
if (currentArea !== area) {
|
|
681
|
+
const targetName = existingWidget.model.name;
|
|
682
|
+
const moved = (await commands.execute(CommandIds.moveChat, {
|
|
683
|
+
name: targetName,
|
|
684
|
+
area
|
|
685
|
+
}));
|
|
686
|
+
if (!moved) {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
const movedWidget = findChatWidget(targetName);
|
|
690
|
+
if (!movedWidget) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (area === 'side') {
|
|
694
|
+
chatPanel.open({ model: movedWidget.model });
|
|
695
|
+
}
|
|
696
|
+
if (shouldFocus) {
|
|
697
|
+
focusOnChat(area, movedWidget);
|
|
698
|
+
}
|
|
699
|
+
applyInputArgs(movedWidget.model, {
|
|
700
|
+
...args,
|
|
701
|
+
focus: shouldFocus
|
|
702
|
+
});
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
if (area === 'side') {
|
|
706
|
+
chatPanel.open({ model: existingWidget.model });
|
|
707
|
+
}
|
|
708
|
+
if (shouldFocus) {
|
|
709
|
+
focusOnChat(area, existingWidget);
|
|
710
|
+
}
|
|
711
|
+
applyInputArgs(existingWidget.model, {
|
|
712
|
+
...args,
|
|
713
|
+
focus: shouldFocus
|
|
714
|
+
});
|
|
715
|
+
return true;
|
|
716
|
+
},
|
|
717
|
+
describedBy: {
|
|
718
|
+
args: {
|
|
719
|
+
type: 'object',
|
|
720
|
+
properties: {
|
|
721
|
+
area: {
|
|
722
|
+
type: 'string',
|
|
723
|
+
enum: ['main', 'side'],
|
|
724
|
+
description: trans.__('The name of the area to open or reveal the chat in')
|
|
725
|
+
},
|
|
726
|
+
name: {
|
|
727
|
+
type: 'string',
|
|
728
|
+
description: trans.__('The name of the chat')
|
|
729
|
+
},
|
|
730
|
+
provider: {
|
|
731
|
+
type: 'string',
|
|
732
|
+
description: trans.__('The provider/model to use with this chat')
|
|
733
|
+
},
|
|
734
|
+
input: {
|
|
735
|
+
type: 'string',
|
|
736
|
+
description: trans.__('The input text to prefill in the chat')
|
|
737
|
+
},
|
|
738
|
+
focus: {
|
|
739
|
+
type: 'boolean',
|
|
740
|
+
description: trans.__('Whether to focus the chat input after opening it')
|
|
741
|
+
},
|
|
742
|
+
autoSend: {
|
|
743
|
+
type: 'boolean',
|
|
744
|
+
description: trans.__('Whether to auto-send the provided input after opening the chat')
|
|
570
745
|
}
|
|
571
746
|
}
|
|
572
747
|
}
|
|
@@ -615,7 +790,8 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
615
790
|
activeProvider: previousModel.agentManager.activeProvider,
|
|
616
791
|
tokenUsage: previousModel.agentManager.tokenUsage,
|
|
617
792
|
messages: previousModel.messages,
|
|
618
|
-
autosave: previousModel.autosave
|
|
793
|
+
autosave: previousModel.autosave,
|
|
794
|
+
title: previousModel.title
|
|
619
795
|
});
|
|
620
796
|
// Wait (with timeout) for the tracker to have updated the previous widget.
|
|
621
797
|
const status = await Promise.any([
|
|
@@ -630,6 +806,14 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
|
|
|
630
806
|
}
|
|
631
807
|
if (area === 'main') {
|
|
632
808
|
openInMain(model);
|
|
809
|
+
if (previousWidget instanceof ChatWidget) {
|
|
810
|
+
// Clean up the side-panel model entry before disposing the previous
|
|
811
|
+
// widget/model state.
|
|
812
|
+
if (!disposeSideChatModel(previousModel)) {
|
|
813
|
+
previousWidget.dispose();
|
|
814
|
+
previousModel.dispose();
|
|
815
|
+
}
|
|
816
|
+
}
|
|
633
817
|
}
|
|
634
818
|
else {
|
|
635
819
|
previousWidget?.dispose();
|
|
@@ -1118,6 +1302,7 @@ export default [
|
|
|
1118
1302
|
skillRegistryPlugin,
|
|
1119
1303
|
skillsCommandPlugin,
|
|
1120
1304
|
chatModelHandler,
|
|
1305
|
+
activeCellManager,
|
|
1121
1306
|
plugin,
|
|
1122
1307
|
toolRegistry,
|
|
1123
1308
|
agentManagerFactory,
|
|
@@ -3,6 +3,7 @@ import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
|
3
3
|
import { createMistral } from '@ai-sdk/mistral';
|
|
4
4
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
5
5
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
6
|
+
import { BUILT_IN_PROVIDER_MODEL_INFO } from './generated-context-windows';
|
|
6
7
|
/**
|
|
7
8
|
* Anthropic provider
|
|
8
9
|
*/
|
|
@@ -26,6 +27,7 @@ export const anthropicProvider = {
|
|
|
26
27
|
'claude-sonnet-4-0',
|
|
27
28
|
'claude-sonnet-4-20250514'
|
|
28
29
|
],
|
|
30
|
+
modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.anthropic,
|
|
29
31
|
supportsBaseURL: true,
|
|
30
32
|
supportsHeaders: true,
|
|
31
33
|
providerToolCapabilities: {
|
|
@@ -72,6 +74,7 @@ export const googleProvider = {
|
|
|
72
74
|
'gemini-flash-latest',
|
|
73
75
|
'gemini-flash-lite-latest'
|
|
74
76
|
],
|
|
77
|
+
modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.google,
|
|
75
78
|
supportsBaseURL: true,
|
|
76
79
|
factory: (options) => {
|
|
77
80
|
if (!options.apiKey) {
|
|
@@ -107,6 +110,7 @@ export const mistralProvider = {
|
|
|
107
110
|
'codestral-latest',
|
|
108
111
|
'devstral-latest'
|
|
109
112
|
],
|
|
113
|
+
modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.mistral,
|
|
110
114
|
supportsBaseURL: true,
|
|
111
115
|
factory: (options) => {
|
|
112
116
|
if (!options.apiKey) {
|
|
@@ -175,6 +179,7 @@ export const openaiProvider = {
|
|
|
175
179
|
'gpt-3.5-turbo',
|
|
176
180
|
'gpt-3.5-turbo-0125'
|
|
177
181
|
],
|
|
182
|
+
modelInfo: BUILT_IN_PROVIDER_MODEL_INFO.openai,
|
|
178
183
|
supportsBaseURL: true,
|
|
179
184
|
supportsHeaders: true,
|
|
180
185
|
providerToolCapabilities: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is generated by `jlpm sync:model-context-windows`.
|
|
3
|
+
* Source: https://models.dev/api.json
|
|
4
|
+
* Backed by: https://github.com/anomalyco/models.dev
|
|
5
|
+
* Generated: 2026-04-08T16:23:34.080Z
|
|
6
|
+
*/
|
|
7
|
+
import type { IProviderModelInfo } from '../tokens';
|
|
8
|
+
export declare const BUILT_IN_PROVIDER_MODEL_INFO: Record<string, Record<string, IProviderModelInfo>>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is generated by `jlpm sync:model-context-windows`.
|
|
3
|
+
* Source: https://models.dev/api.json
|
|
4
|
+
* Backed by: https://github.com/anomalyco/models.dev
|
|
5
|
+
* Generated: 2026-04-08T16:23:34.080Z
|
|
6
|
+
*/
|
|
7
|
+
export const BUILT_IN_PROVIDER_MODEL_INFO = {
|
|
8
|
+
anthropic: {
|
|
9
|
+
'claude-opus-4-6': { contextWindow: 1000000 },
|
|
10
|
+
'claude-sonnet-4-6': { contextWindow: 1000000 },
|
|
11
|
+
'claude-opus-4-5': { contextWindow: 200000 },
|
|
12
|
+
'claude-opus-4-5-20251101': { contextWindow: 200000 },
|
|
13
|
+
'claude-sonnet-4-5': { contextWindow: 200000 },
|
|
14
|
+
'claude-sonnet-4-5-20250929': { contextWindow: 200000 },
|
|
15
|
+
'claude-haiku-4-5': { contextWindow: 200000 },
|
|
16
|
+
'claude-haiku-4-5-20251001': { contextWindow: 200000 },
|
|
17
|
+
'claude-opus-4-1': { contextWindow: 200000 },
|
|
18
|
+
'claude-opus-4-1-20250805': { contextWindow: 200000 },
|
|
19
|
+
'claude-opus-4-0': { contextWindow: 200000 },
|
|
20
|
+
'claude-opus-4-20250514': { contextWindow: 200000 },
|
|
21
|
+
'claude-sonnet-4-0': { contextWindow: 200000 },
|
|
22
|
+
'claude-sonnet-4-20250514': { contextWindow: 200000 }
|
|
23
|
+
},
|
|
24
|
+
google: {
|
|
25
|
+
'gemini-3.1-pro-preview': { contextWindow: 1048576 },
|
|
26
|
+
'gemini-3.1-pro-preview-customtools': { contextWindow: 1048576 },
|
|
27
|
+
'gemini-3.1-flash-image-preview': { contextWindow: 131072 },
|
|
28
|
+
'gemini-3.1-flash-lite-preview': { contextWindow: 1048576 },
|
|
29
|
+
'gemini-3-flash-preview': { contextWindow: 1048576 },
|
|
30
|
+
'gemini-2.5-pro': { contextWindow: 1048576 },
|
|
31
|
+
'gemini-2.5-flash': { contextWindow: 1048576 },
|
|
32
|
+
'gemini-2.5-flash-image': { contextWindow: 32768 },
|
|
33
|
+
'gemini-2.5-flash-lite': { contextWindow: 1048576 },
|
|
34
|
+
'gemini-flash-latest': { contextWindow: 1048576 },
|
|
35
|
+
'gemini-flash-lite-latest': { contextWindow: 1048576 }
|
|
36
|
+
},
|
|
37
|
+
mistral: {
|
|
38
|
+
'mistral-large-latest': { contextWindow: 262144 },
|
|
39
|
+
'mistral-medium-latest': { contextWindow: 128000 },
|
|
40
|
+
'mistral-medium-2508': { contextWindow: 262144 },
|
|
41
|
+
'mistral-small-latest': { contextWindow: 256000 },
|
|
42
|
+
'mistral-small-2506': { contextWindow: 128000 },
|
|
43
|
+
'ministral-3b-latest': { contextWindow: 128000 },
|
|
44
|
+
'ministral-8b-latest': { contextWindow: 128000 },
|
|
45
|
+
'magistral-small-latest': { contextWindow: 128000 },
|
|
46
|
+
'magistral-medium-latest': { contextWindow: 128000 },
|
|
47
|
+
'pixtral-large-latest': { contextWindow: 128000 },
|
|
48
|
+
'codestral-latest': { contextWindow: 256000 },
|
|
49
|
+
'devstral-latest': { contextWindow: 262144 },
|
|
50
|
+
'devstral-2512': { contextWindow: 262144 }
|
|
51
|
+
},
|
|
52
|
+
openai: {
|
|
53
|
+
'gpt-5.4': { contextWindow: 1050000 },
|
|
54
|
+
'gpt-5.4-mini': { contextWindow: 400000 },
|
|
55
|
+
'gpt-5.4-nano': { contextWindow: 400000 },
|
|
56
|
+
'gpt-5.2': { contextWindow: 400000 },
|
|
57
|
+
'gpt-5.2-2025-12-11': { contextWindow: 400000 },
|
|
58
|
+
'gpt-5.2-chat-latest': { contextWindow: 128000 },
|
|
59
|
+
'gpt-5.2-pro': { contextWindow: 400000 },
|
|
60
|
+
'gpt-5.2-pro-2025-12-11': { contextWindow: 400000 },
|
|
61
|
+
'gpt-5.2-codex': { contextWindow: 400000 },
|
|
62
|
+
'gpt-5.1': { contextWindow: 400000 },
|
|
63
|
+
'gpt-5.1-2025-11-13': { contextWindow: 400000 },
|
|
64
|
+
'gpt-5.1-chat-latest': { contextWindow: 128000 },
|
|
65
|
+
'gpt-5': { contextWindow: 400000 },
|
|
66
|
+
'gpt-5-2025-08-07': { contextWindow: 400000 },
|
|
67
|
+
'gpt-5-chat-latest': { contextWindow: 400000 },
|
|
68
|
+
'gpt-5-mini': { contextWindow: 400000 },
|
|
69
|
+
'gpt-5-mini-2025-08-07': { contextWindow: 400000 },
|
|
70
|
+
'gpt-5-nano': { contextWindow: 400000 },
|
|
71
|
+
'gpt-5-nano-2025-08-07': { contextWindow: 400000 },
|
|
72
|
+
'o4-mini': { contextWindow: 200000 },
|
|
73
|
+
'o4-mini-2025-04-16': { contextWindow: 200000 },
|
|
74
|
+
'o3-pro': { contextWindow: 200000 },
|
|
75
|
+
o3: { contextWindow: 200000 },
|
|
76
|
+
'o3-2025-04-16': { contextWindow: 200000 },
|
|
77
|
+
'o3-mini': { contextWindow: 200000 },
|
|
78
|
+
'o3-mini-2025-01-31': { contextWindow: 200000 },
|
|
79
|
+
o1: { contextWindow: 200000 },
|
|
80
|
+
'o1-2024-12-17': { contextWindow: 200000 },
|
|
81
|
+
'gpt-4.1': { contextWindow: 1047576 },
|
|
82
|
+
'gpt-4.1-2025-04-14': { contextWindow: 1047576 },
|
|
83
|
+
'gpt-4.1-mini': { contextWindow: 1047576 },
|
|
84
|
+
'gpt-4.1-mini-2025-04-14': { contextWindow: 1047576 },
|
|
85
|
+
'gpt-4.1-nano': { contextWindow: 1047576 },
|
|
86
|
+
'gpt-4.1-nano-2025-04-14': { contextWindow: 1047576 },
|
|
87
|
+
'gpt-4o': { contextWindow: 128000 },
|
|
88
|
+
'gpt-4o-2024-05-13': { contextWindow: 128000 },
|
|
89
|
+
'gpt-4o-2024-08-06': { contextWindow: 128000 },
|
|
90
|
+
'gpt-4o-2024-11-20': { contextWindow: 128000 },
|
|
91
|
+
'gpt-4o-mini': { contextWindow: 128000 },
|
|
92
|
+
'gpt-4o-mini-2024-07-18': { contextWindow: 128000 },
|
|
93
|
+
'gpt-3.5-turbo': { contextWindow: 16385 },
|
|
94
|
+
'gpt-3.5-turbo-0125': { contextWindow: 16385 }
|
|
95
|
+
}
|
|
96
|
+
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { IProviderConfig, IProviderInfo, IProviderModelInfo, IProviderRegistry } from '../tokens';
|
|
2
|
+
export declare function getProviderModelInfo(providerInfo: IProviderInfo | null | undefined, model: string | undefined): IProviderModelInfo | undefined;
|
|
3
|
+
export declare function getEffectiveContextWindow(providerConfig: IProviderConfig | undefined, providerRegistry?: IProviderRegistry): number | undefined;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const DATE_SUFFIX = /^(.*)-\d{4}-\d{2}-\d{2}$/;
|
|
2
|
+
const SHORT_VERSION_SUFFIX = /^(.*)-\d{4}$/;
|
|
3
|
+
// Treat rolling aliases and dated releases as the same model family so they
|
|
4
|
+
// can share provider metadata such as context windows.
|
|
5
|
+
function normalizeModelId(modelId) {
|
|
6
|
+
if (modelId.endsWith('-latest')) {
|
|
7
|
+
return modelId.slice(0, -7);
|
|
8
|
+
}
|
|
9
|
+
const dateSuffixMatch = modelId.match(DATE_SUFFIX);
|
|
10
|
+
if (dateSuffixMatch) {
|
|
11
|
+
return dateSuffixMatch[1];
|
|
12
|
+
}
|
|
13
|
+
const shortVersionSuffixMatch = modelId.match(SHORT_VERSION_SUFFIX);
|
|
14
|
+
if (shortVersionSuffixMatch) {
|
|
15
|
+
return shortVersionSuffixMatch[1];
|
|
16
|
+
}
|
|
17
|
+
return modelId;
|
|
18
|
+
}
|
|
19
|
+
function getCandidateModelIds(modelId) {
|
|
20
|
+
const candidates = [modelId];
|
|
21
|
+
const normalizedModelId = normalizeModelId(modelId);
|
|
22
|
+
candidates.push(normalizedModelId);
|
|
23
|
+
if (normalizedModelId !== modelId) {
|
|
24
|
+
candidates.push(`${normalizedModelId}-latest`);
|
|
25
|
+
}
|
|
26
|
+
return [...new Set(candidates)];
|
|
27
|
+
}
|
|
28
|
+
export function getProviderModelInfo(providerInfo, model) {
|
|
29
|
+
if (!providerInfo || !model) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const modelInfo = providerInfo.modelInfo;
|
|
33
|
+
if (!modelInfo) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
for (const candidateId of getCandidateModelIds(model)) {
|
|
37
|
+
if (modelInfo[candidateId]) {
|
|
38
|
+
return modelInfo[candidateId];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const normalizedModelId = normalizeModelId(model);
|
|
42
|
+
// As a last resort, match any known model entry that normalizes to the same
|
|
43
|
+
// base ID, even if the exact alias/version string differs.
|
|
44
|
+
return Object.entries(modelInfo).find(([candidateId]) => {
|
|
45
|
+
return normalizeModelId(candidateId) === normalizedModelId;
|
|
46
|
+
})?.[1];
|
|
47
|
+
}
|
|
48
|
+
export function getEffectiveContextWindow(providerConfig, providerRegistry) {
|
|
49
|
+
if (!providerConfig) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (providerConfig.parameters?.contextWindow !== undefined) {
|
|
53
|
+
return providerConfig.parameters.contextWindow;
|
|
54
|
+
}
|
|
55
|
+
const providerInfo = providerRegistry?.getProviderInfo(providerConfig.provider);
|
|
56
|
+
return getProviderModelInfo(providerInfo, providerConfig.model)
|
|
57
|
+
?.contextWindow;
|
|
58
|
+
}
|