@jupyterlite/ai 0.9.1 → 0.11.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 (71) hide show
  1. package/README.md +5 -214
  2. package/lib/agent.d.ts +58 -66
  3. package/lib/agent.js +291 -310
  4. package/lib/approval-buttons.d.ts +19 -82
  5. package/lib/approval-buttons.js +36 -289
  6. package/lib/chat-model-registry.d.ts +6 -0
  7. package/lib/chat-model-registry.js +4 -1
  8. package/lib/chat-model.d.ts +26 -54
  9. package/lib/chat-model.js +277 -303
  10. package/lib/components/clear-button.d.ts +6 -1
  11. package/lib/components/clear-button.js +10 -6
  12. package/lib/components/completion-status.d.ts +5 -0
  13. package/lib/components/completion-status.js +5 -4
  14. package/lib/components/model-select.d.ts +6 -1
  15. package/lib/components/model-select.js +13 -16
  16. package/lib/components/stop-button.d.ts +6 -1
  17. package/lib/components/stop-button.js +12 -8
  18. package/lib/components/token-usage-display.d.ts +5 -0
  19. package/lib/components/token-usage-display.js +2 -2
  20. package/lib/components/tool-select.d.ts +6 -1
  21. package/lib/components/tool-select.js +10 -9
  22. package/lib/index.d.ts +1 -0
  23. package/lib/index.js +61 -81
  24. package/lib/models/settings-model.d.ts +1 -1
  25. package/lib/models/settings-model.js +40 -26
  26. package/lib/providers/built-in-providers.js +38 -19
  27. package/lib/providers/models.d.ts +3 -3
  28. package/lib/providers/provider-registry.d.ts +3 -4
  29. package/lib/providers/provider-registry.js +1 -4
  30. package/lib/tokens.d.ts +5 -6
  31. package/lib/tools/commands.d.ts +2 -1
  32. package/lib/tools/commands.js +36 -49
  33. package/lib/widgets/ai-settings.d.ts +6 -0
  34. package/lib/widgets/ai-settings.js +72 -71
  35. package/lib/widgets/main-area-chat.d.ts +2 -0
  36. package/lib/widgets/main-area-chat.js +5 -2
  37. package/lib/widgets/provider-config-dialog.d.ts +2 -0
  38. package/lib/widgets/provider-config-dialog.js +34 -34
  39. package/package.json +13 -13
  40. package/schema/settings-model.json +3 -2
  41. package/src/agent.ts +360 -372
  42. package/src/approval-buttons.ts +43 -389
  43. package/src/chat-model-registry.ts +9 -1
  44. package/src/chat-model.ts +399 -370
  45. package/src/completion/completion-provider.ts +2 -3
  46. package/src/components/clear-button.tsx +18 -6
  47. package/src/components/completion-status.tsx +18 -4
  48. package/src/components/model-select.tsx +25 -16
  49. package/src/components/stop-button.tsx +22 -9
  50. package/src/components/token-usage-display.tsx +14 -2
  51. package/src/components/tool-select.tsx +27 -9
  52. package/src/index.ts +78 -134
  53. package/src/models/settings-model.ts +41 -27
  54. package/src/providers/built-in-providers.ts +38 -19
  55. package/src/providers/models.ts +3 -3
  56. package/src/providers/provider-registry.ts +4 -8
  57. package/src/tokens.ts +5 -6
  58. package/src/tools/commands.ts +40 -53
  59. package/src/widgets/ai-settings.tsx +153 -84
  60. package/src/widgets/main-area-chat.ts +8 -2
  61. package/src/widgets/provider-config-dialog.tsx +54 -41
  62. package/style/base.css +24 -73
  63. package/lib/mcp/browser.d.ts +0 -68
  64. package/lib/mcp/browser.js +0 -138
  65. package/lib/tools/file.d.ts +0 -36
  66. package/lib/tools/file.js +0 -351
  67. package/lib/tools/notebook.d.ts +0 -40
  68. package/lib/tools/notebook.js +0 -779
  69. package/src/mcp/browser.ts +0 -220
  70. package/src/tools/file.ts +0 -438
  71. package/src/tools/notebook.ts +0 -986
package/src/chat-model.ts CHANGED
@@ -12,6 +12,12 @@ import { PathExt } from '@jupyterlab/coreutils';
12
12
 
13
13
  import { IDocumentManager } from '@jupyterlab/docmanager';
14
14
 
15
+ import { IDocumentWidget } from '@jupyterlab/docregistry';
16
+
17
+ import { INotebookModel, Notebook } from '@jupyterlab/notebook';
18
+
19
+ import { TranslationBundle } from '@jupyterlab/translation';
20
+
15
21
  import { UUID } from '@lumino/coreutils';
16
22
 
17
23
  import { ISignal, Signal } from '@lumino/signaling';
@@ -24,12 +30,58 @@ import { AISettingsModel } from './models/settings-model';
24
30
 
25
31
  import { ITokenUsage } from './tokens';
26
32
 
33
+ import { YNotebook } from '@jupyter/ydoc';
34
+
27
35
  import * as nbformat from '@jupyterlab/nbformat';
28
36
 
