@jupyterlite/ai 0.8.0 → 0.9.0-a0

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 (162) hide show
  1. package/lib/agent.d.ts +233 -0
  2. package/lib/agent.js +604 -0
  3. package/lib/chat-model.d.ts +195 -0
  4. package/lib/chat-model.js +590 -0
  5. package/lib/completion/completion-provider.d.ts +83 -0
  6. package/lib/completion/completion-provider.js +209 -0
  7. package/lib/completion/index.d.ts +1 -0
  8. package/lib/completion/index.js +1 -0
  9. package/lib/components/clear-button.d.ts +18 -0
  10. package/lib/components/clear-button.js +31 -0
  11. package/lib/components/index.d.ts +3 -0
  12. package/lib/components/index.js +3 -0
  13. package/lib/components/model-select.d.ts +19 -0
  14. package/lib/components/model-select.js +154 -0
  15. package/lib/components/stop-button.d.ts +3 -3
  16. package/lib/components/stop-button.js +8 -9
  17. package/lib/components/token-usage-display.d.ts +45 -0
  18. package/lib/components/token-usage-display.js +74 -0
  19. package/lib/components/tool-select.d.ts +27 -0
  20. package/lib/components/tool-select.js +130 -0
  21. package/lib/icons.d.ts +3 -1
  22. package/lib/icons.js +10 -13
  23. package/lib/index.d.ts +4 -5
  24. package/lib/index.js +322 -167
  25. package/lib/mcp/browser.d.ts +68 -0
  26. package/lib/mcp/browser.js +132 -0
  27. package/lib/models/settings-model.d.ts +69 -0
  28. package/lib/models/settings-model.js +295 -0
  29. package/lib/providers/built-in-providers.d.ts +9 -0
  30. package/lib/providers/built-in-providers.js +192 -0
  31. package/lib/providers/models.d.ts +37 -0
  32. package/lib/providers/models.js +28 -0
  33. package/lib/providers/provider-registry.d.ts +94 -0
  34. package/lib/providers/provider-registry.js +155 -0
  35. package/lib/tokens.d.ts +157 -86
  36. package/lib/tokens.js +16 -12
  37. package/lib/tools/commands.d.ts +11 -0
  38. package/lib/tools/commands.js +126 -0
  39. package/lib/tools/file.d.ts +27 -0
  40. package/lib/tools/file.js +262 -0
  41. package/lib/tools/notebook.d.ts +40 -0
  42. package/lib/tools/notebook.js +762 -0
  43. package/lib/tools/tool-registry.d.ts +35 -0
  44. package/lib/tools/tool-registry.js +55 -0
  45. package/lib/widgets/ai-settings.d.ts +39 -0
  46. package/lib/widgets/ai-settings.js +506 -0
  47. package/lib/widgets/chat-wrapper.d.ts +144 -0
  48. package/lib/widgets/chat-wrapper.js +390 -0
  49. package/lib/widgets/provider-config-dialog.d.ts +13 -0
  50. package/lib/widgets/provider-config-dialog.js +104 -0
  51. package/package.json +150 -41
  52. package/schema/settings-model.json +153 -0
  53. package/src/agent.ts +800 -0
  54. package/src/chat-model.ts +770 -0
  55. package/src/completion/completion-provider.ts +308 -0
  56. package/src/completion/index.ts +1 -0
  57. package/src/components/clear-button.tsx +56 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/components/model-select.tsx +245 -0
  60. package/src/components/stop-button.tsx +11 -11
  61. package/src/components/token-usage-display.tsx +130 -0
  62. package/src/components/tool-select.tsx +218 -0
  63. package/src/icons.ts +12 -14
  64. package/src/index.ts +468 -238
  65. package/src/mcp/browser.ts +213 -0
  66. package/src/models/settings-model.ts +409 -0
  67. package/src/providers/built-in-providers.ts +216 -0
  68. package/src/providers/models.ts +79 -0
  69. package/src/providers/provider-registry.ts +189 -0
  70. package/src/tokens.ts +203 -90
  71. package/src/tools/commands.ts +151 -0
  72. package/src/tools/file.ts +307 -0
  73. package/src/tools/notebook.ts +964 -0
  74. package/src/tools/tool-registry.ts +63 -0
  75. package/src/types.d.ts +4 -0
  76. package/src/widgets/ai-settings.tsx +1100 -0
  77. package/src/widgets/chat-wrapper.tsx +543 -0
  78. package/src/widgets/provider-config-dialog.tsx +256 -0
  79. package/style/base.css +335 -14
  80. package/style/icons/jupyternaut-lite.svg +1 -1
  81. package/lib/base-completer.d.ts +0 -49
  82. package/lib/base-completer.js +0 -14
  83. package/lib/chat-handler.d.ts +0 -56
  84. package/lib/chat-handler.js +0 -201
  85. package/lib/completion-provider.d.ts +0 -34
  86. package/lib/completion-provider.js +0 -32
  87. package/lib/default-prompts.d.ts +0 -2
  88. package/lib/default-prompts.js +0 -31
  89. package/lib/default-providers/Anthropic/completer.d.ts +0 -12
  90. package/lib/default-providers/Anthropic/completer.js +0 -46
  91. package/lib/default-providers/Anthropic/settings-schema.json +0 -70
  92. package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
  93. package/lib/default-providers/ChromeAI/completer.js +0 -56
  94. package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
  95. package/lib/default-providers/ChromeAI/instructions.js +0 -42
  96. package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
  97. package/lib/default-providers/Gemini/completer.d.ts +0 -12
  98. package/lib/default-providers/Gemini/completer.js +0 -48
  99. package/lib/default-providers/Gemini/instructions.d.ts +0 -2
  100. package/lib/default-providers/Gemini/instructions.js +0 -9
  101. package/lib/default-providers/Gemini/settings-schema.json +0 -64
  102. package/lib/default-providers/MistralAI/completer.d.ts +0 -13
  103. package/lib/default-providers/MistralAI/completer.js +0 -52
  104. package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
  105. package/lib/default-providers/MistralAI/instructions.js +0 -18
  106. package/lib/default-providers/MistralAI/settings-schema.json +0 -75
  107. package/lib/default-providers/Ollama/completer.d.ts +0 -12
  108. package/lib/default-providers/Ollama/completer.js +0 -43
  109. package/lib/default-providers/Ollama/instructions.d.ts +0 -2
  110. package/lib/default-providers/Ollama/instructions.js +0 -70
  111. package/lib/default-providers/Ollama/settings-schema.json +0 -143
  112. package/lib/default-providers/OpenAI/completer.d.ts +0 -12
  113. package/lib/default-providers/OpenAI/completer.js +0 -43
  114. package/lib/default-providers/OpenAI/settings-schema.json +0 -628
  115. package/lib/default-providers/WebLLM/completer.d.ts +0 -21
  116. package/lib/default-providers/WebLLM/completer.js +0 -127
  117. package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
  118. package/lib/default-providers/WebLLM/instructions.js +0 -32
  119. package/lib/default-providers/WebLLM/settings-schema.json +0 -19
  120. package/lib/default-providers/index.d.ts +0 -2
  121. package/lib/default-providers/index.js +0 -179
  122. package/lib/provider.d.ts +0 -144
  123. package/lib/provider.js +0 -412
  124. package/lib/settings/base.json +0 -7
  125. package/lib/settings/index.d.ts +0 -3
  126. package/lib/settings/index.js +0 -3
  127. package/lib/settings/panel.d.ts +0 -226
  128. package/lib/settings/panel.js +0 -510
  129. package/lib/settings/textarea.d.ts +0 -2
  130. package/lib/settings/textarea.js +0 -18
  131. package/lib/settings/utils.d.ts +0 -2
  132. package/lib/settings/utils.js +0 -4
  133. package/lib/types/ai-model.d.ts +0 -24
  134. package/lib/types/ai-model.js +0 -5
  135. package/schema/chat.json +0 -28
  136. package/schema/provider-registry.json +0 -29
  137. package/schema/system-prompts.json +0 -22
  138. package/src/base-completer.ts +0 -75
  139. package/src/chat-handler.ts +0 -262
  140. package/src/completion-provider.ts +0 -64
  141. package/src/default-prompts.ts +0 -33
  142. package/src/default-providers/Anthropic/completer.ts +0 -59
  143. package/src/default-providers/ChromeAI/completer.ts +0 -73
  144. package/src/default-providers/ChromeAI/instructions.ts +0 -45
  145. package/src/default-providers/Gemini/completer.ts +0 -61
  146. package/src/default-providers/Gemini/instructions.ts +0 -9
  147. package/src/default-providers/MistralAI/completer.ts +0 -69
  148. package/src/default-providers/MistralAI/instructions.ts +0 -18
  149. package/src/default-providers/Ollama/completer.ts +0 -54
  150. package/src/default-providers/Ollama/instructions.ts +0 -70
  151. package/src/default-providers/OpenAI/completer.ts +0 -54
  152. package/src/default-providers/WebLLM/completer.ts +0 -151
  153. package/src/default-providers/WebLLM/instructions.ts +0 -33
  154. package/src/default-providers/index.ts +0 -211
  155. package/src/global.d.ts +0 -9
  156. package/src/provider.ts +0 -514
  157. package/src/settings/index.ts +0 -3
  158. package/src/settings/panel.tsx +0 -773
  159. package/src/settings/textarea.tsx +0 -33
  160. package/src/settings/utils.ts +0 -5
  161. package/src/types/ai-model.ts +0 -37
  162. package/src/types/service-worker.d.ts +0 -6
