@jupyterlite/ai 0.17.0 → 0.19.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 (114) hide show
  1. package/lib/chat-commands/clear.d.ts +1 -0
  2. package/lib/chat-commands/index.d.ts +1 -0
  3. package/lib/chat-commands/skills.d.ts +2 -1
  4. package/lib/chat-model-handler.d.ts +4 -3
  5. package/lib/chat-model-handler.js +2 -1
  6. package/lib/chat-model.d.ts +148 -8
  7. package/lib/chat-model.js +368 -79
  8. package/lib/completion/completion-provider.d.ts +3 -1
  9. package/lib/completion/completion-provider.js +1 -2
  10. package/lib/completion/index.d.ts +1 -0
  11. package/lib/components/clear-button.d.ts +1 -0
  12. package/lib/components/clear-button.js +3 -4
  13. package/lib/components/completion-status.d.ts +1 -0
  14. package/lib/components/completion-status.js +5 -4
  15. package/lib/components/index.d.ts +1 -0
  16. package/lib/components/model-select.d.ts +1 -0
  17. package/lib/components/model-select.js +62 -67
  18. package/lib/components/save-button.d.ts +3 -2
  19. package/lib/components/save-button.js +4 -5
  20. package/lib/components/stop-button.d.ts +1 -0
  21. package/lib/components/stop-button.js +3 -4
  22. package/lib/components/tool-select.d.ts +3 -1
  23. package/lib/components/tool-select.js +47 -60
  24. package/lib/components/usage-display.d.ts +4 -2
  25. package/lib/components/usage-display.js +50 -61
  26. package/lib/diff-manager.d.ts +3 -1
  27. package/lib/index.d.ts +3 -2
  28. package/lib/index.js +50 -59
  29. package/lib/models/settings-model.d.ts +3 -1
  30. package/lib/models/settings-model.js +1 -0
  31. package/lib/rendered-message-outputarea.d.ts +1 -0
  32. package/lib/tokens.d.ts +48 -597
  33. package/lib/tokens.js +2 -31
  34. package/lib/widgets/ai-settings.d.ts +3 -1
  35. package/lib/widgets/ai-settings.js +185 -344
  36. package/lib/widgets/main-area-chat.d.ts +3 -3
  37. package/lib/widgets/main-area-chat.js +2 -4
  38. package/lib/widgets/provider-config-dialog.d.ts +2 -1
  39. package/lib/widgets/provider-config-dialog.js +102 -167
  40. package/package.json +111 -258
  41. package/schema/settings-model.json +6 -0
  42. package/src/chat-commands/skills.ts +2 -2
  43. package/src/chat-model-handler.ts +10 -6
  44. package/src/chat-model.ts +488 -96
  45. package/src/completion/completion-provider.ts +6 -6
  46. package/src/components/clear-button.tsx +0 -2
  47. package/src/components/completion-status.tsx +2 -2
  48. package/src/components/model-select.tsx +1 -1
  49. package/src/components/save-button.tsx +3 -3
  50. package/src/components/stop-button.tsx +0 -2
  51. package/src/components/tool-select.tsx +10 -9
  52. package/src/components/usage-display.tsx +4 -2
  53. package/src/diff-manager.ts +4 -3
  54. package/src/index.ts +103 -107
  55. package/src/models/settings-model.ts +7 -6
  56. package/src/tokens.ts +54 -744
  57. package/src/widgets/ai-settings.tsx +40 -11
  58. package/src/widgets/main-area-chat.ts +5 -8
  59. package/src/widgets/provider-config-dialog.tsx +8 -8
  60. package/LICENSE +0 -30
  61. package/README.md +0 -49
  62. package/lib/agent.d.ts +0 -277
  63. package/lib/agent.js +0 -1116
  64. package/lib/icons.d.ts +0 -3
  65. package/lib/icons.js +0 -8
  66. package/lib/providers/built-in-providers.d.ts +0 -21
  67. package/lib/providers/built-in-providers.js +0 -233
  68. package/lib/providers/generated-context-windows.d.ts +0 -8
  69. package/lib/providers/generated-context-windows.js +0 -96
  70. package/lib/providers/model-info.d.ts +0 -3
  71. package/lib/providers/model-info.js +0 -58
  72. package/lib/providers/models.d.ts +0 -37
  73. package/lib/providers/models.js +0 -28
  74. package/lib/providers/provider-registry.d.ts +0 -49
  75. package/lib/providers/provider-registry.js +0 -72
  76. package/lib/providers/provider-tools.d.ts +0 -36
  77. package/lib/providers/provider-tools.js +0 -93
  78. package/lib/skills/index.d.ts +0 -4
  79. package/lib/skills/index.js +0 -7
  80. package/lib/skills/parse-skill.d.ts +0 -25
  81. package/lib/skills/parse-skill.js +0 -69
  82. package/lib/skills/skill-loader.d.ts +0 -25
  83. package/lib/skills/skill-loader.js +0 -133
  84. package/lib/skills/skill-registry.d.ts +0 -31
  85. package/lib/skills/skill-registry.js +0 -100
  86. package/lib/skills/types.d.ts +0 -29
  87. package/lib/skills/types.js +0 -5
  88. package/lib/tools/commands.d.ts +0 -11
  89. package/lib/tools/commands.js +0 -154
  90. package/lib/tools/skills.d.ts +0 -9
  91. package/lib/tools/skills.js +0 -73
  92. package/lib/tools/tool-registry.d.ts +0 -35
  93. package/lib/tools/tool-registry.js +0 -55
  94. package/lib/tools/web.d.ts +0 -8
  95. package/lib/tools/web.js +0 -196
  96. package/src/agent.ts +0 -1441
  97. package/src/icons.ts +0 -11
  98. package/src/providers/built-in-providers.ts +0 -241
  99. package/src/providers/generated-context-windows.ts +0 -102
  100. package/src/providers/model-info.ts +0 -88
  101. package/src/providers/models.ts +0 -76
  102. package/src/providers/provider-registry.ts +0 -88
  103. package/src/providers/provider-tools.ts +0 -179
  104. package/src/skills/index.ts +0 -14
  105. package/src/skills/parse-skill.ts +0 -91
  106. package/src/skills/skill-loader.ts +0 -175
  107. package/src/skills/skill-registry.ts +0 -137
  108. package/src/skills/types.ts +0 -37
  109. package/src/tools/commands.ts +0 -210
  110. package/src/tools/skills.ts +0 -84
  111. package/src/tools/tool-registry.ts +0 -63
  112. package/src/tools/web.ts +0 -238
  113. package/src/types.d.ts +0 -4
  114. package/style/icons/jupyternaut-lite.svg +0 -7
