@jupyterlite/ai 0.12.0 → 0.14.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 (49) hide show
  1. package/lib/agent.d.ts +36 -2
  2. package/lib/agent.js +249 -24
  3. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  4. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  5. package/lib/chat-model.d.ts +8 -0
  6. package/lib/chat-model.js +156 -8
  7. package/lib/completion/completion-provider.d.ts +1 -1
  8. package/lib/completion/completion-provider.js +14 -2
  9. package/lib/components/model-select.js +4 -4
  10. package/lib/components/tool-select.d.ts +11 -2
  11. package/lib/components/tool-select.js +77 -18
  12. package/lib/index.d.ts +3 -3
  13. package/lib/index.js +249 -117
  14. package/lib/models/settings-model.d.ts +2 -0
  15. package/lib/models/settings-model.js +2 -0
  16. package/lib/providers/built-in-providers.js +33 -32
  17. package/lib/providers/provider-tools.d.ts +36 -0
  18. package/lib/providers/provider-tools.js +93 -0
  19. package/lib/rendered-message-outputarea.d.ts +24 -0
  20. package/lib/rendered-message-outputarea.js +48 -0
  21. package/lib/tokens.d.ts +65 -7
  22. package/lib/tokens.js +1 -1
  23. package/lib/tools/commands.js +62 -22
  24. package/lib/tools/web.d.ts +8 -0
  25. package/lib/tools/web.js +196 -0
  26. package/lib/widgets/ai-settings.d.ts +4 -9
  27. package/lib/widgets/ai-settings.js +123 -69
  28. package/lib/widgets/main-area-chat.d.ts +6 -0
  29. package/lib/widgets/main-area-chat.js +28 -0
  30. package/lib/widgets/provider-config-dialog.js +211 -11
  31. package/package.json +17 -11
  32. package/schema/settings-model.json +89 -1
  33. package/src/agent.ts +327 -42
  34. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  35. package/src/chat-model.ts +223 -14
  36. package/src/completion/completion-provider.ts +26 -12
  37. package/src/components/model-select.tsx +4 -5
  38. package/src/components/tool-select.tsx +110 -7
  39. package/src/index.ts +359 -184
  40. package/src/models/settings-model.ts +6 -0
  41. package/src/providers/built-in-providers.ts +33 -32
  42. package/src/providers/provider-tools.ts +179 -0
  43. package/src/rendered-message-outputarea.ts +62 -0
  44. package/src/tokens.ts +82 -9
  45. package/src/tools/commands.ts +99 -31
  46. package/src/tools/web.ts +238 -0
  47. package/src/widgets/ai-settings.tsx +279 -124
  48. package/src/widgets/main-area-chat.ts +34 -1
  49. package/src/widgets/provider-config-dialog.tsx +504 -11
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ILabShell, ILayoutRestorer } from '@jupyterlab/application';
2
- import { ActiveCellManager, AttachmentOpenerRegistry, chatIcon, ChatCommandRegistry, ChatWidget, IChatCommandRegistry, IInputToolbarRegistryFactory, InputToolbarRegistry, MultiChatPanel } from '@jupyter/chat';
3
- import { ICommandPalette, IThemeManager, WidgetTracker } from '@jupyterlab/apputils';
2
+ import { ActiveCellManager, AttachmentOpenerRegistry, chatIcon, ChatCommandRegistry, ChatWidget, IChatCommandRegistry, IChatTracker, IInputToolbarRegistryFactory, InputToolbarRegistry, MultiChatPanel } from '@jupyter/chat';
3
+ import { ICommandPalette, IThemeManager, showErrorMessage, WidgetTracker } from '@jupyterlab/apputils';
4
4
  import { ICompletionProviderManager } from '@jupyterlab/completer';
5
5
  import { IDocumentManager } from '@jupyterlab/docmanager';
6
6
  import { INotebookTracker } from '@jupyterlab/notebook';
@@ -14,12 +14,13 @@ import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
14
14
  import { PromiseDelegate, UUID } from '@lumino/coreutils';
