@jupyternaut/persona 0.20.0 → 0.20.1

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.
@@ -0,0 +1,3 @@
1
+ export * from './mention';
2
+ export * from './skills';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ export * from './mention';
2
+ export * from './skills';
@@ -0,0 +1,25 @@
1
+ import { ChatCommand, IChatCommandProvider, IInputModel } from '@jupyter/chat';
2
+ import type { ISkillRegistry } from '@jupyternaut/agent';
3
+ import { CommandRegistry } from '@lumino/commands';
4
+ export declare class SkillsCommandProvider implements IChatCommandProvider {
5
+ constructor(options: SkillsCommandProvider.IOptions);
6
+ id: string;
7
+ listCommandCompletions(inputModel: IInputModel): Promise<ChatCommand[]>;
8
+ onSubmit(inputModel: IInputModel): Promise<void>;
9
+ private _isActive;
10
+ private _command;
11
+ private _regex;
12
+ private _commands;
13
+ private _skillRegistry;
14
+ private _sendSystemMessage;
15
+ private _isDefault;
16
+ }
17
+ export declare namespace SkillsCommandProvider {
18
+ interface IOptions {
19
+ skillRegistry: ISkillRegistry;
20
+ commands: CommandRegistry;
21
+ sendSystemMessage: (chatName: string, body: string) => void;
22
+ isDefault: (chatName: string) => boolean;
23
+ }
24
+ }
25
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +1,73 @@
1
+ import { CommandIds } from '../tokens';
2
+ export class SkillsCommandProvider {
3
+ constructor(options) {
4
+ this._skillRegistry = options.skillRegistry;
5
+ this._commands = options.commands;
6
+ this._sendSystemMessage = options.sendSystemMessage;
7
+ this._isDefault = options.isDefault;
8
+ }
9
+ id = '@jupyternaut/persona:skills-command';
10
+ async listCommandCompletions(inputModel) {
11
+ if (!this._isActive(inputModel)) {
12
+ return [];
13
+ }
14
+ const match = inputModel.currentWord?.match(this._regex)?.[0];
15
+ if (!match) {
16
+ return [];
17
+ }
18
+ if (this._command.name.startsWith(match)) {
19
+ return [this._command];
20
+ }
21
+ return [];
22
+ }
23
+ async onSubmit(inputModel) {
24
+ if (!this._isActive(inputModel)) {
25
+ return;
26
+ }
27
+ const trimmed = inputModel.value.trim();
28
+ const match = trimmed.match(/^\/skills(?:\s+(.+))?$/);
29
+ if (!match) {
30
+ return;
31
+ }
32
+ if (this._commands.hasCommand(CommandIds.refreshSkills)) {
33
+ await this._commands.execute(CommandIds.refreshSkills);
34
+ }
35
+ const query = match[1]?.trim();
36
+ const filtered = this._skillRegistry.listSkills(query);
37
+ let body = '';
38
+ if (filtered.length === 0) {
39
+ body = query
40
+ ? `No skills found matching "${query}".`
41
+ : 'No skills are currently registered.';
42
+ }
43
+ else {
44
+ const heading = query
45
+ ? `Skills matching "${query}" (${filtered.length}):`
46
+ : `Available skills (${filtered.length}):`;
47
+ const lines = filtered.map(skill => `- \`${skill.name}\` — ${skill.description}`);
48
+ body = [heading, '', ...lines].join('\n');
49
+ }
50
+ const chatName = inputModel.chatContext?.name;
51
+ if (chatName) {
52
+ this._sendSystemMessage(chatName, body);
53
+ }
54
+ // Prevent the message to be actually sent.
55
+ inputModel.value = '';
56
+ inputModel.clearAttachments();
57
+ inputModel.clearMentions();
58
+ }
59
+ _isActive(inputModel) {
60
+ const chatName = inputModel.chatContext?.name;
61
+ return !!chatName && this._isDefault(chatName);
62
+ }
63
+ _command = {
64
+ name: '/skills',
65
+ providerId: this.id,
66
+ description: 'List available skills'
67
+ };
68
+ _regex = /^\/\w*$/;
69
+ _commands;
70
+ _skillRegistry;
71
+ _sendSystemMessage;
72
+ _isDefault;
73
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry } from '@jupyternaut/agent';
2
1
  import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry } from '@jupyternaut/agent';