package/lib/chat-model.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { AbstractChatModel } from '@jupyter/chat';
2
2
  import { PathExt } from '@jupyterlab/coreutils';
3
3
  import * as nbformat from '@jupyterlab/nbformat';
4
+ import { AI_AVATAR } from '@jupyternaut/agent';
4
5
  import { UUID } from '@lumino/coreutils';
5
6
  import { Debouncer } from '@lumino/polling';
6
7
  import { Signal } from '@lumino/signaling';
7
- import { AI_AVATAR } from './icons';
8
+ import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from '@jupyternaut/agent';
8
9
  /**
9
10
  * AI Chat Model implementation that provides chat functionality tool integration,
10
11
  * and MCP server support.
@@ -27,10 +28,14 @@ export class AIChatModel extends AbstractChatModel {
27
28
  this._user = options.user;
28
29
  this._agentManager = options.agentManager;
29
30
  this._contentsManager = options.contentsManager;
31
+ this._providerRegistry = options.providerRegistry;
30
32
  // Listen for agent events
31
33
  this._agentManager.agentEvent.connect(this._onAgentEvent, this);
32
34
  // Listen for settings changes to update chat behavior
33
35
  this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
36
+ // Rebuild history when the model changes
37
+ this._agentManager.activeProviderChanged.connect(this._onModelChanged, this);
38
+ this._settingsModel.stateChanged.connect(this._onModelChanged, this);
34
39
  this._autosaveDebouncer = new Debouncer(this.save, 3000);
35
40
  }
36
41
  /**
@@ -81,17 +86,20 @@ export class AIChatModel extends AbstractChatModel {
81
86
  return this._autosave;
82
87
  }
83
88
  set autosave(value) {
89
+ if (value === this._autosave) {
90
+ return;
91
+ }
84
92
  this._autosave = value;
85
93
  this._autosaveChanged.emit(value);
86
94
  if (value) {
87
95
  this.messagesUpdated.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
88
96
  this.messageChanged.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
89
- this._autosaveDebouncer.invoke();
90
97
  }
91
98
  else {
92
99
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
93
100
  this.messageChanged.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
94
101
  }
102
+ this._autosaveDebouncer.invoke();
95
103
  }
96
104
  /**
97
105
  * A signal emitting when the autosave flag changed.
@@ -112,7 +120,7 @@ export class AIChatModel extends AbstractChatModel {
112
120
  return this._agentManager.tokenUsageChanged;
113
121
  }
114
122
  /**
115
- * Get the agent manager associated to the model.
123
+ * The agent manager used in the model.
116
124
  */
