@jupyterlite/ai 0.8.0 → 0.9.0-a0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/lib/agent.d.ts +233 -0
  2. package/lib/agent.js +604 -0
  3. package/lib/chat-model.d.ts +195 -0
  4. package/lib/chat-model.js +590 -0
  5. package/lib/completion/completion-provider.d.ts +83 -0
  6. package/lib/completion/completion-provider.js +209 -0
  7. package/lib/completion/index.d.ts +1 -0
  8. package/lib/completion/index.js +1 -0
  9. package/lib/components/clear-button.d.ts +18 -0
  10. package/lib/components/clear-button.js +31 -0
  11. package/lib/components/index.d.ts +3 -0
  12. package/lib/components/index.js +3 -0
  13. package/lib/components/model-select.d.ts +19 -0
  14. package/lib/components/model-select.js +154 -0
  15. package/lib/components/stop-button.d.ts +3 -3
  16. package/lib/components/stop-button.js +8 -9
  17. package/lib/components/token-usage-display.d.ts +45 -0
  18. package/lib/components/token-usage-display.js +74 -0
  19. package/lib/components/tool-select.d.ts +27 -0
  20. package/lib/components/tool-select.js +130 -0
  21. package/lib/icons.d.ts +3 -1
  22. package/lib/icons.js +10 -13
  23. package/lib/index.d.ts +4 -5
  24. package/lib/index.js +322 -167
  25. package/lib/mcp/browser.d.ts +68 -0
  26. package/lib/mcp/browser.js +132 -0
  27. package/lib/models/settings-model.d.ts +69 -0
  28. package/lib/models/settings-model.js +295 -0
  29. package/lib/providers/built-in-providers.d.ts +9 -0
  30. package/lib/providers/built-in-providers.js +192 -0
  31. package/lib/providers/models.d.ts +37 -0
  32. package/lib/providers/models.js +28 -0
  33. package/lib/providers/provider-registry.d.ts +94 -0
  34. package/lib/providers/provider-registry.js +155 -0
  35. package/lib/tokens.d.ts +157 -86
  36. package/lib/tokens.js +16 -12
  37. package/lib/tools/commands.d.ts +11 -0
  38. package/lib/tools/commands.js +126 -0
  39. package/lib/tools/file.d.ts +27 -0
  40. package/lib/tools/file.js +262 -0
  41. package/lib/tools/notebook.d.ts +40 -0
  42. package/lib/tools/notebook.js +762 -0
  43. package/lib/tools/tool-registry.d.ts +35 -0
  44. package/lib/tools/tool-registry.js +55 -0
  45. package/lib/widgets/ai-settings.d.ts +39 -0
  46. package/lib/widgets/ai-settings.js +506 -0
  47. package/lib/widgets/chat-wrapper.d.ts +144 -0
  48. package/lib/widgets/chat-wrapper.js +390 -0
  49. package/lib/widgets/provider-config-dialog.d.ts +13 -0
  50. package/lib/widgets/provider-config-dialog.js +104 -0
  51. package/package.json +150 -41
  52. package/schema/settings-model.json +153 -0
  53. package/src/agent.ts +800 -0
  54. package/src/chat-model.ts +770 -0
  55. package/src/completion/completion-provider.ts +308 -0
  56. package/src/completion/index.ts +1 -0
  57. package/src/components/clear-button.tsx +56 -0
  58. package/src/components/index.ts +3 -0
  59. package/src/components/model-select.tsx +245 -0
  60. package/src/components/stop-button.tsx +11 -11
  61. package/src/components/token-usage-display.tsx +130 -0
  62. package/src/components/tool-select.tsx +218 -0
  63. package/src/icons.ts +12 -14
  64. package/src/index.ts +468 -238
  65. package/src/mcp/browser.ts +213 -0
  66. package/src/models/settings-model.ts +409 -0
  67. package/src/providers/built-in-providers.ts +216 -0
  68. package/src/providers/models.ts +79 -0
  69. package/src/providers/provider-registry.ts +189 -0
  70. package/src/tokens.ts +203 -90
  71. package/src/tools/commands.ts +151 -0
  72. package/src/tools/file.ts +307 -0
  73. package/src/tools/notebook.ts +964 -0
  74. package/src/tools/tool-registry.ts +63 -0
  75. package/src/types.d.ts +4 -0
  76. package/src/widgets/ai-settings.tsx +1100 -0
  77. package/src/widgets/chat-wrapper.tsx +543 -0
  78. package/src/widgets/provider-config-dialog.tsx +256 -0
  79. package/style/base.css +335 -14
  80. package/style/icons/jupyternaut-lite.svg +1 -1
  81. package/lib/base-completer.d.ts +0 -49
  82. package/lib/base-completer.js +0 -14
  83. package/lib/chat-handler.d.ts +0 -56
  84. package/lib/chat-handler.js +0 -201
  85. package/lib/completion-provider.d.ts +0 -34
  86. package/lib/completion-provider.js +0 -32
  87. package/lib/default-prompts.d.ts +0 -2
  88. package/lib/default-prompts.js +0 -31
  89. package/lib/default-providers/Anthropic/completer.d.ts +0 -12
  90. package/lib/default-providers/Anthropic/completer.js +0 -46
  91. package/lib/default-providers/Anthropic/settings-schema.json +0 -70
  92. package/lib/default-providers/ChromeAI/completer.d.ts +0 -12
  93. package/lib/default-providers/ChromeAI/completer.js +0 -56
  94. package/lib/default-providers/ChromeAI/instructions.d.ts +0 -6
  95. package/lib/default-providers/ChromeAI/instructions.js +0 -42
  96. package/lib/default-providers/ChromeAI/settings-schema.json +0 -18
  97. package/lib/default-providers/Gemini/completer.d.ts +0 -12
  98. package/lib/default-providers/Gemini/completer.js +0 -48
  99. package/lib/default-providers/Gemini/instructions.d.ts +0 -2
  100. package/lib/default-providers/Gemini/instructions.js +0 -9
  101. package/lib/default-providers/Gemini/settings-schema.json +0 -64
  102. package/lib/default-providers/MistralAI/completer.d.ts +0 -13
  103. package/lib/default-providers/MistralAI/completer.js +0 -52
  104. package/lib/default-providers/MistralAI/instructions.d.ts +0 -2
  105. package/lib/default-providers/MistralAI/instructions.js +0 -18
  106. package/lib/default-providers/MistralAI/settings-schema.json +0 -75
  107. package/lib/default-providers/Ollama/completer.d.ts +0 -12
  108. package/lib/default-providers/Ollama/completer.js +0 -43
  109. package/lib/default-providers/Ollama/instructions.d.ts +0 -2
  110. package/lib/default-providers/Ollama/instructions.js +0 -70
  111. package/lib/default-providers/Ollama/settings-schema.json +0 -143
  112. package/lib/default-providers/OpenAI/completer.d.ts +0 -12
  113. package/lib/default-providers/OpenAI/completer.js +0 -43
  114. package/lib/default-providers/OpenAI/settings-schema.json +0 -628
  115. package/lib/default-providers/WebLLM/completer.d.ts +0 -21
  116. package/lib/default-providers/WebLLM/completer.js +0 -127
  117. package/lib/default-providers/WebLLM/instructions.d.ts +0 -6
  118. package/lib/default-providers/WebLLM/instructions.js +0 -32
  119. package/lib/default-providers/WebLLM/settings-schema.json +0 -19
  120. package/lib/default-providers/index.d.ts +0 -2
  121. package/lib/default-providers/index.js +0 -179
  122. package/lib/provider.d.ts +0 -144
  123. package/lib/provider.js +0 -412
  124. package/lib/settings/base.json +0 -7
  125. package/lib/settings/index.d.ts +0 -3
  126. package/lib/settings/index.js +0 -3
  127. package/lib/settings/panel.d.ts +0 -226
  128. package/lib/settings/panel.js +0 -510
  129. package/lib/settings/textarea.d.ts +0 -2
  130. package/lib/settings/textarea.js +0 -18
  131. package/lib/settings/utils.d.ts +0 -2
  132. package/lib/settings/utils.js +0 -4
  133. package/lib/types/ai-model.d.ts +0 -24
  134. package/lib/types/ai-model.js +0 -5
  135. package/schema/chat.json +0 -28
  136. package/schema/provider-registry.json +0 -29
  137. package/schema/system-prompts.json +0 -22
  138. package/src/base-completer.ts +0 -75
  139. package/src/chat-handler.ts +0 -262
  140. package/src/completion-provider.ts +0 -64
  141. package/src/default-prompts.ts +0 -33
  142. package/src/default-providers/Anthropic/completer.ts +0 -59
  143. package/src/default-providers/ChromeAI/completer.ts +0 -73
  144. package/src/default-providers/ChromeAI/instructions.ts +0 -45
  145. package/src/default-providers/Gemini/completer.ts +0 -61
  146. package/src/default-providers/Gemini/instructions.ts +0 -9
  147. package/src/default-providers/MistralAI/completer.ts +0 -69
  148. package/src/default-providers/MistralAI/instructions.ts +0 -18
  149. package/src/default-providers/Ollama/completer.ts +0 -54
  150. package/src/default-providers/Ollama/instructions.ts +0 -70
  151. package/src/default-providers/OpenAI/completer.ts +0 -54
  152. package/src/default-providers/WebLLM/completer.ts +0 -151
  153. package/src/default-providers/WebLLM/instructions.ts +0 -33
  154. package/src/default-providers/index.ts +0 -211
  155. package/src/global.d.ts +0 -9
  156. package/src/provider.ts +0 -514
  157. package/src/settings/index.ts +0 -3
  158. package/src/settings/panel.tsx +0 -773
  159. package/src/settings/textarea.tsx +0 -33
  160. package/src/settings/utils.ts +0 -5
  161. package/src/types/ai-model.ts +0 -37
  162. package/src/types/service-worker.d.ts +0 -6
