@jupyterlite/ai 0.16.0 → 0.18.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 (40) hide show
  1. package/lib/agent.d.ts +19 -10
  2. package/lib/agent.js +82 -46
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.d.ts +2 -3
  5. package/lib/chat-model-handler.js +6 -2
  6. package/lib/chat-model.d.ts +129 -26
  7. package/lib/chat-model.js +543 -160
  8. package/lib/components/clear-button.d.ts +1 -1
  9. package/lib/components/clear-button.js +1 -1
  10. package/lib/components/save-button.d.ts +2 -2
  11. package/lib/index.js +224 -59
  12. package/lib/models/settings-model.js +1 -0
  13. package/lib/providers/built-in-providers.js +1 -1
  14. package/lib/providers/{generated-context-windows.d.ts → generated-model-info.d.ts} +2 -2
  15. package/lib/providers/generated-model-info.js +502 -0
  16. package/lib/providers/model-info.d.ts +3 -0
  17. package/lib/providers/model-info.js +33 -0
  18. package/lib/tokens.d.ts +98 -15
  19. package/lib/tokens.js +1 -0
  20. package/lib/widgets/ai-settings.js +5 -0
  21. package/lib/widgets/main-area-chat.d.ts +3 -3
  22. package/lib/widgets/main-area-chat.js +9 -5
  23. package/package.json +3 -3
  24. package/schema/settings-model.json +6 -0
  25. package/src/agent.ts +100 -52
  26. package/src/chat-commands/clear.ts +1 -1
  27. package/src/chat-model-handler.ts +10 -3
  28. package/src/chat-model.ts +727 -210
  29. package/src/components/clear-button.tsx +3 -3
  30. package/src/components/save-button.tsx +3 -3
  31. package/src/index.ts +289 -83
  32. package/src/models/settings-model.ts +1 -0
  33. package/src/providers/built-in-providers.ts +1 -1
  34. package/src/providers/generated-model-info.ts +508 -0
  35. package/src/providers/model-info.ts +57 -0
  36. package/src/tokens.ts +100 -15
  37. package/src/widgets/ai-settings.tsx +26 -0
  38. package/src/widgets/main-area-chat.ts +14 -9
  39. package/lib/providers/generated-context-windows.js +0 -96
  40. package/src/providers/generated-context-windows.ts +0 -102
@@ -7,7 +7,7 @@ export interface IClearButtonProps extends InputToolbarRegistry.IToolbarItemProp
7
7
  /**
8
8
  * The function to clear messages.
9
9
  */
10
- clearMessages: () => void;
10
+ clearMessages: () => Promise<void>;
11
11
  /**
12
12
  * The application language translator.
13
13
  */
