@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/lib/chat-model.js CHANGED
@@ -4,9 +4,8 @@ import { UUID } from '@lumino/coreutils';
4
4
  import { Signal } from '@lumino/signaling';
5
5
  import { AI_AVATAR } from './icons';
6
6
  /**
7
- * AI Chat Model implementation that provides chat functionality with OpenAI agents,
8
- * tool integration, and MCP server support.
9
- * Extends the base AbstractChatModel to provide AI-powered conversations.
7
+ * AI Chat Model implementation that provides chat functionality tool integration,
8
+ * and MCP server support.
10
9
  */
11
10
  export class AIChatModel extends AbstractChatModel {
12
11
  /**
@@ -25,6 +24,7 @@ export class AIChatModel extends AbstractChatModel {
25
24
  this._settingsModel = options.settingsModel;
26
25
  this._user = options.user;
27
26
  this._agentManager = options.agentManager;
27
+ this._trans = options.trans;
28
28
  // Listen for agent events
29
29
  this._agentManager.agentEvent.connect(this._onAgentEvent, this);
30
30
  // Listen for settings changes to update chat behavior
@@ -90,7 +90,7 @@ export class AIChatModel extends AbstractChatModel {
90
90
  */
91
91
  clearMessages = () => {
92
92
  this.messagesDeleted(0, this.messages.length);
93
- this._pendingToolCalls.clear();
93
+ this._toolContexts.clear();
94
94
  this._agentManager.clearHistory();
95
95
  };
96
96
  /**
@@ -127,7 +127,6 @@ export class AIChatModel extends AbstractChatModel {
127
127
  let enhancedMessage = message.body;
128
128
  if (this.input.attachments.length > 0) {
129
129
  const attachmentContents = await this._processAttachments(this.input.attachments);
130
- // Clear attachments right after processing
131
130
  this.input.clearAttachments();
132
131
  if (attachmentContents.length > 0) {
133
132
  enhancedMessage +=
@@ -152,56 +151,6 @@ export class AIChatModel extends AbstractChatModel {
152
151
  this.updateWriters([]);
153
152
  }
154
153
  }
155
- /**
156
- * Approves a tool call and updates the UI accordingly.
157
- * @param interruptionId The interruption ID to approve
158
- * @param messageId Optional message ID for UI updates
159
- */
160
- async approveToolCall(interruptionId, messageId) {
161
- await this._agentManager.approveToolCall(interruptionId);
162
- // Update the tool call box to show "Approved" status
163
- if (messageId) {
164
- this._updateToolCallBoxStatus(messageId, 'Approved', true);
165
- }
166
- }
167
- /**
168
- * Rejects a tool call and updates the UI accordingly.
169
- * @param interruptionId The interruption ID to reject
170
- * @param messageId Optional message ID for UI updates
171
- */
172
- async rejectToolCall(interruptionId, messageId) {
173
- await this._agentManager.rejectToolCall(interruptionId);
174
- // Update the tool call box to show "Rejected" status
175
- if (messageId) {
176
- this._updateToolCallBoxStatus(messageId, 'Rejected', false);
177
- }
178
- }
179
- /**
180
- * Approves all tools in a group.
181
- * @param groupId The group ID containing the tool calls
182
- * @param interruptionIds Array of interruption IDs to approve
183
- * @param messageId Optional message ID for UI updates
184
- */
185
- async approveGroupedToolCalls(groupId, interruptionIds, messageId) {
186
- await this._agentManager.approveGroupedToolCalls(groupId, interruptionIds);
187
- // Update the grouped approval message to show approved status
188
- if (messageId) {
189
- this._updateGroupedApprovalStatus(messageId, 'Tools approved', true);
190
- }
191
- }
192
- /**
193
- * Rejects all tools in a group.
194
- * @param groupId The group ID containing the tool calls
195
- * @param interruptionIds Array of interruption IDs to reject
196
- * @param messageId Optional message ID for UI updates
197
- */
198
- async rejectGroupedToolCalls(groupId, interruptionIds, messageId) {
199
- await this._agentManager.rejectGroupedToolCalls(groupId, interruptionIds);
200
- // Update the grouped approval message to show rejected status
201
- if (messageId) {
202
- this._updateGroupedApprovalStatus(messageId, 'Tools rejected', false);
203
- }
204
- }
205
154
  /**
206
155
  * Gets the AI user information for system messages.
207
156
  */
@@ -243,11 +192,11 @@ export class AIChatModel extends AbstractChatModel {
243
192
  case 'tool_call_complete':
244
193
  this._handleToolCallCompleteEvent(event);
245
194
  break;
246
- case 'tool_approval_required':
247
- this._handleToolApprovalRequired(event);
195
+ case 'tool_approval_request':
196
+ this._handleToolApprovalRequest(event);
248
197
  break;
249
- case 'grouped_approval_required':
250
- this._handleGroupedApprovalRequired(event);
198
+ case 'tool_approval_resolved':
199
+ this._handleToolApprovalResolved(event);
251
200
  break;
252
201
  case 'error':
253
202
  this._handleErrorEvent(event);
@@ -293,179 +242,137 @@ export class AIChatModel extends AbstractChatModel {
293
242
  this._currentStreamingMessage = null;
294
243
  }
295
244
  }
245
+ /**
246
+ * Extracts a human-readable summary from tool input for display in the header.
247
+ * @param toolName The name of the tool being called
248
+ * @param input The formatted JSON input string
249
+ * @returns A short summary string or empty string if none available
250
+ */
251
+ _extractToolSummary(toolName, input) {
252
+ try {
253
+ const parsedInput = JSON.parse(input);
254
+ switch (toolName) {
255
+ case 'execute_command':
256
+ if (parsedInput.commandId) {
257
+ return parsedInput.commandId;
258
+ }
259
+ break;
260
+ case 'discover_commands':
261
+ if (parsedInput.query) {
262
+ return `query: "${parsedInput.query}"`;
263
+ }
264
+ break;
265
+ }
266
+ }
267
+ catch {
268
+ // If parsing fails, return empty string
269
+ }
270
+ return '';
271
+ }
296
272
  /**
297
273
  * Handles the start of a tool call execution.
298
274
  * @param event Event containing the tool call start data
299
275
  */
300
276
  _handleToolCallStartEvent(event) {
301
- const toolCallMessageId = UUID.uuid4();
277
+ const messageId = UUID.uuid4();
278
+ const summary = this._extractToolSummary(event.data.toolName, event.data.input);
279
+ const context = {
280
+ toolCallId: event.data.callId,
281
+ messageId,
282
+ toolName: event.data.toolName,
283
+ input: event.data.input,
284
+ status: 'pending',
285
+ summary
286
+ };
287
+ this._toolContexts.set(event.data.callId, context);
302
288
  const toolCallMessage = {
303
- body: `<details class="jp-ai-tool-call jp-ai-tool-pending">
304
- <summary class="jp-ai-tool-header">
305
- <div class="jp-ai-tool-icon">⚡</div>
306
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
307
- <div class="jp-ai-tool-status jp-ai-tool-status-pending">Running...</div>
308
- </summary>
309
- <div class="jp-ai-tool-body">
310
- <div class="jp-ai-tool-section">
311
- <div class="jp-ai-tool-label">Input</div>
312
- <pre class="jp-ai-tool-code"><code>${event.data.input}</code></pre>
313
- </div>
314
- </div>
315
- </details>`,
289
+ body: Private.buildToolCallHtml({
290
+ toolName: context.toolName,
291
+ input: context.input,
292
+ status: context.status,
293
+ summary: context.summary,
294
+ trans: this._trans
295
+ }),
316
296
  sender: this._getAIUser(),
317
- id: toolCallMessageId,
297
+ id: messageId,
318
298
  time: Date.now() / 1000,
319
299
  type: 'msg',
320
300
  raw_time: false
321
301
  };
322
- if (event.data.callId) {
323
- this._pendingToolCalls.set(event.data.callId, toolCallMessageId);
324
- }
325
302
  this.messageAdded(toolCallMessage);
326
303
  }
327
304
  /**
328
305
  * Handles the completion of a tool call execution.
329
- * @param event Event containing the tool call completion data
330
306
  */
331
307
  _handleToolCallCompleteEvent(event) {
332
- const messageId = this._pendingToolCalls.get(event.data.callId);
333
- if (messageId) {
334
- const existingMessageIndex = this.messages.findIndex(msg => msg.id === messageId);
335
- if (existingMessageIndex !== -1) {
336
- const existingMessage = this.messages[existingMessageIndex];
337
- const inputJson = existingMessage.body.match(/<code>([\s\S]*?)<\/code>/)?.[1] || '';
338
- const statusClass = event.data.isError
339
- ? 'jp-ai-tool-error'
340
- : 'jp-ai-tool-completed';
341
- const statusText = event.data.isError ? 'Error' : 'Completed';
342
- const statusColor = event.data.isError
343
- ? 'jp-ai-tool-status-error'
344
- : 'jp-ai-tool-status-completed';
345
- const updatedMessage = {
346
- ...existingMessage,
347
- body: `<details class="jp-ai-tool-call ${statusClass}">
348
- <summary class="jp-ai-tool-header">
349
- <div class="jp-ai-tool-icon">⚡</div>
350
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
351
- <div class="jp-ai-tool-status ${statusColor}">${statusText}</div>
352
- </summary>
353
- <div class="jp-ai-tool-body">
354
- <div class="jp-ai-tool-section">
355
- <div class="jp-ai-tool-label">Input</div>
356
- <pre class="jp-ai-tool-code"><code>${inputJson}</code></pre>
357
- </div>
358
- <div class="jp-ai-tool-section">
359
- <div class="jp-ai-tool-label">${event.data.isError ? 'Error' : 'Result'}</div>
360
- <pre class="jp-ai-tool-code"><code>${event.data.output}</code></pre>
361
- </div>
362
- </div>
363
- </details>`
364
- };
365
- this.messageAdded(updatedMessage);
366
- this._pendingToolCalls.delete(event.data.callId);
367
- }
368
- }
308
+ const status = event.data.isError ? 'error' : 'completed';
309
+ this._updateToolCallUI(event.data.callId, status, event.data.output);
310
+ this._toolContexts.delete(event.data.callId);
369
311
  }
370
312
  /**
371
- * Handles tool approval requests from the AI agent.
372
- * @param event Event containing the tool approval request data
313
+ * Handles error events from the AI agent.
373
314
  */
374
- _handleToolApprovalRequired(event) {
375
- // Handle single tool approval - either update existing tool call message or create new approval message
376
- if (event.data.callId) {
377
- const messageId = this._pendingToolCalls.get(event.data.callId);
378
- if (messageId) {
379
- const existingMessageIndex = this.messages.findIndex(msg => msg.id === messageId);
380
- if (existingMessageIndex !== -1) {
381
- const existingMessage = this.messages[existingMessageIndex];
382
- const assistantName = this._getAIUser().display_name;
383
- const updatedMessage = {
384
- ...existingMessage,
385
- body: `<details class="jp-ai-tool-call jp-ai-tool-pending" open>
386
- <summary class="jp-ai-tool-header">
387
- <div class="jp-ai-tool-icon">⚡</div>
388
- <div class="jp-ai-tool-title">${event.data.toolName}</div>
389
- <div class="jp-ai-tool-status jp-ai-tool-status-pending">Needs Approval</div>
390
- </summary>
391
- <div class="jp-ai-tool-body">
392
- <div class="jp-ai-tool-section">
393
- <div class="jp-ai-tool-label">${assistantName} wants to execute this tool. Do you approve?</div>
394
- <pre class="jp-ai-tool-code"><code>${event.data.toolInput}</code></pre>
395
- </div>
396
- [APPROVAL_BUTTONS:${event.data.interruptionId}]
397
- </div>
398
- </details>`
399
- };
400
- this.messageAdded(updatedMessage);
401
- this.updateWriters([]);
402
- return;
403
- }
404
- }
405
- }
406
- // Fallback: create separate approval message
407
- const approvalMessageId = UUID.uuid4();
408
- const assistantName = this._getAIUser().display_name;
409
- const approvalMessage = {
410
- body: `**🤖 Tool Approval Required: ${event.data.toolName}**
411
-
412
- ${assistantName} wants to execute this tool. Do you approve?
413
-
414
- \`\`\`json
415
- ${event.data.toolInput}
416
- \`\`\`
417
-
418
- [APPROVAL_BUTTONS:${event.data.interruptionId}]`,
315
+ _handleErrorEvent(event) {
316
+ this.messageAdded({
317
+ body: `Error generating response: ${event.data.error.message}`,
419
318
  sender: this._getAIUser(),
420
- id: approvalMessageId,
319
+ id: UUID.uuid4(),
421
320
  time: Date.now() / 1000,
422
321
  type: 'msg',
423
322
  raw_time: false
424
- };
425
- this.messageAdded(approvalMessage);
426
- this.updateWriters([]); // Stop showing "AI is writing"
323
+ });
427
324
  }
428
325
  /**
429
- * Handles grouped tool approval requests from the AI agent.
430
- * @param event Event containing the grouped tool approval request data
326
+ * Handles tool approval request events from the AI agent.
431
327
  */
432
- _handleGroupedApprovalRequired(event) {
433
- const assistantName = this._getAIUser().display_name;
434
- const approvalMessageId = UUID.uuid4();
435
- const toolsList = event.data.approvals
436
- .map((info, index) => `**${index + 1}. ${info.toolName}**\n\`\`\`json\n${info.toolInput}\n\`\`\`\n`)
437
- .join('\n\n');
438
- const approvalMessage = {
439
- body: `**🤖 Multiple Tool Approvals Required**
440
-
441
- ${assistantName} wants to execute ${event.data.approvals.length} tools. Do you approve?
442
-
443
- ${toolsList}
444
-
445
- [GROUP_APPROVAL_BUTTONS:${event.data.groupId}:${event.data.approvals.map(info => info.interruptionId).join(',')}]`,
446
- sender: this._getAIUser(),
447
- id: approvalMessageId,
448
- time: Date.now() / 1000,
449
- type: 'msg',
450
- raw_time: false
451
- };
452
- this.messageAdded(approvalMessage);
453
- this.updateWriters([]); // Stop showing "AI is writing"
328
+ _handleToolApprovalRequest(event) {
329
+ const context = this._toolContexts.get(event.data.toolCallId);
330
+ if (!context) {
331
+ return;
332
+ }
333
+ context.approvalId = event.data.approvalId;
334
+ context.input = JSON.stringify(event.data.args, null, 2);
335
+ this._updateToolCallUI(event.data.toolCallId, 'awaiting_approval');
454
336
  }
455
337
  /**
456
- * Handles error events from the AI agent.
457
- * @param event Event containing the error information
338
+ * Handles tool approval resolved events from the AI agent.
458
339
  */
459
- _handleErrorEvent(event) {
460
- const errorMessage = {
461
- body: `Error generating response: ${event.data.error.message}`,
462
- sender: this._getAIUser(),
463
- id: UUID.uuid4(),
464
- time: Date.now() / 1000,
465
- type: 'msg',
466
- raw_time: false
467
- };
468
- this.messageAdded(errorMessage);
340
+ _handleToolApprovalResolved(event) {
341
+ const context = Array.from(this._toolContexts.values()).find(ctx => ctx.approvalId === event.data.approvalId);
342
+ if (!context) {
343
+ return;
344
+ }
345
+ const status = event.data.approved ? 'approved' : 'rejected';
346
+ this._updateToolCallUI(context.toolCallId, status);
347
+ if (!event.data.approved) {
348
+ this._toolContexts.delete(context.toolCallId);
349
+ }
350
+ }
351
+ /**
352
+ * Updates a tool call's UI with new status and optional output.
353
+ */
354
+ _updateToolCallUI(toolCallId, status, output) {
355
+ const context = this._toolContexts.get(toolCallId);
356
+ if (!context) {
357
+ return;
358
+ }
359
+ const existingMessage = this.messages.find(msg => msg.id === context.messageId);
360
+ if (!existingMessage) {
361
+ return;
362
+ }
363
+ context.status = status;
364
+ this.messageAdded({
365
+ ...existingMessage,
366
+ body: Private.buildToolCallHtml({
367
+ toolName: context.toolName,
368
+ input: context.input,
369
+ status: context.status,
370
+ summary: context.summary,
371
+ output,
372
+ approvalId: context.approvalId,
373
+ trans: this._trans
374
+ })
375
+ });
469
376
  }
470
377
  /**
471
378
  * Processes file attachments and returns their content as formatted strings.
@@ -508,16 +415,34 @@ ${toolsList}
508
415
  return null;
509
416
  }
510
417
  try {
511
- const model = await this.input.documentManager?.services.contents.get(attachment.value);
512
- if (!model || model.type !== 'notebook') {
513
- return null;
418
+ // Try reading from live notebook if open
419
+ const widget = this.input.documentManager?.findWidget(attachment.value);
420
+ let cellData;
421
+ let kernelLang = 'text';
422
+ const ymodel = widget?.context.model.sharedModel;
423
+ if (ymodel) {
424
+ const nb = ymodel.toJSON();
425
+ cellData = nb.cells;
426
+ const lang = nb.metadata.language_info?.name ||
427
+ nb.metadata.kernelspec?.language ||
428
+ 'text';
429
+ kernelLang = String(lang);
430
+ }
431
+ else {
432
+ // Fallback: reading from disk
433
+ const model = await this.input.documentManager?.services.contents.get(attachment.value);
434
+ if (!model || model.type !== 'notebook') {
435
+ return null;
436
+ }
437
+ cellData = model.content.cells ?? [];
438
+ kernelLang =
439
+ model.content.metadata.language_info?.name ||
440
+ model.content.metadata.kernelspec?.language ||
441
+ 'text';
514
442
  }
515
- const kernelLang = model.content?.metadata?.language_info?.name ||
516
- model.content?.metadata?.kernelspec?.language ||
517
- 'text';
518
443
  const selectedCells = attachment.cells
519
444
  .map(cellInfo => {
520
- const cell = model.content.cells.find((c) => c.id === cellInfo.id);
445
+ const cell = cellData.find(c => c.id === cellInfo.id);
521
446
  if (!cell) {
522
447
  return null;
523
448
  }
@@ -548,9 +473,9 @@ ${toolsList}
548
473
  case 'application/vnd.jupyter.widget-view+json':
549
474
  return `Widget: ${value.model_id ?? 'unknown model'}`;
550
475
  case 'image/png':
551
- return `![image](data:image/png;base64,${value.slice(0, 100)}...)`;
476
+ return `![image](data:image/png;base64,${String(value).slice(0, 100)}...)`;
552
477
  case 'image/jpeg':
553
- return `![image](data:image/jpeg;base64,${value.slice(0, 100)}...)`;
478
+ return `![image](data:image/jpeg;base64,${String(value).slice(0, 100)}...)`;
554
479
  case 'image/svg+xml':
555
480
  return String(value).slice(0, 500) + '...\n[svg truncated]';
556
481
  case 'text/html':
@@ -575,8 +500,9 @@ ${toolsList}
575
500
  }
576
501
  let outputs = '';
577
502
  if (cellType === 'code' && Array.isArray(cell.outputs)) {
578
- outputs = cell.outputs
579
- .map((output) => {
503
+ const outputsArray = cell.outputs;
504
+ outputs = outputsArray
505
+ .map(output => {
580
506
  if (output.output_type === 'stream') {
581
507
  return output.text;
582
508
  }
@@ -630,34 +556,37 @@ ${toolsList}
630
556
  return null;
631
557
  }
632
558
  try {
633
- const model = await this.input.documentManager?.services.contents.get(attachment.value);
634
- if (!model?.content) {
559
+ // Try reading from an open widget first
560
+ const widget = this.input.documentManager?.findWidget(attachment.value);
561
+ if (widget && widget.context && widget.context.model) {
562
+ const model = widget.context.model;
563
+ const ymodel = model.sharedModel;
564
+ if (typeof ymodel.getSource === 'function') {
565
+ const source = ymodel.getSource();
566
+ return typeof source === 'string'
567
+ ? source
568
+ : JSON.stringify(source, null, 2);
569
+ }
570
+ }
571
+ // If not open, load from disk
572
+ const diskModel = await this.input.documentManager?.services.contents.get(attachment.value);
573
+ if (!diskModel?.content) {
635
574
  return null;
636
575
  }
637
- if (model.type === 'file') {
576
+ if (diskModel.type === 'file') {
638
577
  // Regular file content
639
- return model.content;
578
+ return diskModel.content;
640
579
  }
641
- else if (model.type === 'notebook') {
642
- // Clear outputs from notebook cells before sending to LLM
643
- // TODO: make this configurable?
644
- const cells = model.content.cells.map((cell) => {
645
- const cleanCell = { ...cell };
646
- if (cleanCell.outputs) {
647
- cleanCell.outputs = [];
648
- }
649
- if (cleanCell.execution_count) {
650
- cleanCell.execution_count = null;
651
- }
652
- return cleanCell;
653
- });
654
- const notebookModel = {
655
- cells,
656
- metadata: model.metadata || {},
657
- nbformat: model.nbformat || 4,
658
- nbformat_minor: model.nbformat_minor || 4
580
+ if (diskModel.type === 'notebook') {
581
+ const cleaned = {
582
+ ...diskModel,
583
+ cells: diskModel.content.cells.map((cell) => ({
584
+ ...cell,
585
+ outputs: [],
586
+ execution_count: null
587
+ }))
659
588
  };
660
- return JSON.stringify(notebookModel);
589
+ return JSON.stringify(cleaned);
661
590
  }
662
591
  return null;
663
592
  }
@@ -666,80 +595,125 @@ ${toolsList}
666
595
  return null;
667
596
  }
668
597
  }
598
+ // Private fields
599
+ _settingsModel;
600
+ _user;
601
+ _toolContexts = new Map();
602
+ _agentManager;
603
+ _currentStreamingMessage = null;
604
+ _nameChanged = new Signal(this);
605
+ _trans;
606
+ }
607
+ var Private;
608
+ (function (Private) {
609
+ function escapeHtml(value) {
610
+ // Prefer the same native escaping approach used in JupyterLab itself
611
+ // (e.g. `@jupyterlab/completer`).
612
+ if (typeof document !== 'undefined') {
613
+ const node = document.createElement('span');
614
+ node.textContent = value;
615
+ return node.innerHTML;
616
+ }
617
+ // Fallback
618
+ return value
619
+ .replace(/&/g, '&amp;')
620
+ .replace(/</g, '&lt;')
621
+ .replace(/>/g, '&gt;')
622
+ .replace(/"/g, '&quot;')
623
+ .replace(/'/g, '&#39;');
624
+ }
625
+ Private.escapeHtml = escapeHtml;
626
+ const STATUS_CONFIG = {
627
+ pending: {
628
+ cssClass: 'jp-ai-tool-pending',
629
+ statusClass: 'jp-ai-tool-status-pending'
630
+ },
631
+ awaiting_approval: {
632
+ cssClass: 'jp-ai-tool-pending',
633
+ statusClass: 'jp-ai-tool-status-approval',
634
+ open: true
635
+ },
636
+ approved: {
637
+ cssClass: 'jp-ai-tool-pending',
638
+ statusClass: 'jp-ai-tool-status-completed'
639
+ },
640
+ rejected: {
641
+ cssClass: 'jp-ai-tool-error',
642
+ statusClass: 'jp-ai-tool-status-error'
643
+ },
644
+ completed: {
645
+ cssClass: 'jp-ai-tool-completed',
646
+ statusClass: 'jp-ai-tool-status-completed'
647
+ },
648
+ error: {
649
+ cssClass: 'jp-ai-tool-error',
650
+ statusClass: 'jp-ai-tool-status-error'
651
+ }
652
+ };
669
653
  /**
670
- * Updates the status display of a grouped approval message.
671
- * @param messageId The message ID to update
672
- * @param status The status text to display
673
- * @param isSuccess Whether the action was successful
654
+ * Returns the translated status text for a given tool status.
674
655
  */
675
- _updateGroupedApprovalStatus(messageId, status, isSuccess) {
676
- const existingMessageIndex = this.messages.findIndex(msg => msg.id === messageId);
677
- if (existingMessageIndex !== -1) {
678
- const existingMessage = this.messages[existingMessageIndex];
679
- // Extract tool count and names from existing message
680
- const toolCountMatch = existingMessage.body.match(/execute (\d+) tools/);
681
- const toolCount = toolCountMatch ? toolCountMatch[1] : 'multiple';
682
- const statusIcon = isSuccess ? '✅' : '❌';
683
- const statusClass = isSuccess ? 'approved' : 'rejected';
684
- const updatedMessage = {
685
- ...existingMessage,
686
- body: `**${statusIcon} Group Tool Approval: ${status}**
687
-
688
- The request to execute ${toolCount} tools has been **${statusClass}**.
689
-
690
- <div class="jp-ai-group-approval-${statusClass}">
691
- Status: ${status}
692
- </div>`
693
- };
694
- this.messageAdded(updatedMessage);
656
+ const getStatusText = (status, trans) => {
657
+ switch (status) {
658
+ case 'pending':
659
+ return trans.__('Running...');
660
+ case 'awaiting_approval':
661
+ return trans.__('Awaiting Approval');
662
+ case 'approved':
663
+ return trans.__('Approved - Executing...');
664
+ case 'rejected':
665
+ return trans.__('Rejected');
666
+ case 'completed':
667
+ return trans.__('Completed');
668
+ case 'error':
669
+ return trans.__('Error');
695
670
  }
696
- }
671
+ };
697
672
  /**
698
- * Updates the status display of a tool call box.
699
- * @param messageId The message ID to update
700
- * @param status The status text to display
701
- * @param isSuccess Whether the action was successful
673
+ * Builds HTML for a tool call display.
702
674
  */
703
- _updateToolCallBoxStatus(messageId, status, isSuccess) {
704
- const existingMessageIndex = this.messages.findIndex(msg => msg.id === messageId);
705
- if (existingMessageIndex !== -1) {
706
- const existingMessage = this.messages[existingMessageIndex];
707
- // Extract tool name and input from existing message
708
- const toolNameMatch = existingMessage.body.match(/<div class="jp-ai-tool-title">([^<]+)<\/div>/);
709
- const toolName = toolNameMatch ? toolNameMatch[1] : 'Unknown Tool';
710
- const codeMatch = existingMessage.body.match(/<code>([\s\S]*?)<\/code>/);
711
- const toolInput = codeMatch ? codeMatch[1] : '{}';
712
- // Determine styling based on status
713
- const statusClass = isSuccess
714
- ? 'jp-ai-tool-completed'
715
- : 'jp-ai-tool-error';
716
- const statusColor = isSuccess
717
- ? 'jp-ai-tool-status-completed'
718
- : 'jp-ai-tool-status-error';
719
- const updatedMessage = {
720
- ...existingMessage,
721
- body: `<details class="jp-ai-tool-call ${statusClass}">
675
+ function buildToolCallHtml(options) {
676
+ const { toolName, input, status, summary, output, approvalId, trans } = options;
677
+ const config = STATUS_CONFIG[status];
678
+ const statusText = getStatusText(status, trans);
679
+ const escapedToolName = escapeHtml(toolName);
680
+ const escapedInput = escapeHtml(input);
681
+ const openAttr = config.open ? ' open' : '';
682
+ const summaryHtml = summary
683
+ ? `<span class="jp-ai-tool-summary">${escapeHtml(summary)}</span>`
684
+ : '';
685
+ let bodyContent = `
686
+ <div class="jp-ai-tool-section">
687
+ <div class="jp-ai-tool-label">${trans.__('Input')}</div>
688
+ <pre class="jp-ai-tool-code"><code>${escapedInput}</code></pre>
689
+ </div>`;
690
+ // Add approval buttons if awaiting approval
691
+ if (status === 'awaiting_approval' && approvalId) {
692
+ bodyContent += `
693
+ <div class="jp-ai-tool-approval-buttons jp-ai-approval-id--${approvalId}">
694
+ <button class="jp-ai-approval-btn jp-ai-approval-approve">${trans.__('Approve')}</button>
695
+ <button class="jp-ai-approval-btn jp-ai-approval-reject">${trans.__('Reject')}</button>
696
+ </div>`;
697
+ }
698
+ // Add output/result section if provided
699
+ if (output !== undefined) {
700
+ const escapedOutput = escapeHtml(output);
701
+ const label = status === 'error' ? trans.__('Error') : trans.__('Result');
702
+ bodyContent += `
703
+ <div class="jp-ai-tool-section">
704
+ <div class="jp-ai-tool-label">${label}</div>
705
+ <pre class="jp-ai-tool-code"><code>${escapedOutput}</code></pre>
706
+ </div>`;
707
+ }
708
+ return `<details class="jp-ai-tool-call ${config.cssClass}"${openAttr}>
722
709
  <summary class="jp-ai-tool-header">
723
710
  <div class="jp-ai-tool-icon">⚡</div>
724
- <div class="jp-ai-tool-title">${toolName}</div>
725
- <div class="jp-ai-tool-status ${statusColor}">${status}</div>
711
+ <div class="jp-ai-tool-title">${escapedToolName}${summaryHtml}</div>
712
+ <div class="jp-ai-tool-status ${config.statusClass}">${statusText}</div>
726
713
  </summary>
727
- <div class="jp-ai-tool-body">
728
- <div class="jp-ai-tool-section">
729
- <div class="jp-ai-tool-label">Input</div>
730
- <pre class="jp-ai-tool-code"><code>${toolInput}</code></pre>
731
- </div>
714
+ <div class="jp-ai-tool-body">${bodyContent}
732
715
  </div>
733
- </details>`
734
- };
735
- this.messageAdded(updatedMessage);
736
- }
716
+ </details>`;
737
717
  }
738
- // Private fields
739
- _settingsModel;
740
- _user;
741
- _pendingToolCalls = new Map();
742
- _agentManager;
743
- _currentStreamingMessage = null;
744
- _nameChanged = new Signal(this);
745
- }
718
+ Private.buildToolCallHtml = buildToolCallHtml;
719
+ })(Private || (Private = {}));