3
3
  import { IPersonaRegistry } from './tokens';
4
4
  declare const _default: (JupyterFrontEndPlugin<IProviderRegistry> | JupyterFrontEndPlugin<void> | JupyterFrontEndPlugin<IPersonaRegistry> | JupyterFrontEndPlugin<IAgentManagerFactory> | JupyterFrontEndPlugin<IAISettingsModel> | JupyterFrontEndPlugin<IDiffManager> | JupyterFrontEndPlugin<ISkillRegistry> | JupyterFrontEndPlugin<IToolRegistry>)[];
5
5
  export default _default;
package/lib/index.js CHANGED
@@ -1,6 +1,5 @@
1
- import { anthropicProvider, createBrowserFetchTool, createDiscoverCommandsTool, createDiscoverSkillsTool, createExecuteCommandTool, createLoadSkillTool, genericProvider, googleProvider, loadSkillsFromPaths, mistralProvider, openaiProvider, AgentManagerFactory, IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, ProviderRegistry, SECRETS_NAMESPACE, SkillRegistry, ToolRegistry } from '@jupyternaut/agent';
2
- import { ILayoutRestorer } from '@jupyterlab/application';
3
1
  import { IChatTracker, IChatCommandRegistry } from '@jupyter/chat';
2
+ import { ILayoutRestorer } from '@jupyterlab/application';
4
3
  import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
5
4
  import { ICompletionProviderManager } from '@jupyterlab/completer';
6
5
  import { IDocumentManager } from '@jupyterlab/docmanager';
@@ -9,10 +8,12 @@ import { IStatusBar } from '@jupyterlab/statusbar';
9
8
  import { PathExt } from '@jupyterlab/coreutils';
10
9
  import { ITranslator, nullTranslator } from '@jupyterlab/translation';
11
10
  import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
11
+ import { anthropicProvider, createBrowserFetchTool, createDiscoverCommandsTool, createDiscoverSkillsTool, createExecuteCommandTool, createLoadSkillTool, genericProvider, googleProvider, loadSkillsFromPaths, mistralProvider, openaiProvider, AgentManagerFactory, IAgentManagerFactory, IAISettingsModel, IDiffManager, IProviderRegistry, IToolRegistry, ISkillRegistry, ProviderRegistry, SECRETS_NAMESPACE, SkillRegistry, ToolRegistry } from '@jupyternaut/agent';
12
12
  import { DisposableSet } from '@lumino/disposable';
13
+ import { IComponentsRendererFactory } from 'jupyter-chat-components';
13
14
  import { IMcpManager } from 'jupyter-mcp-manager';
14
15
  import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
15
- import { MentionCommandProvider } from './chat-commands/mention';
16
+ import { MentionCommandProvider, SkillsCommandProvider } from './chat-commands';
16
17
  import { AICompletionProvider } from './completion';
17
18
  import { CompletionStatusWidget } from './components';
18
19
  import { DiffManager } from './diff-manager';
@@ -180,6 +181,41 @@ const persona = {
180
181
  chatTracker?.widgetAdded.connect((_, widget) => attachPersona(widget));
181
182
  }
182
183
  };
184
+ /**
185
+ * Registers groupedToolCall callbacks to route tool call approval decisions
186
+ * to the correct persona's agent manager.
187
+ */
188
+ const chatComponentsCallbacks = {
189
+ id: '@jupyternaut/persona:chat-components-callbacks',
190
+ description: 'Register chat components callbacks for the persona.',
191
+ autoStart: true,
192
+ requires: [IPersonaRegistry],
193
+ optional: [IChatTracker, IComponentsRendererFactory],
194
+ activate: (_app, personaRegistry, chatTracker, chatComponentsFactory) => {
195
+ if (!chatComponentsFactory) {
196
+ return;
197
+ }
198
+ const findPersona = (sessionId) => {
199
+ const model = chatTracker?.find(chat => chat.model.name === sessionId)?.model;
200
+ return model ? personaRegistry.get(model) : undefined;
201
+ };
202
+ chatComponentsFactory.groupedToolCallCallbacks = {
203
+ ...chatComponentsFactory.groupedToolCallCallbacks,
204
+ toolCallPermissionDecision: (sessionId, toolCallId, optionId) => {
205
+ const agent = findPersona(sessionId)?.agentManager;
206
+ if (!agent) {
207
+ return;
208
+ }
209
+ if (optionId === 'approve') {
210
+ agent.approveToolCall(toolCallId);
211
+ }
212
+ else {
213
+ agent.rejectToolCall(toolCallId);
214
+ }
215
+ }
216
+ };
217
+ }
218
+ };
183
219
  /**
184
220
  * Clear chat command plugin.
185
221
  */