@@ -0,0 +1,543 @@
1
+ import {
2
+ settingsIcon,
3
+ Toolbar,
4
+ ToolbarButton
5
+ } from '@jupyterlab/ui-components';
6
+ import { CommandRegistry } from '@lumino/commands';
7
+ import { Panel, Widget } from '@lumino/widgets';
8
+ import { AIChatModel } from '../chat-model';
9
+ import { TokenUsageWidget } from '../components/token-usage-display';
10
+ import { AISettingsModel } from '../models/settings-model';
11
+
12
+ /**
13
+ * CSS class for the chat toolbar
14
+ */
15
+ const CHAT_TOOLBAR_CLASS = 'jp-AIChatToolbar';
16
+
17
+ /**
18
+ * CSS class for the chat panel
19
+ */
20
+ const CHAT_PANEL_CLASS = 'jp-AIChatPanel';
21
+
22
+ /**
23
+ * A widget wrapper for the chat panel that provides toolbar functionality
24
+ * and approval button handling for AI tool calls.
25
+ */
26
+ export class ChatWrapperWidget extends Panel {
27
+ /**
28
+ * Constructs a new ChatWrapperWidget.
29
+ *
30
+ * @param options - Configuration options for the widget
31
+ */
32
+ constructor(options: ChatWrapperWidget.IOptions) {
33
+ super();
34
+ this._chatPanel = options.chatPanel;
35
+ this._chatModel = options.chatModel;
36
+ this._settingsModel = options.settingsModel;
37
+ this._commands = options.commands;
38
+ this._toolbar = this._createToolbar();
39
+
40
+ this.id = '@jupyterlite/ai:chat-wrapper';
41
+ this.title.caption = 'Chat with AI assistant';
42
+ this.title.icon = this._chatPanel.title.icon;
43
+
44
+ this.addClass('jp-AIChatWrapper');
45
+
46
+ this._toolbar.addClass(CHAT_TOOLBAR_CLASS);
47
+ this._chatPanel.addClass(CHAT_PANEL_CLASS);
48
+
49
+ // Add widgets to the panel
50
+ this.addWidget(this._toolbar);
51
+ this.addWidget(this._chatPanel);
52
+
53
+ // Set up approval button event handling
54
+ this._setupApprovalHandlers();
55
+
56
+ // Set up message processing for approval buttons
57
+ this._setupMessageProcessing();
58
+
59
+ // Fix the focus issue: override the global click handler
60
+ // TODO: remove after https://github.com/jupyterlab/jupyter-chat/issues/267
61
+ this._fixCopyFocusIssue();
62
+ }
63
+
64
+ /**
65
+ * Creates and configures the toolbar with token usage display and settings button.
66
+ *
67
+ * TODO: integrate with IToolbarRegistry to allow adding custom toolbar items
68
+ *
69
+ * @returns The configured toolbar widget
70
+ */
71
+ private _createToolbar() {
72
+ const toolbar = new Toolbar();
73
+ const tokenUsageWidget = new TokenUsageWidget({
74
+ tokenUsageChanged: this._chatModel.tokenUsageChanged,
75
+ settingsModel: this._settingsModel
76
+ });
77
+ toolbar.addItem('token-usage', tokenUsageWidget);
78
+
79
+ toolbar.addItem('spacer', Toolbar.createSpacerItem());
80
+ toolbar.addItem(
81
+ 'settings',
82
+ new ToolbarButton({
83
+ icon: settingsIcon,
84
+ onClick: () => {
85
+ this._commands.execute('@jupyterlite/ai:open-settings');
86
+ },
87
+ tooltip: 'Open AI Settings'
88
+ })
89
+ );
90
+ return toolbar;
91
+ }
92
+
93
+ /**
94
+ * Sets up event handlers for existing approval buttons in the chat panel.
95
+ */
96
+ private _setupApprovalHandlers() {
97
+ // This method will be called to add handlers to existing buttons
98
+ // New buttons get handlers added in _processApprovalButtons
99
+ const existingButtons = this._chatPanel.node.querySelectorAll(
100
+ '.jp-ai-approval-btn'
101
+ );
102
+ existingButtons.forEach(button => {
103
+ this._addButtonHandler(button as HTMLButtonElement);
104
+ });
105
+ }
106
+
107
+ /**
108
+ * Adds click event handler to an approval button.
109
+ *
110
+ * @param button - The button element to add handler to
111
+ */
112
+ private _addButtonHandler(button: HTMLButtonElement) {
113
+ // Remove any existing listeners to avoid duplicates
114
+ button.removeEventListener('click', this._handleButtonClick);
115
+ button.addEventListener('click', this._handleButtonClick);
116
+ }
117
+
118
+ /**
119
+ * Handles click events for individual approval buttons.
120
+ *
121
+ * @param event - The click event
122
+ */
123
+ private _handleButtonClick = async (event: Event) => {
124
+ const target = event.target as HTMLElement;
125
+ event.preventDefault();
126
+ event.stopPropagation();
127
+
128
+ const buttonsContainer = target.closest('.jp-ai-tool-approval-buttons');
129
+ if (!buttonsContainer) {
130
+ return;
131
+ }
132
+
133
+ const interruptionId = buttonsContainer.getAttribute(
134
+ 'data-interruption-id'
135
+ );
136
+ if (!interruptionId) {
137
+ return;
138
+ }
139
+
140
+ // Get message ID for updating the tool call box
141
+ const messageId = buttonsContainer.getAttribute('data-message-id');
142
+
143
+ // Hide buttons immediately and show status
144
+ const isApprove = target.classList.contains('jp-ai-approval-approve');
145
+ this._showApprovalStatus(buttonsContainer, isApprove);
146
+
147
+ if (isApprove) {
148
+ // Execute approval with message ID for updating the tool call box
149
+ await this._chatModel.approveToolCall(
150
+ interruptionId,
151
+ messageId || undefined
152
+ );
153
+ } else if (target.classList.contains('jp-ai-approval-reject')) {
154
+ // Execute rejection with message ID for updating the tool call box
155
+ await this._chatModel.rejectToolCall(
156
+ interruptionId,
157
+ messageId || undefined
158
+ );
159
+ }
160
+ };
161
+
162
+ /**
163
+ * Adds click event handler to a grouped approval button.
164
+ *
165
+ * @param button - The button element to add handler to
166
+ */
167
+ private _addGroupedButtonHandler(button: HTMLButtonElement) {
168
+ // Remove any existing listeners to avoid duplicates
169
+ button.removeEventListener('click', this._handleGroupedButtonClick);
170
+ button.addEventListener('click', this._handleGroupedButtonClick);
171
+ }
172
+
173
+ /**
174
+ * Handles click events for grouped approval buttons.
175
+ *
176
+ * @param event - The click event
177
+ */
178
+ private _handleGroupedButtonClick = async (event: Event) => {
179
+ const target = event.target as HTMLElement;
180
+ event.preventDefault();
181
+ event.stopPropagation();
182
+
183
+ const buttonsContainer = target.closest('.jp-ai-group-approval-buttons');
184
+ if (!buttonsContainer) {
185
+ return;
186
+ }
187
+
188
+ const groupId = buttonsContainer.getAttribute('data-group-id');
189
+ const interruptionIdsStr = buttonsContainer.getAttribute(
190
+ 'data-interruption-ids'
191
+ );
192
+ if (!groupId || !interruptionIdsStr) {
193
+ return;
194
+ }
195
+
196
+ const interruptionIds = interruptionIdsStr.split(',');
197
+ const messageId = buttonsContainer.getAttribute('data-message-id');
198
+
199
+ // Hide buttons immediately and show status
200
+ const isApprove = target.classList.contains('jp-ai-group-approve-all');
201
+ this._showGroupApprovalStatus(buttonsContainer, isApprove);
202
+
203
+ if (isApprove) {
204
+ // Execute grouped approval
205
+ await this._chatModel.approveGroupedToolCalls(
206
+ groupId,
207
+ interruptionIds,
208
+ messageId || undefined
209
+ );
210
+ } else if (target.classList.contains('jp-ai-group-reject-all')) {
211
+ // Execute grouped rejection
212
+ await this._chatModel.rejectGroupedToolCalls(
213
+ groupId,
214
+ interruptionIds,
215
+ messageId || undefined
216
+ );
217
+ }
218
+ };
219
+
220
+ /**
221
+ * Shows approval status by replacing buttons with status indicator.
222
+ *
223
+ * @param buttonsContainer - The container element holding the buttons
224
+ * @param isApprove - Whether the action was approval or rejection
225
+ */
226
+ private _showApprovalStatus(
227
+ buttonsContainer: Element,
228
+ isApprove: boolean
229
+ ): void {
230
+ // Clear the container and add status indicator
231
+ buttonsContainer.innerHTML = '';
232
+
233
+ const statusDiv = document.createElement('div');
234
+ statusDiv.className = `jp-ai-approval-status ${isApprove ? 'jp-ai-approval-status-approved' : 'jp-ai-approval-status-rejected'}`;
235
+
236
+ const icon = document.createElement('span');
237
+ icon.className = 'jp-ai-approval-icon';
238
+ icon.textContent = isApprove ? '✅' : '❌';
239
+
240
+ const text = document.createElement('span');
241
+ text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
242
+
243
+ statusDiv.appendChild(icon);
244
+ statusDiv.appendChild(text);
245
+ buttonsContainer.appendChild(statusDiv);
246
+ }
247
+
248
+ /**
249
+ * Shows group approval status by replacing buttons with status indicator.
250
+ *
251
+ * @param buttonsContainer - The container element holding the buttons
252
+ * @param isApprove - Whether the action was approval or rejection
253
+ * @param toolCount - The number of tools that were approved/rejected
254
+ */
255
+ private _showGroupApprovalStatus(
256
+ buttonsContainer: Element,
257
+ isApprove: boolean
258
+ ): void {
259
+ // Clear the container and add status indicator
260
+ buttonsContainer.innerHTML = '';
261
+
262
+ const statusDiv = document.createElement('div');
263
+ statusDiv.className = `jp-ai-group-approval-status ${isApprove ? 'jp-ai-group-approval-status-approved' : 'jp-ai-group-approval-status-rejected'}`;
264
+
265
+ const icon = document.createElement('span');
266
+ icon.className = 'jp-ai-approval-icon';
267
+ icon.textContent = isApprove ? '✅' : '❌';
268
+
269
+ const text = document.createElement('span');
270
+ text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
271
+
272
+ statusDiv.appendChild(icon);
273
+ statusDiv.appendChild(text);
274
+ buttonsContainer.appendChild(statusDiv);
275
+ }
276
+
277
+ /**
278
+ * Sets up mutation observer to watch for new messages and process approval buttons.
279
+ */
280
+ private _setupMessageProcessing() {
281
+ // Use a MutationObserver to watch for new messages and process approval buttons
282
+ const observer = new MutationObserver(mutations => {
283
+ mutations.forEach(mutation => {
284
+ mutation.addedNodes.forEach(node => {
285
+ if (node.nodeType === Node.ELEMENT_NODE) {
286
+ const element = node as Element;
287
+ this._processApprovalButtons(element);
288
+ }
289
+ });
290
+ });
291
+ });
292
+
293
+ observer.observe(this._chatPanel.node, {
294
+ childList: true,
295
+ subtree: true
296
+ });
297
+ }
298
+
299
+ /**
300
+ * Processes text nodes to replace approval button placeholders with actual button elements.
301
+ *
302
+ * @param element - The element to search for approval button placeholders
303
+ */
304
+ private _processApprovalButtons(element: Element) {
305
+ // Find all text nodes that contain approval buttons and replace them with actual buttons
306
+ const walker = document.createTreeWalker(
307
+ element,
308
+ NodeFilter.SHOW_TEXT,
309
+ null
310
+ );
311
+
312
+ const textNodes: Text[] = [];
313
+ let node;
314
+ while ((node = walker.nextNode())) {
315
+ textNodes.push(node as Text);
316
+ }
317
+
318
+ textNodes.forEach(textNode => {
319
+ const text = textNode.textContent || '';
320
+
321
+ // Handle single tool approval buttons [APPROVAL_BUTTONS:id]
322
+ const singleMatch = text.match(/\[APPROVAL_BUTTONS:([^\]]+)\]/);
323
+ if (singleMatch) {
324
+ this._createSingleApprovalButtons(textNode, singleMatch[1]);
325
+ return;
326
+ }
327
+
328
+ // Handle grouped tool approval buttons [GROUP_APPROVAL_BUTTONS:groupId:id1,id2,id3]
329
+ const groupMatch = text.match(
330
+ /\[GROUP_APPROVAL_BUTTONS:([^:]+):([^\]]+)\]/
331
+ );
332
+ if (groupMatch) {
333
+ this._createGroupedApprovalButtons(
334
+ textNode,
335
+ groupMatch[1],
336
+ groupMatch[2]
337
+ );
338
+ return;
339
+ }
340
+ });
341
+ }
342
+
343
+ /**
344
+ * Creates an approval button element with appropriate styling and classes.
345
+ *
346
+ * @param text - The button text
347
+ * @param isApprove - Whether this is an approve or reject button
348
+ * @param additionalClasses - Additional CSS classes to add
349
+ * @returns The created button element
350
+ */
351
+ private _createApprovalButton(
352
+ text: string,
353
+ isApprove: boolean,
354
+ additionalClasses: string = ''
355
+ ): HTMLButtonElement {
356
+ const button = document.createElement('button');
357
+ const baseClass = isApprove
358
+ ? 'jp-ai-approval-approve'
359
+ : 'jp-ai-approval-reject';
360
+ button.className = `jp-ai-approval-btn ${baseClass}${additionalClasses ? ' ' + additionalClasses : ''}`;
361
+ button.textContent = text;
362
+ return button;
363
+ }
364
+
365
+ /**
366
+ * Creates and inserts approval buttons for a single tool call.
367
+ *
368
+ * @param textNode - The text node to replace with buttons
369
+ * @param interruptionId - The interruption ID for the tool call
370
+ */
371
+ private _createSingleApprovalButtons(textNode: Text, interruptionId: string) {
372
+ // Create approval buttons for single tool
373
+ const buttonContainer = document.createElement('div');
374
+ buttonContainer.className = 'jp-ai-tool-approval-buttons';
375
+ buttonContainer.setAttribute('data-interruption-id', interruptionId);
376
+
377
+ // Try to find the message ID from the closest message container
378
+ const messageId = this._findMessageId(textNode);
379
+ if (messageId) {
380
+ buttonContainer.setAttribute('data-message-id', messageId);
381
+ }
382
+
383
+ const approveBtn = this._createApprovalButton('Approve', true);
384
+ const rejectBtn = this._createApprovalButton('Reject', false);
385
+
386
+ // Add click handlers directly to the buttons
387
+ this._addButtonHandler(approveBtn);
388
+ this._addButtonHandler(rejectBtn);
389
+
390
+ buttonContainer.appendChild(approveBtn);
391
+ buttonContainer.appendChild(rejectBtn);
392
+
393
+ // Replace the text node with the button container
394
+ const parent = textNode.parentNode;
395
+ if (parent) {
396
+ parent.replaceChild(buttonContainer, textNode);
397
+ }
398
+ }
399
+
400
+ /**
401
+ * Creates and inserts approval buttons for grouped tool calls.
402
+ *
403
+ * @param textNode - The text node to replace with buttons
404
+ * @param groupId - The group ID for the tool calls
405
+ * @param interruptionIds - Comma-separated interruption IDs
406
+ */
407
+ private _createGroupedApprovalButtons(
408
+ textNode: Text,
409
+ groupId: string,
410
+ interruptionIds: string
411
+ ) {
412
+ // Create approval buttons for grouped tools
413
+ const buttonContainer = document.createElement('div');
414
+ buttonContainer.className = 'jp-ai-group-approval-buttons';
415
+ buttonContainer.setAttribute('data-group-id', groupId);
416
+ buttonContainer.setAttribute('data-interruption-ids', interruptionIds);
417
+
418
+ // Try to find the message ID from the closest message container
419
+ const messageId = this._findMessageId(textNode);
420
+ if (messageId) {
421
+ buttonContainer.setAttribute('data-message-id', messageId);
422
+ }
423
+
424
+ const approveBtn = this._createApprovalButton(
425
+ 'Approve',
426
+ true,
427
+ 'jp-ai-group-approve-all'
428
+ );
429
+ const rejectBtn = this._createApprovalButton(
430
+ 'Reject',
431
+ false,
432
+ 'jp-ai-group-reject-all'
433
+ );
434
+
435
+ // Add click handlers for grouped approvals
436
+ this._addGroupedButtonHandler(approveBtn);
437
+ this._addGroupedButtonHandler(rejectBtn);
438
+
439
+ buttonContainer.appendChild(approveBtn);
440
+ buttonContainer.appendChild(rejectBtn);
441
+
442
+ // Replace the text node with the button container
443
+ const parent = textNode.parentNode;
444
+ if (parent) {
445
+ parent.replaceChild(buttonContainer, textNode);
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Finds the message ID by traversing up the DOM tree from a text node.
451
+ *
452
+ * @param textNode - The text node to start searching from
453
+ * @returns The message ID if found, null otherwise
454
+ */
455
+ private _findMessageId(textNode: Text): string | null {
456
+ let messageElement = textNode.parentNode;
457
+ while (messageElement && messageElement !== document.body) {
458
+ if (messageElement.nodeType === Node.ELEMENT_NODE) {
459
+ const element = messageElement as Element;
460
+ // Look for common message container attributes or classes
461
+ const messageId =
462
+ element.getAttribute('data-message-id') ||
463
+ element.getAttribute('id') ||
464
+ element
465
+ .querySelector('[data-message-id]')
466
+ ?.getAttribute('data-message-id');
467
+ if (messageId) {
468
+ return messageId;
469
+ }
470
+ }
471
+ messageElement = messageElement.parentNode;
472
+ }
473
+ return null;
474
+ }
475
+
476
+ /**
477
+ * Fixes focus issue by replacing the global click handler with a more selective one.
478
+ * Only focuses the input when clicking empty areas, not on interactive elements.
479
+ */
480
+ private _fixCopyFocusIssue() {
481
+ // Remove the global click handler that causes focus on any click
482
+ // The original handler is: this.node.onclick = () => this.model.input.focus();
483
+ if (this._chatPanel.node.onclick) {
484
+ this._chatPanel.node.onclick = null;
485
+ }
486
+
487
+ // Add a more selective click handler that only focuses when clicking empty areas
488
+ this._chatPanel.node.addEventListener('click', (event: Event) => {
489
+ const target = event.target as HTMLElement;
490
+
491
+ // Don't focus if clicking on selectable text elements
492
+ const selection = window.getSelection();
493
+ if (
494
+ target.closest('pre') ||
495
+ target.closest('code') ||
496
+ target.closest('.jp-RenderedMarkdown') ||
497
+ target.closest('.jp-ai-tool-call') ||
498
+ target.closest('button') ||
499
+ target.closest('.jp-chat-input-container') ||
500
+ (selection && selection.toString().length > 0)
501
+ ) {
502
+ return;
503
+ }
504
+
505
+ // Only focus input when clicking empty chat areas
506
+ this._chatModel.input.focus();
507
+ });
508
+ }
509
+
510
+ // Private fields
511
+ private _chatPanel: Widget;
512
+ private _chatModel: AIChatModel;
513
+ private _settingsModel: AISettingsModel;
514
+ private _toolbar: Toolbar;
515
+ private _commands: CommandRegistry;
516
+ }
517
+
518
+ /**
519
+ * Namespace for ChatWrapperWidget statics.
520
+ */
521
+ export namespace ChatWrapperWidget {
522
+ /**
523
+ * The options for the constructor of the chat wrapper widget.
524
+ */
525
+ export interface IOptions {
526
+ /**
527
+ * The chat panel widget to wrap.
528
+ */
529
+ chatPanel: Widget;
530
+ /**
531
+ * The command registry for the chat wrapper.
532
+ */
533
+ commands: CommandRegistry;
534
+ /**
535
+ * The chat model for the chat wrapper.
536
+ */
537
+ chatModel: AIChatModel;
538
+ /**
539
+ * The settings model for the chat wrapper.
540
+ */
541
+ settingsModel: AISettingsModel;
542
+ }
543
+ }