29
37
  /**
30
- * AI Chat Model implementation that provides chat functionality with OpenAI agents,
31
- * tool integration, and MCP server support.
32
- * Extends the base AbstractChatModel to provide AI-powered conversations.
38
+ * Tool call status types.
39
+ */
40
+ type ToolStatus =
41
+ | 'pending'
42
+ | 'awaiting_approval'
43
+ | 'approved'
44
+ | 'rejected'
45
+ | 'completed'
46
+ | 'error';
47
+
48
+ /**
49
+ * Context for tracking tool execution state.
50
+ */
51
+ interface IToolExecutionContext {
52
+ /**
53
+ * The tool call ID from the AI SDK.
54
+ */
55
+ toolCallId: string;
56
+ /**
57
+ * The chat message ID for UI updates.
58
+ */
59
+ messageId: string;
60
+ /**
61
+ * The tool name.
62
+ */
63
+ toolName: string;
64
+ /**
65
+ * The tool input (formatted).
66
+ */
67
+ input: string;
68
+ /**
69
+ * Optional approval ID if awaiting approval.
70
+ */
71
+ approvalId?: string;
72
+ /**
73
+ * Current status.
74
+ */
75
+ status: ToolStatus;
76
+ /**
77
+ * Human-readable summary extracted from tool input for display.
78
+ */
79
+ summary?: string;
80
+ }
81
+
82
+ /**
83
+ * AI Chat Model implementation that provides chat functionality tool integration,
84
+ * and MCP server support.
33
85
  */
