@jupyterlite/ai 0.16.0 → 0.18.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 (40) hide show
  1. package/lib/agent.d.ts +19 -10
  2. package/lib/agent.js +82 -46
  3. package/lib/chat-commands/clear.js +1 -1
  4. package/lib/chat-model-handler.d.ts +2 -3
  5. package/lib/chat-model-handler.js +6 -2
  6. package/lib/chat-model.d.ts +129 -26
  7. package/lib/chat-model.js +543 -160
  8. package/lib/components/clear-button.d.ts +1 -1
  9. package/lib/components/clear-button.js +1 -1
  10. package/lib/components/save-button.d.ts +2 -2
  11. package/lib/index.js +224 -59
  12. package/lib/models/settings-model.js +1 -0
  13. package/lib/providers/built-in-providers.js +1 -1
  14. package/lib/providers/{generated-context-windows.d.ts → generated-model-info.d.ts} +2 -2
  15. package/lib/providers/generated-model-info.js +502 -0
  16. package/lib/providers/model-info.d.ts +3 -0
  17. package/lib/providers/model-info.js +33 -0
  18. package/lib/tokens.d.ts +98 -15
  19. package/lib/tokens.js +1 -0
  20. package/lib/widgets/ai-settings.js +5 -0
  21. package/lib/widgets/main-area-chat.d.ts +3 -3
  22. package/lib/widgets/main-area-chat.js +9 -5
  23. package/package.json +3 -3
  24. package/schema/settings-model.json +6 -0
  25. package/src/agent.ts +100 -52
  26. package/src/chat-commands/clear.ts +1 -1
  27. package/src/chat-model-handler.ts +10 -3
  28. package/src/chat-model.ts +727 -210
  29. package/src/components/clear-button.tsx +3 -3
  30. package/src/components/save-button.tsx +3 -3
  31. package/src/index.ts +289 -83
  32. package/src/models/settings-model.ts +1 -0
  33. package/src/providers/built-in-providers.ts +1 -1
  34. package/src/providers/generated-model-info.ts +508 -0
  35. package/src/providers/model-info.ts +57 -0
  36. package/src/tokens.ts +100 -15
  37. package/src/widgets/ai-settings.tsx +26 -0
  38. package/src/widgets/main-area-chat.ts +14 -9
  39. package/lib/providers/generated-context-windows.js +0 -96
  40. package/src/providers/generated-context-windows.ts +0 -102
package/lib/chat-model.js CHANGED
@@ -5,6 +5,7 @@ import { UUID } from '@lumino/coreutils';
5
5
  import { Debouncer } from '@lumino/polling';
6
6
  import { Signal } from '@lumino/signaling';
7
7
  import { AI_AVATAR } from './icons';
8
+ import { modelSupportsAudio, modelSupportsImages, modelSupportsPdf } from './providers/model-info';
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
  /**
@@ -49,6 +54,31 @@ export class AIChatModel extends AbstractChatModel {
49
54
  }
50
55
  this.setReady();
51
56
  }
57
+ /**
58
+ * A signal emitting when the chat name has changed.
59
+ */
60
+ get nameChanged() {
61
+ return this._nameChanged;
62
+ }
63
+ /**
64
+ * The title of the chat.
65
+ */
66
+ get title() {
67
+ return this._title;
68
+ }
69
+ set title(value) {
70
+ this._title = value;
71
+ if (this.autosave) {
72
+ this._autosaveDebouncer.invoke();
73
+ }
74
+ this._titleChanged.emit(this._title);
75
+ }
76
+ /**
77
+ * A signal emitting when the chat title has changed.
78
+ */
79
+ get titleChanged() {
80
+ return this._titleChanged;
81
+ }
52
82
  /**
53
83
  * Whether to save the chat automatically.
54
84
  */
@@ -56,17 +86,20 @@ export class AIChatModel extends AbstractChatModel {
56
86
  return this._autosave;
57
87
  }
58
88
  set autosave(value) {
89
+ if (value === this._autosave) {
90
+ return;
91
+ }
59
92
  this._autosave = value;
60
93
  this._autosaveChanged.emit(value);
61
94
  if (value) {
62
95
  this.messagesUpdated.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
63
96
  this.messageChanged.connect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
64
- this._autosaveDebouncer.invoke();
65
97
  }
66
98
  else {
67
99
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
68
100
  this.messageChanged.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
69
101
  }
102
+ this._autosaveDebouncer.invoke();
70
103
  }
71
104
  /**
72
105
  * A signal emitting when the autosave flag changed.
@@ -74,12 +107,6 @@ export class AIChatModel extends AbstractChatModel {
74
107
  get autosaveChanged() {
75
108
  return this._autosaveChanged;
76
109
  }
77
- /**
78
- * A signal emitting when the chat name has changed.
79
- */
80
- get nameChanged() {
81
- return this._nameChanged;
82
- }
83
110
  /**
84
111
  * Gets the current user information.
85
112
  */