@@ -192,6 +228,39 @@ const mentionCommandPlugin = {
192
228
  registry.addProvider(new MentionCommandProvider());
193
229
  }
194
230
  };
231
+ /**
232
+ * Skills chat command plugin.
233
+ */
234
+ const skillsCommandPlugin = {
235
+ id: '@jupyternaut/persona:skills-command',
236
+ description: 'Register the /skills chat command.',
237
+ autoStart: true,
238
+ requires: [IChatCommandRegistry, ISkillRegistry, IPersonaRegistry],
239
+ optional: [IChatTracker],
240
+ activate: (app, registry, skillRegistry, personaRegistry, chatTracker) => {
241
+ const findModel = (chatName) => chatTracker?.find(c => c.model.name === chatName)?.model;
242
+ const isDefault = (chatName) => {
243
+ const model = findModel(chatName);
244
+ if (!model) {
245
+ return false;
246
+ }
247
+ const persona = personaRegistry.get(model);
248
+ return persona ? !persona.requireMention : false;
249
+ };
250
+ const sendSystemMessage = (chatName, body) => {
251
+ const model = findModel(chatName);
252
+ if (model) {
253
+ personaRegistry.get(model)?.sendSystemMessage(body);
254
+ }
255
+ };
256
+ registry.addProvider(new SkillsCommandProvider({
257
+ skillRegistry,
258
+ commands: app.commands,
259
+ isDefault,
260
+ sendSystemMessage
261
+ }));
262
+ }
263
+ };
195
264
  /**
196
265
  * A plugin to provide the agent manager factory and completion provider.
197
266
  * These objects require the secrets manager token with the same namespace.
@@ -509,7 +578,9 @@ export default [
509
578
  // Persona
510
579
  personaRegistry,
511
580
  persona,
581
+ chatComponentsCallbacks,
512
582
  mentionCommandPlugin,
583
+ skillsCommandPlugin,
513
584
  // Settings
514
585
  settingsModel,
515
586
  settingsPanelPlugin,
package/lib/persona.d.ts CHANGED
@@ -34,6 +34,7 @@ export declare class Persona implements IPersona {
34
34
  private _handleToolApprovalResolved;
35
35
  private _handleError;
36
36
  private _updateToolCallUI;
37
+ sendSystemMessage(body: string): void;
37
38
  /**
38
39
  * Whether a mention is required to trigger a response.
39
40
  * When false, the persona responds to all non-bot messages.
package/lib/persona.js CHANGED
@@ -106,12 +106,15 @@ export class Persona {
106
106
  this._settingsModel = options.settingsModel;
107
107
  this._providerRegistry = options.providerRegistry;
108
108
  this._documentManager = options.documentManager;
109
- for (const message of options.model.messages) {
110
- this._respondedToIds.add(message.id);
111
- }
112
109
  this._agent.agentEvent.connect(this._onAgentEvent, this);
113
110
  this._agent.activeProviderChanged.connect(this._onActiveProviderChanged, this);
114
- this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
111
+ // Wait for the chat to be ready before connect to message update.
112
+ this._model.ready.then(() => {
113
+ for (const message of this._model.messages) {
114
+ this._respondedToIds.add(message.id);
115
+ }
116
+ this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
117
+ });
115
118
  this._model.disposed.connect(this.dispose, this);
116
119
  }
117
120
  dispose() {
@@ -241,13 +244,29 @@ export class Persona {
241
244
  _handleMessageChunk(event) {
242
245
  const streamingMessage = this._streamingMessage.get(event.data.messageId);
243
246
  if (streamingMessage) {
244
- streamingMessage.update({ body: event.data.fullContent });
247
+ if (!this._model.updateMessage) {
248
+ streamingMessage.update({ body: event.data.fullContent });
249
+ }
250
+ else {
251
+ this._model.updateMessage(streamingMessage.id, {
252
+ ...streamingMessage.content,
253
+ body: event.data.fullContent
254
+ });
255
+ }
245
256
  }
246
257
  }
247
258
  _handleMessageComplete(event) {
248
259
  const streamingMessage = this._streamingMessage.get(event.data.messageId);
249
260
  if (streamingMessage) {
250
- streamingMessage.update({ body: event.data.content });
261
+ if (!this._model.updateMessage) {
262
+ streamingMessage.update({ body: event.data.content });
263
+ }
264
+ else {
265
+ this._model.updateMessage(streamingMessage.id, {
266
+ ...streamingMessage.content,
267
+ body: event.data.content
268
+ });
269
+ }
251
270
  this._streamingMessage.delete(event.data.messageId);
252
271
  }
253
272
  }
@@ -363,34 +382,47 @@ export class Persona {
363
382
  }
364
383
  context.status = status;
365
384
  const displayName = context.title ?? context.toolName;
366
- message.update({
367
- mime_model: {
368
- data: {
369
- 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
370
- },
371
- metadata: {
372
- toolCalls: [
373
- {
374
- toolCallId: context.toolCallId,
375
- title: context.summary
376
- ? `${displayName} : ${context.summary}`
377
- : displayName,
378
- kind: context.toolName,
379
- status: context.status,
380
- rawInput: context.input,
381
- rawOutput: output,
382
- sessionId: this._model.name,
383
- permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
384
- ...(status === 'awaiting_approval' && {
385
- permissionOptions: [
386
- { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
387
- { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
388
- ]
389
- })
390
- }
391
- ]
392
- }
385
+ const mime_model = {
386
+ data: {
387
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
388
+ },
389
+ metadata: {
390
+ toolCalls: [
391
+ {
392
+ toolCallId: context.toolCallId,
393
+ title: context.summary
394
+ ? `${displayName} : ${context.summary}`
395
+ : displayName,
396
+ kind: context.toolName,
397
+ status: context.status,
398
+ rawInput: context.input,
399
+ rawOutput: output,
400
+ sessionId: this._model.name,
401
+ permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
402
+ ...(status === 'awaiting_approval' && {
403
+ permissionOptions: [
404
+ { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
405
+ { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
406
+ ]
407
+ })
408
+ }
409
+ ]
393
410
  }
411
+ };
412
+ if (!this._model.updateMessage) {
413
+ message.update({ mime_model });
414
+ }
415
+ else {
416
+ this._model.updateMessage(message.id, {
417
+ ...message.content,
418
+ mime_model
419
+ });
420
+ }
421
+ }
422
+ sendSystemMessage(body) {
423
+ this._model.sendMessage({
424
+ body,
425
+ sender: this._persona
394
426
  });
395
427
  }
396
428
  /**
package/lib/tokens.d.ts CHANGED
@@ -51,6 +51,10 @@ export interface IPersona {
51
51
  * Called after restoring a saved chat.
52
52
  */
