@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
package/lib/index.js CHANGED
@@ -1,29 +1,31 @@
1
1
  import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
2
2
  import { ActiveCellManager, AttachmentOpenerRegistry, chatIcon, ChatCommandRegistry, ChatWidget, IChatCommandRegistry, IChatTracker, IInputToolbarRegistryFactory, InputToolbarRegistry, MultiChatPanel } from '@jupyter/chat';
3
- import { ICommandPalette, IThemeManager, showErrorMessage, WidgetTracker } from '@jupyterlab/apputils';
3
+ import { ICommandPalette, IThemeManager, showDialog, showErrorMessage, WidgetTracker } from '@jupyterlab/apputils';
4
4
  import { ICompletionProviderManager } from '@jupyterlab/completer';
5
5
  import { IDocumentManager } from '@jupyterlab/docmanager';
6
+ import { FileDialog } from '@jupyterlab/filebrowser';
6
7
  import { INotebookTracker } from '@jupyterlab/notebook';
7
8
  import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
8
9
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
9
10
  import { IStatusBar } from '@jupyterlab/statusbar';
10
11
  import { PathExt } from '@jupyterlab/coreutils';
11
12
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
12
- import { settingsIcon, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
13
- import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
13
+ import { fileUploadIcon, saveIcon, settingsIcon, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
14
14
  import { PromiseDelegate, UUID } from '@lumino/coreutils';
15
15
  import { DisposableSet } from '@lumino/disposable';
16
+ import { IComponentsRendererFactory } from 'jupyter-chat-components';
17
+ import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
16
18
  import { AgentManagerFactory } from './agent';
17
19
  import { RenderedMessageOutputAreaCompat } from './rendered-message-outputarea';
18
20
  import { ClearCommandProvider } from './chat-commands/clear';
19
21
  import { SkillsCommandProvider } from './chat-commands/skills';
20
22
  import { ProviderRegistry } from './providers/provider-registry';
21
- import { ApprovalButtons } from './approval-buttons';
23
+ import { SaveComponentWidget } from './components/save-button';
22
24
  import { ChatModelHandler } from './chat-model-handler';
23
- import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelHandler, IDiffManager } from './tokens';
25
+ import { CommandIds, IAgentManagerFactory, IAISettingsModel, IChatModelHandler, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE } from './tokens';
24
26
  import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
25
27
  import { AICompletionProvider } from './completion';
26
- import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, TokenUsageWidget } from './components';
28
+ import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, UsageWidget } from './components';
27
29
  import { AISettingsModel } from './models/settings-model';
28
30
  import { loadSkillsFromPaths, SkillRegistry } from './skills';
29
31
  import { DiffManager } from './diff-manager';
@@ -198,8 +200,8 @@ const chatModelHandler = {
198
200
  ],
199
201
  optional: [IProviderRegistry, IToolRegistry, ITranslator],
200
202
  provides: IChatModelHandler,
201
- activate: (app, settingsModel, agentManagerFactory, docManager, rmRegistry, providerRegistry, toolRegistry, translator) => {
202
- const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
203
+ activate: async (app, settingsModel, agentManagerFactory, docManager, rmRegistry, providerRegistry, toolRegistry) => {
204
+ await app.serviceManager.ready;
203
205
  return new ChatModelHandler({
204
206
  settingsModel,
205
207
  agentManagerFactory,
@@ -207,7 +209,7 @@ const chatModelHandler = {
207
209
  rmRegistry,
208
210
  providerRegistry,
209
211
  toolRegistry,
210
- trans
212
+ contentsManager: app.serviceManager.contents
211
213
  });
212
214
  }
213
215
  };
@@ -231,9 +233,12 @@ const plugin = {
231
233
  ILayoutRestorer,
232
234
  ILabShell,
233
235
  INotebookTracker,
234
- ITranslator
236
+ ITranslator,
237
+ IComponentsRendererFactory,
238
+ ICommandPalette,
239
+ IDocumentManager
235
240
  ],
236
- activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator) => {
241
+ activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator, chatComponentsFactory, palette, documentManager) => {
237
242
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
238
243
  // Create attachment opener registry to handle file attachments
239
244
  const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
@@ -286,7 +291,10 @@ const plugin = {
286
291
  name = `${modelName}-${i}`;
287
292
  i += 1;
288
293
  }
289
- const model = modelHandler.createModel(name, provider);
294
+ const model = modelHandler.createModel({
295
+ name,
296
+ activeProvider: provider
297
+ });
290
298
  return { model };
291
299
  },