117
125
  get agentManager() {
118
126
  return this._agentManager;
@@ -127,6 +135,7 @@ export class AIChatModel extends AbstractChatModel {
127
135
  * Dispose of the model.
128
136
  */
129
137
  dispose() {
138
+ this.stopStreaming();
130
139
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
131
140
  super.dispose();
132
141
  }
@@ -142,7 +151,10 @@ export class AIChatModel extends AbstractChatModel {
142
151
  stopStreaming: () => this.stopStreaming(),
143
152
  clearMessages: () => this.clearMessages(),
144
153
  agentManager: this._agentManager,
145
- addSystemMessage: (body) => this.addSystemMessage(body)
154
+ addSystemMessage: (body) => this._addSystemMessage(body),
155
+ removeQueuedMessage: (id) => this.removeQueuedMessage(id),
156
+ reorderQueuedMessages: (ids) => this.reorderQueuedMessages(ids),
157
+ editQueuedMessage: (id, body) => this.editQueuedMessage(id, body)
146
158
  };
147
159
  }
148
160
  /**
@@ -155,14 +167,29 @@ export class AIChatModel extends AbstractChatModel {
155
167
  * Clears all messages from the chat and resets conversation state.
156
168
  */
157
169
  clearMessages = async () => {
170
+ this.stopStreaming();
171
+ this._messageQueue = [];
172
+ this._isBusy = false;
173
+ this._queueMessageId = null;
174
+ this._currentStreamingMessage = null;
158
175
  this.messagesDeleted(0, this.messages.length);
176
+ this.title = null;
159
177
  this._toolContexts.clear();
160
178
  await this._agentManager.clearHistory();
161
179
  };
180
+ /**
181
+ * Overrides messageAdded to ensure queued messages stay at the bottom.
182
+ */
183
+ messageAdded(message) {
184
+ super.messageAdded(message);
185
+ if (this._queueMessageId && message.id !== this._queueMessageId) {
186
+ this._updateQueueUI();
187
+ }
188
+ }
162
189
  /**
163
190
  * Adds a non-user message to the chat (used by chat commands).
164
191
  */
165
- addSystemMessage(body) {
192
+ _addSystemMessage(body) {
166
193
  const message = {
167
194
  body,
168
195
  sender: this._getAIUser(),
@@ -193,9 +220,9 @@ export class AIChatModel extends AbstractChatModel {
193
220
  raw_time: false,
194
221
  attachments: [...this.input.attachments]
195
222
  };
196
- this.messageAdded(userMessage);
197
223
  // Check if we have valid configuration
198
224
  if (!this._agentManager.hasValidConfig()) {
225
+ this.messageAdded(userMessage);
199
226
  const errorMessage = {
200
227
  body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
201
228
  sender: this._getAIUser(),
@@ -207,30 +234,48 @@ export class AIChatModel extends AbstractChatModel {
207
234
  this.messageAdded(errorMessage);
208
235
  return;
209
236
  }
237
+ if (this._isBusy) {
238
+ this._messageQueue.push({
239
+ id: UUID.uuid4(),
240
+ body: message.body,
241
+ _originalMsg: userMessage
242
+ });
243
+ this.input.clearAttachments();
244
+ this._updateQueueUI();
245
+ return;
246
+ }
247
+ this._isBusy = true;
248
+ this.messageAdded(userMessage);
249
+ this.input.clearAttachments();
250
+ await this._processMessage(userMessage);
251
+ }
252
+ /**
253
+ * Internal method to process attachments and send the message to the agent.
254
+ */
255
+ async _processMessage(userMessage) {
210
256
  try {
211
- // Process attachments and add their content to the message
212
- let enhancedMessage = message.body;
213
- if (this.input.attachments.length > 0) {
214
- const { textContents, binaryParts } = await Private.processAttachments(this.input.attachments, this.input.documentManager);
215
- this.input.clearAttachments();
216
- let textPart = message.body;
217
- if (textContents.length > 0) {
218
- textPart +=
219
- '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
220
- }
221
- if (binaryParts.length > 0) {
222
- enhancedMessage = [{ type: 'text', text: textPart }, ...binaryParts];
223
- }
224
- else {
225
- enhancedMessage = textPart;
226
- }
227
- }
228
257
  this.updateWriters([{ user: this._getAIUser() }]);
258
+ let enhancedMessage = userMessage.body;
259
+ if (userMessage.attachments && userMessage.attachments.length > 0) {
260
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
261
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
262
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
263
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
264
+ enhancedMessage = await Private.processAttachments(userMessage.attachments, this.input.documentManager, userMessage.body, supportsImages, supportsPdf, supportsAudio);
265
+ }
229
266
  await this._agentManager.generateResponse(enhancedMessage);
230
267
  }
231
268
  catch (error) {
232
269
  const errorMessage = {
233
- body: `Error generating AI response: ${error.message}`,
270
+ body: '',
271
+ mime_model: {
272
+ data: {
273
+ 'application/vnd.jupyter.chat.components': 'error'
274
+ },
275
+ metadata: {
276
+ errorMessage: `Error generating AI response: ${error.message}`
277
+ }
278
+ },
234
279
  sender: this._getAIUser(),
235
280
  id: UUID.uuid4(),
236
281
  time: Date.now() / 1000,
@@ -240,7 +285,112 @@ export class AIChatModel extends AbstractChatModel {
240
285
  this.messageAdded(errorMessage);
241
286
  }
242
287
  finally {
288
+ this._drainQueue();
289
+ if (this._settingsModel.config.autoTitle &&
290
+ (this.messages.filter(msg => msg.sender.username !== 'ai-assistant')
291
+ .length <= 5 ||
292
+ this.title === null)) {
293
+ try {
294
+ this.title = await this.requestTitle();
295
+ }
296
+ catch (e) {
297
+ console.warn('Error while generating a title\n', e);
298
+ }
299
+ }
300
+ }
301
+ }
302
+ /**
303
+ * Removes the message-queue chat component.
304
+ */
305
+ _removeQueueUI() {
306
+ if (this._queueMessageId) {
307
+ const existingMsg = this.messages.find(msg => msg.id === this._queueMessageId);
308
+ if (existingMsg) {
309
+ const idx = this.messages.indexOf(existingMsg);
310
+ if (idx !== -1) {
311
+ this.messagesDeleted(idx, 1);
312
+ }
313
+ }
314
+ this._queueMessageId = null;
315
+ }
316
+ }
317
+ /**
318
+ * Creates or updates the message-queue chat component.
319
+ */
320
+ _updateQueueUI() {
321
+ this._removeQueueUI();
322
+ if (this._messageQueue.length === 0) {
323
+ return;
324
+ }
325
+ const queueBody = {
326
+ data: {
327
+ 'application/vnd.jupyter.chat.components': 'message-queue'
328
+ },
329
+ metadata: {
330
+ messages: this._messageQueue.map(m => ({
331
+ id: m.id,
332
+ body: m.body,
333
+ attachments: m._originalMsg.attachments
334
+ })),
335
+ targetId: this.name
336
+ }
337
+ };
338
+ this._queueMessageId = UUID.uuid4();
339
+ const queueMessage = {
340
+ body: '',
341
+ mime_model: queueBody,
342
+ sender: { username: 'system', display_name: '' },
343
+ id: this._queueMessageId,
344
+ time: Date.now() / 1000,
345
+ type: 'msg',
346
+ raw_time: false
347
+ };
348
+ this.messageAdded(queueMessage);
349
+ }
350
+ /**
351
+ * Processes the next message in the queue, or marks the agent as idle.
352
+ */
353
+ async _drainQueue() {
354
+ if (this._messageQueue.length === 0) {
355
+ this._isBusy = false;
243
356
  this.updateWriters([]);
357
+ this._removeQueueUI();
358
+ return;
359
+ }
360
+ // Dequeue and push to chat
361
+ const next = this._messageQueue.shift();
362
+ next._originalMsg.time = Date.now() / 1000;
363
+ this.messageAdded(next._originalMsg);
364
+ await this._processMessage(next._originalMsg);
365
+ }
366
+ /**
367
+ * Removes a queued message by its ID.
368
+ * @param messageId The ID of the queued message to remove
369
+ */
370
+ removeQueuedMessage(messageId) {
371
+ this.messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
372
+ }
373
+ /**
374
+ * Reorders queued messages by their IDs.
375
+ * @param messageIds Array of message IDs in the desired order
376
+ */
377
+ reorderQueuedMessages(messageIds) {
378
+ const byId = new Map(this._messageQueue.map(m => [m.id, m]));
379
+ this.messageQueue = messageIds
380
+ .map(id => byId.get(id))
381
+ .filter((m) => m !== undefined);
382
+ }
383
+ /**
384
+ * Edits a queued message by its ID.
385
+ * @param messageId The ID of the queued message to edit
386
+ * @param newBody The new body of the message
387
+ */
388
+ editQueuedMessage(messageId, newBody) {
389
+ const queue = [...this._messageQueue];
390
+ const index = queue.findIndex(m => m.id === messageId);
391
+ if (index !== -1) {
392
+ queue[index] = { ...queue[index], body: newBody };
393
+ this.messageQueue = queue;
244
394
  }
245
395
  }
246
396
  /**
@@ -310,7 +460,7 @@ export class AIChatModel extends AbstractChatModel {
310
460
  }
311
461
  }
312
462
  else if (!silent) {
313
- console.log(`Provider not providing when restoring ${filepath}.`);
463
+ console.log(`Provider not provided when restoring ${filepath}.`);
314
464
  }
315
465
  const messages = content.messages.map(message => {
316
466
  let attachments = [];
@@ -327,7 +477,7 @@ export class AIChatModel extends AbstractChatModel {
327
477
  });
328
478
  await this.clearMessages();
329
479
  this.messagesInserted(0, messages);
330
- this._agentManager.setHistory(messages);
480
+ await this._rebuildHistory();
331
481
  this.autosave = content.metadata?.autosave ?? false;
332
482
  this.title = content.metadata?.title ?? null;
333
483
  return true;
@@ -343,7 +493,7 @@ export class AIChatModel extends AbstractChatModel {
343
493
  const messages = [
344
494
  {
345
495
  role: 'system',
346
- content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting. Focus on the user's main intent."
496
+ content: "Generate a concise title (no more than 10 words) for the following conversation. Do not use formatting, quotes, or punctuation. Focus on the subject matter and specific content the user is working on, not on the actions taken (e.g. prefer 'Pandas DataFrame filtering' over 'Opening a notebook'). The title should be a noun phrase describing the topic."
347
497
  },
348
498
  {
349
499
  role: 'user',
@@ -362,6 +512,9 @@ export class AIChatModel extends AbstractChatModel {
362
512
  const attachmentMap = new Map(); // JSON → index
363
513
  const attachmentsList = []; // Actual attachments
364
514
  this.messages.forEach(message => {
515
+ if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
516
+ return;
517
+ }
365
518
  let attachmentIndexes = [];
366
519
  if (message.attachments) {
367
520
  attachmentIndexes = message.attachments.map(attachment => {
@@ -420,6 +573,49 @@ export class AIChatModel extends AbstractChatModel {
420
573
  this.config = { ...config, enableCodeToolbar: true };
421
574
  // Agent manager handles agent recreation automatically via its own settings listener
422
575
  }
576
+ /**
577
+ * Rebuild history when the active model changes.
578
+ */
579
+ _onModelChanged() {
580
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
581
+ const modelKey = providerConfig
582
+ ? `${providerConfig.provider}:${providerConfig.model}`
583
+ : undefined;
584
+ if (modelKey && modelKey !== this._currentModelKey) {
585
+ this._currentModelKey = modelKey;
586
+ this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
587
+ }
588
+ }
589
+ /**
590
+ * Rebuilds the agent history from the current messages.
591
+ * For vision-capable models, re-reads binary attachments from disk.
592
+ * For text-only models, uses message text only.
593
+ */
594
+ async _rebuildHistory() {
595
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
596
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
597
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
598
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
599
+ const modelMessages = [];
600
+ for (const msg of this.messages) {
601
+ const isAI = msg.sender.username === 'ai-assistant';
602
+ if (!isAI && msg.attachments?.length) {
603
+ const enhancedContent = await Private.processAttachments(msg.attachments, this.input.documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
604
+ modelMessages.push({
605
+ role: 'user',
606
+ content: enhancedContent
607
+ });
608
+ }
609
+ else if (msg.body) {
610
+ modelMessages.push({
611
+ role: isAI ? 'assistant' : 'user',
612
+ content: msg.body
613
+ });
614
+ }
615
+ // Skip messages with empty body like tool calls
616
+ }
617
+ this._agentManager.setHistory(modelMessages);
618
+ }
423
619
  /**
424
620
  * Handles events emitted by the agent manager.
425
621
  * @param event The event data containing type and payload
@@ -468,6 +664,7 @@ export class AIChatModel extends AbstractChatModel {
468
664
  this.messageAdded(aiMessage);
469
665
  this._currentStreamingMessage =
470
666
  this.messages.find(message => message.id === aiMessage.id) ?? null;
667
+ this._updateQueueUI();
471
668
  }
472
669
  /**
473
670
  * Handles streaming message chunks from the AI agent.
@@ -583,13 +780,18 @@ export class AIChatModel extends AbstractChatModel {
583
780
  body: '',
584
781
  mime_model: {
585
782
  data: {
586
- 'application/vnd.jupyter.chat.components': 'tool-call'
783
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
587
784
  },
588
785
  metadata: {
589
- toolName: context.toolName,
590
- input: context.input,
591
- status: context.status,
592
- summary: context.summary
786
+ toolCalls: [
787
+ {
788
+ toolCallId: context.toolCallId,
789
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
790
+ kind: context.toolName,
791
+ status: 'in_progress',
792
+ rawInput: context.input
793
+ }
794
+ ]
593
795
  }
594
796
  },
595
797
  sender: this._getAIUser(),
@@ -599,6 +801,7 @@ export class AIChatModel extends AbstractChatModel {
599
801
  raw_time: false
600
802
  };
601
803
  this.messageAdded(toolCallMessage);
804
+ this._updateQueueUI();
602
805
  }
603
806
  /**
604
807
  * Handles the completion of a tool call execution.
@@ -642,13 +845,22 @@ export class AIChatModel extends AbstractChatModel {
642
845
  */
643
846
  _handleErrorEvent(event) {
644
847
  this.messageAdded({
645
- body: `Error generating response: ${event.data.error.message}`,
848
+ body: '',
849
+ mime_model: {
850
+ data: {
851
+ 'application/vnd.jupyter.chat.components': 'error'
852
+ },
853
+ metadata: {
854
+ errorMessage: `Error generating response: ${event.data.error.message}`
855
+ }
856
+ },
646
857
  sender: this._getAIUser(),
647
858
  id: UUID.uuid4(),
648
859
  time: Date.now() / 1000,
649
860
  type: 'msg',
650
861
  raw_time: false
651
862
  });
863
+ this._updateQueueUI();
652
864
  }
653
865
  /**
654
866
  * Handles tool approval request events from the AI agent.
@@ -658,7 +870,6 @@ export class AIChatModel extends AbstractChatModel {
658
870
  if (!context) {
659
871
  return;
660
872
  }
661
- context.approvalId = event.data.approvalId;
662
873
  context.input = JSON.stringify(event.data.args, null, 2);
663
874
  this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
664
875
  }
@@ -666,12 +877,12 @@ export class AIChatModel extends AbstractChatModel {
666
877
  * Handles tool approval resolved events from the AI agent.
667
878
  */
668
879
  _handleToolApprovalResolved(event) {
669
- const context = Array.from(this._toolContexts.values()).find(ctx => ctx.approvalId === event.data.approvalId);
880
+ const context = this._toolContexts.get(event.data.toolCallId);
670
881
  if (!context) {
671
882
  return;
672
883
  }
673
884
  const status = event.data.approved ? 'approved' : 'rejected';
674
- this._updateToolCallUI(context.toolCallId, status);
885
+ this._updateToolCallUI(event.data.toolCallId, status);
675
886
  if (!event.data.approved) {
676
887
  this._toolContexts.delete(context.toolCallId);
677
888
  }
@@ -692,41 +903,79 @@ export class AIChatModel extends AbstractChatModel {
692
903
  existingMessage.update({
693
904
  mime_model: {
694
905
  data: {
695
- 'application/vnd.jupyter.chat.components': 'tool-call'
906
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
696
907
  },
697
908
  metadata: {
698
- toolName: context.toolName,
699
- input: context.input,
700
- status: context.status,
701
- summary: context.summary,
702
- output,
703
- targetId: this.name,
704
- approvalId: context.approvalId
909
+ toolCalls: [
910
+ {
911
+ toolCallId: context.toolCallId,
912
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
913
+ kind: context.toolName,
914
+ status: context.status,
915
+ rawInput: context.input,
916
+ rawOutput: output,
917
+ sessionId: this.name,
918
+ permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
919
+ ...(status === 'awaiting_approval' && {
920
+ permissionOptions: [
921
+ { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
922
+ { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
923
+ ]
924
+ })
925
+ }
926
+ ]
705
927
  }
706
928
  }
707
929
  });
708
930
  }
931
+ /**
932
+ * The current message queue
933
+ */
934
+ get messageQueue() {
935
+ return this._messageQueue;
936
+ }
937
+ set messageQueue(value) {
938
+ this._messageQueue = value;
939
+ this._updateQueueUI();
940
+ if (this._messageQueue.length > 0 && !this._isBusy) {
941
+ this._drainQueue();
942
+ }
943
+ }
944
+ /**
945
+ * Whether the chat is busy
946
+ */
947
+ get isBusy() {
948
+ return this._isBusy;
949
+ }
950
+ set isBusy(value) {
951
+ this._isBusy = value;
952
+ }
709
953
  // Private fields
710
954
  _settingsModel;
711
955
  _user;
712
956
  _toolContexts = new Map();
713
957
  _agentManager;
958
+ _providerRegistry;
959
+ _currentModelKey;
714
960
  _currentStreamingMessage = null;
715
961
  _nameChanged = new Signal(this);
716
962
  _contentsManager;
717
963
  _autosave = false;
718
964
  _autosaveChanged = new Signal(this);
719
965
  _autosaveDebouncer;
966
+ _messageQueue = [];
967
+ _isBusy = false;
968
+ _queueMessageId = null;
720
969
  _title = null;
721
970
  _titleChanged = new Signal(this);
722
971
  }
723
972
  var Private;
724
973
  (function (Private) {
725
- const isPlainObject = (value) => {
974
+ Private.isPlainObject = (value) => {
726
975
  return typeof value === 'object' && value !== null && !Array.isArray(value);
727
976
  };
728
- const isDisplayOutput = (value) => {
729
- if (!isPlainObject(value)) {
977
+ Private.isDisplayOutput = (value) => {
978
+ if (!Private.isPlainObject(value)) {
730
979
  return false;
731
980
  }
732
981
  const output = value;
@@ -734,14 +983,14 @@ var Private;
734
983
  nbformat.isDisplayUpdate(output) ||
735
984
  nbformat.isExecuteResult(output));
736
985
  };
737
- const toMimeBundle = (value, trustedMimeTypes) => {
986
+ Private.toMimeBundle = (value, trustedMimeTypes) => {
738
987
  const data = value.data;
739
- if (!isPlainObject(data) || Object.keys(data).length === 0) {
988
+ if (!Private.isPlainObject(data) || Object.keys(data).length === 0) {
740
989
  return null;
741
990
  }
742
991
  return {
743
992
  data: data,
744
- ...(isPlainObject(value.metadata)
993
+ ...(Private.isPlainObject(value.metadata)
745
994
  ? { metadata: value.metadata }
746
995
  : {}),
747
996
  // MIME auto-rendering only runs for explicitly configured command IDs.
@@ -757,21 +1006,21 @@ var Private;
757
1006
  * Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
758
1007
  * often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
759
1008
  */
760
- const toDisplayOutputs = (value) => {
761
- if (isDisplayOutput(value)) {
1009
+ Private.toDisplayOutputs = (value) => {
1010
+ if (Private.isDisplayOutput(value)) {
762
1011
  return [value];
763
1012
  }
764
1013
  if (Array.isArray(value)) {
765
- return value.filter(isDisplayOutput);
1014
+ return value.filter(Private.isDisplayOutput);
766
1015
  }
767
- if (!isPlainObject(value)) {
1016
+ if (!Private.isPlainObject(value)) {
768
1017
  return [];
769
1018
  }
770
1019
  if (Array.isArray(value.outputs)) {
771
- return value.outputs.filter(isDisplayOutput);
1020
+ return value.outputs.filter(Private.isDisplayOutput);
772
1021
  }
773
1022
  if ('result' in value) {
774
- return toDisplayOutputs(value.result);
1023
+ return Private.toDisplayOutputs(value.result);
775
1024
  }
776
1025
  return [];
777
1026
  };
@@ -780,10 +1029,10 @@ var Private;
780
1029
  */
781
1030
  function extractMimeBundlesFromUnknown(content, options = {}) {
782
1031
  const bundles = [];
783
- const outputs = toDisplayOutputs(content);
1032
+ const outputs = Private.toDisplayOutputs(content);
784
1033
  const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
785
1034
  for (const output of outputs) {
786
- const bundle = toMimeBundle(output, trustedMimeTypes);
1035
+ const bundle = Private.toMimeBundle(output, trustedMimeTypes);
787
1036
  if (bundle) {
788
1037
  bundles.push(bundle);
789
1038
  }
@@ -804,16 +1053,21 @@ var Private;
804
1053
  }
805
1054
  Private.formatToolOutput = formatToolOutput;
806
1055
  /**
807
- * Processes file attachments and returns text contents and binary parts separately.
1056
+ * Processes file attachments and returns the message content with the attachments.
808
1057
  * @param attachments Array of file attachments to process
809
1058
  * @param documentManager Optional document manager for file operations
810
- * @returns Text contents and binary parts
1059
+ * @param body The message body
1060
+ * @param supportsImages Whether the model supports images
1061
+ * @param supportsPdf Whether the model supports pdfs
1062
+ * @param supportsAudio Whether the model supports audio
1063
+ * @returns Enhanced message content
811
1064
  */
812
- async function processAttachments(attachments, documentManager) {
1065
+ async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
813
1066
  const textContents = [];
814
- const binaryParts = [];
1067
+ const includedParts = [];
1068
+ const omittedNames = [];
815
1069
  if (!documentManager) {
816
- return { textContents, binaryParts };
1070
+ return body;
817
1071
  }
818
1072
  for (const attachment of attachments) {
819
1073
  try {
@@ -837,24 +1091,50 @@ var Private;
837
1091
  }
838
1092
  }
839
1093
  if (mimetype?.startsWith('image/')) {
840
- const data = await readBinaryAttachment(attachment, documentManager);
841
- if (data) {
842
- binaryParts.push({
843
- type: 'image',
844
- image: data,
845
- mediaType: mimetype
846
- });
1094
+ if (supportsImages) {
1095
+ const data = await readBinaryAttachment(attachment, documentManager);
1096
+ if (data) {
1097
+ includedParts.push({
1098
+ type: 'image',
1099
+ image: data,
1100
+ mediaType: mimetype
1101
+ });
1102
+ }
1103
+ }
1104
+ else {
1105
+ omittedNames.push(PathExt.basename(attachment.value));
847
1106
  }
848
1107
  }
849
1108
  else if (mimetype === 'application/pdf') {
850
- const data = await readBinaryAttachment(attachment, documentManager);
851
- if (data) {
852
- binaryParts.push({
853
- type: 'file',
854
- data,
855
- mediaType: mimetype,
856
- filename: PathExt.basename(attachment.value)
857
- });
1109
+ if (supportsPdf) {
1110
+ const data = await readBinaryAttachment(attachment, documentManager);
1111
+ if (data) {
1112
+ includedParts.push({
1113
+ type: 'file',
1114
+ data,
1115
+ mediaType: mimetype,
1116
+ filename: PathExt.basename(attachment.value)
1117
+ });
1118
+ }
1119
+ }
1120
+ else {
1121
+ omittedNames.push(PathExt.basename(attachment.value));
1122
+ }
1123
+ }
1124
+ else if (mimetype?.startsWith('audio/')) {
1125
+ if (supportsAudio) {
1126
+ const data = await readBinaryAttachment(attachment, documentManager);
1127
+ if (data) {
1128
+ includedParts.push({
1129
+ type: 'file',
1130
+ data,
1131
+ mediaType: mimetype,
1132
+ filename: PathExt.basename(attachment.value)
1133
+ });
1134
+ }
1135
+ }
1136
+ else {
1137
+ omittedNames.push(PathExt.basename(attachment.value));
858
1138
  }
859
1139
  }
860
1140
  else {
@@ -874,7 +1154,16 @@ var Private;
874
1154
  textContents.push(`**File: ${attachment.value}** (Could not read file)`);
875
1155
  }
876
1156
  }
877
- return { textContents, binaryParts };
1157
+ let textPart = body;
1158
+ if (textContents.length > 0) {
1159
+ textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
1160
+ }
1161
+ if (omittedNames.length > 0) {
1162
+ textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
1163
+ }
1164
+ return includedParts.length > 0
1165
+ ? [{ type: 'text', text: textPart }, ...includedParts]
1166
+ : textPart;
878
1167
  }
879
1168
  Private.processAttachments = processAttachments;
880
1169
  /**