@jupyterlite/ai 0.8.1 → 0.9.0-a1

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