292
300
  getChatNames: async () => {
@@ -330,7 +338,7 @@ const plugin = {
330
338
  app.commands.commandChanged.connect(onCommandChanged);
331
339
  chatPanel.disposed.connect(disconnectSettingsButtonListener);
332
340
  }
333
- let tokenUsageWidget = null;
341
+ let usageWidget = null;
334
342
  chatPanel.chatOpened.connect((_, widget) => {
335
343
  const model = widget.model;
336
344
  // Add the widget to the tracker.
@@ -343,14 +351,21 @@ const plugin = {
343
351
  // Update the tracker if the active provider changed.
344
352
  model.agentManager.activeProviderChanged.connect(saveTracker);
345
353
  // Update the token usage widget.
346
- tokenUsageWidget?.dispose();
347
- tokenUsageWidget = new TokenUsageWidget({
354
+ usageWidget?.dispose();
355
+ usageWidget = new UsageWidget({
348
356
  tokenUsageChanged: model.tokenUsageChanged,
349
357
  settingsModel,
350
358
  initialTokenUsage: model.agentManager.tokenUsage,
351
359
  translator: trans
352
360
  });
353
- chatPanel.current?.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
361
+ chatPanel.current?.toolbar.insertBefore('markRead', 'usage', usageWidget);
362
+ if (model.saveAvailable) {
363
+ const saveChatButton = new SaveComponentWidget({
364
+ model,
365
+ translator: trans
366
+ });
367
+ chatPanel.current?.toolbar.insertAfter('markRead', 'saveChat', saveChatButton);
368
+ }
354
369
  // Listen for writers change to display the stop button.
355
370
  function writersChanged(_, writers) {
356
371
  // Check if AI is currently writing (streaming)
@@ -365,11 +380,6 @@ const plugin = {
365
380
  }
366
381
  }
367
382
  model.writersChanged?.connect(writersChanged);
368
- // Associate an approval buttons object to the chat.
369
- const approvalButton = new ApprovalButtons({
370
- chatPanel: widget,
371
- agentManager: model.agentManager
372
- });
373
383
  // Temporary compat: keep output-area CSS context for MIME renderers
374
384
  // until jupyter-chat provides it natively.
375
385
  const outputAreaCompat = new RenderedMessageOutputAreaCompat({
@@ -380,10 +390,15 @@ const plugin = {
380
390
  model.agentManager.activeProviderChanged.disconnect(saveTracker);
381
391
  model.writersChanged?.disconnect(writersChanged);
382
392
  // Dispose of the approval buttons widget when the chat is disposed.
383
- approvalButton.dispose();
384
393
  outputAreaCompat.dispose();
385
394
  });
386
395
  });
396
+ /**
397
+ * Update the available chat list when settings config changed.
398
+ */
399
+ settingsModel.stateChanged.connect(() => {
400
+ chatPanel.updateChatList();
401
+ });
387
402
  app.shell.add(chatPanel, 'left', { rank: 1000 });
388
403
  if (restorer) {
389
404
  restorer.add(chatPanel, chatPanel.id);
@@ -406,11 +421,26 @@ const plugin = {
406
421
  app.commands.execute(CommandIds.openChat);
407
422
  }
408
423
  });
409
- registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelHandler, trans, themeManager, labShell);
424
+ registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelHandler, trans, themeManager, labShell, palette, documentManager);
425
+ /**
426
+ * The callback to approve or reject a tool.
427
+ */
428
+ function toolCallApproval(targetId, approvalId, isApproved) {
429
+ const model = tracker.find(chat => chat.model.name === targetId)?.model;
430
+ if (!model) {
431
+ return;
432
+ }
433
+ isApproved
434
+ ? model.agentManager.approveToolCall(approvalId)
435
+ : model.agentManager.rejectToolCall(approvalId);
436
+ }
437
+ if (chatComponentsFactory) {
438
+ chatComponentsFactory.toolCallApproval = toolCallApproval;
439
+ }
410
440
  return tracker;
411
441
  }
412
442
  };
