@jupyterlite/ai 0.16.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/src/index.ts CHANGED
@@ -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 });
@@ -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
 
@@ -582,6 +602,7 @@ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
582
602
  });
583
603
 
584
604
  widget.disposed.connect(() => {
605
+ model.titleChanged.disconnect(updateToolbarTitleOverlay);
585
606
  model.nameChanged.disconnect(saveTracker);
586
607
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
587
608
  model.writersChanged?.disconnect(writersChanged);
@@ -734,7 +755,7 @@ function registerCommands(
734
755
  }
735
756
  });
736
757
 
737
- const openInMain = (model: AIChatModel) => {
758
+ const openInMain = (model: AIChatModel): MainAreaChat => {
738
759
  const content = new ChatWidget({
739
760
  model,
740
761
  rmRegistry,
@@ -767,6 +788,64 @@ function registerCommands(
767
788
  model.nameChanged.disconnect(saveTracker);
768
789
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
769
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;
770
849
  };
771
850
 
772
851
  commands.addCommand(CommandIds.openChat, {
@@ -801,11 +880,18 @@ function registerCommands(
801
880
  return false;
802
881
  }
803
882
 
883
+ const shouldFocus = args.focus === true;
884
+ let widget: ChatWidget | MainAreaChat | undefined;
804
885
  if (area === 'main') {
805
- openInMain(model);
886
+ widget = openInMain(model);
806
887
  } else {
807
- chatPanel.open({ model });
888
+ widget = chatPanel.open({ model });
889
+ }
890
+ if (shouldFocus) {
891
+ focusOnChat(area, widget);
808
892
  }
893
+ applyInputArgs(model, { ...args, focus: shouldFocus });
894
+
809
895
  return true;
810
896
  },
811
897
  describedBy: {
@@ -824,6 +910,137 @@ function registerCommands(
824
910
  provider: {
825
911
  type: 'string',
826
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
+ )
827
1044
  }
828
1045
  }
829
1046
  }
@@ -883,7 +1100,8 @@ function registerCommands(
883
1100
  activeProvider: previousModel.agentManager.activeProvider,
884
1101
  tokenUsage: previousModel.agentManager.tokenUsage,
885
1102
  messages: previousModel.messages,
886
- autosave: previousModel.autosave
1103
+ autosave: previousModel.autosave,
1104
+ title: previousModel.title
887
1105
  });
888
1106
 
889
1107
  // Wait (with timeout) for the tracker to have updated the previous widget.
@@ -903,6 +1121,15 @@ function registerCommands(
903
1121
 
904
1122
  if (area === 'main') {
905
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
+ }
906
1133
  } else {
907
1134
  previousWidget?.dispose();
908
1135
  previousModel.dispose();
@@ -1512,6 +1739,7 @@ export default [
1512
1739
  skillRegistryPlugin,
1513
1740
  skillsCommandPlugin,
1514
1741
  chatModelHandler,
1742
+ activeCellManager,
1515
1743
  plugin,
1516
1744
  toolRegistry,
1517
1745
  agentManagerFactory,
package/src/tokens.ts CHANGED
@@ -4,7 +4,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
4
4
  import { Token } from '@lumino/coreutils';
5
5
  import type { IDisposable } from '@lumino/disposable';
6
6
  import { ISignal } from '@lumino/signaling';
7
- import type { Tool, LanguageModel } from 'ai';
7
+ import type { Tool, LanguageModel, UserContent, ModelMessage } from 'ai';
8
8
  import { ISecretsManager } from 'jupyter-secrets-manager';
9
9
 
10
10
  import type { IModelOptions } from './providers/models';
@@ -30,6 +30,7 @@ export namespace CommandIds {
30
30
  export const openSettings = '@jupyterlite/ai:open-settings';
31
31
  export const reposition = '@jupyterlite/ai:reposition';
32
32
  export const openChat = '@jupyterlite/ai:open-chat';
33
+ export const openOrRevealChat = '@jupyterlite/ai:open-or-reveal-chat';
33
34
  export const moveChat = '@jupyterlite/ai:move-chat';
34
35
  export const refreshSkills = '@jupyterlite/ai:refresh-skills';
35
36
  export const saveChat = '@jupyterlite/ai:save-chat';
@@ -584,7 +585,7 @@ export interface IAgentManager {
584
585
  /**
585
586
  * Clears conversation history and resets agent state.
586
587
  */
587
- clearHistory(): void;
588
+ clearHistory(): Promise<void>;
588
589
  /**
589
590
  * Sets the conversation history with a list of messages from the chat.
590
591
  * @param messages The chat messages to set as history
@@ -611,7 +612,12 @@ export interface IAgentManager {
611
612
  * Handles the complete execution cycle including tool calls.
612
613
  * @param message The user message to respond to (may include processed attachment content)
613
614
  */
614
- generateResponse(message: string): Promise<void>;
615
+ generateResponse(message: UserContent): Promise<void>;
616
+ /**
617
+ * Create a transient language model to request a text response, which won't be added to history.
618
+ * @param messages - the messages sequence to send to the model.
619
+ */
620
+ textResponse(messages: ModelMessage[]): Promise<string>;
615
621
  /**
616
622
  * Initializes the AI agent with current settings and tools.
617
623
  * Sets up the agent with model configuration, tools, and MCP tools.
@@ -695,6 +701,10 @@ export interface ICreateChatOptions {
695
701
  * Whether the chat is autosaved or not.
696
702
  */
697
703
  autosave?: boolean;
704
+ /**
705
+ * An optional title to the chat.
706
+ */
707
+ title?: string | null;
698
708
  }
699
709
  /**
700
710
  * Token for the chat model handler.
@@ -24,7 +24,8 @@ export namespace MainAreaChat {
24
24
  export class MainAreaChat extends MainAreaWidget<ChatWidget> {
25
25
  constructor(options: MainAreaChat.IOptions) {
26
26
  super(options);
27
- this.title.label = this.content.model.name;
27
+ this.title.label = this.model.name;
28
+ this.title.caption = this.model.title ?? this.model.name;
28
29
 
29
30
  const { trans } = options;
30
31
 
@@ -69,6 +70,8 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
69
70
  });
70
71
 
71
72
  this.model.writersChanged.connect(this._writersChanged);
73
+
74
+ this.model.titleChanged.connect(this._titleChanged);
72
75
  }
73
76
 
74
77
  dispose(): void {
@@ -76,6 +79,7 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
76
79
  // Dispose of the approval buttons widget when the chat is disposed.
77
80
  this._outputAreaCompat.dispose();
78
81
  this.model.writersChanged.disconnect(this._writersChanged);
82
+ this.model.titleChanged.disconnect(this._titleChanged);
79
83
  }
80
84
 
81
85
  /**
@@ -107,5 +111,9 @@ export class MainAreaChat extends MainAreaWidget<ChatWidget> {
107
111
  }
108
112
  };
109
113
 
114
+ private _titleChanged = () => {
115
+ this.title.caption = this.model.title ?? this.model.name;
116
+ };
117
+
110
118
  private _outputAreaCompat: RenderedMessageOutputAreaCompat;
111
119
  }