@@ -93,7 +120,7 @@ export class AIChatModel extends AbstractChatModel {
93
120
  return this._agentManager.tokenUsageChanged;
94
121
  }
95
122
  /**
96
- * Get the agent manager associated to the model.
123
+ * The agent manager used in the model.
97
124
  */
98
125
  get agentManager() {
99
126
  return this._agentManager;
@@ -108,6 +135,7 @@ export class AIChatModel extends AbstractChatModel {
108
135
  * Dispose of the model.
109
136
  */
110
137
  dispose() {
138
+ this.stopStreaming();
111
139
  this.messagesUpdated.disconnect(this._autosaveDebouncer.invoke, this._autosaveDebouncer);
112
140
  super.dispose();
113
141
  }
@@ -123,7 +151,7 @@ export class AIChatModel extends AbstractChatModel {
123
151
  stopStreaming: () => this.stopStreaming(),
124
152
  clearMessages: () => this.clearMessages(),
125
153
  agentManager: this._agentManager,
126
- addSystemMessage: (body) => this.addSystemMessage(body)
154
+ addSystemMessage: (body) => this._addSystemMessage(body)
127
155
  };
128
156
  }
129
157
  /**
@@ -135,15 +163,30 @@ export class AIChatModel extends AbstractChatModel {
135
163
  /**
136
164
  * Clears all messages from the chat and resets conversation state.
137
165
  */
138
- clearMessages = () => {
166
+ clearMessages = async () => {
167
+ this.stopStreaming();
168
+ this._messageQueue = [];
169
+ this._isBusy = false;
170
+ this._queueMessageId = null;
171
+ this._currentStreamingMessage = null;
139
172
  this.messagesDeleted(0, this.messages.length);
173
+ this.title = null;
140
174
  this._toolContexts.clear();
141
- this._agentManager.clearHistory();
175
+ await this._agentManager.clearHistory();
142
176
  };
177
+ /**
178
+ * Overrides messageAdded to ensure queued messages stay at the bottom.
179
+ */
180
+ messageAdded(message) {
181
+ super.messageAdded(message);
182
+ if (this._queueMessageId && message.id !== this._queueMessageId) {
183
+ this._updateQueueUI();
184
+ }
185
+ }
143
186
  /**
144
187
  * Adds a non-user message to the chat (used by chat commands).
145
188
  */
146
- addSystemMessage(body) {
189
+ _addSystemMessage(body) {
147
190
  const message = {
148
191
  body,
149
192
  sender: this._getAIUser(),
@@ -174,9 +217,9 @@ export class AIChatModel extends AbstractChatModel {
174
217
  raw_time: false,
175
218
  attachments: [...this.input.attachments]
176
219
  };
177
- this.messageAdded(userMessage);
178
220
  // Check if we have valid configuration
179
221
  if (!this._agentManager.hasValidConfig()) {
222
+ this.messageAdded(userMessage);
180
223
  const errorMessage = {
181
224
  body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
182
225
  sender: this._getAIUser(),
@@ -188,23 +231,48 @@ export class AIChatModel extends AbstractChatModel {
188
231
  this.messageAdded(errorMessage);
189
232
  return;
190
233
  }
234
+ if (this._isBusy) {
235
+ this._messageQueue.push({
236
+ id: UUID.uuid4(),
237
+ body: message.body,
238
+ _originalMsg: userMessage
239
+ });
240
+ this.input.clearAttachments();
241
+ this._updateQueueUI();
242
+ return;
243
+ }
244
+ this._isBusy = true;
245
+ this.messageAdded(userMessage);
246
+ this.input.clearAttachments();
247
+ await this._processMessage(userMessage);
248
+ }
249
+ /**
250
+ * Internal method to process attachments and send the message to the agent.
251
+ */
252
+ async _processMessage(userMessage) {
191
253
  try {
192
- // Process attachments and add their content to the message
193
- let enhancedMessage = message.body;
194
- if (this.input.attachments.length > 0) {
195
- const attachmentContents = await this._processAttachments(this.input.attachments);
196
- this.input.clearAttachments();
197
- if (attachmentContents.length > 0) {
198
- enhancedMessage +=
199
- '\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
200
- }
201
- }
202
254
  this.updateWriters([{ user: this._getAIUser() }]);
255
+ let enhancedMessage = userMessage.body;
256
+ if (userMessage.attachments && userMessage.attachments.length > 0) {
257
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
258
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
259
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
260
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
261
+ enhancedMessage = await Private.processAttachments(userMessage.attachments, this.input.documentManager, userMessage.body, supportsImages, supportsPdf, supportsAudio);
262
+ }
203
263
  await this._agentManager.generateResponse(enhancedMessage);
204
264
  }
205
265
  catch (error) {
206
266
  const errorMessage = {
207
- body: `Error generating AI response: ${error.message}`,
267
+ body: '',
268
+ mime_model: {
269
+ data: {
270
+ 'application/vnd.jupyter.chat.components': 'error'
271
+ },
272
+ metadata: {
273
+ errorMessage: `Error generating AI response: ${error.message}`
274
+ }
275
+ },
208
276
  sender: this._getAIUser(),
209
277
  id: UUID.uuid4(),
210
278
  time: Date.now() / 1000,
@@ -214,8 +282,89 @@ export class AIChatModel extends AbstractChatModel {
214
282
  this.messageAdded(errorMessage);
215
283
  }
216
284
  finally {
285
+ this._drainQueue();
286
+ if (this._settingsModel.config.autoTitle &&
287
+ (this.messages.length <= 5 || this.title === null)) {
288
+ try {
289
+ this.title = await this.requestTitle();
290
+ }
291
+ catch (e) {
292
+ console.warn('Error while generating a title\n', e);
293
+ }
294
+ }
295
+ }
296
+ }
297
+ /**
298
+ * Removes the message-queue chat component.
299
+ */
300
+ _removeQueueUI() {
301
+ if (this._queueMessageId) {
302
+ const existingMsg = this.messages.find(msg => msg.id === this._queueMessageId);
303
+ if (existingMsg) {
304
+ const idx = this.messages.indexOf(existingMsg);
305
+ if (idx !== -1) {
306
+ this.messagesDeleted(idx, 1);
307
+ }
308
+ }
309
+ this._queueMessageId = null;
310
+ }
311
+ }
312
+ /**
313
+ * Creates or updates the message-queue chat component.
314
+ */
315
+ _updateQueueUI() {
316
+ this._removeQueueUI();
317
+ if (this._messageQueue.length === 0) {
318
+ return;
319
+ }
320
+ const queueBody = {
321
+ data: {
322
+ 'application/vnd.jupyter.chat.components': 'message-queue'
323
+ },
324
+ metadata: {
325
+ messages: this._messageQueue.map(m => ({
326
+ id: m.id,
327
+ body: m.body,
328
+ attachments: m._originalMsg.attachments
329
+ })),
330
+ targetId: this.name
331
+ }
332
+ };
333
+ this._queueMessageId = UUID.uuid4();
334
+ const queueMessage = {
335
+ body: '',
336
+ mime_model: queueBody,
337
+ sender: { username: 'system', display_name: '' },
338
+ id: this._queueMessageId,
339
+ time: Date.now() / 1000,
340
+ type: 'msg',
341
+ raw_time: false
342
+ };
343
+ this.messageAdded(queueMessage);
344
+ }
345
+ /**
346
+ * Processes the next message in the queue, or marks the agent as idle.
347
+ */
348
+ async _drainQueue() {
349
+ if (this._messageQueue.length === 0) {
350
+ this._isBusy = false;
217
351
  this.updateWriters([]);
352
+ this._removeQueueUI();
353
+ return;
218
354
  }
355
+ // Dequeue and push to chat
356
+ const next = this._messageQueue.shift();
357
+ next._originalMsg.time = Date.now() / 1000;
358
+ this.messageAdded(next._originalMsg);
359
+ await this._processMessage(next._originalMsg);
360
+ }
361
+ /**
362
+ * Removes a queued message by its ID.
363
+ * @param messageId The ID of the queued message to remove
364
+ */
365
+ removeQueuedMessage(messageId) {
366
+ this._messageQueue = this._messageQueue.filter(msg => msg.id !== messageId);
367
+ this._updateQueueUI();
219
368
  }
220
369
  /**
221
370
  * Save the chat as json file.
@@ -299,12 +448,33 @@ export class AIChatModel extends AbstractChatModel {
299
448
  attachments
300
449
  };
301
450
  });
302
- this.clearMessages();
451
+ await this.clearMessages();
303
452
  this.messagesInserted(0, messages);
304
- this._agentManager.setHistory(messages);
453
+ await this._rebuildHistory();
305
454
  this.autosave = content.metadata?.autosave ?? false;
455
+ this.title = content.metadata?.title ?? null;
306
456
  return true;
307
457
  };
458
+ /**
459
+ * Request a title to this chat, regarding the message history.
460
+ */
461
+ async requestTitle() {
462
+ const history = this.messages
463
+ .filter(msg => msg.body !== '')
464
+ .map(msg => `${msg.sender.username === 'ai-assistant' ? 'assistant' : 'user'}: ${msg.body}`)
465
+ .join('\n');
466
+ const messages = [
467
+ {
468
+ role: 'system',
469
+ 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."
470
+ },
471
+ {
472
+ role: 'user',
473
+ content: history
474
+ }
475
+ ];
476
+ return this.agentManager.textResponse(messages);
477
+ }
308
478
  /**
309
479
  * Serialize the model for backup
310
480
  */
@@ -315,6 +485,9 @@ export class AIChatModel extends AbstractChatModel {
315
485
  const attachmentMap = new Map(); // JSON → index
316
486
  const attachmentsList = []; // Actual attachments
317
487
  this.messages.forEach(message => {
488
+ if (message.content?.mime_model?.data?.['application/vnd.jupyter.chat.components'] === 'message-queue') {
489
+ return;
490
+ }
318
491
  let attachmentIndexes = [];
319
492
  if (message.attachments) {
320
493
  attachmentIndexes = message.attachments.map(attachment => {
@@ -348,7 +521,8 @@ export class AIChatModel extends AbstractChatModel {
348
521
  attachments,
349
522
  metadata: {
350
523
  provider,
351
- autosave: this.autosave
524
+ autosave: this.autosave,
525
+ ...(this.title ? { title: this.title } : {})
352
526
  }
353
527
  };
354
528
  }
@@ -372,6 +546,49 @@ export class AIChatModel extends AbstractChatModel {
372
546
  this.config = { ...config, enableCodeToolbar: true };
373
547
  // Agent manager handles agent recreation automatically via its own settings listener
374
548
  }
549
+ /**
550
+ * Rebuild history when the active model changes.
551
+ */
552
+ _onModelChanged() {
553
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
554
+ const modelKey = providerConfig
555
+ ? `${providerConfig.provider}:${providerConfig.model}`
556
+ : undefined;
557
+ if (modelKey && modelKey !== this._currentModelKey) {
558
+ this._currentModelKey = modelKey;
559
+ this._rebuildHistory().catch(e => console.warn('Failed to rebuild history on model change:', e));
560
+ }
561
+ }
562
+ /**
563
+ * Rebuilds the agent history from the current messages.
564
+ * For vision-capable models, re-reads binary attachments from disk.
565
+ * For text-only models, uses message text only.
566
+ */
567
+ async _rebuildHistory() {
568
+ const providerConfig = this._settingsModel.getProvider(this._agentManager.activeProvider);
569
+ const supportsImages = modelSupportsImages(providerConfig, this._providerRegistry);
570
+ const supportsPdf = modelSupportsPdf(providerConfig, this._providerRegistry);
571
+ const supportsAudio = modelSupportsAudio(providerConfig, this._providerRegistry);
572
+ const modelMessages = [];
573
+ for (const msg of this.messages) {
574
+ const isAI = msg.sender.username === 'ai-assistant';
575
+ if (!isAI && msg.attachments?.length) {
576
+ const enhancedContent = await Private.processAttachments(msg.attachments, this.input.documentManager, msg.body, supportsImages, supportsPdf, supportsAudio);
577
+ modelMessages.push({
578
+ role: 'user',
579
+ content: enhancedContent
580
+ });
581
+ }
582
+ else if (msg.body) {
583
+ modelMessages.push({
584
+ role: isAI ? 'assistant' : 'user',
585
+ content: msg.body
586
+ });
587
+ }
588
+ // Skip messages with empty body like tool calls
589
+ }
590
+ this._agentManager.setHistory(modelMessages);
591
+ }
375
592
  /**
376
593
  * Handles events emitted by the agent manager.
377
594
  * @param event The event data containing type and payload
@@ -535,13 +752,18 @@ export class AIChatModel extends AbstractChatModel {
535
752
  body: '',
536
753
  mime_model: {
537
754
  data: {
538
- 'application/vnd.jupyter.chat.components': 'tool-call'
755
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
539
756
  },
540
757
  metadata: {
541
- toolName: context.toolName,
542
- input: context.input,
543
- status: context.status,
544
- summary: context.summary
758
+ toolCalls: [
759
+ {
760
+ toolCallId: context.toolCallId,
761
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
762
+ kind: context.toolName,
763
+ status: 'in_progress',
764
+ rawInput: context.input
765
+ }
766
+ ]
545
767
  }
546
768
  },
547
769
  sender: this._getAIUser(),
@@ -594,7 +816,15 @@ export class AIChatModel extends AbstractChatModel {
594
816
  */
595
817
  _handleErrorEvent(event) {
596
818
  this.messageAdded({
597
- body: `Error generating response: ${event.data.error.message}`,
819
+ body: '',
820
+ mime_model: {
821
+ data: {
822
+ 'application/vnd.jupyter.chat.components': 'error'
823
+ },
824
+ metadata: {
825
+ errorMessage: `Error generating response: ${event.data.error.message}`
826
+ }
827
+ },
598
828
  sender: this._getAIUser(),
599
829
  id: UUID.uuid4(),
600
830
  time: Date.now() / 1000,
@@ -610,7 +840,6 @@ export class AIChatModel extends AbstractChatModel {
610
840
  if (!context) {
611
841
  return;
612
842
  }
613
- context.approvalId = event.data.approvalId;
614
843
  context.input = JSON.stringify(event.data.args, null, 2);
615
844
  this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
616
845
  }
@@ -618,12 +847,12 @@ export class AIChatModel extends AbstractChatModel {
618
847
  * Handles tool approval resolved events from the AI agent.
619
848
  */
620
849
  _handleToolApprovalResolved(event) {
621
- const context = Array.from(this._toolContexts.values()).find(ctx => ctx.approvalId === event.data.approvalId);
850
+ const context = this._toolContexts.get(event.data.toolCallId);
622
851
  if (!context) {
623
852
  return;
624
853
  }
625
854
  const status = event.data.approved ? 'approved' : 'rejected';
626
- this._updateToolCallUI(context.toolCallId, status);
855
+ this._updateToolCallUI(event.data.toolCallId, status);
627
856
  if (!event.data.approved) {
628
857
  this._toolContexts.delete(context.toolCallId);
629
858
  }
@@ -644,63 +873,308 @@ export class AIChatModel extends AbstractChatModel {
644
873
  existingMessage.update({
645
874
  mime_model: {
646
875
  data: {
647
- 'application/vnd.jupyter.chat.components': 'tool-call'
876
+ 'application/vnd.jupyter.chat.components': 'grouped-tool-calls'
648
877
  },
649
878
  metadata: {
650
- toolName: context.toolName,
651
- input: context.input,
652
- status: context.status,
653
- summary: context.summary,
654
- output,
655
- targetId: this.name,
656
- approvalId: context.approvalId
879
+ toolCalls: [
880
+ {
881
+ toolCallId: context.toolCallId,
882
+ title: `${context.toolName}${context.summary ? ' : ' + context.summary : ''}`,
883
+ kind: context.toolName,
884
+ status: context.status,
885
+ rawInput: context.input,
886
+ rawOutput: output,
887
+ sessionId: this.name,
888
+ permissionStatus: status === 'awaiting_approval' ? 'pending' : 'resolved',
889
+ ...(status === 'awaiting_approval' && {
890
+ permissionOptions: [
891
+ { optionId: 'approve', name: 'Approve', kind: 'allow_once' },
892
+ { optionId: 'reject', name: 'Reject', kind: 'reject_once' }
893
+ ]
894
+ })
895
+ }
896
+ ]
657
897
  }
658
898
  }
659
899
  });
660
900
  }
661
901
  /**
662
- * Processes file attachments and returns their content as formatted strings.
902
+ * The current message queue
903
+ */
904
+ get messageQueue() {
905
+ return this._messageQueue;
906
+ }
907
+ set messageQueue(value) {
908
+ this._messageQueue = value;
909
+ this._updateQueueUI();
910
+ if (this._messageQueue.length > 0 && !this._isBusy) {
911
+ this._drainQueue();
912
+ }
913
+ }
914
+ /**
915
+ * Whether the chat is busy
916
+ */
917
+ get isBusy() {
918
+ return this._isBusy;
919
+ }
920
+ set isBusy(value) {
921
+ this._isBusy = value;
922
+ }
923
+ // Private fields
924
+ _settingsModel;
925
+ _user;
926
+ _toolContexts = new Map();
927
+ _agentManager;
928
+ _providerRegistry;
929
+ _currentModelKey;
930
+ _currentStreamingMessage = null;
931
+ _nameChanged = new Signal(this);
932
+ _contentsManager;
933
+ _autosave = false;
934
+ _autosaveChanged = new Signal(this);
935
+ _autosaveDebouncer;
936
+ _messageQueue = [];
937
+ _isBusy = false;
938
+ _queueMessageId = null;
939
+ _title = null;
940
+ _titleChanged = new Signal(this);
941
+ }
942
+ var Private;
943
+ (function (Private) {
944
+ const isPlainObject = (value) => {
945
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
946
+ };
947
+ const isDisplayOutput = (value) => {
948
+ if (!isPlainObject(value)) {
949
+ return false;
950
+ }
951
+ const output = value;
952
+ return (nbformat.isDisplayData(output) ||
953
+ nbformat.isDisplayUpdate(output) ||
954
+ nbformat.isExecuteResult(output));
955
+ };
956
+ const toMimeBundle = (value, trustedMimeTypes) => {
957
+ const data = value.data;
958
+ if (!isPlainObject(data) || Object.keys(data).length === 0) {
959
+ return null;
960
+ }
961
+ return {
962
+ data: data,
963
+ ...(isPlainObject(value.metadata)
964
+ ? { metadata: value.metadata }
965
+ : {}),
966
+ // MIME auto-rendering only runs for explicitly configured command IDs.
967
+ // Trust handling is configurable to keep risky MIME execution opt-in.
968
+ ...(Object.keys(data).some(m => trustedMimeTypes.has(m))
969
+ ? { trusted: true }
970
+ : {})
971
+ };
972
+ };
973
+ /**
974
+ * Normalize arbitrary tool payloads into canonical display outputs.
975
+ *
976
+ * Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
977
+ * often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
978
+ */
979
+ const toDisplayOutputs = (value) => {
980
+ if (isDisplayOutput(value)) {
981
+ return [value];
982
+ }
983
+ if (Array.isArray(value)) {
984
+ return value.filter(isDisplayOutput);
985
+ }
986
+ if (!isPlainObject(value)) {
987
+ return [];
988
+ }
989
+ if (Array.isArray(value.outputs)) {
990
+ return value.outputs.filter(isDisplayOutput);
991
+ }
992
+ if ('result' in value) {
993
+ return toDisplayOutputs(value.result);
994
+ }
995
+ return [];
996
+ };
997
+ /**
998
+ * Extract rendermime-ready mime bundles from arbitrary tool results.
999
+ */
1000
+ function extractMimeBundlesFromUnknown(content, options = {}) {
1001
+ const bundles = [];
1002
+ const outputs = toDisplayOutputs(content);
1003
+ const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
1004
+ for (const output of outputs) {
1005
+ const bundle = toMimeBundle(output, trustedMimeTypes);
1006
+ if (bundle) {
1007
+ bundles.push(bundle);
1008
+ }
1009
+ }
1010
+ return bundles;
1011
+ }
1012
+ Private.extractMimeBundlesFromUnknown = extractMimeBundlesFromUnknown;
1013
+ function formatToolOutput(outputData) {
1014
+ if (typeof outputData === 'string') {
1015
+ return outputData;
1016
+ }
1017
+ try {
1018
+ return JSON.stringify(outputData, null, 2);
1019
+ }
1020
+ catch {
1021
+ return '[Complex object - cannot serialize]';
1022
+ }
1023
+ }
1024
+ Private.formatToolOutput = formatToolOutput;
1025
+ /**
1026
+ * Processes file attachments and returns the message content with the attachments.
663
1027
  * @param attachments Array of file attachments to process
664
- * @returns Array of formatted attachment contents
1028
+ * @param documentManager Optional document manager for file operations
1029
+ * @param body The message body
1030
+ * @param supportsImages Whether the model supports images
1031
+ * @param supportsPdf Whether the model supports pdfs
1032
+ * @param supportsAudio Whether the model supports audio
1033
+ * @returns Enhanced message content
665
1034
  */
666
- async _processAttachments(attachments) {
667
- const contents = [];
1035
+ async function processAttachments(attachments, documentManager, body, supportsImages, supportsPdf, supportsAudio) {
1036
+ const textContents = [];
1037
+ const includedParts = [];
1038
+ const omittedNames = [];
1039
+ if (!documentManager) {
1040
+ return body;
1041
+ }
668
1042
  for (const attachment of attachments) {
669
1043
  try {
670
1044
  if (attachment.type === 'notebook' && attachment.cells?.length) {
671
- const cellContents = await this._readNotebookCells(attachment);
1045
+ const cellContents = await readNotebookCells(attachment, documentManager);
672
1046
  if (cellContents) {
673
- contents.push(cellContents);
1047
+ textContents.push(cellContents);
674
1048
  }
675
1049
  }
676
1050
  else {
677
- const fileContent = await this._readFileAttachment(attachment);
678
- if (fileContent) {
679
- const fileExtension = PathExt.extname(attachment.value).toLowerCase();
680
- const language = fileExtension === '.ipynb' ? 'json' : '';
681
- contents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
1051
+ let mimetype = attachment.mimetype;
1052
+ const fileExtension = PathExt.extname(attachment.value).toLowerCase();
1053
+ // Fetch mimetype from server metadata if not provided
1054
+ if (!mimetype) {
1055
+ try {
1056
+ const diskModel = await documentManager.services.contents.get(attachment.value, { content: false });
1057
+ mimetype = diskModel?.mimetype;
1058
+ }
1059
+ catch (e) {
1060
+ console.warn(`Failed to fetch metadata for ${attachment.value}:`, e);
1061
+ }
1062
+ }
1063
+ if (mimetype?.startsWith('image/')) {
1064
+ if (supportsImages) {
1065
+ const data = await readBinaryAttachment(attachment, documentManager);
1066
+ if (data) {
1067
+ includedParts.push({
1068
+ type: 'image',
1069
+ image: data,
1070
+ mediaType: mimetype
1071
+ });
1072
+ }
1073
+ }
1074
+ else {
1075
+ omittedNames.push(PathExt.basename(attachment.value));
1076
+ }
1077
+ }
1078
+ else if (mimetype === 'application/pdf') {
1079
+ if (supportsPdf) {
1080
+ const data = await readBinaryAttachment(attachment, documentManager);
1081
+ if (data) {
1082
+ includedParts.push({
1083
+ type: 'file',
1084
+ data,
1085
+ mediaType: mimetype,
1086
+ filename: PathExt.basename(attachment.value)
1087
+ });
1088
+ }
1089
+ }
1090
+ else {
1091
+ omittedNames.push(PathExt.basename(attachment.value));
1092
+ }
1093
+ }
1094
+ else if (mimetype?.startsWith('audio/')) {
1095
+ if (supportsAudio) {
1096
+ const data = await readBinaryAttachment(attachment, documentManager);
1097
+ if (data) {
1098
+ includedParts.push({
1099
+ type: 'file',
1100
+ data,
1101
+ mediaType: mimetype,
1102
+ filename: PathExt.basename(attachment.value)
1103
+ });
1104
+ }
1105
+ }
1106
+ else {
1107
+ omittedNames.push(PathExt.basename(attachment.value));
1108
+ }
1109
+ }
1110
+ else {
1111
+ const fileContent = await readFileAttachment(attachment, documentManager);
1112
+ if (fileContent) {
1113
+ const language = fileExtension === '.ipynb' ||
1114
+ mimetype === 'application/x-ipynb+json'
1115
+ ? 'json'
1116
+ : '';
1117
+ textContents.push(`**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``);
1118
+ }
682
1119
  }
683
1120
  }
684
1121
  }
685
1122
  catch (error) {
686
1123
  console.warn(`Failed to read attachment ${attachment.value}:`, error);
687
- contents.push(`**File: ${attachment.value}** (Could not read file)`);
1124
+ textContents.push(`**File: ${attachment.value}** (Could not read file)`);
688
1125
  }
689
1126
  }
690
- return contents;
1127
+ let textPart = body;
1128
+ if (textContents.length > 0) {
1129
+ textPart += '\n\n--- Attached Files ---\n' + textContents.join('\n\n');
1130
+ }
1131
+ if (omittedNames.length > 0) {
1132
+ textPart += `\n[Attachments omitted (not supported by this model): ${omittedNames.join(', ')}.]`;
1133
+ }
1134
+ return includedParts.length > 0
1135
+ ? [{ type: 'text', text: textPart }, ...includedParts]
1136
+ : textPart;
691
1137
  }
1138
+ Private.processAttachments = processAttachments;
1139
+ /**
1140
+ * Reads a binary attachment and returns its base64-encoded content.
1141
+ * @param attachment The attachment to read
1142
+ * @param documentManager Optional document manager for file operations
1143
+ * @returns Base64 string or null if unable to read
1144
+ */
1145
+ async function readBinaryAttachment(attachment, documentManager) {
1146
+ if (!documentManager) {
1147
+ return null;
1148
+ }
1149
+ try {
1150
+ const diskModel = await documentManager.services.contents.get(attachment.value, { content: true });
1151
+ if (diskModel?.content && diskModel.format === 'base64') {
1152
+ // Strip whitespace/newlines
1153
+ return diskModel.content.replace(/\s/g, '');
1154
+ }
1155
+ return null;
1156
+ }
1157
+ catch (error) {
1158
+ console.warn(`Failed to read binary attachment ${attachment.value}:`, error);
1159
+ return null;
1160
+ }
1161
+ }
1162
+ Private.readBinaryAttachment = readBinaryAttachment;
692
1163
  /**
693
1164
  * Reads the content of a notebook cell.
694
1165
  * @param attachment The notebook attachment to read
1166
+ * @param documentManager Optional document manager for file operations
695
1167
  * @returns Cell content as string or null if unable to read
696
1168
  */
697
- async _readNotebookCells(attachment) {
698
- if (attachment.type !== 'notebook' || !attachment.cells) {
1169
+ async function readNotebookCells(attachment, documentManager) {
1170
+ if (attachment.type !== 'notebook' ||
1171
+ !attachment.cells ||
1172
+ !documentManager) {
699
1173
  return null;
700
1174
  }
701
1175
  try {
702
1176
  // Try reading from live notebook if open
703
- const widget = this.input.documentManager?.findWidget(attachment.value);
1177
+ const widget = documentManager.findWidget(attachment.value);
704
1178
  let cellData;
705
1179
  let kernelLang = 'text';
706
1180
  const ymodel = widget?.context.model.sharedModel;
@@ -714,7 +1188,7 @@ export class AIChatModel extends AbstractChatModel {
714
1188
  }
715
1189
  else {
716
1190
  // Fallback: reading from disk
717
- const model = await this.input.documentManager?.services.contents.get(attachment.value);
1191
+ const model = await documentManager.services.contents.get(attachment.value);
718
1192
  if (!model || model.type !== 'notebook') {
719
1193
  return null;
720
1194
  }
@@ -829,19 +1303,22 @@ export class AIChatModel extends AbstractChatModel {
829
1303
  return null;
830
1304
  }
831
1305
  }
1306
+ Private.readNotebookCells = readNotebookCells;
832
1307
  /**
833
1308
  * Reads the content of a file attachment.
834
1309
  * @param attachment The file attachment to read
1310
+ * @param documentManager Optional document manager for file operations
835
1311
  * @returns File content as string or null if unable to read
836
1312
  */
837
- async _readFileAttachment(attachment) {
1313
+ async function readFileAttachment(attachment, documentManager) {
838
1314
  // Handle both 'file' and 'notebook' types since both have a 'value' path
839
- if (attachment.type !== 'file' && attachment.type !== 'notebook') {
1315
+ if ((attachment.type !== 'file' && attachment.type !== 'notebook') ||
1316
+ !documentManager) {
840
1317
  return null;
841
1318
  }
842
1319
  try {
843
1320
  // Try reading from an open widget first
844
- const widget = this.input.documentManager?.findWidget(attachment.value);
1321
+ const widget = documentManager.findWidget(attachment.value);
845
1322
  if (widget && widget.context && widget.context.model) {
846
1323
  const model = widget.context.model;
847
1324
  const ymodel = model.sharedModel;
@@ -853,7 +1330,7 @@ export class AIChatModel extends AbstractChatModel {
853
1330
  }
854
1331
  }
855
1332
  // If not open, load from disk
856
- const diskModel = await this.input.documentManager?.services.contents.get(attachment.value);
1333
+ const diskModel = await documentManager.services.contents.get(attachment.value);
857
1334
  if (!diskModel?.content) {
858
1335
  return null;
859
1336
  }
@@ -879,99 +1356,5 @@ export class AIChatModel extends AbstractChatModel {
879
1356
  return null;
880
1357
  }
881
1358
  }
882
- // Private fields
883
- _settingsModel;
884
- _user;
885
- _toolContexts = new Map();
886
- _agentManager;
887
- _currentStreamingMessage = null;
888
- _nameChanged = new Signal(this);
889
- _contentsManager;
890
- _autosave = false;
891
- _autosaveChanged = new Signal(this);
892
- _autosaveDebouncer;
893
- }
894
- var Private;
895
- (function (Private) {
896
- const isPlainObject = (value) => {
897
- return typeof value === 'object' && value !== null && !Array.isArray(value);
898
- };
899
- const isDisplayOutput = (value) => {
900
- if (!isPlainObject(value)) {
901
- return false;
902
- }
903
- const output = value;
904
- return (nbformat.isDisplayData(output) ||
905
- nbformat.isDisplayUpdate(output) ||
906
- nbformat.isExecuteResult(output));
907
- };
908
- const toMimeBundle = (value, trustedMimeTypes) => {
909
- const data = value.data;
910
- if (!isPlainObject(data) || Object.keys(data).length === 0) {
911
- return null;
912
- }
913
- return {
914
- data: data,
915
- ...(isPlainObject(value.metadata)
916
- ? { metadata: value.metadata }
917
- : {}),
918
- // MIME auto-rendering only runs for explicitly configured command IDs.
919
- // Trust handling is configurable to keep risky MIME execution opt-in.
920
- ...(Object.keys(data).some(m => trustedMimeTypes.has(m))
921
- ? { trusted: true }
922
- : {})
923
- };
924
- };
925
- /**
926
- * Normalize arbitrary tool payloads into canonical display outputs.
927
- *
928
- * Tool outputs are not guaranteed to be raw Jupyter IOPub messages; they are
929
- * often wrapped objects (for example `{ success, result: { outputs: [...] } }`).
930
- */
931
- const toDisplayOutputs = (value) => {
932
- if (isDisplayOutput(value)) {
933
- return [value];
934
- }
935
- if (Array.isArray(value)) {
936
- return value.filter(isDisplayOutput);
937
- }
938
- if (!isPlainObject(value)) {
939
- return [];
940
- }
941
- if (Array.isArray(value.outputs)) {
942
- return value.outputs.filter(isDisplayOutput);
943
- }
944
- if ('result' in value) {
945
- return toDisplayOutputs(value.result);
946
- }
947
- return [];
948
- };
949
- /**
950
- * Extract rendermime-ready mime bundles from arbitrary tool results.
951
- */
952
- function extractMimeBundlesFromUnknown(content, options = {}) {
953
- const bundles = [];
954
- const outputs = toDisplayOutputs(content);
955
- const trustedMimeTypes = new Set(options.trustedMimeTypes ?? []);
956
- for (const output of outputs) {
957
- const bundle = toMimeBundle(output, trustedMimeTypes);
958
- if (bundle) {
959
- bundles.push(bundle);
960
- }
961
- }
962
- return bundles;
963
- }
964
- Private.extractMimeBundlesFromUnknown = extractMimeBundlesFromUnknown;
965
- function formatToolOutput(outputData) {
966
- if (typeof outputData === 'string') {
967
- return outputData;
968
- }
969
- try {
970
- return JSON.stringify(outputData, null, 2);
971
- }
972
- catch {
973
- return '[Complex object - cannot serialize]';
974
- }
975
- }
976
- Private.formatToolOutput = formatToolOutput;
1359
+ Private.readFileAttachment = readFileAttachment;
977
1360
  })(Private || (Private = {}));