@@ -21,7 +21,7 @@ export function clearItem(translator) {
21
21
  return {
22
22
  element: (props) => {
23
23
  const { model } = props;
24
- const clearMessages = () => model.chatContext.clearMessages();
24
+ const clearMessages = async () => await model.chatContext.clearMessages();
25
25
  const clearProps = {
26
26
  ...props,
27
27
  clearMessages,
@@ -1,7 +1,7 @@
1
1
  import { ReactWidget } from '@jupyterlab/ui-components';
2
2
  import type { TranslationBundle } from '@jupyterlab/translation';
3
3
  import React from 'react';
4
- import { AIChatModel } from '../chat-model';
4
+ import { IAIChatModel } from '../tokens';
5
5
  /**
6
6
  * Properties for the SaveButton component.
7
7
  */
@@ -9,7 +9,7 @@ export interface ISaveButtonProps {
9
9
  /**
10
10
  * The chat model, used to listen for message changes for auto-save.
11
11
  */
12
- model: AIChatModel;
12
+ model: IAIChatModel;
13
13
  /**
14
14
  * The application language translator.
15
15
  */
package/lib/index.js CHANGED
@@ -11,7 +11,7 @@ import { IStatusBar } from '@jupyterlab/statusbar';
11
11
  import { PathExt } from '@jupyterlab/coreutils';
12
12
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
13
13
  import { fileUploadIcon, saveIcon, settingsIcon, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
14
- import { PromiseDelegate, UUID } from '@lumino/coreutils';
14
+ import { UUID } from '@lumino/coreutils';
15
15
  import { DisposableSet } from '@lumino/disposable';
16
16
  import { IComponentsRendererFactory } from 'jupyter-chat-components';
17
17
  import { ISecretsManager, SecretsManager } from 'jupyter-secrets-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, notebookTracker, translator, chatComponentsFactory, palette, documentManager) => {
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 });
@@ -346,6 +351,16 @@ 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.
@@ -371,12 +386,10 @@ const plugin = {
371
386
  // Check if AI is currently writing (streaming)
372
387
  const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
373
388
  if (aiWriting) {
374
- widget.inputToolbarRegistry?.hide('send');
375
389
  widget.inputToolbarRegistry?.show('stop');
376
390
  }
377
391
  else {
378
392
  widget.inputToolbarRegistry?.hide('stop');
379
- widget.inputToolbarRegistry?.show('send');
380
393
  }
381
394
  }
382
395
  model.writersChanged?.connect(writersChanged);
@@ -386,6 +399,7 @@ const plugin = {
386
399
  chatPanel: widget
387
400
  });
388
401
  widget.disposed.connect(() => {
402
+ model.titleChanged.disconnect(updateToolbarTitleOverlay);
389
403
  model.nameChanged.disconnect(saveTracker);
390
404
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
391
405
  model.writersChanged?.disconnect(writersChanged);
@@ -423,19 +437,29 @@ const plugin = {
423
437
  });
424
438
  registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelHandler, trans, themeManager, labShell, palette, documentManager);
425
439
  /**
426
- * The callback to approve or reject a tool.
440
+ * The callback for grouped tool calls permission decisions.
427
441
  */
428
- function toolCallApproval(targetId, approvalId, isApproved) {
429
- const model = tracker.find(chat => chat.model.name === targetId)?.model;
442
+ function toolCallPermissionDecision(sessionId, toolCallId, optionId) {
443
+ const model = tracker.find(chat => chat.model.name === sessionId)
444
+ ?.model;
430
445
  if (!model) {
431
446
  return;
432
447
  }
448
+ const isApproved = optionId === 'approve';
433
449
  isApproved
434
- ? model.agentManager.approveToolCall(approvalId)
435
- : model.agentManager.rejectToolCall(approvalId);
450
+ ? model.agentManager.approveToolCall(toolCallId)
451
+ : model.agentManager.rejectToolCall(toolCallId);
436
452
  }
437
453
  if (chatComponentsFactory) {
438
- chatComponentsFactory.toolCallApproval = toolCallApproval;
454
+ chatComponentsFactory.toolCallPermissionDecision =
455
+ toolCallPermissionDecision;
456
+ chatComponentsFactory.removeQueuedMessage = (targetId, messageId) => {
457
+ const model = tracker.find(chat => chat.model.name === targetId)?.model;
458
+ if (!model) {
459
+ return;
460
+ }
461
+ model.removeQueuedMessage(messageId);
462
+ };
439
463
  }
440
464
  return tracker;
441
465
  }
@@ -486,11 +510,12 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
486
510
  }
487
511
  });
488
512
  const openInMain = (model) => {
513
+ const inputToolbarRegistry = inputToolbarFactory.create();
489
514
  const content = new ChatWidget({
490
515
  model,
491
516
  rmRegistry,
492
517
  themeManager: themeManager ?? null,
493
- inputToolbarRegistry: inputToolbarFactory.create(),
518
+ inputToolbarRegistry,
494
519
  attachmentOpenerRegistry,
495
520
  chatCommandRegistry
496
521
  });
@@ -514,6 +539,49 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
514
539
  model.nameChanged.disconnect(saveTracker);
515
540
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
516
541
  });
542
+ return widget;
543
+ };
544
+ const focusOnChat = (area, widget) => {
545
+ if (area === 'main' && widget) {
546
+ app.shell.activateById(widget.id);
547
+ }
548
+ else {
549
+ app.shell.activateById(chatPanel.id);
550
+ }
551
+ };
552
+ const applyInputArgs = (model, args) => {
553
+ const input = typeof args.input === 'string' ? args.input : undefined;
554
+ const autoSend = args.autoSend === true;
555
+ const shouldFocus = args.focus !== false;
556
+ if (input !== undefined) {
557
+ model.input.value = input;
558
+ }
559
+ if (autoSend && input !== undefined) {
560
+ model.input.send(model.input.value);
561
+ }
562
+ if (shouldFocus) {
563
+ model.input.focus();
564
+ }
565
+ };
566
+ const findChatWidget = (name, provider) => {
567
+ if (!name && !provider) {
568
+ return;
569
+ }
570
+ return tracker.find(widget => {
571
+ const model = widget.model;
572
+ return ((!name || widget.model.name === name) &&
573
+ (!provider || model.agentManager.activeProvider === provider));
574
+ });
575
+ };
576
+ const disposeSideChatModel = (model) => {
577
+ const loadedName = chatPanel
578
+ .getLoadedModelNames()
579
+ .find(name => chatPanel.getLoadedModel(name) === model);
580
+ if (!loadedName) {
581
+ return false;
582
+ }
583
+ chatPanel.disposeLoadedModel(loadedName);
584
+ return true;
517
585
  };