15
15
  import { DisposableSet } from '@lumino/disposable';
16
16
  import { AgentManagerFactory } from './agent';
17
+ import { RenderedMessageOutputAreaCompat } from './rendered-message-outputarea';
17
18
  import { ClearCommandProvider } from './chat-commands/clear';
18
19
  import { SkillsCommandProvider } from './chat-commands/skills';
19
20
  import { ProviderRegistry } from './providers/provider-registry';
20
21
  import { ApprovalButtons } from './approval-buttons';
21
- import { ChatModelRegistry } from './chat-model-registry';
22
- import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelRegistry, IDiffManager } from './tokens';
22
+ import { ChatModelHandler } from './chat-model-handler';
23
+ import { CommandIds, IAgentManagerFactory, IProviderRegistry, IToolRegistry, ISkillRegistry, SECRETS_NAMESPACE, IAISettingsModel, IChatModelHandler, IDiffManager } from './tokens';
23
24
  import { anthropicProvider, googleProvider, mistralProvider, openaiProvider, genericProvider } from './providers/built-in-providers';
24
25
  import { AICompletionProvider } from './completion';
25
26
  import { clearItem, createModelSelectItem, createToolSelectItem, stopItem, CompletionStatusWidget, TokenUsageWidget } from './components';
@@ -29,8 +30,48 @@ import { DiffManager } from './diff-manager';
29
30
  import { ToolRegistry } from './tools/tool-registry';
30
31
  import { createDiscoverCommandsTool, createExecuteCommandTool } from './tools/commands';
31
32
  import { createDiscoverSkillsTool, createLoadSkillTool } from './tools/skills';
33
+ import { createBrowserFetchTool } from './tools/web';
32
34
  import { AISettingsWidget } from './widgets/ai-settings';
33
35
  import { MainAreaChat } from './widgets/main-area-chat';
36
+ var Private;
37
+ (function (Private) {
38
+ let aiSecretsToken = null;
39
+ function setAISecretsToken(token) {
40
+ aiSecretsToken = token;
41
+ }
42
+ Private.setAISecretsToken = setAISecretsToken;
43
+ function createAISecretsAccess(secretsManager) {
44
+ return {
45
+ get isAvailable() {
46
+ return !!(aiSecretsToken && secretsManager);
47
+ },
48
+ async get(id) {
49
+ if (!aiSecretsToken || !secretsManager) {
50
+ return;
51
+ }
52
+ const secret = await secretsManager.get(aiSecretsToken, SECRETS_NAMESPACE, id);
53
+ return secret?.value;
54
+ },
55
+ async set(id, value) {
56
+ if (!aiSecretsToken || !secretsManager) {
57
+ return;
58
+ }
59
+ await secretsManager.set(aiSecretsToken, SECRETS_NAMESPACE, id, {
60
+ namespace: SECRETS_NAMESPACE,
61
+ id,
62
+ value
63
+ });
64
+ },
65
+ async attach(id, input, callback) {
66
+ if (!aiSecretsToken || !secretsManager) {
67
+ return;
68
+ }
69
+ await secretsManager.attach(aiSecretsToken, SECRETS_NAMESPACE, id, input, callback);
70
+ }
71
+ };
72
+ }
73
+ Private.createAISecretsAccess = createAISecretsAccess;
74
+ })(Private || (Private = {}));
34
75
  /**
35
76
  * Provider registry plugin
36
77
  */
@@ -143,21 +184,27 @@ const skillsCommandPlugin = {
143
184
  }
144
185
  };
145
186
  /**
146
- * The chat model registry.
187
+ * The chat model handler.
147
188
  */