53
53
  rebuildHistory(): Promise<void>;
54
+ /**
55
+ * Adds an assistant message to the chat (used by chat commands).
56
+ */
57
+ sendSystemMessage(body: string): void;
54
58
  }
55
59
  /**
56
60
  * Registry mapping chat models to their persona.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyternaut/persona",
3
- "version": "0.20.0",
3
+ "version": "0.20.1",
4
4
  "description": "AI code completions and chat for JupyterLite",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -58,7 +58,7 @@
58
58
  "@jupyterlab/statusbar": "^4.5.8",
59
59
  "@jupyterlab/translation": "^4.5.8",
60
60
  "@jupyterlab/ui-components": "^4.5.8",
61
- "@jupyternaut/agent": "^0.20.0",
61
+ "@jupyternaut/agent": "^0.20.1",
62
62
  "@lumino/commands": "^2.3.2",
63
63
  "@lumino/coreutils": "^2.2.1",
64
64
  "@lumino/disposable": "^2.1.4",
@@ -67,6 +67,7 @@
67
67
  "@mui/icons-material": "^7",
68
68
  "@mui/material": "^7",
69
69
  "ai": "^7.0.106",
70
+ "jupyter-chat-components": "^0.6.0",
70
71
  "jupyter-mcp-manager": "^0.2.0",
71
72
  "jupyter-secrets-manager": "^0.5.0",
72
73
  "react": "^18.3.1"
@@ -101,10 +102,18 @@
101
102
  "bundled": true,
102
103
  "singleton": true
103
104
  },
105
+ "@jupyter/chat": {
106
+ "bundled": false,
107
+ "singleton": true
108
+ },
104
109
  "@jupyternaut/agent": {
105
110
  "bundled": true,
106
111
  "singleton": true
107
112
  },
113
+ "jupyter-chat-components": {
114
+ "bundled": false,
115
+ "singleton": true
116
+ },
108
117
  "jupyter-mcp-manager": {
109
118
  "bundled": false,
110
119
  "singleton": true
@@ -0,0 +1,2 @@
1
+ export * from './mention';
2
+ export * from './skills';
@@ -0,0 +1,105 @@
1
+ import { ChatCommand, IChatCommandProvider, IInputModel } from '@jupyter/chat';
2
+ import type { ISkillRegistry } from '@jupyternaut/agent';
3
+ import { CommandRegistry } from '@lumino/commands';
4
+
5
+ import { CommandIds } from '../tokens';
6
+
7
+ export class SkillsCommandProvider implements IChatCommandProvider {
8
+ constructor(options: SkillsCommandProvider.IOptions) {
9
+ this._skillRegistry = options.skillRegistry;
10
+ this._commands = options.commands;
11
+ this._sendSystemMessage = options.sendSystemMessage;
12
+ this._isDefault = options.isDefault;
13
+ }
14
+
15
+ public id: string = '@jupyternaut/persona:skills-command';
16
+
17
+ async listCommandCompletions(
18
+ inputModel: IInputModel
19
+ ): Promise<ChatCommand[]> {
20
+ if (!this._isActive(inputModel)) {
21
+ return [];
22
+ }
23
+
24
+ const match = inputModel.currentWord?.match(this._regex)?.[0];
25
+ if (!match) {
26
+ return [];
27
+ }
28
+
29
+ if (this._command.name.startsWith(match)) {
30
+ return [this._command];
31
+ }
32
+
33
+ return [];
34
+ }
35
+
36
+ async onSubmit(inputModel: IInputModel): Promise<void> {
37
+ if (!this._isActive(inputModel)) {
38
+ return;
39
+ }
40
+
41
+ const trimmed = inputModel.value.trim();
42
+ const match = trimmed.match(/^\/skills(?:\s+(.+))?$/);
43
+ if (!match) {
44
+ return;
45
+ }
46
+
47
+ if (this._commands.hasCommand(CommandIds.refreshSkills)) {
48
+ await this._commands.execute(CommandIds.refreshSkills);
49
+ }
50
+
51
+ const query = match[1]?.trim();
52
+ const filtered = this._skillRegistry.listSkills(query);
53
+
54
+ let body = '';
55
+ if (filtered.length === 0) {
56
+ body = query
57
+ ? `No skills found matching "${query}".`
58
+ : 'No skills are currently registered.';
59
+ } else {
60
+ const heading = query
61
+ ? `Skills matching "${query}" (${filtered.length}):`
62
+ : `Available skills (${filtered.length}):`;
63
+ const lines = filtered.map(
64
+ skill => `- \`${skill.name}\` — ${skill.description}`
65
+ );
66
+ body = [heading, '', ...lines].join('\n');
67
+ }
68
+
69
+ const chatName = inputModel.chatContext?.name;
70
+ if (chatName) {
71
+ this._sendSystemMessage(chatName, body);
72
+ }
73
+
74
+ // Prevent the message to be actually sent.
75
+ inputModel.value = '';
76
+ inputModel.clearAttachments();
77
+ inputModel.clearMentions();
78
+ }
79
+
80
+ private _isActive(inputModel: IInputModel): boolean {
81
+ const chatName = inputModel.chatContext?.name;
82
+ return !!chatName && this._isDefault(chatName);
83
+ }
84
+
85
+ private _command: ChatCommand = {
86
+ name: '/skills',
87
+ providerId: this.id,
88
+ description: 'List available skills'
89
+ };
90
+
91
+ private _regex: RegExp = /^\/\w*$/;
92
+ private _commands: CommandRegistry;
93
+ private _skillRegistry: ISkillRegistry;
94
+ private _sendSystemMessage: (chatName: string, body: string) => void;
95
+ private _isDefault: (chatName: string) => boolean;
96
+ }
97
+
98
+ export namespace SkillsCommandProvider {
99
+ export interface IOptions {
100
+ skillRegistry: ISkillRegistry;
101
+ commands: CommandRegistry;
102
+ sendSystemMessage: (chatName: string, body: string) => void;
103
+ isDefault: (chatName: string) => boolean;
104
+ }
105
+ }
package/src/index.ts CHANGED
@@ -1,3 +1,27 @@
1
+ import { IChatTracker, IChatPanel, IChatCommandRegistry } from '@jupyter/chat';
2
+
3
+ import {
4
+ ILayoutRestorer,
5
+ JupyterFrontEnd,
6
+ JupyterFrontEndPlugin
7
+ } from '@jupyterlab/application';
8
+
9
+ import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
10
+
11
+ import { ICompletionProviderManager } from '@jupyterlab/completer';
12
+
13
+ import { IDocumentManager } from '@jupyterlab/docmanager';
14
+
15
+ import { ISettingRegistry } from '@jupyterlab/settingregistry';
16
+
17
+ import { IStatusBar } from '@jupyterlab/statusbar';
18
+
19
+ import { PathExt } from '@jupyterlab/coreutils';
20
+
21
+ import { ITranslator, nullTranslator } from '@jupyterlab/translation';
22
+
23
+ import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
24
+
1
25
  import {
2
26
  anthropicProvider,
3
27
  createBrowserFetchTool,
@@ -25,37 +49,15 @@ import {
25
49
 
26
50
  import type { IAISecretsAccess } from '@jupyternaut/agent';
27
51
 
28
- import {
29
- ILayoutRestorer,
30
- JupyterFrontEnd,
31
- JupyterFrontEndPlugin
32
- } from '@jupyterlab/application';
33
-
34
- import { IChatTracker, IChatPanel, IChatCommandRegistry } from '@jupyter/chat';
35
-
36
- import { ICommandPalette, IThemeManager } from '@jupyterlab/apputils';
37
-
38
- import { ICompletionProviderManager } from '@jupyterlab/completer';
39
-
40
- import { IDocumentManager } from '@jupyterlab/docmanager';
41
-
42
- import { ISettingRegistry } from '@jupyterlab/settingregistry';
43
-
44
- import { IStatusBar } from '@jupyterlab/statusbar';
45
-
46
- import { PathExt } from '@jupyterlab/coreutils';
47
-
48
- import { ITranslator, nullTranslator } from '@jupyterlab/translation';
49
-
50
- import { IFormRendererRegistry, settingsIcon } from '@jupyterlab/ui-components';
51
-
52
52
  import { DisposableSet } from '@lumino/disposable';
53
53
 
54
+ import { IComponentsRendererFactory } from 'jupyter-chat-components';
55
+
54
56
  import { IMcpManager } from 'jupyter-mcp-manager';
55
57
 
56
58
  import { ISecretsManager, SecretsManager } from 'jupyter-secrets-manager';
57
59
 
58
- import { MentionCommandProvider } from './chat-commands/mention';
60
+ import { MentionCommandProvider, SkillsCommandProvider } from './chat-commands';
59
61
 
60
62
  import { AICompletionProvider } from './completion';
61
63
 
@@ -271,6 +273,54 @@ const persona: JupyterFrontEndPlugin<void> = {
271
273
  }
272
274
  };
273
275
 
276
+ /**
277
+ * Registers groupedToolCall callbacks to route tool call approval decisions
278
+ * to the correct persona's agent manager.
279
+ */
280
+ const chatComponentsCallbacks: JupyterFrontEndPlugin<void> = {
281
+ id: '@jupyternaut/persona:chat-components-callbacks',
282
+ description: 'Register chat components callbacks for the persona.',
283
+ autoStart: true,
284
+ requires: [IPersonaRegistry],
285
+ optional: [IChatTracker, IComponentsRendererFactory],
286
+ activate: (
287
+ _app: JupyterFrontEnd,
288
+ personaRegistry: IPersonaRegistry,
289
+ chatTracker: IChatTracker | null,
290
+ chatComponentsFactory?: IComponentsRendererFactory
291
+ ): void => {
292
+ if (!chatComponentsFactory) {
293
+ return;
294
+ }
295
+
296
+ const findPersona = (sessionId: string) => {
297
+ const model = chatTracker?.find(
298
+ chat => chat.model.name === sessionId
299
+ )?.model;
300
+ return model ? personaRegistry.get(model) : undefined;
301
+ };
302
+
303
+ chatComponentsFactory.groupedToolCallCallbacks = {
304
+ ...chatComponentsFactory.groupedToolCallCallbacks,
305
+ toolCallPermissionDecision: (
306
+ sessionId: string,
307
+ toolCallId: string,
308
+ optionId: string
309
+ ) => {
310
+ const agent = findPersona(sessionId)?.agentManager;
311
+ if (!agent) {
312
+ return;
313
+ }
314
+ if (optionId === 'approve') {
315
+ agent.approveToolCall(toolCallId);
316
+ } else {
317
+ agent.rejectToolCall(toolCallId);
318
+ }
319
+ }
320
+ };
321
+ }
322
+ };
323
+
274
324
  /**
275
325
  * Clear chat command plugin.
276
326
  */
