@jupyterlite/ai 0.9.1 → 0.10.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 (66) hide show
  1. package/README.md +5 -214
  2. package/lib/agent.d.ts +58 -66
  3. package/lib/agent.js +274 -300
  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 +19 -54
  9. package/lib/chat-model.js +243 -303
  10. package/lib/components/clear-button.d.ts +6 -1
  11. package/lib/components/clear-button.js +8 -3
  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 +9 -8
  16. package/lib/components/stop-button.d.ts +6 -1
  17. package/lib/components/stop-button.js +8 -3
  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 +6 -5
  22. package/lib/index.js +58 -38
  23. package/lib/models/settings-model.d.ts +1 -1
  24. package/lib/providers/built-in-providers.js +38 -19
  25. package/lib/providers/models.d.ts +3 -3
  26. package/lib/providers/provider-registry.d.ts +3 -4
  27. package/lib/providers/provider-registry.js +1 -4
  28. package/lib/tokens.d.ts +5 -6
  29. package/lib/tools/commands.d.ts +2 -1
  30. package/lib/tools/commands.js +37 -46
  31. package/lib/tools/file.js +49 -73
  32. package/lib/tools/notebook.js +370 -445
  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 +12 -12
  40. package/src/agent.ts +342 -361
  41. package/src/approval-buttons.ts +43 -389
  42. package/src/chat-model-registry.ts +9 -1
  43. package/src/chat-model.ts +355 -370
  44. package/src/completion/completion-provider.ts +2 -3
  45. package/src/components/clear-button.tsx +16 -3
  46. package/src/components/completion-status.tsx +18 -4
  47. package/src/components/model-select.tsx +21 -8
  48. package/src/components/stop-button.tsx +16 -3
  49. package/src/components/token-usage-display.tsx +14 -2
  50. package/src/components/tool-select.tsx +23 -5
  51. package/src/index.ts +75 -36
  52. package/src/models/settings-model.ts +1 -1
  53. package/src/providers/built-in-providers.ts +38 -19
  54. package/src/providers/models.ts +3 -3
  55. package/src/providers/provider-registry.ts +4 -8
  56. package/src/tokens.ts +5 -6
  57. package/src/tools/commands.ts +39 -50
  58. package/src/tools/file.ts +49 -75
  59. package/src/tools/notebook.ts +451 -510
  60. package/src/widgets/ai-settings.tsx +153 -84
  61. package/src/widgets/main-area-chat.ts +8 -2
  62. package/src/widgets/provider-config-dialog.tsx +54 -41
  63. package/style/base.css +13 -73
  64. package/lib/mcp/browser.d.ts +0 -68
  65. package/lib/mcp/browser.js +0 -138
  66. package/src/mcp/browser.ts +0 -220
@@ -1,17 +1,16 @@
1
1
  import { ChatWidget } from '@jupyter/chat';
2
2
  import { IDisposable } from '@lumino/disposable';
3
- import { AIChatModel } from './chat-model';
3
+ import type { AgentManager } from './agent';
4
4
 