413
- function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelRegistry, trans, themeManager, labShell) {
443
+ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelRegistry, trans, themeManager, labShell, palette, documentManager) {
414
444
  const { commands } = app;
415
445
  if (labShell) {
416
446
  commands.addCommand(CommandIds.reposition, {
@@ -506,7 +536,10 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
506
536
  if (!name) {
507
537
  name = providerConfig.name;
508
538
  }
509
- const model = modelRegistry.createModel(name, provider);
539
+ const model = modelRegistry.createModel({
540
+ name,
541
+ activeProvider: provider
542
+ });
510
543
  if (!model) {
511
544
  return false;
512
545
  }
@@ -577,9 +610,12 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
577
610
  // model. The previous is intended to be disposed anyway.
578
611
  previousModel.name = UUID.uuid4();
579
612
  // Create a new model by duplicating the previous model attributes.
580
- const model = modelRegistry.createModel(args.name, previousModel.agentManager.activeProvider, previousModel.agentManager.tokenUsage);
581
- previousModel?.messages.forEach(message => {
582
- model?.messageAdded({ ...message.content });
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
583
619
  });
584
620
  // Wait (with timeout) for the tracker to have updated the previous widget.
585
621
  const status = await Promise.any([
@@ -620,6 +656,128 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
620
656
  }
621
657
  }
622
658
  });
659
+ commands.addCommand(CommandIds.saveChat, {
660
+ label: args => (args.isPalette ? trans.__('Save chat') : ''),
661
+ caption: trans.__('Save the chat as local file'),
662
+ icon: saveIcon,
663
+ execute: async (args) => {
664
+ let model = null;
665
+ if (args.name) {
666
+ tracker.forEach(widget => {
667
+ if (widget.model.name === args.name) {
668
+ model = widget.model;
669
+ }
670
+ });
671
+ }
672
+ else {
673
+ model = tracker.currentWidget?.model ?? null;
674
+ }
675
+ if (model === null) {
676
+ console.log('No chat to save');
677
+ return false;
678
+ }
679
+ model.save();
680
+ return true;
681
+ },
682
+ describedBy: {
683
+ args: {
684
+ type: 'object',
685
+ properties: {
686
+ isPalette: {
687
+ type: 'boolean',
688
+ description: trans.__('Whether the command is in palette')
689
+ },
690
+ name: {
691
+ type: 'string',
692
+ description: trans.__('The name of the chat to save')
693
+ }
694
+ }
695
+ }
696
+ }
697
+ });
698
+ commands.addCommand(CommandIds.restoreChat, {
699
+ label: args => (args.isPalette ? trans.__('Restore chat') : ''),
700
+ caption: trans.__('Restore the chat from a local file'),
701
+ icon: fileUploadIcon,
702
+ isVisible: () => !!documentManager,
703
+ execute: async (args) => {
704
+ if (!documentManager) {
705
+ console.warn('The restoration is not possible');
706
+ return false;
707
+ }
708
+ let model = null;
709
+ if (args.name) {
710
+ tracker.forEach(widget => {
711
+ if (widget.model.name === args.name) {
712
+ model = widget.model;
713
+ }
714
+ });
715
+ }
716
+ else {
717
+ model = tracker.currentWidget?.model ?? null;
718
+ }
719
+ if (model === null) {
720
+ console.warn('There is no chat to restore');
721
+ return false;
722
+ }
723
+ let backupDirExists = false;
724
+ await app.serviceManager.contents
725
+ .get(settingsModel.config.chatBackupDirectory, { content: false })
726
+ .then(() => (backupDirExists = true))
727
+ .catch(() => (backupDirExists = false));
728
+ const selection = await FileDialog.getOpenFiles({
729
+ title: trans.__('Select files to attach'),
730
+ manager: documentManager,
731
+ defaultPath: backupDirExists
732
+ ? settingsModel.config.chatBackupDirectory
733
+ : ''
734
+ });
735
+ const filepath = selection.value?.[0].path;
736
+ if (!filepath) {
737
+ return false;
738
+ }
739
+ if (model.messages.length) {
740
+ const result = await showDialog({
741
+ body: trans.__('All the message will be deleted')
742
+ });
743
+ if (!result.button.accept) {
744
+ return false;
745
+ }
746
+ }
747
+ return model.restore(filepath);
748
+ },
749
+ describedBy: {
750
+ args: {
751
+ type: 'object',
752
+ properties: {
753
+ isPalette: {
754
+ type: 'boolean',
755
+ description: trans.__('Whether the command is in palette')
756
+ },
757
+ name: {
758
+ type: 'string',
759
+ description: trans.__('The name of the chat to save')
760
+ }
761
+ }
762
+ }
763
+ }
764
+ });
765
+ if (palette) {
766
+ palette.addItem({
767
+ category: trans.__('AI Assistant'),
768
+ command: CommandIds.saveChat,
769
+ args: {
770
+ isPalette: true
771
+ }
772
+ });
773
+ palette.addItem({
774
+ category: trans.__('AI Assistant'),
775
+ command: CommandIds.restoreChat,
776
+ args: {
777
+ isPalette: true
778
+ }
779
+ });
780
+ }
623
781
  }
624
782
  }