518
586
  commands.addCommand(CommandIds.openChat, {
519
587
  label: trans.__('Open a chat'),
@@ -543,12 +611,18 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
543
611
  if (!model) {
544
612
  return false;
545
613
  }
614
+ const shouldFocus = args.focus === true;
615
+ let widget;
546
616
  if (area === 'main') {
547
- openInMain(model);
617
+ widget = openInMain(model);
548
618
  }
549
619
  else {
550
- chatPanel.open({ model });
620
+ widget = chatPanel.open({ model });
621
+ }
622
+ if (shouldFocus) {
623
+ focusOnChat(area, widget);
551
624
  }
625
+ applyInputArgs(model, { ...args, focus: shouldFocus });
552
626
  return true;
553
627
  },
554
628
  describedBy: {
@@ -567,6 +641,116 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
567
641
  provider: {
568
642
  type: 'string',
569
643
  description: trans.__('The provider/model to use with this chat')
644
+ },
645
+ input: {
646
+ type: 'string',
647
+ description: trans.__('The input text to prefill in the chat')
648
+ },
649
+ focus: {
650
+ type: 'boolean',
651
+ description: trans.__('Whether to focus the chat input after opening it')
652
+ },
653
+ autoSend: {
654
+ type: 'boolean',
655
+ description: trans.__('Whether to auto-send the provided input after opening the chat')
656
+ }
657
+ }
658
+ }
659
+ }
660
+ });
661
+ commands.addCommand(CommandIds.openOrRevealChat, {
662
+ label: trans.__('Open or reveal the chat panel'),
663
+ execute: async (args) => {
664
+ const area = args.area === 'main' ? 'main' : 'side';
665
+ const provider = args.provider ?? undefined;
666
+ const name = args.name ?? undefined;
667
+ const shouldFocus = args.focus === true;
668
+ let existingWidget = findChatWidget(name, provider);
669
+ if (!existingWidget && !name) {
670
+ const providerConfig = provider
671
+ ? settingsModel.getProvider(provider)
672
+ : settingsModel.getDefaultProvider();
673
+ existingWidget = findChatWidget(undefined, providerConfig?.id);
674
+ }
675
+ // If the side chat model is loaded but not currently displayed, reveal it first.
676
+ if (!existingWidget && name) {
677
+ const loadedModel = chatPanel.getLoadedModel(name);
678
+ if (loadedModel) {
679
+ existingWidget = chatPanel.open({ model: loadedModel });
680
+ }
681
+ }
682
+ if (!existingWidget) {
683
+ return commands.execute(CommandIds.openChat, {
684
+ ...args,
685
+ focus: shouldFocus
686
+ });
687
+ }
688
+ const currentArea = existingWidget instanceof MainAreaChat ? 'main' : 'side';
689
+ if (currentArea !== area) {
690
+ const targetName = existingWidget.model.name;
691
+ const moved = (await commands.execute(CommandIds.moveChat, {
692
+ name: targetName,
693
+ area
694
+ }));
695
+ if (!moved) {
696
+ return false;
697
+ }
698
+ const movedWidget = findChatWidget(targetName);
699
+ if (!movedWidget) {
700
+ return false;
701
+ }
702
+ if (area === 'side') {
703
+ chatPanel.open({ model: movedWidget.model });
704
+ }
705
+ if (shouldFocus) {
706
+ focusOnChat(area, movedWidget);
707
+ }
708
+ applyInputArgs(movedWidget.model, {
709
+ ...args,
710
+ focus: shouldFocus
711
+ });
712
+ return true;
713
+ }
714
+ if (area === 'side') {
715
+ chatPanel.open({ model: existingWidget.model });
716
+ }
717
+ if (shouldFocus) {
718
+ focusOnChat(area, existingWidget);
719
+ }
720
+ applyInputArgs(existingWidget.model, {
721
+ ...args,
722
+ focus: shouldFocus
723
+ });
724
+ return true;
725
+ },
726
+ describedBy: {
727
+ args: {
728
+ type: 'object',
729
+ properties: {
730
+ area: {
731
+ type: 'string',
732
+ enum: ['main', 'side'],
733
+ description: trans.__('The name of the area to open or reveal the chat in')
734
+ },
735
+ name: {
736
+ type: 'string',
737
+ description: trans.__('The name of the chat')
738
+ },
739
+ provider: {
740
+ type: 'string',
741
+ description: trans.__('The provider/model to use with this chat')
742
+ },
743
+ input: {
744
+ type: 'string',
745
+ description: trans.__('The input text to prefill in the chat')
746
+ },
747
+ focus: {
748
+ type: 'boolean',
749
+ description: trans.__('Whether to focus the chat input after opening it')
750
+ },
751
+ autoSend: {
752
+ type: 'boolean',
753
+ description: trans.__('Whether to auto-send the provided input after opening the chat')
570
754
  }
571
755
  }
572
756
  }
@@ -596,45 +780,25 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
596
780
  console.error('Error while moving the chat to main area: there is no reference model');
597
781
  return false;
598
782
  }
599
- // Listen for the widget updated in tracker, to ensure the previous model name
600
- // has been updated. This is required to remove the widget from the restorer
601
- // when the previous widget is disposed.
602
- const trackerUpdated = new PromiseDelegate();
603
- const widgetUpdated = (_, widget) => {
604
- if (widget.model === previousModel) {
605
- trackerUpdated.resolve(true);
606
- }
607
- };
608
- tracker.widgetUpdated.connect(widgetUpdated);
609
- // Rename temporary the previous model to be able to reuse this name for the new
610
- // model. The previous is intended to be disposed anyway.
611
- previousModel.name = UUID.uuid4();
612
- // Create a new model by duplicating the previous model attributes.
613
- const model = modelRegistry.createModel({
614
- name: args.name,
615
- activeProvider: previousModel.agentManager.activeProvider,
616
- tokenUsage: previousModel.agentManager.tokenUsage,
617
- messages: previousModel.messages,
618
- autosave: previousModel.autosave
619
- });
620
- // Wait (with timeout) for the tracker to have updated the previous widget.
621
- const status = await Promise.any([
622
- trackerUpdated.promise,
623
- new Promise(r => setTimeout(() => {
624
- r(false);
625
- }, 2000))
626
- ]);
627
- tracker.widgetUpdated.disconnect(widgetUpdated);
628
- if (!status) {
629
- return false;
630
- }
631
783
  if (area === 'main') {
632
- openInMain(model);
784
+ // Temporarily bypass model disposal to transport model to main view
785
+ // to keep the conversation when switching views
786
+ // TODO: Remove this code when jupyter-chat PR #423 is merged and released
787
+ const originalDispose = previousModel.dispose.bind(previousModel);
788
+ previousModel.dispose = () => { };
789
+ if (previousWidget instanceof ChatWidget) {
790
+ if (!disposeSideChatModel(previousModel)) {
791
+ previousWidget.dispose();
792
+ }
793
+ }
794
+ // Restore model disposal and transport to main view
795
+ previousModel.dispose = originalDispose;
796
+ openInMain(previousModel);
633
797
  }
634
798
  else {
799
+ // MainAreaChat disposal does not dispose the model internally, so this is safe.
635
800
  previousWidget?.dispose();
636
- previousModel.dispose();
637
- chatPanel.open({ model });
801
+ chatPanel.open({ model: previousModel });
638
802
  }
639
803
  return true;
640
804
  },
@@ -1118,6 +1282,7 @@ export default [
1118
1282
  skillRegistryPlugin,
1119
1283
  skillsCommandPlugin,
1120
1284
  chatModelHandler,
1285
+ activeCellManager,
1121
1286
  plugin,
1122
1287
  toolRegistry,
1123
1288
  agentManagerFactory,
@@ -19,6 +19,7 @@ export class AISettingsModel extends VDomModel {
19
19
  diffDisplayMode: 'split',
20
20
  skillsPaths: ['.agents/skills', '_agents/skills'],
21
21
  chatBackupDirectory: '',
22
+ autoTitle: false,
22
23
  commandsRequiringApproval: [
23
24
  'notebook:restart-run-all',
24
25
  'notebook:run-cell',
@@ -3,7 +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
+ import { BUILT_IN_PROVIDER_MODEL_INFO } from './generated-model-info';
7
7
  /**
8
8
  * Anthropic provider
9
9
  */
@@ -1,8 +1,8 @@
1
1
  /**
2
- * This file is generated by `jlpm sync:model-context-windows`.
2
+ * This file is generated by `jlpm sync:model-info`.
3
3
  * Source: https://models.dev/api.json
4
4
  * Backed by: https://github.com/anomalyco/models.dev
5
- * Generated: 2026-04-08T16:23:34.080Z
5
+ * Generated: 2026-04-28T11:55:05.327Z
6
6
  */
7
7
  import type { IProviderModelInfo } from '../tokens';
8
8
  export declare const BUILT_IN_PROVIDER_MODEL_INFO: Record<string, Record<string, IProviderModelInfo>>;