5
+ /**
6
+ * Handles click events for approval buttons in the chat panel.
7
+ */
5
8
  export class ApprovalButtons implements IDisposable {
6
9
  constructor(options: ApprovalButtons.IOptions) {
7
10
  this._chatPanel = options.chatPanel;
8
- this._chatModel = this._chatPanel.model as AIChatModel;
9
-
10
- // Set up approval button event handling
11
- this._setupApprovalHandlers();
11
+ this._agentManager = options.agentManager;
12
12
 
13
- // Set up message processing for approval buttons
14
- this._setupMessageProcessing();
13
+ this._chatPanel.node.addEventListener('click', this._handleClick);
15
14
  }
16
15
 
17
16
  get isDisposed(): boolean {
@@ -27,435 +26,90 @@ export class ApprovalButtons implements IDisposable {
27
26
  }
28
27
  this._isDisposed = true;
29
28
 
30
- // Stop the mutation observer.
31
- if (this._mutationObserver) {
32
- this._mutationObserver.disconnect();
33
- this._mutationObserver = undefined;
34
- }
35
-
36
- // Remove all listener on existing buttons.
37
- const existingButtons = this._chatPanel.node.querySelectorAll(
38
- '.jp-ai-approval-btn'
39
- );
40
- existingButtons.forEach(button => {
41
- button.removeEventListener('click', this._handleButtonClick);
42
- });
43
-
44
- const existingGroupButtons = this._chatPanel.node.querySelectorAll(
45
- '.jp-ai-group-approval-buttons button'
46
- );
47
- existingGroupButtons.forEach(button => {
48
- button.removeEventListener('click', this._handleGroupedButtonClick);
49
- });
50
-
51
- // Clean the references.
52
- this._chatModel = null!;
29
+ this._chatPanel.node.removeEventListener('click', this._handleClick);
53
30
  this._chatPanel = null!;
54
31
  }
55
32
 
56
33
  /**
57
- * Sets up event handlers for existing approval buttons in the chat panel.
58
- */
59
- private _setupApprovalHandlers() {
60
- // This method will be called to add handlers to existing buttons
61
- // New buttons get handlers added in _processApprovalButtons
62
- const existingButtons = this._chatPanel.node.querySelectorAll(
63
- '.jp-ai-approval-btn'
64
- );
65
- existingButtons.forEach(button => {
66
- this._addButtonHandler(button as HTMLButtonElement);
67
- });
68
- }
69
-
70
- /**
71
- * Adds click event handler to an approval button.
72
- *
73
- * @param button - The button element to add handler to
74
- */
75
- private _addButtonHandler(button: HTMLButtonElement) {
76
- // Remove any existing listeners to avoid duplicates
77
- button.removeEventListener('click', this._handleButtonClick);
78
- button.addEventListener('click', this._handleButtonClick);
79
- }
80
-
81
- /**
82
- * Handles click events for individual approval buttons.
83
- *
84
- * @param event - The click event
34
+ * Handles click events using event delegation.
35
+ * Detects clicks on approval buttons and calls the appropriate handler.
85
36
  */
86
- private _handleButtonClick = async (event: Event) => {
37
+ private _handleClick = (event: Event): void => {
87
38
  const target = event.target as HTMLElement;
88
- event.preventDefault();
89
- event.stopPropagation();
90
-
91
- const buttonsContainer = target.closest('.jp-ai-tool-approval-buttons');
92
- if (!buttonsContainer) {
93
- return;
94
- }
95
39
 
96
- const interruptionId = buttonsContainer.getAttribute(
97
- 'data-interruption-id'
98
- );
99
- if (!interruptionId) {
40
+ // Check if the click target is an approval button
41
+ if (!target.classList.contains('jp-ai-approval-btn')) {
100
42
  return;
101
43
  }
102
44
 
103
- // Get message ID for updating the tool call box
104
- const messageId = buttonsContainer.getAttribute('data-message-id');
45
+ event.preventDefault();
46
+ event.stopPropagation();
105
47
 
106
- // Hide buttons immediately and show status
107
48
  const isApprove = target.classList.contains('jp-ai-approval-approve');
108
- this._showApprovalStatus(buttonsContainer, isApprove);
109
-
110
- if (isApprove) {
111
- // Execute approval with message ID for updating the tool call box
112
- await this._chatModel.approveToolCall(
113
- interruptionId,
114
- messageId || undefined
115
- );
116
- } else if (target.classList.contains('jp-ai-approval-reject')) {
117
- // Execute rejection with message ID for updating the tool call box
118
- await this._chatModel.rejectToolCall(
119
- interruptionId,
120
- messageId || undefined
121
- );
122
- }
49
+ this._handleApproval(target, isApprove);
123
50
  };
124
51
 
125
52
  /**
126
- * Adds click event handler to a grouped approval button.
127
- *
128
- * @param button - The button element to add handler to
129
- */
130
- private _addGroupedButtonHandler(button: HTMLButtonElement) {
131
- // Remove any existing listeners to avoid duplicates
132
- button.removeEventListener('click', this._handleGroupedButtonClick);
133
- button.addEventListener('click', this._handleGroupedButtonClick);
134
- }
135
-
136
- /**
137
- * Handles click events for grouped approval buttons.
138
- *
139
- * @param event - The click event
53
+ * Handles approval/rejection of a tool call.
140
54
  */
141
- private _handleGroupedButtonClick = async (event: Event) => {
142
- const target = event.target as HTMLElement;
143
- event.preventDefault();
144
- event.stopPropagation();
145
-
146
- const buttonsContainer = target.closest('.jp-ai-group-approval-buttons');
147
- if (!buttonsContainer) {
55
+ private _handleApproval(target: HTMLElement, isApprove: boolean): void {
56
+ const container = target.closest('.jp-ai-tool-approval-buttons');
57
+ if (!container) {
148
58
  return;
149
59
  }
150
60
 
151
- const groupId = buttonsContainer.getAttribute('data-group-id');
152
- const interruptionIdsStr = buttonsContainer.getAttribute(
153
- 'data-interruption-ids'
154
- );
155
- if (!groupId || !interruptionIdsStr) {
61
+ // Extract approval ID from class name (encoded as jp-ai-approval-id--{id})
62
+ const approvalId = this._extractApprovalId(container);
63
+ if (!approvalId) {
64
+ console.warn('No approval ID found for button');
156
65
  return;
157
66
  }
158
67
 
159
- const interruptionIds = interruptionIdsStr.split(',');
160
- const messageId = buttonsContainer.getAttribute('data-message-id');
161
-
162
- // Hide buttons immediately and show status
163
- const isApprove = target.classList.contains('jp-ai-group-approve-all');
164
- this._showGroupApprovalStatus(buttonsContainer, isApprove);
68
+ // Disable buttons to prevent double-clicks
69
+ const buttons = container.querySelectorAll('button');
70
+ buttons.forEach(btn => btn.setAttribute('disabled', 'true'));
165
71
 
166
72
  if (isApprove) {
167
- // Execute grouped approval
168
- await this._chatModel.approveGroupedToolCalls(
169
- groupId,
170
- interruptionIds,
171
- messageId || undefined
172
- );
173
- } else if (target.classList.contains('jp-ai-group-reject-all')) {
174
- // Execute grouped rejection
175
- await this._chatModel.rejectGroupedToolCalls(
176
- groupId,
177
- interruptionIds,
178
- messageId || undefined
179
- );
73
+ this._agentManager.approveToolCall(approvalId);
74
+ } else {
75
+ this._agentManager.rejectToolCall(approvalId);
180
76
  }
181
- };
182
-
183
- /**
184
- * Shows approval status by replacing buttons with status indicator.
185
- *
186
- * @param buttonsContainer - The container element holding the buttons
187
- * @param isApprove - Whether the action was approval or rejection
188
- */
189
- private _showApprovalStatus(
190
- buttonsContainer: Element,
191
- isApprove: boolean
192
- ): void {
193
- // Clear the container and add status indicator
194
- buttonsContainer.innerHTML = '';
195
-
196
- const statusDiv = document.createElement('div');
197
- statusDiv.className = `jp-ai-approval-status ${isApprove ? 'jp-ai-approval-status-approved' : 'jp-ai-approval-status-rejected'}`;
198
-
199
- const icon = document.createElement('span');
200
- icon.className = 'jp-ai-approval-icon';
201
- icon.textContent = isApprove ? '✅' : '❌';
202
-
203
- const text = document.createElement('span');
204
- text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
205
-
206
- statusDiv.appendChild(icon);
207
- statusDiv.appendChild(text);
208
- buttonsContainer.appendChild(statusDiv);
209
77
  }
210
78
 
211
79
  /**
212
- * Shows group approval status by replacing buttons with status indicator.
213
- *
214
- * @param buttonsContainer - The container element holding the buttons
215
- * @param isApprove - Whether the action was approval or rejection
216
- * @param toolCount - The number of tools that were approved/rejected
80
+ * Extracts the approval ID from an element's class list.
81
+ * The ID is encoded in a class name like "jp-ai-approval-id--{id}".
217
82
  */
218
- private _showGroupApprovalStatus(
219
- buttonsContainer: Element,
220
- isApprove: boolean
221
- ): void {
222
- // Clear the container and add status indicator
223
- buttonsContainer.innerHTML = '';
224
-
225
- const statusDiv = document.createElement('div');
226
- statusDiv.className = `jp-ai-group-approval-status ${isApprove ? 'jp-ai-group-approval-status-approved' : 'jp-ai-group-approval-status-rejected'}`;
227
-
228
- const icon = document.createElement('span');
229
- icon.className = 'jp-ai-approval-icon';
230
- icon.textContent = isApprove ? '✅' : '❌';
231
-
232
- const text = document.createElement('span');
233
- text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
234
-
235
- statusDiv.appendChild(icon);
236
- statusDiv.appendChild(text);
237
- buttonsContainer.appendChild(statusDiv);
238
- }
239
-
240
- /**
241
- * Sets up mutation observer to watch for new messages and process approval buttons.
242
- */
243
- private _setupMessageProcessing() {
244
- // Use a MutationObserver to watch for new messages and process approval buttons
245
- this._mutationObserver = new MutationObserver(mutations => {
246
- if (this._isDisposed) {
247
- return;
248
- }
249
- mutations.forEach(mutation => {
250
- mutation.addedNodes.forEach(node => {
251
- if (node.nodeType === Node.ELEMENT_NODE) {
252
- const element = node as Element;
253
- this._processApprovalButtons(element);
254
- }
255
- });
256
- });
257
- });
258
-
259
- this._mutationObserver.observe(this._chatPanel.node, {
260
- childList: true,
261
- subtree: true
262
- });
263
- }
264
-
265
- /**
266
- * Processes text nodes to replace approval button placeholders with actual button elements.
267
- *
268
- * @param element - The element to search for approval button placeholders
269
- */
270
- private _processApprovalButtons(element: Element) {
271
- // Find all text nodes that contain approval buttons and replace them with actual buttons
272
- const walker = document.createTreeWalker(
273
- element,
274
- NodeFilter.SHOW_TEXT,
275
- null
276
- );
277
-
278
- const textNodes: Text[] = [];
279
- let node;
280
- while ((node = walker.nextNode())) {
281
- textNodes.push(node as Text);
282
- }
283
-
284
- textNodes.forEach(textNode => {
285
- const text = textNode.textContent || '';
286
-
287
- // Handle single tool approval buttons [APPROVAL_BUTTONS:id]
288
- const singleMatch = text.match(/\[APPROVAL_BUTTONS:([^\]]+)\]/);
289
- if (singleMatch) {
290
- this._createSingleApprovalButtons(textNode, singleMatch[1]);
291
- return;
83
+ private _extractApprovalId(element: Element): string | null {
84
+ const prefix = 'jp-ai-approval-id--';
85
+ for (const className of element.classList) {
86
+ if (className.startsWith(prefix)) {
87
+ return className.slice(prefix.length);
292
88
  }
293
-
294
- // Handle grouped tool approval buttons [GROUP_APPROVAL_BUTTONS:groupId:id1,id2,id3]
295
- const groupMatch = text.match(
296
- /\[GROUP_APPROVAL_BUTTONS:([^:]+):([^\]]+)\]/
297
- );
298
- if (groupMatch) {
299
- this._createGroupedApprovalButtons(
300
- textNode,
301
- groupMatch[1],
302
- groupMatch[2]
303
- );
304
- return;
305
- }
306
- });
307
- }
308
-
309
- /**
310
- * Creates an approval button element with appropriate styling and classes.
311
- *
312
- * @param text - The button text
313
- * @param isApprove - Whether this is an approve or reject button
314
- * @param additionalClasses - Additional CSS classes to add
315
- * @returns The created button element
316
- */
317
- private _createApprovalButton(
318
- text: string,
319
- isApprove: boolean,
320
- additionalClasses: string = ''
321
- ): HTMLButtonElement {
322
- const button = document.createElement('button');
323
- const baseClass = isApprove
324
- ? 'jp-ai-approval-approve'
325
- : 'jp-ai-approval-reject';
326
- button.className = `jp-ai-approval-btn ${baseClass}${additionalClasses ? ' ' + additionalClasses : ''}`;
327
- button.textContent = text;
328
- return button;
329
- }
330
-
331
- /**
332
- * Creates and inserts approval buttons for a single tool call.
333
- *
334
- * @param textNode - The text node to replace with buttons
335
- * @param interruptionId - The interruption ID for the tool call
336
- */
337
- private _createSingleApprovalButtons(textNode: Text, interruptionId: string) {
338
- // Create approval buttons for single tool
339
- const buttonContainer = document.createElement('div');
340
- buttonContainer.className = 'jp-ai-tool-approval-buttons';
341
- buttonContainer.setAttribute('data-interruption-id', interruptionId);
342
-
343
- // Try to find the message ID from the closest message container
344
- const messageId = this._findMessageId(textNode);
345
- if (messageId) {
346
- buttonContainer.setAttribute('data-message-id', messageId);
347
- }
348
-
349
- const approveBtn = this._createApprovalButton('Approve', true);
350
- const rejectBtn = this._createApprovalButton('Reject', false);
351
-
352
- // Add click handlers directly to the buttons
353
- this._addButtonHandler(approveBtn);
354
- this._addButtonHandler(rejectBtn);
355
-
356
- buttonContainer.appendChild(approveBtn);
357
- buttonContainer.appendChild(rejectBtn);
358
-
359
- // Replace the text node with the button container
360
- const parent = textNode.parentNode;
361
- if (parent) {
362
- parent.replaceChild(buttonContainer, textNode);
363
- }
364
- }
365
-
366
- /**
367
- * Creates and inserts approval buttons for grouped tool calls.
368
- *
369
- * @param textNode - The text node to replace with buttons
370
- * @param groupId - The group ID for the tool calls
371
- * @param interruptionIds - Comma-separated interruption IDs
372
- */
373
- private _createGroupedApprovalButtons(
374
- textNode: Text,
375
- groupId: string,
376
- interruptionIds: string
377
- ) {
378
- // Create approval buttons for grouped tools
379
- const buttonContainer = document.createElement('div');
380
- buttonContainer.className = 'jp-ai-group-approval-buttons';
381
- buttonContainer.setAttribute('data-group-id', groupId);
382
- buttonContainer.setAttribute('data-interruption-ids', interruptionIds);
383
-
384
- // Try to find the message ID from the closest message container
385
- const messageId = this._findMessageId(textNode);
386
- if (messageId) {
387
- buttonContainer.setAttribute('data-message-id', messageId);
388
- }
389
-
390
- const approveBtn = this._createApprovalButton(
391
- 'Approve',
392
- true,
393
- 'jp-ai-group-approve-all'
394
- );
395
- const rejectBtn = this._createApprovalButton(
396
- 'Reject',
397
- false,
398
- 'jp-ai-group-reject-all'
399
- );
400
-
401
- // Add click handlers for grouped approvals
402
- this._addGroupedButtonHandler(approveBtn);
403
- this._addGroupedButtonHandler(rejectBtn);
404
-
405
- buttonContainer.appendChild(approveBtn);
406
- buttonContainer.appendChild(rejectBtn);
407
-
408
- // Replace the text node with the button container
409
- const parent = textNode.parentNode;
410
- if (parent) {
411
- parent.replaceChild(buttonContainer, textNode);
412
- }
413
- }
414
-
415
- /**
416
- * Finds the message ID by traversing up the DOM tree from a text node.
417
- *
418
- * @param textNode - The text node to start searching from
419
- * @returns The message ID if found, null otherwise
420
- */
421
- private _findMessageId(textNode: Text): string | null {
422
- let messageElement = textNode.parentNode;
423
- while (messageElement && messageElement !== document.body) {
424
- if (messageElement.nodeType === Node.ELEMENT_NODE) {
425
- const element = messageElement as Element;
426
- // Look for common message container attributes or classes
427
- const messageId =
428
- element.getAttribute('data-message-id') ||
429
- element.getAttribute('id') ||
430
- element
431
- .querySelector('[data-message-id]')
432
- ?.getAttribute('data-message-id');
433
- if (messageId) {
434
- return messageId;
435
- }
436
- }
437
- messageElement = messageElement.parentNode;
438
89
  }
439
90
  return null;
440
91
  }
441
92
 
442
93
  private _chatPanel: ChatWidget;
443
- private _chatModel: AIChatModel;
444
94
  private _isDisposed: boolean = false;
445
- private _mutationObserver?: MutationObserver;
95
+ private _agentManager: AgentManager;
446
96
  }
447
97
 
448
98
  /**
449
- * Namespace for ChatWrapperWidget statics.
99
+ * Namespace for ApprovalButtons statics.
450
100
  */
451
101
  export namespace ApprovalButtons {
452
102
  /**
453
- * The options for the constructor of the chat wrapper widget.
103
+ * The options for the constructor of the approval buttons.
454
104
  */
455
105
  export interface IOptions {
456
106
  /**
457
107
  * The chat panel widget to wrap.
458
108
  */
459
109
  chatPanel: ChatWidget;
110
+ /**
111
+ * The agent manager for handling approvals.
112
+ */
113
+ agentManager: AgentManager;
460
114
  }
461
115
  }
@@ -1,4 +1,5 @@
1
1
  import { ActiveCellManager } from '@jupyter/chat';
2
+ import { TranslationBundle } from '@jupyterlab/translation';
2
3
  import { AgentManagerFactory } from './agent';
3
4
  import { AIChatModel } from './chat-model';
4
5
  import { AISettingsModel } from './models/settings-model';
@@ -22,6 +23,7 @@ export class ChatModelRegistry implements IChatModelRegistry {
22
23
  this._toolRegistry = options.toolRegistry;
23
24
  this._providerRegistry = options.providerRegistry;
24
25
  this._activeCellManager = options.activeCellManager;
26
+ this._trans = options.trans;
25
27
  }
26
28
 
27
29
  createModel(
@@ -44,7 +46,8 @@ export class ChatModelRegistry implements IChatModelRegistry {
44
46
  settingsModel: this._settingsModel,
45
47
  agentManager,
46
48
  activeCellManager: this._activeCellManager,
47
- documentManager: this._docManager
49
+ documentManager: this._docManager,
50
+ trans: this._trans
48
51
  });
49
52
 
50
53
  // Set the name of the chat if not provided.
@@ -97,6 +100,7 @@ export class ChatModelRegistry implements IChatModelRegistry {
97
100
  private _toolRegistry?: IToolRegistry;
98
101
  private _providerRegistry?: IProviderRegistry;
99
102
  private _activeCellManager?: ActiveCellManager;
103
+ private _trans: TranslationBundle;
100
104
  }
101
105
 
102
106
  export namespace ChatModelRegistry {
@@ -125,5 +129,9 @@ export namespace ChatModelRegistry {
125
129
  * The active cell manager.
126
130
  */
127
131
  activeCellManager: ActiveCellManager | undefined;
132
+ /**
133
+ * The application language translation bundle.
134
+ */
135
+ trans: TranslationBundle;
128
136
  }
129
137
  }