@jupyterlite/ai 0.11.1 → 0.13.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 (76) hide show
  1. package/lib/agent.d.ts +61 -7
  2. package/lib/agent.js +286 -103
  3. package/lib/chat-commands/clear.d.ts +8 -0
  4. package/lib/chat-commands/clear.js +30 -0
  5. package/lib/chat-commands/index.d.ts +2 -0
  6. package/lib/chat-commands/index.js +2 -0
  7. package/lib/chat-commands/skills.d.ts +19 -0
  8. package/lib/chat-commands/skills.js +57 -0
  9. package/lib/{chat-model-registry.d.ts → chat-model-handler.d.ts} +12 -11
  10. package/lib/{chat-model-registry.js → chat-model-handler.js} +6 -40
  11. package/lib/chat-model.d.ts +16 -0
  12. package/lib/chat-model.js +191 -11
  13. package/lib/completion/completion-provider.d.ts +1 -1
  14. package/lib/completion/completion-provider.js +14 -2
  15. package/lib/components/model-select.js +4 -4
  16. package/lib/components/tool-select.d.ts +11 -2
  17. package/lib/components/tool-select.js +77 -18
  18. package/lib/index.d.ts +3 -3
  19. package/lib/index.js +311 -72
  20. package/lib/models/settings-model.d.ts +3 -0
  21. package/lib/models/settings-model.js +63 -14
  22. package/lib/providers/built-in-providers.js +12 -7
  23. package/lib/providers/provider-tools.d.ts +36 -0
  24. package/lib/providers/provider-tools.js +93 -0
  25. package/lib/rendered-message-outputarea.d.ts +24 -0
  26. package/lib/rendered-message-outputarea.js +48 -0
  27. package/lib/skills/index.d.ts +4 -0
  28. package/lib/skills/index.js +7 -0
  29. package/lib/skills/parse-skill.d.ts +25 -0
  30. package/lib/skills/parse-skill.js +69 -0
  31. package/lib/skills/skill-loader.d.ts +25 -0
  32. package/lib/skills/skill-loader.js +133 -0
  33. package/lib/skills/skill-registry.d.ts +31 -0
  34. package/lib/skills/skill-registry.js +100 -0
  35. package/lib/skills/types.d.ts +29 -0
  36. package/lib/skills/types.js +5 -0
  37. package/lib/tokens.d.ts +77 -7
  38. package/lib/tokens.js +6 -1
  39. package/lib/tools/commands.js +4 -2
  40. package/lib/tools/skills.d.ts +9 -0
  41. package/lib/tools/skills.js +73 -0
  42. package/lib/tools/web.d.ts +8 -0
  43. package/lib/tools/web.js +196 -0
  44. package/lib/widgets/ai-settings.d.ts +1 -1
  45. package/lib/widgets/ai-settings.js +157 -38
  46. package/lib/widgets/main-area-chat.d.ts +6 -0
  47. package/lib/widgets/main-area-chat.js +28 -0
  48. package/lib/widgets/provider-config-dialog.js +207 -4
  49. package/package.json +18 -11
  50. package/schema/settings-model.json +97 -2
  51. package/src/agent.ts +397 -123
  52. package/src/chat-commands/clear.ts +46 -0
  53. package/src/chat-commands/index.ts +2 -0
  54. package/src/chat-commands/skills.ts +87 -0
  55. package/src/{chat-model-registry.ts → chat-model-handler.ts} +16 -51
  56. package/src/chat-model.ts +270 -23
  57. package/src/completion/completion-provider.ts +26 -12
  58. package/src/components/model-select.tsx +4 -5
  59. package/src/components/tool-select.tsx +110 -7
  60. package/src/index.ts +395 -87
  61. package/src/models/settings-model.ts +70 -15
  62. package/src/providers/built-in-providers.ts +12 -7
  63. package/src/providers/provider-tools.ts +179 -0
  64. package/src/rendered-message-outputarea.ts +62 -0
  65. package/src/skills/index.ts +14 -0
  66. package/src/skills/parse-skill.ts +91 -0
  67. package/src/skills/skill-loader.ts +175 -0
  68. package/src/skills/skill-registry.ts +137 -0
  69. package/src/skills/types.ts +37 -0
  70. package/src/tokens.ts +109 -9
  71. package/src/tools/commands.ts +4 -2
  72. package/src/tools/skills.ts +84 -0
  73. package/src/tools/web.ts +238 -0
  74. package/src/widgets/ai-settings.tsx +357 -77
  75. package/src/widgets/main-area-chat.ts +34 -1
  76. package/src/widgets/provider-config-dialog.tsx +496 -3