34
86
  export class AIChatModel extends AbstractChatModel {
35
87
  /**
@@ -48,6 +100,7 @@ export class AIChatModel extends AbstractChatModel {
48
100
  this._settingsModel = options.settingsModel;
49
101
  this._user = options.user;
50
102
  this._agentManager = options.agentManager;
103
+ this._trans = options.trans;
51
104
 
52
105
  // Listen for agent events
53
106
  this._agentManager.agentEvent.connect(this._onAgentEvent, this);
@@ -123,7 +176,7 @@ export class AIChatModel extends AbstractChatModel {
123
176
  */
124
177
  clearMessages = (): void => {
125
178
  this.messagesDeleted(0, this.messages.length);
126
- this._pendingToolCalls.clear();
179
+ this._toolContexts.clear();
127
180
  this._agentManager.clearHistory();
128
181
  };
129
182
 
@@ -165,7 +218,6 @@ export class AIChatModel extends AbstractChatModel {
165
218
  const attachmentContents = await this._processAttachments(
166
219
  this.input.attachments
167
220
  );
168
- // Clear attachments right after processing
169
221
  this.input.clearAttachments();
170
222
 
171
223
  if (attachmentContents.length > 0) {
@@ -192,78 +244,6 @@ export class AIChatModel extends AbstractChatModel {
192
244
  }
193
245
  }
194
246
 
195
- /**
196
- * Approves a tool call and updates the UI accordingly.
197
- * @param interruptionId The interruption ID to approve
198
- * @param messageId Optional message ID for UI updates
199
- */
200
- async approveToolCall(
201
- interruptionId: string,
202
- messageId?: string
203
- ): Promise<void> {
204
- await this._agentManager.approveToolCall(interruptionId);
205
-
206
- // Update the tool call box to show "Approved" status
207
- if (messageId) {
208
- this._updateToolCallBoxStatus(messageId, 'Approved', true);
209
- }
210
- }
211
-
212
- /**
213
- * Rejects a tool call and updates the UI accordingly.
214
- * @param interruptionId The interruption ID to reject
215
- * @param messageId Optional message ID for UI updates
216
- */
217
- async rejectToolCall(
218
- interruptionId: string,
219
- messageId?: string
220
- ): Promise<void> {
221
- await this._agentManager.rejectToolCall(interruptionId);
222
-
223
- // Update the tool call box to show "Rejected" status
224
- if (messageId) {
225
- this._updateToolCallBoxStatus(messageId, 'Rejected', false);
226
- }
227
- }
228
-
229
- /**
230
- * Approves all tools in a group.
231
- * @param groupId The group ID containing the tool calls
232
- * @param interruptionIds Array of interruption IDs to approve
233
- * @param messageId Optional message ID for UI updates
234
- */
235
- async approveGroupedToolCalls(
236
- groupId: string,
237
- interruptionIds: string[],
238
- messageId?: string
239
- ): Promise<void> {
240
- await this._agentManager.approveGroupedToolCalls(groupId, interruptionIds);
241
-
242
- // Update the grouped approval message to show approved status
243
- if (messageId) {
244
- this._updateGroupedApprovalStatus(messageId, 'Tools approved', true);
245
- }
246
- }
247
-
248
- /**
249
- * Rejects all tools in a group.
250
- * @param groupId The group ID containing the tool calls
251
- * @param interruptionIds Array of interruption IDs to reject
252
- * @param messageId Optional message ID for UI updates
253
- */
254
- async rejectGroupedToolCalls(
255
- groupId: string,
256
- interruptionIds: string[],
257
- messageId?: string
258
- ): Promise<void> {
259
- await this._agentManager.rejectGroupedToolCalls(groupId, interruptionIds);
260
-
261
- // Update the grouped approval message to show rejected status
262
- if (messageId) {
263
- this._updateGroupedApprovalStatus(messageId, 'Tools rejected', false);
264
- }
265
- }
266
-
267
247
  /**
268
248
  * Gets the AI user information for system messages.
269
249
  */
@@ -307,11 +287,11 @@ export class AIChatModel extends AbstractChatModel {
307
287
  case 'tool_call_complete':
308
288
  this._handleToolCallCompleteEvent(event);
309
289
  break;
310
- case 'tool_approval_required':
311
- this._handleToolApprovalRequired(event);
290
+ case 'tool_approval_request':
291
+ this._handleToolApprovalRequest(event);
312
292
  break;
313
- case 'grouped_approval_required':
314
- this._handleGroupedApprovalRequired(event);
293
+ case 'tool_approval_resolved':
294
+ this._handleToolApprovalResolved(event);
315
295
  break;
316
296
  case 'error':
317
297
  this._handleErrorEvent(event);
@@ -365,6 +345,34 @@ export class AIChatModel extends AbstractChatModel {
365
345
  }
366
346
  }
367
347
 
348
+ /**
349
+ * Extracts a human-readable summary from tool input for display in the header.
350
+ * @param toolName The name of the tool being called
351
+ * @param input The formatted JSON input string
352
+ * @returns A short summary string or empty string if none available
353
+ */
354
+ private _extractToolSummary(toolName: string, input: string): string {
355
+ try {
356
+ const parsedInput = JSON.parse(input);
357
+
358
+ switch (toolName) {
359
+ case 'execute_command':
360
+ if (parsedInput.commandId) {
361
+ return parsedInput.commandId;
362
+ }
363
+ break;
364
+ case 'discover_commands':
365
+ if (parsedInput.query) {
366
+ return `query: "${parsedInput.query}"`;
367
+ }
368
+ break;
369
+ }
370
+ } catch {
371
+ // If parsing fails, return empty string
372
+ }
373
+ return '';
374
+ }
375
+
368
376
  /**
369
377
  * Handles the start of a tool call execution.
370
378
  * @param event Event containing the tool call start data
@@ -372,204 +380,134 @@ export class AIChatModel extends AbstractChatModel {
372
380
  private _handleToolCallStartEvent(
373
381
  event: IAgentEvent<'tool_call_start'>
374
382
  ): void {
375
- const toolCallMessageId = UUID.uuid4();
383
+ const messageId = UUID.uuid4();
384
+ const summary = this._extractToolSummary(
385
+ event.data.toolName,
386
+ event.data.input
387
+ );
388
+ const context: IToolExecutionContext = {
389
+ toolCallId: event.data.callId,
390
+ messageId,
391
+ toolName: event.data.toolName,
392
+ input: event.data.input,
393
+ status: 'pending',
394
+ summary
395
+ };
396
+
397
+ this._toolContexts.set(event.data.callId, context);
398
+
376
399
  const toolCallMessage: IChatMessage = {
377
- body: `<details class="jp-ai-tool-call jp-ai-tool-pending">
378
- <summary class="jp-ai-tool-header">
379
- <div class="jp-ai-tool-icon">⚡</div>
380
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
381
- <div class="jp-ai-tool-status jp-ai-tool-status-pending">Running...</div>
382
- </summary>
383
- <div class="jp-ai-tool-body">
384
- <div class="jp-ai-tool-section">
385
- <div class="jp-ai-tool-label">Input</div>
386
- <pre class="jp-ai-tool-code"><code>${event.data.input}</code></pre>
387
- </div>
388
- </div>
389
- </details>`,
400
+ body: Private.buildToolCallHtml({
401
+ toolName: context.toolName,
402
+ input: context.input,
403
+ status: context.status,
404
+ summary: context.summary,
405
+ trans: this._trans
406
+ }),
390
407
  sender: this._getAIUser(),
391
- id: toolCallMessageId,
408
+ id: messageId,
392
409
  time: Date.now() / 1000,
393
410
  type: 'msg',
394
411
  raw_time: false
395
412
  };
396
413
 
397
- if (event.data.callId) {
398
- this._pendingToolCalls.set(event.data.callId, toolCallMessageId);
399
- }
400
414
  this.messageAdded(toolCallMessage);
401
415
  }
402
416
 
403
417
  /**
404
418
  * Handles the completion of a tool call execution.
405
- * @param event Event containing the tool call completion data
406
419
  */
407
420
  private _handleToolCallCompleteEvent(
408
421
  event: IAgentEvent<'tool_call_complete'>
409
422
  ): void {
410
- const messageId = this._pendingToolCalls.get(event.data.callId);
411
- if (messageId) {
412
- const existingMessageIndex = this.messages.findIndex(
413
- msg => msg.id === messageId
414
- );
415
- if (existingMessageIndex !== -1) {
416
- const existingMessage = this.messages[existingMessageIndex];
417
- const inputJson =
418
- existingMessage.body.match(/<code>([\s\S]*?)<\/code>/)?.[1] || '';
419
-
420
- const statusClass = event.data.isError
421
- ? 'jp-ai-tool-error'
422
- : 'jp-ai-tool-completed';
423
- const statusText = event.data.isError ? 'Error' : 'Completed';
424
- const statusColor = event.data.isError
425
- ? 'jp-ai-tool-status-error'
426
- : 'jp-ai-tool-status-completed';
427
-
428
- const updatedMessage: IChatMessage = {
429
- ...existingMessage,
430
- body: `<details class="jp-ai-tool-call ${statusClass}">
431
- <summary class="jp-ai-tool-header">
432
- <div class="jp-ai-tool-icon">⚡</div>
433
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
434
- <div class="jp-ai-tool-status ${statusColor}">${statusText}</div>
435
- </summary>
436
- <div class="jp-ai-tool-body">
437
- <div class="jp-ai-tool-section">
438
- <div class="jp-ai-tool-label">Input</div>
439
- <pre class="jp-ai-tool-code"><code>${inputJson}</code></pre>
440
- </div>
441
- <div class="jp-ai-tool-section">
442
- <div class="jp-ai-tool-label">${event.data.isError ? 'Error' : 'Result'}</div>
443
- <pre class="jp-ai-tool-code"><code>${event.data.output}</code></pre>
444
- </div>
445
- </div>
446
- </details>`
447
- };
448
-
449
- this.messageAdded(updatedMessage);
450
- this._pendingToolCalls.delete(event.data.callId);
451
- }
452
- }
423
+ const status = event.data.isError ? 'error' : 'completed';
424
+ this._updateToolCallUI(event.data.callId, status, event.data.output);
425
+ this._toolContexts.delete(event.data.callId);
453
426
  }
454
427
 
455
428
  /**
456
- * Handles tool approval requests from the AI agent.
457
- * @param event Event containing the tool approval request data
429
+ * Handles error events from the AI agent.
458
430
  */
459
- private _handleToolApprovalRequired(
460
- event: IAgentEvent<'tool_approval_required'>
461
- ): void {
462
- // Handle single tool approval - either update existing tool call message or create new approval message
463
- if (event.data.callId) {
464
- const messageId = this._pendingToolCalls.get(event.data.callId);
465
- if (messageId) {
466
- const existingMessageIndex = this.messages.findIndex(
467
- msg => msg.id === messageId
468
- );
469
- if (existingMessageIndex !== -1) {
470
- const existingMessage = this.messages[existingMessageIndex];
471
- const assistantName = this._getAIUser().display_name;
472
-
473
- const updatedMessage: IChatMessage = {
474
- ...existingMessage,
475
- body: `<details class="jp-ai-tool-call jp-ai-tool-pending" open>
476
- <summary class="jp-ai-tool-header">
477
- <div class="jp-ai-tool-icon">⚡</div>
478
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
479
- <div class="jp-ai-tool-status jp-ai-tool-status-pending">Needs Approval</div>
480
- </summary>
481
- <div class="jp-ai-tool-body">
482
- <div class="jp-ai-tool-section">
483
- <div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
484
- <pre class="jp-ai-tool-code"><code>${event.data.toolInput}</code></pre>
485
- </div>
486
- [APPROVAL_BUTTONS:${event.data.interruptionId}]
487
- </div>
488
- </details>`
489
- };
490
-
491
- this.messageAdded(updatedMessage);
492
- this.updateWriters([]);
493
- return;
494
- }
495
- }
496
- }
497
-
498
- // Fallback: create separate approval message
499
- const approvalMessageId = UUID.uuid4();
500
- const assistantName = this._getAIUser().display_name;
501
-
502
- const approvalMessage: IChatMessage = {
503
- body: `**🤖 Tool Approval Required: ${event.data.toolName}**
504
-
505
- ${assistantName} wants to execute this tool. Do you approve?
506
-
507
- \`\`\`json
508
- ${event.data.toolInput}
509
- \`\`\`
510
-
511
- [APPROVAL_BUTTONS:${event.data.interruptionId}]`,
431
+ private _handleErrorEvent(event: IAgentEvent<'error'>): void {
432
+ this.messageAdded({
433
+ body: `Error generating response: ${event.data.error.message}`,
512
434
  sender: this._getAIUser(),
513
- id: approvalMessageId,
435
+ id: UUID.uuid4(),
514
436
  time: Date.now() / 1000,
515
437
  type: 'msg',
516
438
  raw_time: false
517
- };
518
-
519
- this.messageAdded(approvalMessage);
520
- this.updateWriters([]); // Stop showing "AI is writing"
439
+ });
521
440
  }
522
441
 
523
442
  /**
524
- * Handles grouped tool approval requests from the AI agent.
525
- * @param event Event containing the grouped tool approval request data
443
+ * Handles tool approval request events from the AI agent.
526
444
  */
527
- private _handleGroupedApprovalRequired(
528
- event: IAgentEvent<'grouped_approval_required'>
445
+ private _handleToolApprovalRequest(
446
+ event: IAgentEvent<'tool_approval_request'>
529
447
  ): void {
530
- const assistantName = this._getAIUser().display_name;
531
- const approvalMessageId = UUID.uuid4();
532
-
533
- const toolsList = event.data.approvals
534
- .map(
535
- (info, index) =>
536
- `**${index + 1}. ${info.toolName}**\n\`\`\`json\n${info.toolInput}\n\`\`\`\n`
537
- )
538
- .join('\n\n');
539
-
540
- const approvalMessage: IChatMessage = {
541
- body: `**🤖 Multiple Tool Approvals Required**
542
-
543
- ${assistantName} wants to execute ${event.data.approvals.length} tools. Do you approve?
448
+ const context = this._toolContexts.get(event.data.toolCallId);
449
+ if (!context) {
450
+ return;
451
+ }
452
+ context.approvalId = event.data.approvalId;
453
+ context.input = JSON.stringify(event.data.args, null, 2);
454
+ this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
455
+ }
544
456
 
545
- ${toolsList}
457
+ /**
458
+ * Handles tool approval resolved events from the AI agent.
459
+ */
460
+ private _handleToolApprovalResolved(
461
+ event: IAgentEvent<'tool_approval_resolved'>
462
+ ): void {
463
+ const context = Array.from(this._toolContexts.values()).find(
464
+ ctx => ctx.approvalId === event.data.approvalId
465
+ );
466
+ if (!context) {
467
+ return;
468
+ }
546
469
 
547
- [GROUP_APPROVAL_BUTTONS:${event.data.groupId}:${event.data.approvals.map(info => info.interruptionId).join(',')}]`,
548
- sender: this._getAIUser(),
549
- id: approvalMessageId,
550
- time: Date.now() / 1000,
551
- type: 'msg',
552
- raw_time: false
553
- };
470
+ const status = event.data.approved ? 'approved' : 'rejected';
471
+ this._updateToolCallUI(context.toolCallId, status);
554
472
 
555
- this.messageAdded(approvalMessage);
556
- this.updateWriters([]); // Stop showing "AI is writing"
473
+ if (!event.data.approved) {
474
+ this._toolContexts.delete(context.toolCallId);
475
+ }
557
476
  }
558
477
 
559
478
  /**
560
- * Handles error events from the AI agent.
561
- * @param event Event containing the error information
479
+ * Updates a tool call's UI with new status and optional output.
562
480
  */
563
- private _handleErrorEvent(event: IAgentEvent<'error'>): void {
564
- const errorMessage: IChatMessage = {
565
- body: `Error generating response: ${event.data.error.message}`,
566
- sender: this._getAIUser(),
567
- id: UUID.uuid4(),
568
- time: Date.now() / 1000,
569
- type: 'msg',
570
- raw_time: false
571
- };
572
- this.messageAdded(errorMessage);
481
+ private _updateToolCallUI(
482
+ toolCallId: string,
483
+ status: ToolStatus,
484
+ output?: string
485
+ ): void {
486
+ const context = this._toolContexts.get(toolCallId);
487
+ if (!context) {
488
+ return;
489
+ }
490
+
491
+ const existingMessage = this.messages.find(
492
+ msg => msg.id === context.messageId
493
+ );
494
+ if (!existingMessage) {
495
+ return;
496
+ }
497
+
498
+ context.status = status;
499
+ this.messageAdded({
500
+ ...existingMessage,
501
+ body: Private.buildToolCallHtml({
502
+ toolName: context.toolName,
503
+ input: context.input,
504
+ status: context.status,
505
+ summary: context.summary,
506
+ output,
507
+ approvalId: context.approvalId,
508
+ trans: this._trans
509
+ })
510
+ });
573
511
  }
574
512
 
575
513
  /**
@@ -623,23 +561,45 @@ ${toolsList}
623
561
  }
624
562
 
625
563
  try {
626
- const model = await this.input.documentManager?.services.contents.get(
564
+ // Try reading from live notebook if open
565
+ const widget = this.input.documentManager?.findWidget(
627
566
  attachment.value
628
- );
629
- if (!model || model.type !== 'notebook') {
630
- return null;
631
- }
567
+ ) as IDocumentWidget<Notebook, INotebookModel> | undefined;
568
+ let cellData: nbformat.ICell[];
569
+ let kernelLang = 'text';
570
+
571
+ const ymodel = widget?.context.model.sharedModel as YNotebook;
632
572
 
633
- const kernelLang =
634
- model.content?.metadata?.language_info?.name ||
635
- model.content?.metadata?.kernelspec?.language ||
636
- 'text';
573
+ if (ymodel) {
574
+ const nb = ymodel.toJSON();
575
+
576
+ cellData = nb.cells;
577
+
578
+ const lang =
579
+ nb.metadata.language_info?.name ||
580
+ nb.metadata.kernelspec?.language ||
581
+ 'text';
582
+
583
+ kernelLang = String(lang);
584
+ } else {
585
+ // Fallback: reading from disk
586
+ const model = await this.input.documentManager?.services.contents.get(
587
+ attachment.value
588
+ );
589
+ if (!model || model.type !== 'notebook') {
590
+ return null;
591
+ }
592
+ cellData = model.content.cells ?? [];
593
+
594
+ kernelLang =
595
+ model.content.metadata.language_info?.name ||
596
+ model.content.metadata.kernelspec?.language ||
597
+ 'text';
598
+ }
637
599
 
638
600
  const selectedCells = attachment.cells
639
601
  .map(cellInfo => {
640
- const cell = model.content.cells.find(
641
- (c: any) => c.id === cellInfo.id
642
- );
602
+ const cell = cellData.find(c => c.id === cellInfo.id);
643
603
  if (!cell) {
644
604
  return null;
645
605
  }
@@ -660,7 +620,7 @@ ${toolsList}
660
620
  'text/plain'
661
621
  ];
662
622
 
663
- function extractDisplay(data: any): string {
623
+ function extractDisplay(data: nbformat.IMimeBundle): string {
664
624
  for (const mime of DISPLAY_PRIORITY) {
665
625
  if (!(mime in data)) {
666
626
  continue;
@@ -673,13 +633,13 @@ ${toolsList}
673
633
 
674
634
  switch (mime) {
675
635
  case 'application/vnd.jupyter.widget-view+json':
676
- return `Widget: ${(value as any).model_id ?? 'unknown model'}`;
636
+ return `Widget: ${(value as { model_id?: string }).model_id ?? 'unknown model'}`;
677
637
 
678
638
  case 'image/png':
679
- return `![image](data:image/png;base64,${value.slice(0, 100)}...)`;
639
+ return `![image](data:image/png;base64,${String(value).slice(0, 100)}...)`;
680
640
 
681
641
  case 'image/jpeg':
682
- return `![image](data:image/jpeg;base64,${value.slice(0, 100)}...)`;
642
+ return `![image](data:image/jpeg;base64,${String(value).slice(0, 100)}...)`;
683
643
 
684
644
  case 'image/svg+xml':
685
645
  return String(value).slice(0, 500) + '...\n[svg truncated]';
@@ -712,8 +672,9 @@ ${toolsList}
712
672
 
713
673
  let outputs = '';
714
674
  if (cellType === 'code' && Array.isArray(cell.outputs)) {
715
- outputs = cell.outputs
716
- .map((output: nbformat.IOutput) => {
675
+ const outputsArray = cell.outputs as nbformat.IOutput[];
676
+ outputs = outputsArray
677
+ .map(output => {
717
678
  if (output.output_type === 'stream') {
718
679
  return (output as nbformat.IStream).text;
719
680
  } else if (output.output_type === 'error') {
@@ -777,36 +738,48 @@ ${toolsList}
777
738
  }
778
739
 
779
740
  try {
780
- const model = await this.input.documentManager?.services.contents.get(
741
+ // Try reading from an open widget first
742
+ const widget = this.input.documentManager?.findWidget(
743
+ attachment.value
744
+ ) as IDocumentWidget<Notebook, INotebookModel> | undefined;
745
+
746
+ if (widget && widget.context && widget.context.model) {
747
+ const model = widget.context.model;
748
+ const ymodel = model.sharedModel as YNotebook;
749
+
750
+ if (typeof ymodel.getSource === 'function') {
751
+ const source = ymodel.getSource();
752
+ return typeof source === 'string'
753
+ ? source
754
+ : JSON.stringify(source, null, 2);
755
+ }
756
+ }
757
+
758
+ // If not open, load from disk
759
+ const diskModel = await this.input.documentManager?.services.contents.get(
781
760
  attachment.value
782
761
  );
783
- if (!model?.content) {
762
+
763
+ if (!diskModel?.content) {
784
764
  return null;
785
765
  }
786
- if (model.type === 'file') {
766
+
767
+ if (diskModel.type === 'file') {
787
768
  // Regular file content
788
- return model.content;
789
- } else if (model.type === 'notebook') {
790
- // Clear outputs from notebook cells before sending to LLM
791
- // TODO: make this configurable?
792
- const cells = model.content.cells.map((cell: any) => {
793
- const cleanCell = { ...cell };
794
- if (cleanCell.outputs) {
795
- cleanCell.outputs = [];
796
- }
797
- if (cleanCell.execution_count) {
798
- cleanCell.execution_count = null;
799
- }
800
- return cleanCell;
801
- });
802
-
803
- const notebookModel = {
804
- cells,
805
- metadata: (model as any).metadata || {},
806
- nbformat: (model as any).nbformat || 4,
807
- nbformat_minor: (model as any).nbformat_minor || 4
769
+ return diskModel.content;
770
+ }
771
+
772
+ if (diskModel.type === 'notebook') {
773
+ const cleaned = {
774
+ ...diskModel,
775
+ cells: diskModel.content.cells.map((cell: nbformat.ICell) => ({
776
+ ...cell,
777
+ outputs: [] as nbformat.IOutput[],
778
+ execution_count: null
779
+ }))
808
780
  };
809
- return JSON.stringify(notebookModel);
781
+
782
+ return JSON.stringify(cleaned);
810
783
  }
811
784
  return null;
812
785
  } catch (error) {
@@ -815,107 +788,159 @@ ${toolsList}
815
788
  }
816
789
  }
817
790
 
818
- /**
819
- * Updates the status display of a grouped approval message.
820
- * @param messageId The message ID to update
821
- * @param status The status text to display
822
- * @param isSuccess Whether the action was successful
823
- */
824
- private _updateGroupedApprovalStatus(
825
- messageId: string,
826
- status: string,
827
- isSuccess: boolean
828
- ): void {
829
- const existingMessageIndex = this.messages.findIndex(
830
- msg => msg.id === messageId
831
- );
832
- if (existingMessageIndex !== -1) {
833
- const existingMessage = this.messages[existingMessageIndex];
834
-
835
- // Extract tool count and names from existing message
836
- const toolCountMatch = existingMessage.body.match(/execute (\d+) tools/);
837
- const toolCount = toolCountMatch ? toolCountMatch[1] : 'multiple';
791
+ // Private fields
792
+ private _settingsModel: AISettingsModel;
793
+ private _user: IUser;
794
+ private _toolContexts: Map<string, IToolExecutionContext> = new Map();
795
+ private _agentManager: AgentManager;
796
+ private _currentStreamingMessage: IChatMessage | null = null;
797
+ private _nameChanged = new Signal<AIChatModel, string>(this);
798
+ private _trans: TranslationBundle;
799
+ }
838
800
 
839
- const statusIcon = isSuccess ? '✅' : '❌';
840
- const statusClass = isSuccess ? 'approved' : 'rejected';
801
+ namespace Private {
802
+ export function escapeHtml(value: string): string {
803
+ // Prefer the same native escaping approach used in JupyterLab itself
804
+ // (e.g. `@jupyterlab/completer`).
805
+ if (typeof document !== 'undefined') {
806
+ const node = document.createElement('span');
807
+ node.textContent = value;
808
+ return node.innerHTML;
809
+ }
841
810
 
842
- const updatedMessage: IChatMessage = {
843
- ...existingMessage,
844
- body: `**${statusIcon} Group Tool Approval: ${status}**
811
+ // Fallback
812
+ return value
813
+ .replace(/&/g, '&amp;')
814
+ .replace(/</g, '&lt;')
815
+ .replace(/>/g, '&gt;')
816
+ .replace(/"/g, '&quot;')
817
+ .replace(/'/g, '&#39;');
818
+ }
845
819
 
846
- The request to execute ${toolCount} tools has been **${statusClass}**.
820
+ /**
821
+ * Configuration for rendering tool call status.
822
+ */
823
+ interface IStatusConfig {
824
+ cssClass: string;
825
+ statusClass: string;
826
+ open?: boolean;
827
+ }
847
828
 
848
- <div class="jp-ai-group-approval-${statusClass}">
849
- Status: ${status}
850
- </div>`
851
- };
829
+ const STATUS_CONFIG: Record<ToolStatus, IStatusConfig> = {
830
+ pending: {
831
+ cssClass: 'jp-ai-tool-pending',
832
+ statusClass: 'jp-ai-tool-status-pending'
833
+ },
834
+ awaiting_approval: {
835
+ cssClass: 'jp-ai-tool-pending',
836
+ statusClass: 'jp-ai-tool-status-approval',
837
+ open: true
838
+ },
839
+ approved: {
840
+ cssClass: 'jp-ai-tool-pending',
841
+ statusClass: 'jp-ai-tool-status-completed'
842
+ },
843
+ rejected: {
844
+ cssClass: 'jp-ai-tool-error',
845
+ statusClass: 'jp-ai-tool-status-error'
846
+ },
847
+ completed: {
848
+ cssClass: 'jp-ai-tool-completed',
849
+ statusClass: 'jp-ai-tool-status-completed'
850
+ },
851
+ error: {
852
+ cssClass: 'jp-ai-tool-error',
853
+ statusClass: 'jp-ai-tool-status-error'
854
+ }
855
+ };
852
856
 
853
- this.messageAdded(updatedMessage);
857
+ /**
858
+ * Returns the translated status text for a given tool status.
859
+ */
860
+ const getStatusText = (
861
+ status: ToolStatus,
862
+ trans: TranslationBundle
863
+ ): string => {
864
+ switch (status) {
865
+ case 'pending':
866
+ return trans.__('Running...');
867
+ case 'awaiting_approval':
868
+ return trans.__('Awaiting Approval');
869
+ case 'approved':
870
+ return trans.__('Approved - Executing...');
871
+ case 'rejected':
872
+ return trans.__('Rejected');
873
+ case 'completed':
874
+ return trans.__('Completed');
875
+ case 'error':
876
+ return trans.__('Error');
854
877
  }
878
+ };
879
+
880
+ /**
881
+ * Options for building tool call HTML.
882
+ */
883
+ interface IToolCallHtmlOptions {
884
+ toolName: string;
885
+ input: string;
886
+ status: ToolStatus;
887
+ summary?: string;
888
+ output?: string;
889
+ approvalId?: string;
890
+ trans: TranslationBundle;
855
891
  }
856
892
 
857
893
  /**
858
- * Updates the status display of a tool call box.
859
- * @param messageId The message ID to update
860
- * @param status The status text to display
861
- * @param isSuccess Whether the action was successful
894
+ * Builds HTML for a tool call display.
862
895
  */
863
- private _updateToolCallBoxStatus(
864
- messageId: string,
865
- status: string,
866
- isSuccess: boolean
867
- ): void {
868
- const existingMessageIndex = this.messages.findIndex(
869
- msg => msg.id === messageId
870
- );
871
- if (existingMessageIndex !== -1) {
872
- const existingMessage = this.messages[existingMessageIndex];
896
+ export function buildToolCallHtml(options: IToolCallHtmlOptions): string {
897
+ const { toolName, input, status, summary, output, approvalId, trans } =
898
+ options;
899
+ const config = STATUS_CONFIG[status];
900
+ const statusText = getStatusText(status, trans);
901
+ const escapedToolName = escapeHtml(toolName);
902
+ const escapedInput = escapeHtml(input);
903
+ const openAttr = config.open ? ' open' : '';
904
+ const summaryHtml = summary
905
+ ? `<span class="jp-ai-tool-summary">${escapeHtml(summary)}</span>`
906
+ : '';
873
907
 
874
- // Extract tool name and input from existing message
875
- const toolNameMatch = existingMessage.body.match(
876
- /<div class="jp-ai-tool-title">([^<]+)<\/div>/
877
- );
878
- const toolName = toolNameMatch ? toolNameMatch[1] : 'Unknown Tool';
879
-
880
- const codeMatch = existingMessage.body.match(/<code>([\s\S]*?)<\/code>/);
881
- const toolInput = codeMatch ? codeMatch[1] : '{}';
882
-
883
- // Determine styling based on status
884
- const statusClass = isSuccess
885
- ? 'jp-ai-tool-completed'
886
- : 'jp-ai-tool-error';
887
- const statusColor = isSuccess
888
- ? 'jp-ai-tool-status-completed'
889
- : 'jp-ai-tool-status-error';
890
-
891
- const updatedMessage: IChatMessage = {
892
- ...existingMessage,
893
- body: `<details class="jp-ai-tool-call ${statusClass}">
908
+ let bodyContent = `
909
+ <div class="jp-ai-tool-section">
910
+ <div class="jp-ai-tool-label">${trans.__('Input')}</div>
911
+ <pre class="jp-ai-tool-code"><code>${escapedInput}</code></pre>
912
+ </div>`;
913
+
914
+ // Add approval buttons if awaiting approval
915
+ if (status === 'awaiting_approval' && approvalId) {
916
+ bodyContent += `
917
+ <div class="jp-ai-tool-approval-buttons jp-ai-approval-id--${approvalId}">
918
+ <button class="jp-ai-approval-btn jp-ai-approval-approve">${trans.__('Approve')}</button>
919
+ <button class="jp-ai-approval-btn jp-ai-approval-reject">${trans.__('Reject')}</button>
920
+ </div>`;
921
+ }
922
+
923
+ // Add output/result section if provided
924
+ if (output !== undefined) {
925
+ const escapedOutput = escapeHtml(output);
926
+ const label = status === 'error' ? trans.__('Error') : trans.__('Result');
927
+ bodyContent += `
928
+ <div class="jp-ai-tool-section">
929
+ <div class="jp-ai-tool-label">${label}</div>
930
+ <pre class="jp-ai-tool-code"><code>${escapedOutput}</code></pre>
931
+ </div>`;
932
+ }
933
+
934
+ return `<details class="jp-ai-tool-call ${config.cssClass}"${openAttr}>
894
935
  <summary class="jp-ai-tool-header">
895
936
  <div class="jp-ai-tool-icon">⚡</div>
896
- <div class="jp-ai-tool-title">${toolName}</div>
897
- <div class="jp-ai-tool-status ${statusColor}">${status}</div>
937
+ <div class="jp-ai-tool-title">${escapedToolName}${summaryHtml}</div>
938
+ <div class="jp-ai-tool-status ${config.statusClass}">${statusText}</div>
898
939
  </summary>
899
- <div class="jp-ai-tool-body">
900
- <div class="jp-ai-tool-section">
901
- <div class="jp-ai-tool-label">Input</div>
902
- <pre class="jp-ai-tool-code"><code>${toolInput}</code></pre>
940
+ <div class="jp-ai-tool-body">${bodyContent}
903
941
  </div>
904
- </div>
905
- </details>`
906
- };
907
-
908
- this.messageAdded(updatedMessage);
909
- }
942
+ </details>`;
910
943
  }
911
-
912
- // Private fields
913
- private _settingsModel: AISettingsModel;
914
- private _user: IUser;
915
- private _pendingToolCalls: Map<string, string> = new Map();
916
- private _agentManager: AgentManager;
917
- private _currentStreamingMessage: IChatMessage | null = null;
918
- private _nameChanged = new Signal<AIChatModel, string>(this);
919
944
  }
920
945
 
921
946
  /**
@@ -946,6 +971,10 @@ export namespace AIChatModel {
946
971
  * Optional document manager for file operations
947
972
  */
948
973
  documentManager?: IDocumentManager;
974
+ /**
975
+ * The application language translation bundle.
976
+ */
977
+ trans: TranslationBundle;
949
978
  }
950
979
 
951
980
  /**