625
783
  /**
@@ -1,61 +1,14 @@
1
1
  import { VDomModel } from '@jupyterlab/ui-components';
2
2
  import { ISettingRegistry } from '@jupyterlab/settingregistry';
3
- export interface IProviderParameters {
4
- temperature?: number;
5
- maxOutputTokens?: number;
6
- maxTurns?: number;
7
- supportsFillInMiddle?: boolean;
8
- useFilterText?: boolean;
9
- }
10
- export interface IProviderConfig {
11
- id: string;
12
- name: string;
13
- provider: string;
14
- model: string;
15
- apiKey?: string;
16
- baseURL?: string;
17
- headers?: Record<string, string>;
18
- parameters?: IProviderParameters;
19
- customSettings?: Record<string, any>;
20
- [key: string]: any;
21
- }
22
- export interface IMCPServerConfig {
23
- id: string;
24
- name: string;
25
- url: string;
26
- enabled: boolean;
27
- [key: string]: any;
28
- }
29
- export interface IAIConfig {
30
- useSecretsManager: boolean;
31
- providers: IProviderConfig[];
32
- defaultProvider: string;
33
- activeCompleterProvider?: string;
34
- useSameProviderForChatAndCompleter: boolean;
35
- mcpServers: IMCPServerConfig[];
36
- contextAwareness: boolean;
37
- codeExecution: boolean;
38
- systemPrompt: string;
39
- completionSystemPrompt: string;
40
- toolsEnabled: boolean;
41
- sendWithShiftEnter: boolean;
42
- showTokenUsage: boolean;
43
- commandsRequiringApproval: string[];
44
- commandsAutoRenderMimeBundles: string[];
45
- trustedMimeTypesForAutoRender: string[];
46
- showCellDiff: boolean;
47
- showFileDiff: boolean;
48
- diffDisplayMode: 'split' | 'unified';
49
- skillsPaths: string[];
50
- }
51
- export declare class AISettingsModel extends VDomModel {
3
+ import { IAIConfig, IAISettingsModel, IMCPServerConfig, IProviderConfig } from '../tokens';
4
+ export declare class AISettingsModel extends VDomModel implements IAISettingsModel {
52
5
  private _config;
53
6
  private _settingRegistry;
54
7
  private _settings;
55
8
  constructor(options: AISettingsModel.IOptions);
56
- private initializeSettings;
57
- private onSettingsChanged;
58
- private loadFromSettings;
9
+ private _initializeSettings;
10
+ private _onSettingsChanged;
11
+ private _loadFromSettings;
59
12
  get config(): IAIConfig;
60
13
  get providers(): IProviderConfig[];
61
14
  getProvider(id: string): IProviderConfig | undefined;
@@ -72,8 +25,13 @@ export declare class AISettingsModel extends VDomModel {
72
25
  removeMCPServer(id: string): Promise<void>;
73
26
  updateMCPServer(id: string, updates: Partial<IMCPServerConfig>): Promise<void>;
74
27
  updateConfig(updates: Partial<IAIConfig>): Promise<void>;
28
+ /**
29
+ * Get the API key saved in the settings file for a given provider.
30
+ *
31
+ * @param id - the id of the provider.
32
+ */
75
33
  getApiKey(id: string): string;
76
- private saveSetting;
34
+ private _saveSetting;
77
35
  }