@@ -0,0 +1,770 @@
1
+ import {
2
+ AbstractChatModel,
3
+ IActiveCellManager,
4
+ IAttachment,
5
+ IChatContext,
6
+ IChatMessage,
7
+ INewMessage,
8
+ IUser
9
+ } from '@jupyter/chat';
10
+
11
+ import { IDocumentManager } from '@jupyterlab/docmanager';
12
+
13
+ import { AgentManager, IAgentEvent } from './agent';
14
+
15
+ import { UUID } from '@lumino/coreutils';
16
+
17
+ import { PathExt } from '@jupyterlab/coreutils';
18
+
19
+ import { AI_AVATAR } from './icons';
20
+
21
+ import { AISettingsModel } from './models/settings-model';
22
+ /**
23
+ * AI Chat Model implementation that provides chat functionality with OpenAI agents,
24
+ * tool integration, and MCP server support.
25
+ * Extends the base AbstractChatModel to provide AI-powered conversations.
26
+ */
27
+ export class AIChatModel extends AbstractChatModel {
28
+ /**
29
+ * Constructs a new AIChatModel instance.
30
+ * @param options Configuration options for the chat model
31
+ */
32
+ constructor(options: AIChatModel.IOptions) {
33
+ super({
34
+ activeCellManager: options.activeCellManager,
35
+ documentManager: options.documentManager,
36
+ config: {
37
+ enableCodeToolbar: true,
38
+ sendWithShiftEnter: options.settingsModel.config.sendWithShiftEnter
39
+ }
40
+ });
41
+ this._settingsModel = options.settingsModel;
42
+ this._user = options.user;
43
+ this._agentManager = options.agentManager;
44
+
45
+ // Listen for agent events
46
+ this._agentManager.agentEvent.connect(this._onAgentEvent, this);
47
+
48
+ // Listen for settings changes to update chat behavior
49
+ this._settingsModel.stateChanged.connect(this._onSettingsChanged, this);
50
+ }
51
+
52
+ /**
53
+ * Gets the current user information.
54
+ */
55
+ get user(): IUser {
56
+ return this._user;
57
+ }
58
+
59
+ get tokenUsageChanged() {
60
+ return this._agentManager.tokenUsageChanged;
61
+ }
62
+
63
+ /**
64
+ * Creates a chat context for the current conversation.
65
+ */
66
+ createChatContext(): AIChatModel.IAIChatContext {
67
+ return {
68
+ name: this.name,
69
+ user: { username: 'me' },
70
+ users: [],
71
+ messages: this.messages,
72
+ stopStreaming: () => this.stopStreaming(),
73
+ clearMessages: () => this.clearMessages(),
74
+ agentManager: this._agentManager
75
+ };
76
+ }
77
+
78
+ /**
79
+ * Stops the current streaming response by aborting the request.
80
+ */
81
+ stopStreaming = (): void => {
82
+ this._agentManager.stopStreaming();
83
+ };
84
+
85
+ /**
86
+ * Clears all messages from the chat and resets conversation state.
87
+ */
88
+ clearMessages = (): void => {
89
+ this.messagesDeleted(0, this.messages.length);
90
+ this._pendingToolCalls.clear();
91
+ this._agentManager.clearHistory();
92
+ };
93
+
94
+ /**
95
+ * Sends a message to the AI and generates a response.
96
+ * @param message The user message to send
97
+ */
98
+ async sendMessage(message: INewMessage): Promise<void> {
99
+ // Add user message to chat
100
+ const userMessage: IChatMessage = {
101
+ body: message.body,
102
+ sender: this.user || { username: 'user', display_name: 'User' },
103
+ id: UUID.uuid4(),
104
+ time: Date.now() / 1000,
105
+ type: 'msg',
106
+ raw_time: false,
107
+ attachments: this.input.attachments
108
+ };
109
+ this.messageAdded(userMessage);
110
+
111
+ // Check if we have valid configuration
112
+ if (!this._agentManager.hasValidConfig()) {
113
+ const errorMessage: IChatMessage = {
114
+ body: 'Please configure your AI settings first. Open the AI Settings to set your API key and model.',
115
+ sender: this._getAIUser(),
116
+ id: UUID.uuid4(),
117
+ time: Date.now() / 1000,
118
+ type: 'msg',
119
+ raw_time: false
120
+ };
121
+ this.messageAdded(errorMessage);
122
+ return;
123
+ }
124
+
125
+ try {
126
+ // Process attachments and add their content to the message
127
+ let enhancedMessage = message.body;
128
+ if (this.input.attachments.length > 0) {
129
+ const attachmentContents = await this._processAttachments(
130
+ this.input.attachments
131
+ );
132
+ if (attachmentContents.length > 0) {
133
+ enhancedMessage +=
134
+ '\n\n--- Attached Files ---\n' + attachmentContents.join('\n\n');
135
+ }
136
+ }
137
+
138
+ this.updateWriters([{ user: this._getAIUser() }]);
139
+
140
+ await this._agentManager.generateResponse(enhancedMessage);
141
+ // Clear attachments after processing
142
+ this.input.clearAttachments();
143
+ } catch (error) {
144
+ const errorMessage: IChatMessage = {
145
+ body: `Error generating AI response: ${(error as Error).message}`,
146
+ sender: this._getAIUser(),
147
+ id: UUID.uuid4(),
148
+ time: Date.now() / 1000,
149
+ type: 'msg',
150
+ raw_time: false
151
+ };
152
+ this.messageAdded(errorMessage);
153
+ } finally {
154
+ this.updateWriters([]);
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Approves a tool call and updates the UI accordingly.
160
+ * @param interruptionId The interruption ID to approve
161
+ * @param messageId Optional message ID for UI updates
162
+ */
163
+ async approveToolCall(
164
+ interruptionId: string,
165
+ messageId?: string
166
+ ): Promise<void> {
167
+ await this._agentManager.approveToolCall(interruptionId);
168
+
169
+ // Update the tool call box to show "Approved" status
170
+ if (messageId) {
171
+ this._updateToolCallBoxStatus(messageId, 'Approved', true);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Rejects a tool call and updates the UI accordingly.
177
+ * @param interruptionId The interruption ID to reject
178
+ * @param messageId Optional message ID for UI updates
179
+ */
180
+ async rejectToolCall(
181
+ interruptionId: string,
182
+ messageId?: string
183
+ ): Promise<void> {
184
+ await this._agentManager.rejectToolCall(interruptionId);
185
+
186
+ // Update the tool call box to show "Rejected" status
187
+ if (messageId) {
188
+ this._updateToolCallBoxStatus(messageId, 'Rejected', false);
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Approves all tools in a group.
194
+ * @param groupId The group ID containing the tool calls
195
+ * @param interruptionIds Array of interruption IDs to approve
196
+ * @param messageId Optional message ID for UI updates
197
+ */
198
+ async approveGroupedToolCalls(
199
+ groupId: string,
200
+ interruptionIds: string[],
201
+ messageId?: string
202
+ ): Promise<void> {
203
+ await this._agentManager.approveGroupedToolCalls(groupId, interruptionIds);
204
+
205
+ // Update the grouped approval message to show approved status
206
+ if (messageId) {
207
+ this._updateGroupedApprovalStatus(messageId, 'Tools approved', true);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Rejects all tools in a group.
213
+ * @param groupId The group ID containing the tool calls
214
+ * @param interruptionIds Array of interruption IDs to reject
215
+ * @param messageId Optional message ID for UI updates
216
+ */
217
+ async rejectGroupedToolCalls(
218
+ groupId: string,
219
+ interruptionIds: string[],
220
+ messageId?: string
221
+ ): Promise<void> {
222
+ await this._agentManager.rejectGroupedToolCalls(groupId, interruptionIds);
223
+
224
+ // Update the grouped approval message to show rejected status
225
+ if (messageId) {
226
+ this._updateGroupedApprovalStatus(messageId, 'Tools rejected', false);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Gets the AI user information for system messages.
232
+ */
233
+ private _getAIUser(): IUser {
234
+ return {
235
+ username: 'ai-assistant',
236
+ display_name: 'Jupyternaut',
237
+ initials: 'JN',
238
+ color: '#2196F3',
239
+ avatar_url: AI_AVATAR
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Handles settings changes and updates chat configuration accordingly.
245
+ */
246
+ private _onSettingsChanged(): void {
247
+ const config = this._settingsModel.config;
248
+ this.config = {
249
+ ...config,
250
+ enableCodeToolbar: true
251
+ };
252
+ // Agent manager handles agent recreation automatically via its own settings listener
253
+ }
254
+
255
+ /**
256
+ * Handles events emitted by the agent manager.
257
+ * @param event The event data containing type and payload
258
+ */
259
+ private _onAgentEvent(_sender: AgentManager, event: IAgentEvent): void {
260
+ switch (event.type) {
261
+ case 'message_start':
262
+ this._handleMessageStart(event);
263
+ break;
264
+ case 'message_chunk':
265
+ this._handleMessageChunk(event);
266
+ break;
267
+ case 'message_complete':
268
+ this._handleMessageComplete(event);
269
+ break;
270
+ case 'tool_call_start':
271
+ this._handleToolCallStartEvent(event);
272
+ break;
273
+ case 'tool_call_complete':
274
+ this._handleToolCallCompleteEvent(event);
275
+ break;
276
+ case 'tool_approval_required':
277
+ this._handleToolApprovalRequired(event);
278
+ break;
279
+ case 'grouped_approval_required':
280
+ this._handleGroupedApprovalRequired(event);
281
+ break;
282
+ case 'error':
283
+ this._handleErrorEvent(event);
284
+ break;
285
+ }
286
+ }
287
+
288
+ /**
289
+ * Handles the start of a new message from the AI agent.
290
+ * @param event Event containing the message start data
291
+ */
292
+ private _handleMessageStart(event: IAgentEvent<'message_start'>): void {
293
+ const aiMessage: IChatMessage = {
294
+ body: '',
295
+ sender: this._getAIUser(),
296
+ id: event.data.messageId,
297
+ time: Date.now() / 1000,
298
+ type: 'msg',
299
+ raw_time: false
300
+ };
301
+ this._currentStreamingMessage = aiMessage;
302
+ this.messageAdded(aiMessage);
303
+ }
304
+
305
+ /**
306
+ * Handles streaming message chunks from the AI agent.
307
+ * @param event Event containing the message chunk data
308
+ */
309
+ private _handleMessageChunk(event: IAgentEvent<'message_chunk'>): void {
310
+ if (
311
+ this._currentStreamingMessage &&
312
+ this._currentStreamingMessage.id === event.data.messageId
313
+ ) {
314
+ this._currentStreamingMessage.body = event.data.fullContent;
315
+ this.messageAdded(this._currentStreamingMessage);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Handles the completion of a message from the AI agent.
321
+ * @param event Event containing the message completion data
322
+ */
323
+ private _handleMessageComplete(event: IAgentEvent<'message_complete'>): void {
324
+ if (
325
+ this._currentStreamingMessage &&
326
+ this._currentStreamingMessage.id === event.data.messageId
327
+ ) {
328
+ this._currentStreamingMessage.body = event.data.content;
329
+ this.messageAdded(this._currentStreamingMessage);
330
+ this._currentStreamingMessage = null;
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Handles the start of a tool call execution.
336
+ * @param event Event containing the tool call start data
337
+ */
338
+ private _handleToolCallStartEvent(
339
+ event: IAgentEvent<'tool_call_start'>
340
+ ): void {
341
+ const toolCallMessageId = UUID.uuid4();
342
+ const toolCallMessage: IChatMessage = {
343
+ body: `<details class="jp-ai-tool-call jp-ai-tool-pending">
344
+ <summary class="jp-ai-tool-header">
345
+ <div class="jp-ai-tool-icon">⚡</div>
346
+ <div class="jp-ai-tool-title">${event.data.toolName}</div>
347
+ <div class="jp-ai-tool-status jp-ai-tool-status-pending">Running...</div>
348
+ </summary>
349
+ <div class="jp-ai-tool-body">
350
+ <div class="jp-ai-tool-section">
351
+ <div class="jp-ai-tool-label">Input</div>
352
+ <pre class="jp-ai-tool-code"><code>${JSON.stringify(event.data.input, null, 2)}</code></pre>
353
+ </div>
354
+ </div>
355
+ </details>`,
356
+ sender: this._getAIUser(),
357
+ id: toolCallMessageId,
358
+ time: Date.now() / 1000,
359
+ type: 'msg',
360
+ raw_time: false
361
+ };
362
+
363
+ if (event.data.callId) {
364
+ this._pendingToolCalls.set(event.data.callId, toolCallMessageId);
365
+ }
366
+ this.messageAdded(toolCallMessage);
367
+ }
368
+
369
+ /**
370
+ * Handles the completion of a tool call execution.
371
+ * @param event Event containing the tool call completion data
372
+ */
373
+ private _handleToolCallCompleteEvent(
374
+ event: IAgentEvent<'tool_call_complete'>
375
+ ): void {
376
+ const messageId = this._pendingToolCalls.get(event.data.callId);
377
+ if (messageId) {
378
+ const existingMessageIndex = this.messages.findIndex(
379
+ msg => msg.id === messageId
380
+ );
381
+ if (existingMessageIndex !== -1) {
382
+ const existingMessage = this.messages[existingMessageIndex];
383
+ const inputJson =
384
+ existingMessage.body.match(/<code>([\s\S]*?)<\/code>/)?.[1] || '';
385
+
386
+ const statusClass = event.data.isError
387
+ ? 'jp-ai-tool-error'
388
+ : 'jp-ai-tool-completed';
389
+ const statusText = event.data.isError ? 'Error' : 'Completed';
390
+ const statusColor = event.data.isError
391
+ ? 'jp-ai-tool-status-error'
392
+ : 'jp-ai-tool-status-completed';
393
+
394
+ const updatedMessage: IChatMessage = {
395
+ ...existingMessage,
396
+ body: `<details class="jp-ai-tool-call ${statusClass}">
397
+ <summary class="jp-ai-tool-header">
398
+ <div class="jp-ai-tool-icon">⚡</div>
399
+ <div class="jp-ai-tool-title">${event.data.toolName}</div>
400
+ <div class="jp-ai-tool-status ${statusColor}">${statusText}</div>
401
+ </summary>
402
+ <div class="jp-ai-tool-body">
403
+ <div class="jp-ai-tool-section">
404
+ <div class="jp-ai-tool-label">Input</div>
405
+ <pre class="jp-ai-tool-code"><code>${inputJson}</code></pre>
406
+ </div>
407
+ <div class="jp-ai-tool-section">
408
+ <div class="jp-ai-tool-label">${event.data.isError ? 'Error' : 'Result'}</div>
409
+ <pre class="jp-ai-tool-code"><code>${event.data.output}</code></pre>
410
+ </div>
411
+ </div>
412
+ </details>`
413
+ };
414
+
415
+ this.messageAdded(updatedMessage);
416
+ this._pendingToolCalls.delete(event.data.callId);
417
+ }
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Handles tool approval requests from the AI agent.
423
+ * @param event Event containing the tool approval request data
424
+ */
425
+ private _handleToolApprovalRequired(
426
+ event: IAgentEvent<'tool_approval_required'>
427
+ ): void {
428
+ // Handle single tool approval - either update existing tool call message or create new approval message
429
+ if (event.data.callId) {
430
+ const messageId = this._pendingToolCalls.get(event.data.callId);
431
+ if (messageId) {
432
+ const existingMessageIndex = this.messages.findIndex(
433
+ msg => msg.id === messageId
434
+ );
435
+ if (existingMessageIndex !== -1) {
436
+ const existingMessage = this.messages[existingMessageIndex];
437
+ const assistantName = this._getAIUser().display_name;
438
+
439
+ const updatedMessage: IChatMessage = {
440
+ ...existingMessage,
441
+ body: `<details class="jp-ai-tool-call jp-ai-tool-pending" open>
442
+ <summary class="jp-ai-tool-header">
443
+ <div class="jp-ai-tool-icon">⚡</div>
444
+ <div class="jp-ai-tool-title">${event.data.toolName}</div>
445
+ <div class="jp-ai-tool-status jp-ai-tool-status-pending">Needs Approval</div>
446
+ </summary>
447
+ <div class="jp-ai-tool-body">
448
+ <div class="jp-ai-tool-section">
449
+ <div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
450
+ <pre class="jp-ai-tool-code"><code>${JSON.stringify(event.data.toolInput, null, 2)}</code></pre>
451
+ </div>
452
+ [APPROVAL_BUTTONS:${event.data.interruptionId}]
453
+ </div>
454
+ </details>`
455
+ };
456
+
457
+ this.messageAdded(updatedMessage);
458
+ this.updateWriters([]);
459
+ return;
460
+ }
461
+ }
462
+ }
463
+
464
+ // Fallback: create separate approval message
465
+ const approvalMessageId = UUID.uuid4();
466
+ const assistantName = this._getAIUser().display_name;
467
+
468
+ const approvalMessage: IChatMessage = {
469
+ body: `**🤖 Tool Approval Required: ${event.data.toolName}**
470
+
471
+ ${assistantName} wants to execute this tool. Do you approve?
472
+
473
+ ${JSON.stringify(event.data.toolInput, null, 2)}
474
+
475
+ [APPROVAL_BUTTONS:${event.data.interruptionId}]`,
476
+ sender: this._getAIUser(),
477
+ id: approvalMessageId,
478
+ time: Date.now() / 1000,
479
+ type: 'msg',
480
+ raw_time: false
481
+ };
482
+
483
+ this.messageAdded(approvalMessage);
484
+ this.updateWriters([]); // Stop showing "AI is writing"
485
+ }
486
+
487
+ /**
488
+ * Handles grouped tool approval requests from the AI agent.
489
+ * @param event Event containing the grouped tool approval request data
490
+ */
491
+ private _handleGroupedApprovalRequired(
492
+ event: IAgentEvent<'grouped_approval_required'>
493
+ ): void {
494
+ const assistantName = this._getAIUser().display_name;
495
+ const approvalMessageId = UUID.uuid4();
496
+
497
+ const toolsList = event.data.approvals
498
+ .map(
499
+ (info, index) =>
500
+ `**${index + 1}. ${info.toolName}**\n${JSON.stringify(info.toolInput, null, 2)}\n`
501
+ )
502
+ .join('\n\n');
503
+
504
+ const approvalMessage: IChatMessage = {
505
+ body: `**🤖 Multiple Tool Approvals Required**
506
+
507
+ ${assistantName} wants to execute ${event.data.approvals.length} tools. Do you approve?
508
+
509
+ ${toolsList}
510
+
511
+ [GROUP_APPROVAL_BUTTONS:${event.data.groupId}:${event.data.approvals.map(info => info.interruptionId).join(',')}]`,
512
+ sender: this._getAIUser(),
513
+ id: approvalMessageId,
514
+ time: Date.now() / 1000,
515
+ type: 'msg',
516
+ raw_time: false
517
+ };
518
+
519
+ this.messageAdded(approvalMessage);
520
+ this.updateWriters([]); // Stop showing "AI is writing"
521
+ }
522
+
523
+ /**
524
+ * Handles error events from the AI agent.
525
+ * @param event Event containing the error information
526
+ */
527
+ private _handleErrorEvent(event: IAgentEvent<'error'>): void {
528
+ const errorMessage: IChatMessage = {
529
+ body: `Error generating response: ${event.data.error.message}`,
530
+ sender: this._getAIUser(),
531
+ id: UUID.uuid4(),
532
+ time: Date.now() / 1000,
533
+ type: 'msg',
534
+ raw_time: false
535
+ };
536
+ this.messageAdded(errorMessage);
537
+ }
538
+
539
+ /**
540
+ * Processes file attachments and returns their content as formatted strings.
541
+ * @param attachments Array of file attachments to process
542
+ * @returns Array of formatted attachment contents
543
+ */
544
+ private async _processAttachments(
545
+ attachments: IAttachment[]
546
+ ): Promise<string[]> {
547
+ const contents: string[] = [];
548
+
549
+ for (const attachment of attachments) {
550
+ try {
551
+ const fileContent = await this._readFileAttachment(attachment);
552
+ if (fileContent) {
553
+ // Get file extension for syntax highlighting
554
+ const fileExtension = PathExt.extname(attachment.value).toLowerCase();
555
+ const language = fileExtension === '.ipynb' ? 'json' : '';
556
+ contents.push(
557
+ `**File: ${attachment.value}**\n\`\`\`${language}\n${fileContent}\n\`\`\``
558
+ );
559
+ }
560
+ } catch (error) {
561
+ console.warn(`Failed to read attachment ${attachment.value}:`, error);
562
+ contents.push(`**File: ${attachment.value}** (Could not read file)`);
563
+ }
564
+ }
565
+
566
+ return contents;
567
+ }
568
+
569
+ /**
570
+ * Reads the content of a file attachment.
571
+ * @param attachment The file attachment to read
572
+ * @returns File content as string or null if unable to read
573
+ */
574
+ private async _readFileAttachment(
575
+ attachment: IAttachment
576
+ ): Promise<string | null> {
577
+ // Handle both 'file' and 'notebook' types since both have a 'value' path
578
+ if (attachment.type !== 'file' && attachment.type !== 'notebook') {
579
+ return null;
580
+ }
581
+
582
+ try {
583
+ const model = await this.input.documentManager?.services.contents.get(
584
+ attachment.value
585
+ );
586
+ if (!model?.content) {
587
+ return null;
588
+ }
589
+ if (model.type === 'file') {
590
+ // Regular file content
591
+ return model.content;
592
+ } else if (model.type === 'notebook') {
593
+ // Clear outputs from notebook cells before sending to LLM
594
+ // TODO: make this configurable?
595
+ const cells = model.content.cells.map((cell: any) => {
596
+ const cleanCell = { ...cell };
597
+ if (cleanCell.outputs) {
598
+ cleanCell.outputs = [];
599
+ }
600
+ if (cleanCell.execution_count) {
601
+ cleanCell.execution_count = null;
602
+ }
603
+ return cleanCell;
604
+ });
605
+
606
+ const notebookModel = {
607
+ cells,
608
+ metadata: (model as any).metadata || {},
609
+ nbformat: (model as any).nbformat || 4,
610
+ nbformat_minor: (model as any).nbformat_minor || 4
611
+ };
612
+ return JSON.stringify(notebookModel);
613
+ }
614
+ return null;
615
+ } catch (error) {
616
+ console.warn(`Failed to read file ${attachment.value}:`, error);
617
+ return null;
618
+ }
619
+ }
620
+
621
+ /**
622
+ * Updates the status display of a grouped approval message.
623
+ * @param messageId The message ID to update
624
+ * @param status The status text to display
625
+ * @param isSuccess Whether the action was successful
626
+ */
627
+ private _updateGroupedApprovalStatus(
628
+ messageId: string,
629
+ status: string,
630
+ isSuccess: boolean
631
+ ): void {
632
+ const existingMessageIndex = this.messages.findIndex(
633
+ msg => msg.id === messageId
634
+ );
635
+ if (existingMessageIndex !== -1) {
636
+ const existingMessage = this.messages[existingMessageIndex];
637
+
638
+ // Extract tool count and names from existing message
639
+ const toolCountMatch = existingMessage.body.match(/execute (\d+) tools/);
640
+ const toolCount = toolCountMatch ? toolCountMatch[1] : 'multiple';
641
+
642
+ const statusIcon = isSuccess ? '✅' : '❌';
643
+ const statusClass = isSuccess ? 'approved' : 'rejected';
644
+
645
+ const updatedMessage: IChatMessage = {
646
+ ...existingMessage,
647
+ body: `**${statusIcon} Group Tool Approval: ${status}**
648
+
649
+ The request to execute ${toolCount} tools has been **${statusClass}**.
650
+
651
+ <div class="jp-ai-group-approval-${statusClass}">
652
+ Status: ${status}
653
+ </div>`
654
+ };
655
+
656
+ this.messageAdded(updatedMessage);
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Updates the status display of a tool call box.
662
+ * @param messageId The message ID to update
663
+ * @param status The status text to display
664
+ * @param isSuccess Whether the action was successful
665
+ */
666
+ private _updateToolCallBoxStatus(
667
+ messageId: string,
668
+ status: string,
669
+ isSuccess: boolean
670
+ ): void {
671
+ const existingMessageIndex = this.messages.findIndex(
672
+ msg => msg.id === messageId
673
+ );
674
+ if (existingMessageIndex !== -1) {
675
+ const existingMessage = this.messages[existingMessageIndex];
676
+
677
+ // Extract tool name and input from existing message
678
+ const toolNameMatch = existingMessage.body.match(
679
+ /<div class="jp-ai-tool-title">([^<]+)<\/div>/
680
+ );
681
+ const toolName = toolNameMatch ? toolNameMatch[1] : 'Unknown Tool';
682
+
683
+ const codeMatch = existingMessage.body.match(/<code>([\s\S]*?)<\/code>/);
684
+ const toolInput = codeMatch ? codeMatch[1] : '{}';
685
+
686
+ // Determine styling based on status
687
+ const statusClass = isSuccess
688
+ ? 'jp-ai-tool-completed'
689
+ : 'jp-ai-tool-error';
690
+ const statusColor = isSuccess
691
+ ? 'jp-ai-tool-status-completed'
692
+ : 'jp-ai-tool-status-error';
693
+
694
+ const updatedMessage: IChatMessage = {
695
+ ...existingMessage,
696
+ body: `<details class="jp-ai-tool-call ${statusClass}">
697
+ <summary class="jp-ai-tool-header">
698
+ <div class="jp-ai-tool-icon">⚡</div>
699
+ <div class="jp-ai-tool-title">${toolName}</div>
700
+ <div class="jp-ai-tool-status ${statusColor}">${status}</div>
701
+ </summary>
702
+ <div class="jp-ai-tool-body">
703
+ <div class="jp-ai-tool-section">
704
+ <div class="jp-ai-tool-label">Input</div>
705
+ <pre class="jp-ai-tool-code"><code>${toolInput}</code></pre>
706
+ </div>
707
+ </div>
708
+ </details>`
709
+ };
710
+
711
+ this.messageAdded(updatedMessage);
712
+ }
713
+ }
714
+
715
+ // Private fields
716
+ private _settingsModel: AISettingsModel;
717
+ private _user: IUser;
718
+ private _pendingToolCalls: Map<string, string> = new Map();
719
+ private _agentManager: AgentManager;
720
+ private _currentStreamingMessage: IChatMessage | null = null;
721
+ }
722
+
723
+ /**
724
+ * Namespace containing types and interfaces for AIChatModel.
725
+ */
726
+ export namespace AIChatModel {
727
+ /**
728
+ * Configuration options for constructing an AIChatModel instance.
729
+ */
730
+ export interface IOptions {
731
+ /**
732
+ * The user information for the chat
733
+ */
734
+ user: IUser;
735
+ /**
736
+ * Settings model for AI configuration
737
+ */
738
+ settingsModel: AISettingsModel;
739
+ /**
740
+ * Optional agent manager for handling AI agent lifecycle
741
+ */
742
+ agentManager: AgentManager;
743
+ /**
744
+ * Optional active cell manager for Jupyter integration
745
+ */
746
+ activeCellManager?: IActiveCellManager;
747
+ /**
748
+ * Optional document manager for file operations
749
+ */
750
+ documentManager?: IDocumentManager;
751
+ }
752
+
753
+ /**
754
+ * The chat context for toolbar buttons.
755
+ */
756
+ export interface IAIChatContext extends IChatContext {
757
+ /**
758
+ * The stop streaming callback.
759
+ */
760
+ stopStreaming: () => void;
761
+ /**
762
+ * The clear messages callback.
763
+ */
764
+ clearMessages: () => void;
765
+ /**
766
+ * The agent manager of the chat.
767
+ */
768
+ agentManager: AgentManager;
769
+ }
770
+ }