148
- const chatModelRegistry = {
149
- id: '@jupyterlite/ai:chat-model-registry',
150
- description: 'Registry for the current chat model',
189
+ const chatModelHandler = {
190
+ id: '@jupyterlite/ai:chat-model-handler',
191
+ description: 'A handler to create current chat model',
151
192
  autoStart: true,
152
- requires: [IAISettingsModel, IAgentManagerFactory, IDocumentManager],
193
+ requires: [
194
+ IAISettingsModel,
195
+ IAgentManagerFactory,
196
+ IDocumentManager,
197
+ IRenderMimeRegistry
198
+ ],
153
199
  optional: [IProviderRegistry, IToolRegistry, ITranslator],
154
- provides: IChatModelRegistry,
155
- activate: (app, settingsModel, agentManagerFactory, docManager, providerRegistry, toolRegistry, translator) => {
200
+ provides: IChatModelHandler,
201
+ activate: (app, settingsModel, agentManagerFactory, docManager, rmRegistry, providerRegistry, toolRegistry, translator) => {
156
202
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
157
- return new ChatModelRegistry({
203
+ return new ChatModelHandler({
158
204
  settingsModel,
159
205
  agentManagerFactory,
160
206
  docManager,
207
+ rmRegistry,
161
208
  providerRegistry,
162
209
  toolRegistry,
163
210
  trans
@@ -171,10 +218,11 @@ const plugin = {
171
218
  id: '@jupyterlite/ai:plugin',
172
219
  description: 'AI in JupyterLab',
173
220
  autoStart: true,
221
+ provides: IChatTracker,
174
222
  requires: [
175
223
  IRenderMimeRegistry,
176
224
  IInputToolbarRegistryFactory,
177
- IChatModelRegistry,
225
+ IChatModelHandler,
178
226
  IAISettingsModel,
179
227
  IChatCommandRegistry
180
228
  ],
@@ -185,7 +233,7 @@ const plugin = {
185
233
  INotebookTracker,
186
234
  ITranslator
187
235
  ],
188
- activate: (app, rmRegistry, inputToolbarFactory, modelRegistry, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator) => {
236
+ activate: (app, rmRegistry, inputToolbarFactory, modelHandler, settingsModel, chatCommandRegistry, themeManager, restorer, labShell, notebookTracker, translator) => {
189
237
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
190
238
  // Create attachment opener registry to handle file attachments
191
239
  const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
@@ -195,6 +243,11 @@ const plugin = {
195
243
  attachmentOpenerRegistry.set('notebook', attachment => {
196
244
  app.commands.execute('docmanager:open', { path: attachment.value });
197
245
  });
246
+ const openSettings = () => {
247
+ if (app.commands.hasCommand(CommandIds.openSettings)) {
248
+ void app.commands.execute(CommandIds.openSettings);
249
+ }
250
+ };
198
251
  // Create ActiveCellManager if notebook tracker is available, and add it to the
199
252
  // model registry.
200
253
  let activeCellManager;
@@ -204,7 +257,10 @@ const plugin = {
204
257
  shell: app.shell
205
258
  });
206
259
  }
207
- modelRegistry.activeCellManager = activeCellManager;
260
+ modelHandler.activeCellManager = activeCellManager;
261
+ // Creating the tracker for the chat widgets
262
+ const namespace = 'ai-chat';
263
+ const tracker = new WidgetTracker({ namespace });
208
264
  // Create chat panel with drag/drop functionality
209
265
  const chatPanel = new MultiChatPanel({
210
266
  rmRegistry,
@@ -212,19 +268,35 @@ const plugin = {
212
268
  inputToolbarFactory,
213
269
  attachmentOpenerRegistry,
214
270
  chatCommandRegistry,
215
- createModel: async (name) => {
216
- const model = modelRegistry.createModel(name);
271
+ createModel: async (provider) => {
272
+ if (!provider) {
273
+ provider = settingsModel.getDefaultProvider()?.id;
274
+ if (!provider) {
275
+ showErrorMessage('Error creating chat', 'Please set up a provider');
276
+ openSettings();
277
+ return {};
278
+ }
279
+ }
280
+ let name = settingsModel.getProvider(provider)?.name ?? UUID.uuid4();
281
+ const modelName = name;
282
+ const existingName = new Set(chatPanel.getLoadedModelNames());
283
+ tracker.forEach(widget => existingName.add(widget.model.name));
284
+ let i = 1;
285
+ while (existingName.has(name)) {
286
+ name = `${modelName}-${i}`;
287
+ i += 1;
288
+ }
289
+ const model = modelHandler.createModel(name, provider);
217
290
  return { model };
218
291
  },
219
- renameChat: async (oldName, newName) => {
220
- const model = modelRegistry.get(oldName);
221
- const concurrencyModel = modelRegistry.get(newName);
222
- if (model && !concurrencyModel) {
223
- model.name = newName;
224
- return true;
225
- }
226
- return false;
292
+ getChatNames: async () => {
293
+ const names = {};
294
+ settingsModel.config.providers.forEach(provider => {
295
+ names[provider.name] = provider.id;
296
+ });
297
+ return names;
227
298
  },
299
+ renameChat: true,
228
300
  openInMain: (name) => app.commands.execute(CommandIds.moveChat, {
229
301
  area: 'main',
230
302
  name
@@ -234,30 +306,53 @@ const plugin = {
234
306
  chatPanel.title.icon = chatIcon;
235
307
  chatPanel.title.caption = trans.__('Chat with AI assistant');
236
308
  chatPanel.toolbar.addItem('spacer', Toolbar.createSpacerItem());
237
- chatPanel.toolbar.addItem('settings', new ToolbarButton({
238
- icon: settingsIcon,
239
- onClick: () => {
240
- app.commands.execute('@jupyterlite/ai:open-settings');
241
- },
242
- tooltip: trans.__('Open AI Settings')
243
- }));
244
- chatPanel.sectionAdded.connect((_, section) => {
245
- const { widget } = section;
246
- const model = section.model;
309
+ const addSettingsButton = () => {
310
+ chatPanel.toolbar.addItem('settings', new ToolbarButton({
311
+ icon: settingsIcon,
312
+ onClick: openSettings,
313
+ tooltip: trans.__('Open AI Settings')
314
+ }));
315
+ };
316
+ if (app.commands.hasCommand(CommandIds.openSettings)) {
317
+ addSettingsButton();
318
+ }
319
+ else {
320
+ const disconnectSettingsButtonListener = () => {
321
+ app.commands.commandChanged.disconnect(onCommandChanged);
322
+ chatPanel.disposed.disconnect(disconnectSettingsButtonListener);
323
+ };
324
+ const onCommandChanged = (_, args) => {
325
+ if (args.id === CommandIds.openSettings && args.type === 'added') {
326
+ disconnectSettingsButtonListener();
327
+ addSettingsButton();
328
+ }
329
+ };
330
+ app.commands.commandChanged.connect(onCommandChanged);
331
+ chatPanel.disposed.connect(disconnectSettingsButtonListener);
332
+ }
333
+ let tokenUsageWidget = null;
334
+ chatPanel.chatOpened.connect((_, widget) => {
335
+ const model = widget.model;
247
336
  // Add the widget to the tracker.
248
337
  tracker.add(widget);
338
+ function saveTracker() {
339
+ tracker.save(widget);
340
+ }
249
341
  // Update the tracker if the model name changed.
250
- model.nameChanged.connect(() => tracker.save(widget));
342
+ model.nameChanged.connect(saveTracker);
251
343
  // Update the tracker if the active provider changed.
252
- model.agentManager.activeProviderChanged.connect(() => tracker.save(widget));
253
- const tokenUsageWidget = new TokenUsageWidget({
344
+ model.agentManager.activeProviderChanged.connect(saveTracker);
345
+ // Update the token usage widget.
346
+ tokenUsageWidget?.dispose();
347
+ tokenUsageWidget = new TokenUsageWidget({
254
348
  tokenUsageChanged: model.tokenUsageChanged,
255
349
  settingsModel,
256
350
  initialTokenUsage: model.agentManager.tokenUsage,
257
351
  translator: trans
258
352
  });
259
- section.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
260
- model.writersChanged?.connect((_, writers) => {
353
+ chatPanel.current?.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
354
+ // Listen for writers change to display the stop button.
355
+ function writersChanged(_, writers) {
261
356
  // Check if AI is currently writing (streaming)
262
357
  const aiWriting = writers.some(writer => writer.user.username === 'ai-assistant');
263
358
  if (aiWriting) {
@@ -268,23 +363,28 @@ const plugin = {
268
363
  widget.inputToolbarRegistry?.hide('stop');
269
364
  widget.inputToolbarRegistry?.show('send');
270
365
  }
271
- });
366
+ }
367
+ model.writersChanged?.connect(writersChanged);
272
368
  // Associate an approval buttons object to the chat.
273
369
  const approvalButton = new ApprovalButtons({
274
370
  chatPanel: widget,
275
371
  agentManager: model.agentManager
276
372
  });
373
+ // Temporary compat: keep output-area CSS context for MIME renderers
374
+ // until jupyter-chat provides it natively.
375
+ const outputAreaCompat = new RenderedMessageOutputAreaCompat({
376
+ chatPanel: widget
377
+ });
277
378
  widget.disposed.connect(() => {
379
+ model.nameChanged.disconnect(saveTracker);
380
+ model.agentManager.activeProviderChanged.disconnect(saveTracker);
381
+ model.writersChanged?.disconnect(writersChanged);
278
382
  // Dispose of the approval buttons widget when the chat is disposed.
279
383
  approvalButton.dispose();
280
- // Remove the model from the registry when the widget is disposed.
281
- modelRegistry.remove(model.name);
384
+ outputAreaCompat.dispose();
282
385
  });
283
386
  });
284
387
  app.shell.add(chatPanel, 'left', { rank: 1000 });
285
- // Creating the tracker for the document
286
- const namespace = 'ai-chat';
287
- const tracker = new WidgetTracker({ namespace });
288
388
  if (restorer) {
289
389
  restorer.add(chatPanel, chatPanel.id);
290
390
  void restorer.restore(tracker, {
@@ -302,12 +402,12 @@ const plugin = {
302
402
  }
303
403
  // Create a chat with default provider at startup.
304
404
  app.restored.then(() => {
305
- if (!modelRegistry.getAll().length &&
306
- settingsModel.config.defaultProvider) {
405
+ if (!tracker.size && settingsModel.config.defaultProvider) {
307
406
  app.commands.execute(CommandIds.openChat);
308
407
  }
309
408
  });
310
- registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelRegistry, trans, themeManager, labShell);
409
+ registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelHandler, trans, themeManager, labShell);
410
+ return tracker;
311
411
  }
312
412
  };
313
413
  function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry, inputToolbarFactory, settingsModel, chatCommandRegistry, tracker, modelRegistry, trans, themeManager, labShell) {
@@ -373,13 +473,16 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
373
473
  app.shell.add(widget, 'main');
374
474
  // Add the widget to the tracker.
375
475
  tracker.add(widget);
476
+ function saveTracker() {
477
+ tracker.save(widget);
478
+ }
376
479
  // Update the tracker if the model name changed.
377
- model.nameChanged.connect(() => tracker.save(widget));
480
+ model.nameChanged.connect(saveTracker);
378
481
  // Update the tracker if the active provider changed.
379
- model.agentManager.activeProviderChanged.connect(() => tracker.save(widget));
380
- // Remove the model from the registry when the widget is disposed.
482
+ model.agentManager.activeProviderChanged.connect(saveTracker);
381
483
  widget.disposed.connect(() => {
382
- modelRegistry.remove(model.name);
484
+ model.nameChanged.disconnect(saveTracker);
485
+ model.agentManager.activeProviderChanged.disconnect(saveTracker);
383
486
  });
384
487
  };
385
488
  commands.addCommand(CommandIds.openChat, {
@@ -387,11 +490,23 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
387
490
  execute: async (args) => {
388
491
  const area = args.area === 'main' ? 'main' : 'side';
389
492
  const provider = args.provider ?? undefined;
390
- // Do not open the chat if the provider in args does not exists in settings.
391
- if (provider && !settingsModel.getProvider(provider)) {
493
+ let name = args.name ?? undefined;
494
+ let providerConfig = undefined;
495
+ if (provider) {
496
+ providerConfig = settingsModel.getProvider(provider);
497
+ }
498
+ else {
499
+ providerConfig = settingsModel.getDefaultProvider();
500
+ }
501
+ // Do not open the chat if the provider in args does not exists in settings or
502
+ // if there is no default provider.
503
+ if (!providerConfig) {
392
504
  return false;
393
505
  }
394
- const model = modelRegistry.createModel(args.name ? args.name : undefined, provider);
506
+ if (!name) {
507
+ name = providerConfig.name;
508
+ }
509
+ const model = modelRegistry.createModel(name, provider);
395
510
  if (!model) {
396
511
  return false;
397
512
  }
@@ -399,7 +514,7 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
399
514
  openInMain(model);
400
515
  }
401
516
  else {
402
- chatPanel.addChat({ model });
517
+ chatPanel.open({ model });
403
518
  }
404
519
  return true;
405
520
  },
@@ -436,7 +551,14 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
436
551
  console.error('Error while moving the chat to main area: the name has not been provided');
437
552
  return false;
438
553
  }
439
- const previousModel = modelRegistry.get(args.name);
554
+ let previousWidget;
555
+ let previousModel;
556
+ tracker.forEach(widget => {
557
+ if (widget.model.name === args.name) {
558
+ previousWidget = widget;
559
+ previousModel = widget.model;
560
+ }
561
+ });
440
562
  if (!previousModel) {
441
563
  console.error('Error while moving the chat to main area: there is no reference model');
442
564
  return false;
@@ -455,8 +577,10 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
455
577
  // model. The previous is intended to be disposed anyway.
456
578
  previousModel.name = UUID.uuid4();
457
579
  // Create a new model by duplicating the previous model attributes.
458
- const model = modelRegistry.createModel(args.name, previousModel?.agentManager.activeProvider, previousModel?.agentManager.tokenUsage);
459
- previousModel?.messages.forEach(message => model?.messageAdded(message));
580
+ const model = modelRegistry.createModel(args.name, previousModel.agentManager.activeProvider, previousModel.agentManager.tokenUsage);
581
+ previousModel?.messages.forEach(message => {
582
+ model?.messageAdded({ ...message.content });
583
+ });
460
584
  // Wait (with timeout) for the tracker to have updated the previous widget.
461
585
  const status = await Promise.any([
462
586
  trackerUpdated.promise,
@@ -472,13 +596,9 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
472
596
  openInMain(model);
473
597
  }
474
598
  else {
475
- const current = app.shell.currentWidget;
476
- // Remove the current main area chat.
477
- if (current instanceof MainAreaChat &&
478
- current.model.name === previousModel.name) {
479
- current.dispose();
480
- }
481
- chatPanel.addChat({ model });
599
+ previousWidget?.dispose();
600
+ previousModel.dispose();
601
+ chatPanel.open({ model });
482
602
  }
483
603
  return true;
484
604
  },
@@ -489,7 +609,7 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
489
609
  area: {
490
610
  type: 'string',
491
611
  enum: ['main', 'side'],
492
- description: trans.__('The name of the area to move the chat to')
612
+ description: trans.__('The area to move the chat to')
493
613
  },
494
614
  name: {
495
615
  type: 'string',
@@ -503,57 +623,78 @@ function registerCommands(app, rmRegistry, chatPanel, attachmentOpenerRegistry,
503
623
  }
504
624
  }
505
625
  /**
506
- * A plugin to provide the settings model.
626
+ * A plugin to provide the agent manager factory and completion provider.
627
+ * These objects require the secrets manager token with the same namespace.
628
+ */
629
+ const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => {
630
+ Private.setAISecretsToken(token);
631
+ return {
632
+ id: SECRETS_NAMESPACE,
633
+ description: 'Provide the AI agent manager',
634
+ autoStart: true,
635
+ provides: IAgentManagerFactory,
636
+ requires: [IAISettingsModel, IProviderRegistry],
637
+ optional: [ISkillRegistry, ICompletionProviderManager, ISecretsManager],
638
+ activate: (app, settingsModel, providerRegistry, skillRegistry, completionManager, secretsManager) => {
639
+ const agentManagerFactory = new AgentManagerFactory({
640
+ settingsModel,
641
+ skillRegistry,
642
+ secretsManager,
643
+ token
644
+ });
645
+ // Build the completion provider
646
+ if (completionManager) {
647
+ const completionProvider = new AICompletionProvider({
648
+ settingsModel,
649
+ providerRegistry,
650
+ secretsManager,
651
+ token
652
+ });
653
+ completionManager.registerInlineProvider(completionProvider);
654
+ }
655
+ else {
656
+ console.info('Completion provider manager not available, skipping AI completion setup');
657
+ }
658
+ return agentManagerFactory;
659
+ }
660
+ };
661
+ });
662
+ /**
663
+ * AI settings panel plugin.
507
664
  */
508
- const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => ({
509
- id: SECRETS_NAMESPACE,
510
- description: 'Provide the AI agent manager',
665
+ const settingsPanelPlugin = {
666
+ id: '@jupyterlite/ai:settings-panel',
667
+ description: 'Provide the AI settings panel',
511
668
  autoStart: true,
512
- provides: IAgentManagerFactory,
513
- requires: [IAISettingsModel, IProviderRegistry],
669
+ requires: [IAISettingsModel, IAgentManagerFactory, IProviderRegistry],
514
670
  optional: [
515
- ISkillRegistry,
516
671
  ICommandPalette,
517
- ICompletionProviderManager,
518
672
  ILayoutRestorer,
519
673
  ISecretsManager,
520
674
  IThemeManager,
521
675
  ITranslator
522
676
  ],
523
- activate: (app, settingsModel, providerRegistry, skillRegistry, palette, completionManager, restorer, secretsManager, themeManager, translator) => {
677
+ activate: (app, settingsModel, agentManagerFactory, providerRegistry, palette, restorer, secretsManager, themeManager, translator) => {
524
678
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
525
- const agentManagerFactory = new AgentManagerFactory({
526
- settingsModel,
527
- skillRegistry,
528
- secretsManager,
529
- token
530
- });
531
- // Build the settings panel
679
+ const secretsAccess = Private.createAISecretsAccess(secretsManager);
532
680
  const settingsWidget = new AISettingsWidget({
533
681
  settingsModel,
534
682
  agentManagerFactory,
535
683
  themeManager,
536
684
  providerRegistry,
537
- secretsManager,
538
- token,
685
+ secretsAccess,
539
686
  trans
540
687
  });
541
- settingsWidget.id = 'jupyterlite-ai-settings';
542
688
  settingsWidget.title.icon = settingsIcon;
543
689
  settingsWidget.title.iconClass = 'jp-ai-settings-icon';
544
- // Build the completion provider
545
- if (completionManager) {
546
- const completionProvider = new AICompletionProvider({
547
- settingsModel,
548
- providerRegistry,
549
- secretsManager,
550
- token
551
- });
552
- completionManager.registerInlineProvider(completionProvider);
553
- }
554
- else {
555
- console.info('Completion provider manager not available, skipping AI completion setup');
556
- }
690
+ const open = () => {
691
+ let widget = Array.from(app.shell.widgets('main')).find(w => w.id === 'jupyterlite-ai-settings');
692
+ if (!widget) {
693
+ widget = settingsWidget;
694
+ app.shell.add(widget, 'main');
695
+ }
696
+ app.shell.activateById(widget.id);
697
+ };
557
698
  if (restorer) {
558
699
  restorer.add(settingsWidget, settingsWidget.id);
559
700
  }
@@ -563,31 +704,20 @@ const agentManagerFactory = SecretsManager.sign(SECRETS_NAMESPACE, token => ({
563
704
  icon: settingsIcon,
564
705
  iconClass: 'jp-ai-settings-icon',
565
706
  execute: () => {
566
- // Check if the widget already exists in shell
567
- let widget = Array.from(app.shell.widgets('main')).find(w => w.id === 'jupyterlite-ai-settings');
568
- if (!widget && settingsWidget) {
569
- // Use the pre-created widget
570
- widget = settingsWidget;
571
- app.shell.add(widget, 'main');
572
- }
573
- if (widget) {
574
- app.shell.activateById(widget.id);
575
- }
707
+ open();
576
708
  },
577
709
  describedBy: {
578
710
  args: {}
579
711
  }
580
712
  });
581
- // Add to command palette if available
582
713
  if (palette) {
583
714
  palette.addItem({
584
715
  command: CommandIds.openSettings,
585
716
  category: trans.__('AI Assistant')
586
717
  });
587
718
  }
588
- return agentManagerFactory;
589
719
  }
590
- }));
720
+ };
591
721
  /**
592
722
  * Built-in completion providers plugin
593
723
  */
@@ -643,6 +773,7 @@ const toolRegistry = {
643
773
  const executeCommandTool = createExecuteCommandTool(app.commands, settingsModel);
644
774
  toolRegistry.add('discover_commands', discoverCommandsTool);
645
775
  toolRegistry.add('execute_command', executeCommandTool);
776
+ toolRegistry.add('browser_fetch', createBrowserFetchTool());
646
777
  if (skillRegistry) {
647
778
  toolRegistry.add('discover_skills', createDiscoverSkillsTool(skillRegistry));
648
779
  toolRegistry.add('load_skill', createLoadSkillTool(skillRegistry));
@@ -658,13 +789,13 @@ const inputToolbarFactory = {
658
789
  description: 'The input toolbar registry plugin.',
659
790
  autoStart: true,
660
791
  provides: IInputToolbarRegistryFactory,
661
- requires: [IAISettingsModel, IToolRegistry],
792
+ requires: [IAISettingsModel, IToolRegistry, IProviderRegistry],
662
793
  optional: [ITranslator],
663
- activate: (app, settingsModel, toolRegistry, translator) => {
794
+ activate: (app, settingsModel, toolRegistry, providerRegistry, translator) => {
664
795
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
665
796
  const stopButton = stopItem(trans);
666
797
  const clearButton = clearItem(trans);
667
- const toolSelectButton = createToolSelectItem(toolRegistry, settingsModel.config.toolsEnabled, trans);
798
+ const toolSelectButton = createToolSelectItem(toolRegistry, settingsModel, providerRegistry, settingsModel.config.toolsEnabled, trans);
668
799
  const modelSelectButton = createModelSelectItem(settingsModel, trans);
669
800
  return {
670
801
  create() {
@@ -828,10 +959,11 @@ export default [
828
959
  clearCommandPlugin,
829
960
  skillRegistryPlugin,
830
961
  skillsCommandPlugin,
831
- chatModelRegistry,
962
+ chatModelHandler,
832
963
  plugin,
833
964
  toolRegistry,
834
965
  agentManagerFactory,
966
+ settingsPanelPlugin,
835
967
  inputToolbarFactory,
836
968
  completionStatus,
837
969
  skillsPlugin
@@ -41,6 +41,8 @@ export interface IAIConfig {
41
41
  sendWithShiftEnter: boolean;
42
42
  showTokenUsage: boolean;
43
43
  commandsRequiringApproval: string[];
44
+ commandsAutoRenderMimeBundles: string[];
45
+ trustedMimeTypesForAutoRender: string[];
44
46
  showCellDiff: boolean;
45
47
  showFileDiff: boolean;
46
48
  diffDisplayMode: 'split' | 'unified';
@@ -33,6 +33,8 @@ export class AISettingsModel extends VDomModel {
33
33
  'runmenu:run-all',
34
34
  'jupyterlab-ai-commands:run-cell'
35
35
  ],
36
+ commandsAutoRenderMimeBundles: ['jupyterlab-ai-commands:execute-in-kernel'],
37
+ trustedMimeTypesForAutoRender: ['text/html'],
36
38
  systemPrompt: `You are Jupyternaut, an AI coding assistant built specifically for the JupyterLab environment.
37
39
 
38
40
  ## Your Core Mission