78
36
  export declare namespace AISettingsModel {
79
37
  interface IOptions {
@@ -13,10 +13,12 @@ export class AISettingsModel extends VDomModel {
13
13
  toolsEnabled: true,
14
14
  sendWithShiftEnter: false,
15
15
  showTokenUsage: false,
16
+ showContextUsage: false,
16
17
  showCellDiff: true,
17
18
  showFileDiff: true,
18
19
  diffDisplayMode: 'split',
19
20
  skillsPaths: ['.agents/skills', '_agents/skills'],
21
+ chatBackupDirectory: '',
20
22
  commandsRequiringApproval: [
21
23
  'notebook:restart-run-all',
22
24
  'notebook:run-cell',
@@ -98,6 +100,15 @@ When asked to run code or perform computations, choose the most appropriate appr
98
100
 
99
101
  This means if the user asks you to "calculate the factorial of 100" or "check what library version is installed", run that directly with the jupyterlab-ai-commands kernel execution command rather than creating a new notebook file.
100
102
 
103
+ ## Notebook State and Cell Identity
104
+ When working with an existing notebook, use the notebook's current structure and kernel state as the source of truth.
105
+ - Before changing notebook content or structure, inspect the notebook and any target cells with the relevant notebook commands you have discovered.
106
+ - If the user may have edited the notebook, or if a previous command could have changed it, refresh your view before continuing rather than relying on earlier results.
107
+ - Treat variables from previously executed cells as part of the active kernel state. When the user asks you to work with existing data or variables, use them by name instead of recreating them unless the user asks you to redefine them or the kernel state is unavailable.
108
+ - Be explicit about the kind of cell reference you are using. A visible execution count (for example In [6]), a notebook position, and an internal cell ID or UUID are different identifiers and may not match.
109
+ - When the user identifies a cell by execution count, relative position, or content, verify the target cell from the current notebook contents before editing it or inserting cells relative to it.
110
+ - For relative insertions, anchor the change to the confirmed target cell rather than to empty placeholder or trailing cells unless the user explicitly refers to those cells.
111
+
101
112
  ## Your Approach
102
113
  - **Context-aware**: You understand the user is working in a data science/research environment
103
114
  - **Practical**: You focus on actionable solutions that work in the user's current setup
@@ -141,7 +152,7 @@ Guidelines:
141
152
 
142
153
  ## Multi-Step Task Handling
143
154
  When users request complex tasks, you use the command system to accomplish them:
144
- - For file and notebook operations, use discover_commands with query 'jupyterlab-ai-commands' to find the curated set of AI commands (~17 commands)
155
+ - For file and notebook operations, use discover_commands with query 'jupyterlab-ai-commands' to find the curated set of AI commands (~22 commands)
145
156
  - For other JupyterLab operations (terminal, launcher, UI), use specific keywords like 'terminal', 'launcher', etc.
146
157
  - IMPORTANT: Always use 'jupyterlab-ai-commands' as the query for file/notebook tasks - this returns a focused set instead of 100+ generic commands
147
158
  - For example, to create a notebook with cells:
@@ -181,14 +192,14 @@ Rules:
181
192
  constructor(options) {
182
193
  super();
183
194
  this._settingRegistry = options.settingRegistry;
184
- this.initializeSettings();
195
+ this._initializeSettings();
185
196
  }
186
- async initializeSettings() {
197
+ async _initializeSettings() {
187
198
  try {
188
199
  this._settings = await this._settingRegistry.load(PLUGIN_ID);
189
- this.loadFromSettings();
200
+ this._loadFromSettings();
190
201
  // Listen for settings changes
191
- this._settings.changed.connect(this.onSettingsChanged, this);
202
+ this._settings.changed.connect(this._onSettingsChanged, this);
192
203
  this.stateChanged.emit(void 0);
193
204
  }
194
205
  catch (error) {
@@ -196,11 +207,11 @@ Rules:
196
207
  this.stateChanged.emit(void 0);
197
208
  }
198
209
  }
199
- onSettingsChanged() {
200
- this.loadFromSettings();
210
+ _onSettingsChanged() {
211
+ this._loadFromSettings();
201
212
  this.stateChanged.emit(void 0);
202
213
  }
203
- loadFromSettings() {
214
+ _loadFromSettings() {
204
215
  if (!this._settings) {
205
216
  return;
206
217
  }
@@ -248,13 +259,13 @@ Rules:
248
259
  // If this is the first provider, make it active
249
260
  if (this._config.providers.length === 1) {
250
261
  // Save both providers and defaultProvider
251
- await this.saveSetting('providers', this._config.providers);
262
+ await this._saveSetting('providers', this._config.providers);
252
263
  this._config.defaultProvider = id;
253
- await this.saveSetting('defaultProvider', this._config.defaultProvider);
264
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
254
265
  }
255
266
  else {
256
267
  // Only save providers
257
- await this.saveSetting('providers', this._config.providers);
268
+ await this._saveSetting('providers', this._config.providers);
258
269
  }
259
270
  return id;
260
271
  }
@@ -264,16 +275,16 @@ Rules:
264
275
  return;
265
276
  }
266
277
  this._config.providers.splice(index, 1);
267
- await this.saveSetting('providers', this._config.providers);
278
+ await this._saveSetting('providers', this._config.providers);
268
279
  // If this was the active provider, select a new one
269
280
  if (this._config.defaultProvider === id) {
270
281
  this._config.defaultProvider =
271
282
  this._config.providers.length > 0 ? this._config.providers[0].id : '';
272
- await this.saveSetting('defaultProvider', this._config.defaultProvider);
283
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
273
284
  }
274
285
  if (this._config.activeCompleterProvider === id) {
275
286
  this._config.activeCompleterProvider = undefined;
276
- await this.saveSetting('activeCompleterProvider', this._config.activeCompleterProvider);
287
+ await this._saveSetting('activeCompleterProvider', this._config.activeCompleterProvider);
277
288
  }
278
289
  }
279
290
  async updateProvider(id, updates) {
@@ -287,17 +298,17 @@ Rules:
287
298
  delete provider[key];
288
299
  }
289
300
  });
290
- await this.saveSetting('providers', this._config.providers);
301
+ await this._saveSetting('providers', this._config.providers);
291
302
  }
292
303
  async setActiveProvider(id) {
293
304
  if (this.getProvider(id)) {
294
305
  this._config.defaultProvider = id;
295
- await this.saveSetting('defaultProvider', this._config.defaultProvider);
306
+ await this._saveSetting('defaultProvider', this._config.defaultProvider);
296
307
  }
297
308
  }
298
309
  async setActiveCompleterProvider(id) {
299
310
  this._config.activeCompleterProvider = id;
300
- await this.saveSetting('activeCompleterProvider', this._config.activeCompleterProvider);
311
+ await this._saveSetting('activeCompleterProvider', this._config.activeCompleterProvider);
301
312
  }
302
313
  get mcpServers() {
303
314
  return [...this._config.mcpServers];
@@ -314,7 +325,7 @@ Rules:
314
325
  enabled: serverConfig.enabled
315
326
  };
316
327
  this._config.mcpServers.push(newServer);
317
- await this.saveSetting('mcpServers', this._config.mcpServers);
328
+ await this._saveSetting('mcpServers', this._config.mcpServers);
318
329
  return id;
319
330
  }
320
331
  async removeMCPServer(id) {
@@ -323,7 +334,7 @@ Rules:
323
334
  return;
324
335
  }
325
336
  this._config.mcpServers.splice(index, 1);
326
- await this.saveSetting('mcpServers', this._config.mcpServers);
337
+ await this._saveSetting('mcpServers', this._config.mcpServers);
327
338
  }