package/src/index.ts CHANGED
@@ -9,16 +9,21 @@ import {
9
9
  ActiveCellManager,
10
10
  AttachmentOpenerRegistry,
11
11
  chatIcon,
12
+ ChatCommandRegistry,
12
13
  ChatWidget,
13
14
  IAttachmentOpenerRegistry,
15
+ IChatCommandRegistry,
16
+ IChatTracker,
14
17
  IInputToolbarRegistryFactory,
15
18
  InputToolbarRegistry,
16
- MultiChatPanel
19
+ MultiChatPanel,
20
+ IChatModel
17
21
  } from '@jupyter/chat';
18
22
 
19
23
  import {
20
24
  ICommandPalette,
21
25
  IThemeManager,
26
+ showErrorMessage,
22
27
  WidgetTracker
23
28
  } from '@jupyterlab/apputils';
24
29
 
@@ -34,6 +39,8 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry';
34
39
 
35
40
  import { IStatusBar } from '@jupyterlab/statusbar';
36
41
 
42
+ import { PathExt } from '@jupyterlab/coreutils';
43
+
37
44
  import {
38
45
  ITranslator,
39
46
  nullTranslator,
@@ -49,25 +56,31 @@ import {
49
56
  import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
50
57
 
51
58
  import { PromiseDelegate, UUID } from '@lumino/coreutils';
59
+ import { DisposableSet } from '@lumino/disposable';
52
60
 
53
61
  import { AgentManagerFactory } from './agent';
54
62
 
55
63
  import { AIChatModel } from './chat-model';
64
+ import { RenderedMessageOutputAreaCompat } from './rendered-message-outputarea';
65
+
66
+ import { ClearCommandProvider } from './chat-commands/clear';
67
+ import { SkillsCommandProvider } from './chat-commands/skills';
56
68
 
57
69
  import { ProviderRegistry } from './providers/provider-registry';
58
70
 
59
71
  import { ApprovalButtons } from './approval-buttons';
60
72
 
61
- import { ChatModelRegistry } from './chat-model-registry';
73
+ import { ChatModelHandler } from './chat-model-handler';
62
74
 
63
75
  import {
64
76
  CommandIds,
65
77
  IAgentManagerFactory,
66
78
  IProviderRegistry,
67
79
  IToolRegistry,
80
+ ISkillRegistry,
68
81
  SECRETS_NAMESPACE,
69
82
  IAISettingsModel,
70
- IChatModelRegistry,
83
+ IChatModelHandler,
71
84
  IDiffManager
72
85
  } from './tokens';
73
86
 
@@ -90,7 +103,9 @@ import {
90
103
  TokenUsageWidget
91
104
  } from './components';
92
105
 
93
- import { AISettingsModel } from './models/settings-model';
106
+ import { AISettingsModel, IProviderConfig } from './models/settings-model';
107
+
108
+ import { loadSkillsFromPaths, SkillRegistry } from './skills';
94
109
 
95
110
  import { DiffManager } from './diff-manager';
96
111
 
@@ -100,6 +115,8 @@ import {
100
115
  createDiscoverCommandsTool,
101
116
  createExecuteCommandTool
102
117
  } from './tools/commands';
118
+ import { createDiscoverSkillsTool, createLoadSkillTool } from './tools/skills';
119
+ import { createBrowserFetchTool } from './tools/web';
103
120
 
104
121
  import { AISettingsWidget } from './widgets/ai-settings';
105
122
 
@@ -184,30 +201,85 @@ const genericProviderPlugin: JupyterFrontEndPlugin<void> = {
184
201
  };
185
202
 
186
203
  /**
187
- * The chat model registry.
204
+ * Chat command registry plugin.
205
+ */
206
+ const chatCommandRegistryPlugin: JupyterFrontEndPlugin<IChatCommandRegistry> = {
207
+ id: '@jupyterlite/ai:chat-command-registry',
208
+ description: 'Provide the chat command registry for JupyterLite AI.',
209
+ autoStart: true,
210
+ provides: IChatCommandRegistry,
211
+ activate: () => {
212
+ return new ChatCommandRegistry();
213
+ }
214
+ };
215
+
216
+ /**
217
+ * Clear chat command plugin.
218
+ */
219
+ const clearCommandPlugin: JupyterFrontEndPlugin<void> = {
220
+ id: '@jupyterlite/ai:clear-command',
221
+ description: 'Register the /clear chat command.',
222
+ autoStart: true,
223
+ requires: [IChatCommandRegistry],
224
+ activate: (app, registry: IChatCommandRegistry) => {
225
+ registry.addProvider(new ClearCommandProvider());
226
+ }
227
+ };
228
+
229
+ /**
230
+ * Skills chat command plugin.
231
+ */
232
+ const skillsCommandPlugin: JupyterFrontEndPlugin<void> = {
233
+ id: '@jupyterlite/ai:skills-command',
234
+ description: 'Register the /skills chat command.',
235
+ autoStart: true,
236
+ requires: [IChatCommandRegistry, ISkillRegistry],
237
+ activate: (
238
+ app,
239
+ registry: IChatCommandRegistry,
240
+ skillRegistry: ISkillRegistry
241
+ ) => {
242
+ registry.addProvider(
243
+ new SkillsCommandProvider({
244
+ skillRegistry,
245
+ commands: app.commands
246
+ })
247
+ );
248
+ }
249
+ };
250
+
251
+ /**
252
+ * The chat model handler.
188
253
  */
189
- const chatModelRegistry: JupyterFrontEndPlugin<IChatModelRegistry> = {
190
- id: '@jupyterlite/ai:chat-model-registry',
191
- description: 'Registry for the current chat model',
254
+ const chatModelHandler: JupyterFrontEndPlugin<IChatModelHandler> = {
255
+ id: '@jupyterlite/ai:chat-model-handler',
256
+ description: 'A handler to create current chat model',
192
257
  autoStart: true,
193
- requires: [IAISettingsModel, IAgentManagerFactory, IDocumentManager],
258
+ requires: [
259
+ IAISettingsModel,
260
+ IAgentManagerFactory,
261
+ IDocumentManager,
262
+ IRenderMimeRegistry
263
+ ],
194
264
  optional: [IProviderRegistry, IToolRegistry, ITranslator],
195
- provides: IChatModelRegistry,
265
+ provides: IChatModelHandler,
196
266
  activate: (
197
267
  app: JupyterFrontEnd,
198
268
  settingsModel: AISettingsModel,
199
269
  agentManagerFactory: AgentManagerFactory,
200
270
  docManager: IDocumentManager,
271
+ rmRegistry: IRenderMimeRegistry,
201
272
  providerRegistry?: IProviderRegistry,
202
273
  toolRegistry?: IToolRegistry,
203
274
  translator?: ITranslator
204
- ): IChatModelRegistry => {
275
+ ): IChatModelHandler => {
205
276
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
206
277
 
207
- return new ChatModelRegistry({
278
+ return new ChatModelHandler({
208
279
  settingsModel,
209
280
  agentManagerFactory,
210
281
  docManager,
282
+ rmRegistry,
211
283
  providerRegistry,
212
284
  toolRegistry,
213
285
  trans
@@ -218,15 +290,17 @@ const chatModelRegistry: JupyterFrontEndPlugin<IChatModelRegistry> = {
218
290
  /**
219
291
  * Initialization data for the extension.
220
292
  */
221
- const plugin: JupyterFrontEndPlugin<void> = {
293
+ const plugin: JupyterFrontEndPlugin<IChatTracker> = {
222
294
  id: '@jupyterlite/ai:plugin',
223
295
  description: 'AI in JupyterLab',
224
296
  autoStart: true,
297
+ provides: IChatTracker,
225
298
  requires: [
226
299
  IRenderMimeRegistry,
227
300
  IInputToolbarRegistryFactory,
228
- IChatModelRegistry,
229
- IAISettingsModel
301
+ IChatModelHandler,
302
+ IAISettingsModel,
303
+ IChatCommandRegistry
230
304
  ],
231
305
  optional: [
232
306
  IThemeManager,
@@ -239,14 +313,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
239
313
  app: JupyterFrontEnd,
240
314
  rmRegistry: IRenderMimeRegistry,
241
315
  inputToolbarFactory: IInputToolbarRegistryFactory,
242
- modelRegistry: IChatModelRegistry,
316
+ modelHandler: IChatModelHandler,
243
317
  settingsModel: AISettingsModel,
318
+ chatCommandRegistry: IChatCommandRegistry,
244
319
  themeManager?: IThemeManager,
245
320
  restorer?: ILayoutRestorer,
246
321
  labShell?: ILabShell,
247
322
  notebookTracker?: INotebookTracker,
248
323
  translator?: ITranslator
249
- ): void => {
324
+ ): IChatTracker => {
250
325
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
251
326
  // Create attachment opener registry to handle file attachments
252
327
  const attachmentOpenerRegistry = new AttachmentOpenerRegistry();
@@ -267,7 +342,11 @@ const plugin: JupyterFrontEndPlugin<void> = {
267
342
  shell: app.shell
268
343
  });
269
344
  }
270
- modelRegistry.activeCellManager = activeCellManager;
345
+ modelHandler.activeCellManager = activeCellManager;
346
+
347
+ // Creating the tracker for the chat widgets
348
+ const namespace = 'ai-chat';
349
+ const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
271
350
 
272
351
  // Create chat panel with drag/drop functionality
273
352
  const chatPanel = new MultiChatPanel({
@@ -275,19 +354,36 @@ const plugin: JupyterFrontEndPlugin<void> = {
275
354
  themeManager: themeManager ?? null,
276
355
  inputToolbarFactory,
277
356
  attachmentOpenerRegistry,
278
- createModel: async (name?: string) => {
279
- const model = modelRegistry.createModel(name);
357
+ chatCommandRegistry,
358
+ createModel: async (provider?: string) => {
359
+ if (!provider) {
360
+ provider = settingsModel.getDefaultProvider()?.id;
361
+ if (!provider) {
362
+ showErrorMessage('Error creating chat', 'Please set up a provider');
363
+ app.commands.execute('@jupyterlite/ai:open-settings');
364
+ return {};
365
+ }
366
+ }
367
+ let name = settingsModel.getProvider(provider)?.name ?? UUID.uuid4();
368
+ const modelName = name;
369
+ const existingName = new Set(chatPanel.getLoadedModelNames());
370
+ tracker.forEach(widget => existingName.add(widget.model.name));
371
+ let i = 1;
372
+ while (existingName.has(name)) {
373
+ name = `${modelName}-${i}`;
374
+ i += 1;
375
+ }
376
+ const model = modelHandler.createModel(name, provider);
280
377
  return { model };
281
378
  },
282
- renameChat: async (oldName: string, newName: string) => {
283
- const model = modelRegistry.get(oldName);
284
- const concurrencyModel = modelRegistry.get(newName);
285
- if (model && !concurrencyModel) {
286
- model.name = newName;
287
- return true;
288
- }
289
- return false;
379
+ getChatNames: async () => {
380
+ const names: { [name: string]: string } = {};
381
+ settingsModel.config.providers.forEach(provider => {
382
+ names[provider.name] = provider.id;
383
+ });
384
+ return names;
290
385
  },
386
+ renameChat: true,
291
387
  openInMain: (name: string) =>
292
388
  app.commands.execute(CommandIds.moveChat, {
293
389
  area: 'main',
@@ -311,28 +407,40 @@ const plugin: JupyterFrontEndPlugin<void> = {
311
407
  })
312
408
  );
313
409
 
314
- chatPanel.sectionAdded.connect((_, section) => {
315
- const { widget } = section;
316
- const model = section.model as AIChatModel;
410
+ let tokenUsageWidget: TokenUsageWidget | null = null;
411
+ chatPanel.chatOpened.connect((_, widget) => {
412
+ const model = widget.model as AIChatModel;
317
413
 
318
414
  // Add the widget to the tracker.
319
415
  tracker.add(widget);
320
416
 
417
+ function saveTracker() {
418
+ tracker.save(widget);
419
+ }
420
+
321
421
  // Update the tracker if the model name changed.
322
- model.nameChanged.connect(() => tracker.save(widget));
422
+ model.nameChanged.connect(saveTracker);
423
+
323
424
  // Update the tracker if the active provider changed.
324
- model.agentManager.activeProviderChanged.connect(() =>
325
- tracker.save(widget)
326
- );
425
+ model.agentManager.activeProviderChanged.connect(saveTracker);
426
+
427
+ // Update the token usage widget.
428
+ tokenUsageWidget?.dispose();
327
429
 
328
- const tokenUsageWidget = new TokenUsageWidget({
430
+ tokenUsageWidget = new TokenUsageWidget({
329
431
  tokenUsageChanged: model.tokenUsageChanged,
330
432
  settingsModel,
331
433
  initialTokenUsage: model.agentManager.tokenUsage,
332
434
  translator: trans
333
435
  });
334
- section.toolbar.insertBefore('markRead', 'token-usage', tokenUsageWidget);
335
- model.writersChanged?.connect((_, writers) => {
436
+ chatPanel.current?.toolbar.insertBefore(
437
+ 'markRead',
438
+ 'token-usage',
439
+ tokenUsageWidget
440
+ );
441
+
442
+ // Listen for writers change to display the stop button.
443
+ function writersChanged(_: IChatModel, writers: IChatModel.IWriter[]) {
336
444
  // Check if AI is currently writing (streaming)
337
445
  const aiWriting = writers.some(
338
446
  writer => writer.user.username === 'ai-assistant'
@@ -345,28 +453,34 @@ const plugin: JupyterFrontEndPlugin<void> = {
345
453
  widget.inputToolbarRegistry?.hide('stop');
346
454
  widget.inputToolbarRegistry?.show('send');
347
455
  }
348
- });
456
+ }
457
+
458
+ model.writersChanged?.connect(writersChanged);
349
459
 
350
460
  // Associate an approval buttons object to the chat.
351
461
  const approvalButton = new ApprovalButtons({
352
462
  chatPanel: widget,
353
463
  agentManager: model.agentManager
354
464
  });
465
+ // Temporary compat: keep output-area CSS context for MIME renderers
466
+ // until jupyter-chat provides it natively.
467
+ const outputAreaCompat = new RenderedMessageOutputAreaCompat({
468
+ chatPanel: widget
469
+ });
355
470
 
356
471
  widget.disposed.connect(() => {
472
+ model.nameChanged.disconnect(saveTracker);
473
+ model.agentManager.activeProviderChanged.disconnect(saveTracker);
474
+ model.writersChanged?.disconnect(writersChanged);
475
+
357
476
  // Dispose of the approval buttons widget when the chat is disposed.
358
477
  approvalButton.dispose();
359
- // Remove the model from the registry when the widget is disposed.
360
- modelRegistry.remove(model.name);
478
+ outputAreaCompat.dispose();
361
479
  });
362
480
  });
363
481
 
364
482
  app.shell.add(chatPanel, 'left', { rank: 1000 });
365
483
 
366
- // Creating the tracker for the document
367
- const namespace = 'ai-chat';
368
- const tracker = new WidgetTracker<MainAreaChat | ChatWidget>({ namespace });
369
-
370
484
  if (restorer) {
371
485
  restorer.add(chatPanel, chatPanel.id);
372
486
  void restorer.restore(tracker, {
@@ -385,10 +499,7 @@ const plugin: JupyterFrontEndPlugin<void> = {
385
499
 
386
500
  // Create a chat with default provider at startup.
387
501
  app.restored.then(() => {
388
- if (
389
- !modelRegistry.getAll().length &&
390
- settingsModel.config.defaultProvider
391
- ) {
502
+ if (!tracker.size && settingsModel.config.defaultProvider) {
392
503
  app.commands.execute(CommandIds.openChat);
393
504
  }
394
505
  });
@@ -400,12 +511,15 @@ const plugin: JupyterFrontEndPlugin<void> = {
400
511
  attachmentOpenerRegistry,
401
512
  inputToolbarFactory,
402
513
  settingsModel,
514
+ chatCommandRegistry,
403
515
  tracker,
404
- modelRegistry,
516
+ modelHandler,
405
517
  trans,
406
518
  themeManager,
407
519
  labShell
408
520
  );
521
+
522
+ return tracker;
409
523
  }
410
524
  };
411
525
 
@@ -416,8 +530,9 @@ function registerCommands(
416
530
  attachmentOpenerRegistry: IAttachmentOpenerRegistry,
417
531
  inputToolbarFactory: IInputToolbarRegistryFactory,
418
532
  settingsModel: AISettingsModel,
533
+ chatCommandRegistry: IChatCommandRegistry,
419
534
  tracker: WidgetTracker<MainAreaChat | ChatWidget>,
420
- modelRegistry: IChatModelRegistry,
535
+ modelRegistry: IChatModelHandler,
421
536
  trans: TranslationBundle,
422
537
  themeManager?: IThemeManager,
423
538
  labShell?: ILabShell
@@ -481,7 +596,8 @@ function registerCommands(
481
596
  rmRegistry,
482
597
  themeManager: themeManager ?? null,
483
598
  inputToolbarRegistry: inputToolbarFactory.create(),
484
- attachmentOpenerRegistry
599
+ attachmentOpenerRegistry,
600
+ chatCommandRegistry
485
601
  });
486
602
  const widget = new MainAreaChat({
487
603
  content,
@@ -494,16 +610,18 @@ function registerCommands(
494
610
  // Add the widget to the tracker.
495
611
  tracker.add(widget);
496
612
 
613
+ function saveTracker() {
614
+ tracker.save(widget);
615
+ }
616
+
497
617
  // Update the tracker if the model name changed.
498
- model.nameChanged.connect(() => tracker.save(widget));
618
+ model.nameChanged.connect(saveTracker);
499
619
  // Update the tracker if the active provider changed.
500
- model.agentManager.activeProviderChanged.connect(() =>
501
- tracker.save(widget)
502
- );
620
+ model.agentManager.activeProviderChanged.connect(saveTracker);
503
621
 
504
- // Remove the model from the registry when the widget is disposed.
505
622
  widget.disposed.connect(() => {
506
- modelRegistry.remove(model.name);
623
+ model.nameChanged.disconnect(saveTracker);
624
+ model.agentManager.activeProviderChanged.disconnect(saveTracker);
507
625
  });
508
626
  };
509
627
 
@@ -512,15 +630,26 @@ function registerCommands(
512
630
  execute: async (args): Promise<boolean> => {
513
631
  const area = (args.area as string) === 'main' ? 'main' : 'side';
514
632
  const provider = (args.provider as string) ?? undefined;
633
+ let name = (args.name as string) ?? undefined;
515
634
 
516
- // Do not open the chat if the provider in args does not exists in settings.
517
- if (provider && !settingsModel.getProvider(provider)) {
635
+ let providerConfig: IProviderConfig | undefined = undefined;
636
+ if (provider) {
637
+ providerConfig = settingsModel.getProvider(provider);
638
+ } else {
639
+ providerConfig = settingsModel.getDefaultProvider();
640
+ }
641
+
642
+ // Do not open the chat if the provider in args does not exists in settings or
643
+ // if there is no default provider.
644
+ if (!providerConfig) {
518
645
  return false;
519
646
  }
520
- const model = modelRegistry.createModel(
521
- args.name ? (args.name as string) : undefined,
522
- provider
523
- );
647
+
648
+ if (!name) {
649
+ name = providerConfig.name;
650
+ }
651
+
652
+ const model = modelRegistry.createModel(name, provider);
524
653
  if (!model) {
525
654
  return false;
526
655
  }
@@ -528,7 +657,7 @@ function registerCommands(
528
657
  if (area === 'main') {
529
658
  openInMain(model);
530
659
  } else {
531
- chatPanel.addChat({ model });
660
+ chatPanel.open({ model });
532
661
  }
533
662
  return true;
534
663
  },
@@ -570,7 +699,15 @@ function registerCommands(
570
699
  );
571
700
  return false;
572
701
  }
573
- const previousModel = modelRegistry.get(args.name as string);
702
+ let previousWidget: ChatWidget | MainAreaChat | undefined;
703
+ let previousModel: AIChatModel | undefined;
704
+ tracker.forEach(widget => {
705
+ if (widget.model.name === args.name) {
706
+ previousWidget = widget;
707
+ previousModel = widget.model as AIChatModel;
708
+ }
709
+ });
710
+
574
711
  if (!previousModel) {
575
712
  console.error(
576
713
  'Error while moving the chat to main area: there is no reference model'
@@ -596,12 +733,12 @@ function registerCommands(
596
733
  // Create a new model by duplicating the previous model attributes.
597
734
  const model = modelRegistry.createModel(
598
735
  args.name as string,
599
- previousModel?.agentManager.activeProvider,
600
- previousModel?.agentManager.tokenUsage
601
- );
602
- previousModel?.messages.forEach(message =>
603
- model?.messageAdded(message)
736
+ previousModel.agentManager.activeProvider,
737
+ previousModel.agentManager.tokenUsage
604
738
  );
739
+ previousModel?.messages.forEach(message => {
740
+ model?.messageAdded({ ...message.content });
741
+ });
605
742
 
606
743
  // Wait (with timeout) for the tracker to have updated the previous widget.
607
744
  const status = await Promise.any([
@@ -621,15 +758,9 @@ function registerCommands(
621
758
  if (area === 'main') {
622
759
  openInMain(model);
623
760
  } else {
624
- const current = app.shell.currentWidget;
625
- // Remove the current main area chat.
626
- if (
627
- current instanceof MainAreaChat &&
628
- current.model.name === previousModel.name
629
- ) {
630
- current.dispose();
631
- }
632
- chatPanel.addChat({ model });
761
+ previousWidget?.dispose();
762
+ previousModel.dispose();
763
+ chatPanel.open({ model });
633
764
  }
634
765
 
635
766
  return true;
@@ -641,7 +772,7 @@ function registerCommands(
641
772
  area: {
642
773
  type: 'string',
643
774
  enum: ['main', 'side'],
644
- description: trans.__('The name of the area to move the chat to')
775
+ description: trans.__('The area to move the chat to')
645
776
  },
646
777
  name: {
647
778
  type: 'string',
@@ -656,7 +787,9 @@ function registerCommands(
656
787
  }
657
788
 
658
789
  /**
659
- * A plugin to provide the settings model.
790
+ * A plugin to provide the agent manager factory, the completion provider and
791
+ * the settings model.
792
+ * All these objects require the secrets manager token with the same namespace.
660
793
  */
661
794
  const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
662
795
  SecretsManager.sign(SECRETS_NAMESPACE, token => ({
@@ -666,6 +799,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
666
799
  provides: IAgentManagerFactory,
667
800
  requires: [IAISettingsModel, IProviderRegistry],
668
801
  optional: [
802
+ ISkillRegistry,
669
803
  ICommandPalette,
670
804
  ICompletionProviderManager,
671
805
  ILayoutRestorer,
@@ -677,7 +811,8 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
677
811
  app: JupyterFrontEnd,
678
812
  settingsModel: AISettingsModel,
679
813
  providerRegistry: IProviderRegistry,
680
- palette: ICommandPalette,
814
+ skillRegistry?: ISkillRegistry,
815
+ palette?: ICommandPalette,
681
816
  completionManager?: ICompletionProviderManager,
682
817
  restorer?: ILayoutRestorer,
683
818
  secretsManager?: ISecretsManager,
@@ -687,6 +822,7 @@ const agentManagerFactory: JupyterFrontEndPlugin<AgentManagerFactory> =
687
822
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
688
823
  const agentManagerFactory = new AgentManagerFactory({
689
824
  settingsModel,
825
+ skillRegistry,
690
826
  secretsManager,
691
827
  token
692
828
  });
@@ -797,13 +933,31 @@ const diffManager: JupyterFrontEndPlugin<IDiffManager> = {
797
933
  }
798
934
  };
799
935
 
936
+ /**
937
+ * Skill registry plugin
938
+ */
939
+ const skillRegistryPlugin: JupyterFrontEndPlugin<ISkillRegistry> = {
940
+ id: '@jupyterlite/ai:skill-registry',
941
+ description: 'Provide the skill registry',
942
+ autoStart: true,
943
+ provides: ISkillRegistry,
944
+ activate: () => {
945
+ return new SkillRegistry();
946
+ }
947
+ };
948
+
800
949
  const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
801
950
  id: '@jupyterlite/ai:tool-registry',
802
951
  description: 'Provide the AI tool registry',
803
952
  autoStart: true,
804
953
  requires: [IAISettingsModel],
954
+ optional: [ISkillRegistry],
805
955
  provides: IToolRegistry,
806
- activate: (app: JupyterFrontEnd, settingsModel: AISettingsModel) => {
956
+ activate: (
957
+ app: JupyterFrontEnd,
958
+ settingsModel: AISettingsModel,
959
+ skillRegistry?: ISkillRegistry
960
+ ) => {
807
961
  const toolRegistry = new ToolRegistry();
808
962
 
809
963
  // Add command operation tools
@@ -815,6 +969,14 @@ const toolRegistry: JupyterFrontEndPlugin<IToolRegistry> = {
815
969
 
816
970
  toolRegistry.add('discover_commands', discoverCommandsTool);
817
971
  toolRegistry.add('execute_command', executeCommandTool);
972
+ toolRegistry.add('browser_fetch', createBrowserFetchTool());
973
+ if (skillRegistry) {
974
+ toolRegistry.add(
975
+ 'discover_skills',
976
+ createDiscoverSkillsTool(skillRegistry)
977
+ );
978
+ toolRegistry.add('load_skill', createLoadSkillTool(skillRegistry));
979
+ }
818
980
 
819
981
  return toolRegistry;
820
982
  }
@@ -829,12 +991,13 @@ const inputToolbarFactory: JupyterFrontEndPlugin<IInputToolbarRegistryFactory> =
829
991
  description: 'The input toolbar registry plugin.',
830
992
  autoStart: true,
831
993
  provides: IInputToolbarRegistryFactory,
832
- requires: [IAISettingsModel, IToolRegistry],
994
+ requires: [IAISettingsModel, IToolRegistry, IProviderRegistry],
833
995
  optional: [ITranslator],
834
996
  activate: (
835
997
  app: JupyterFrontEnd,
836
998
  settingsModel: AISettingsModel,
837
999
  toolRegistry: IToolRegistry,
1000
+ providerRegistry: IProviderRegistry,
838
1001
  translator?: ITranslator
839
1002
  ): IInputToolbarRegistryFactory => {
840
1003
  const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
@@ -842,6 +1005,8 @@ const inputToolbarFactory: JupyterFrontEndPlugin<IInputToolbarRegistryFactory> =
842
1005
  const clearButton = clearItem(trans);
843
1006
  const toolSelectButton = createToolSelectItem(
844
1007
  toolRegistry,
1008
+ settingsModel,
1009
+ providerRegistry,
845
1010
  settingsModel.config.toolsEnabled,
846
1011
  trans
847
1012
  );
@@ -900,6 +1065,144 @@ const completionStatus: JupyterFrontEndPlugin<void> = {
900
1065
  }
901
1066
  };
902
1067
 
1068
+ /**
1069
+ * Skills plugin: discovers and registers agent skills from the filesystem.
1070
+ */
1071
+ const skillsPlugin: JupyterFrontEndPlugin<void> = {
1072
+ id: '@jupyterlite/ai:skills',
1073
+ description: 'Discover and register agent skills',
1074
+ autoStart: true,
1075
+ requires: [IAISettingsModel, IDocumentManager, ISkillRegistry],
1076
+ optional: [ICommandPalette, ITranslator],
1077
+ activate: async (
1078
+ app: JupyterFrontEnd,
1079
+ settingsModel: AISettingsModel,
1080
+ docManager: IDocumentManager,
1081
+ skillRegistry: ISkillRegistry,
1082
+ palette?: ICommandPalette,
1083
+ translator?: ITranslator
1084
+ ) => {
1085
+ const trans = (translator ?? nullTranslator).load('jupyterlite_ai');
1086
+ const validateResourcePath = (resourcePath: string): string | null => {
1087
+ if (resourcePath.startsWith('/')) {
1088
+ return null;
1089
+ }
1090
+
1091
+ const normalized = PathExt.normalize(resourcePath);
1092
+ if (normalized.startsWith('..') || normalized === '') {
1093
+ return null;
1094
+ }
1095
+
1096
+ return normalized;
1097
+ };
1098
+
1099
+ let currentSkillsPaths = settingsModel.config.skillsPaths;
1100
+ let currentSkillDisposables = new DisposableSet();
1101
+
1102
+ const loadAndRegister = async () => {
1103
+ const skillsPaths = settingsModel.config.skillsPaths;
1104
+ const skills = await loadSkillsFromPaths(
1105
+ docManager.services.contents,
1106
+ skillsPaths
1107
+ );
1108
+
1109
+ const registrations = skills.map(skill => ({
1110
+ name: skill.name,
1111
+ description: skill.description,
1112
+ instructions: skill.instructions,
1113
+ resources: skill.resources,
1114
+ loadResource: async (resource: string) => {
1115
+ const validatedPath = validateResourcePath(resource);
1116
+ if (validatedPath === null) {
1117
+ return {
1118
+ name: skill.name,
1119
+ resource,
1120
+ error: 'Invalid resource path: path traversal not allowed'
1121
+ };
1122
+ }
1123
+
1124
+ if (!skill.resources.includes(validatedPath)) {
1125
+ return {
1126
+ name: skill.name,
1127
+ resource,
1128
+ error: `Resource not found: ${resource}`
1129
+ };
1130
+ }
1131
+
1132
+ const resourcePath = `${skill.path}/${validatedPath}`;
1133
+ try {
1134
+ const fileModel = await docManager.services.contents.get(
1135
+ resourcePath,
1136
+ {
1137
+ content: true
1138
+ }
1139
+ );
1140
+ if (typeof fileModel.content !== 'string') {
1141
+ return {
1142
+ name: skill.name,
1143
+ resource,
1144
+ error: 'Resource content is not a string'
1145
+ };
1146
+ }
1147
+ return {
1148
+ name: skill.name,
1149
+ resource,
1150
+ content: fileModel.content
1151
+ };
1152
+ } catch (error) {
1153
+ return {
1154
+ name: skill.name,
1155
+ resource,
1156
+ error: `Failed to read resource: ${error}`
1157
+ };
1158
+ }
1159
+ }
1160
+ }));
1161
+
1162
+ currentSkillDisposables.dispose();
1163
+ currentSkillDisposables = new DisposableSet();
1164
+ for (const registration of registrations) {
1165
+ currentSkillDisposables.add(skillRegistry.registerSkill(registration));
1166
+ }
1167
+ };
1168
+
1169
+ app.commands.addCommand(CommandIds.refreshSkills, {
1170
+ label: trans.__('Refresh Agents Skills'),
1171
+ caption: trans.__(
1172
+ 'Re-scan the agents skills directory and update the registry'
1173
+ ),
1174
+ execute: async () => {
1175
+ await loadAndRegister();
1176
+ }
1177
+ });
1178
+
1179
+ if (palette) {
1180
+ palette.addItem({
1181
+ command: CommandIds.refreshSkills,
1182
+ category: trans.__('AI Assistant')
1183
+ });
1184
+ }
1185
+
1186
+ loadAndRegister().catch(error =>
1187
+ console.warn('Failed to load skills on activation:', error)
1188
+ );
1189
+
1190
+ settingsModel.stateChanged.connect(() => {
1191
+ const newPaths = settingsModel.config.skillsPaths;
1192
+ if (
1193
+ newPaths.length === currentSkillsPaths.length &&
1194
+ newPaths.every((p, i) => p === currentSkillsPaths[i])
1195
+ ) {
1196
+ return;
1197
+ }
1198
+ currentSkillsPaths = newPaths;
1199
+ loadAndRegister().catch(error =>
1200
+ console.warn('Failed to reload skills:', error)
1201
+ );
1202
+ });
1203
+ }
1204
+ };
1205
+
903
1206
  export default [
904
1207
  providerRegistryPlugin,
905
1208
  anthropicProviderPlugin,
@@ -909,12 +1212,17 @@ export default [
909
1212
  genericProviderPlugin,
910
1213
  settingsModel,
911
1214
  diffManager,
912
- chatModelRegistry,
1215
+ chatCommandRegistryPlugin,
1216
+ clearCommandPlugin,
1217
+ skillRegistryPlugin,
1218
+ skillsCommandPlugin,
1219
+ chatModelHandler,
913
1220
  plugin,
914
1221
  toolRegistry,
915
1222
  agentManagerFactory,
916
1223
  inputToolbarFactory,
917
- completionStatus
1224
+ completionStatus,
1225
+ skillsPlugin
918
1226
  ];
919
1227
 
920
1228
  // Export extension points for other extensions to use