@@ -284,6 +334,52 @@ const mentionCommandPlugin: JupyterFrontEndPlugin<void> = {
284
334
  }
285
335
  };
286
336
 
337
+ /**
338
+ * Skills chat command plugin.
339
+ */
340
+ const skillsCommandPlugin: JupyterFrontEndPlugin<void> = {
341
+ id: '@jupyternaut/persona:skills-command',
342
+ description: 'Register the /skills chat command.',
343
+ autoStart: true,
344
+ requires: [IChatCommandRegistry, ISkillRegistry, IPersonaRegistry],
345
+ optional: [IChatTracker],
346
+ activate: (
347
+ app: JupyterFrontEnd,
348
+ registry: IChatCommandRegistry,
349
+ skillRegistry: ISkillRegistry,
350
+ personaRegistry: IPersonaRegistry,
351
+ chatTracker: IChatTracker | null
352
+ ) => {
353
+ const findModel = (chatName: string) =>
354
+ chatTracker?.find(c => c.model.name === chatName)?.model;
355
+
356
+ const isDefault = (chatName: string) => {
357
+ const model = findModel(chatName);
358
+ if (!model) {
359
+ return false;
360
+ }
361
+ const persona = personaRegistry.get(model);
362
+ return persona ? !persona.requireMention : false;
363
+ };
364
+
365
+ const sendSystemMessage = (chatName: string, body: string) => {
366
+ const model = findModel(chatName);
367
+ if (model) {
368
+ personaRegistry.get(model)?.sendSystemMessage(body);
369
+ }
370
+ };
371
+
372
+ registry.addProvider(
373
+ new SkillsCommandProvider({
374
+ skillRegistry,
375
+ commands: app.commands,
376
+ isDefault,
377
+ sendSystemMessage
378
+ })
379
+ );
380
+ }
381
+ };
382
+
287
383
  /**
288
384
  * A plugin to provide the agent manager factory and completion provider.
289
385
  * These objects require the secrets manager token with the same namespace.
@@ -695,7 +791,9 @@ export default [
695
791
  // Persona
696
792
  personaRegistry,
697
793
  persona,
794
+ chatComponentsCallbacks,
698
795
  mentionCommandPlugin,
796
+ skillsCommandPlugin,
699
797
  // Settings
700
798
  settingsModel,
701
799
  settingsPanelPlugin,
package/src/persona.ts CHANGED
@@ -25,8 +25,6 @@ import {
25
25
  modelSupportsPdf
26
26
  } from '@jupyternaut/agent';
27
27
 
28
- import type { IObservableDisposable } from '@lumino/disposable';
29
-
30
28
  import { ISignal, Signal } from '@lumino/signaling';
31
29
 
32
30
  import type { ModelMessage, UserContent } from 'ai';
@@ -173,20 +171,21 @@ export class Persona implements IPersona {
173
171
  this._providerRegistry = options.providerRegistry;
174
172
  this._documentManager = options.documentManager;
175
173
 
176
- for (const message of options.model.messages) {
177
- this._respondedToIds.add(message.id);
178
- }
179
-
180
174
  this._agent.agentEvent.connect(this._onAgentEvent, this);
181
175
  this._agent.activeProviderChanged.connect(
182
176
  this._onActiveProviderChanged,
183
177
  this
184
178
  );
185
- this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
186
- (this._model as unknown as IObservableDisposable).disposed.connect(
187
- this.dispose,
188
- this
189
- );
179
+
180
+ // Wait for the chat to be ready before connect to message update.
181
+ this._model.ready.then(() => {
182
+ for (const message of this._model.messages) {
183
+ this._respondedToIds.add(message.id);
184
+ }
185
+ this._model.messagesUpdated.connect(this._onMessagesUpdated, this);
186
+ });
187
+
188
+ this._model.disposed.connect(this.dispose, this);
190
189
  }
191
190
 
192
191
  dispose(): void {
@@ -378,7 +377,14 @@ export class Persona implements IPersona {
378
377
  ): void {
379
378
  const streamingMessage = this._streamingMessage.get(event.data.messageId);
380
379
  if (streamingMessage) {
381
- streamingMessage.update({ body: event.data.fullContent });
380
+ if (!this._model.updateMessage) {
381
+ streamingMessage.update({ body: event.data.fullContent });
382
+ } else {
383
+ this._model.updateMessage(streamingMessage.id, {
384
+ ...streamingMessage.content,
385
+ body: event.data.fullContent
386
+ });
387
+ }
382
388
  }
383
389
  }
384
390
 
@@ -387,7 +393,15 @@ export class Persona implements IPersona {
387
393
  ): void {
388
394
  const streamingMessage = this._streamingMessage.get(event.data.messageId);
389
395
  if (streamingMessage) {
390
- streamingMessage.update({ body: event.data.content });
396
+ if (!this._model.updateMessage) {
397
+ streamingMessage.update({ body: event.data.content });
398
+ } else {
399
+ this._model.updateMessage(streamingMessage.id, {
400
+ ...streamingMessage.content,
401
+ body: event.data.content
402
+ });
403
+ }
404
+
391
405
  this._streamingMessage.delete(event.data.messageId);
392
406
  }
393
407
  }
@@ -545,35 +559,48 @@ export class Persona implements IPersona {
545
559
  }
546
560
  context.status = status;
547
561
  const displayName = context.title ?? context.toolName;
548
- message.update({
549
- mime_model: {
550
- data: {
551
- 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
552
- },
553
- metadata: {
554
- toolCalls: [
555
- {
556
- toolCallId: context.toolCallId,
557
- title: context.summary
558
- ? `${displayName} : ${context.summary}`
559
- : displayName,
560
- kind: context.toolName,
561
- status: context.status,
562
- rawInput: context.input,
563
- rawOutput: output,
564
- sessionId: this._model.name,
565
- permissionStatus:
566
- status === 'awaiting_approval' ? 'pending' : 'resolved',
567
- ...(status === 'awaiting_approval' && {
568
- permissionOptions: [
569
- { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
570
- { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
571
- ]
572
- })
573
- }
574
- ]
575
- }
562
+ const mime_model = {
563
+ data: {
564
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
565
+ },
566
+ metadata: {
567
+ toolCalls: [
568
+ {
569
+ toolCallId: context.toolCallId,
570
+ title: context.summary
571
+ ? `${displayName} : ${context.summary}`
572
+ : displayName,
573
+ kind: context.toolName,
574
+ status: context.status,
575
+ rawInput: context.input,
576
+ rawOutput: output,
577
+ sessionId: this._model.name,
578
+ permissionStatus:
579
+ status === 'awaiting_approval' ? 'pending' : 'resolved',
580
+ ...(status === 'awaiting_approval' && {
581
+ permissionOptions: [
582
+ { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
583
+ { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
584
+ ]
585
+ })
586
+ }
587
+ ]
576
588
  }
589
+ };
590
+ if (!this._model.updateMessage) {
591
+ message.update({ mime_model });
592
+ } else {
593
+ this._model.updateMessage(message.id, {
594
+ ...message.content,
595
+ mime_model
596
+ });
597
+ }
598
+ }
599
+
600
+ sendSystemMessage(body: string): void {
601
+ this._model.sendMessage({
602
+ body,
603
+ sender: this._persona
577
604
  });
578
605
  }
579
606
 
package/src/tokens.ts CHANGED
@@ -67,6 +67,10 @@ export interface IPersona {
67
67
  * Called after restoring a saved chat.
68
68
  */
69
69
  rebuildHistory(): Promise<void>;
70
+ /**
71
+ * Adds an assistant message to the chat (used by chat commands).
72
+ */
73
+ sendSystemMessage(body: string): void;
70
74
  }
71
75
 
72
76
  /**