328
339
  async updateMCPServer(id, updates) {
329
340
  const server = this.getMCPServer(id);
@@ -331,7 +342,7 @@ Rules:
331
342
  return;
332
343
  }
333
344
  Object.assign(server, updates);
334
- await this.saveSetting('mcpServers', this._config.mcpServers);
345
+ await this._saveSetting('mcpServers', this._config.mcpServers);
335
346
  }
336
347
  async updateConfig(updates) {
337
348
  // Update config and save only changed settings
@@ -340,12 +351,17 @@ Rules:
340
351
  if (key in this._config &&
341
352
  this._config[key] !== value) {
342
353
  this._config[key] = value;
343
- promises.push(this.saveSetting(key, value));
354
+ promises.push(this._saveSetting(key, value));
344
355
  }
345
356
  }
346
357
  // Wait for all settings to be saved
347
358
  await Promise.all(promises);
348
359
  }
360
+ /**
361
+ * Get the API key saved in the settings file for a given provider.
362
+ *
363
+ * @param id - the id of the provider.
364
+ */
349
365
  getApiKey(id) {
350
366
  // First check the active completer provider
351
367
  const activeCompleterProvider = this.getCompleterProvider();
@@ -359,7 +375,7 @@ Rules:
359
375
  }
360
376
  return '';
361
377
  }
362
- async saveSetting(key, value) {
378
+ async _saveSetting(key, value) {
363
379
  try {
364
380
  if (this._settings) {
365
381
  // Only save the specific setting that changed