@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,144 @@
1
+ import { CommandRegistry } from '@lumino/commands';
2
+ import { Panel, Widget } from '@lumino/widgets';
3
+ import { AIChatModel } from '../chat-model';
4
+ import { AISettingsModel } from '../models/settings-model';
5
+ /**
6
+ * A widget wrapper for the chat panel that provides toolbar functionality
7
+ * and approval button handling for AI tool calls.
8
+ */
9
+ export declare class ChatWrapperWidget extends Panel {
10
+ /**
11
+ * Constructs a new ChatWrapperWidget.
12
+ *
13
+ * @param options - Configuration options for the widget
14
+ */
15
+ constructor(options: ChatWrapperWidget.IOptions);
16
+ /**
17
+ * Creates and configures the toolbar with token usage display and settings button.
18
+ *
19
+ * TODO: integrate with IToolbarRegistry to allow adding custom toolbar items
20
+ *
21
+ * @returns The configured toolbar widget
22
+ */
23
+ private _createToolbar;
24
+ /**
25
+ * Sets up event handlers for existing approval buttons in the chat panel.
26
+ */
27
+ private _setupApprovalHandlers;
28
+ /**
29
+ * Adds click event handler to an approval button.
30
+ *
31
+ * @param button - The button element to add handler to
32
+ */
33
+ private _addButtonHandler;
34
+ /**
35
+ * Handles click events for individual approval buttons.
36
+ *
37
+ * @param event - The click event
38
+ */
39
+ private _handleButtonClick;
40
+ /**
41
+ * Adds click event handler to a grouped approval button.
42
+ *
43
+ * @param button - The button element to add handler to
44
+ */
45
+ private _addGroupedButtonHandler;
46
+ /**
47
+ * Handles click events for grouped approval buttons.
48
+ *
49
+ * @param event - The click event
50
+ */
51
+ private _handleGroupedButtonClick;
52
+ /**
53
+ * Shows approval status by replacing buttons with status indicator.
54
+ *
55
+ * @param buttonsContainer - The container element holding the buttons
56
+ * @param isApprove - Whether the action was approval or rejection
57
+ */
58
+ private _showApprovalStatus;
59
+ /**
60
+ * Shows group approval status by replacing buttons with status indicator.
61
+ *
62
+ * @param buttonsContainer - The container element holding the buttons
63
+ * @param isApprove - Whether the action was approval or rejection
64
+ * @param toolCount - The number of tools that were approved/rejected
65
+ */
66
+ private _showGroupApprovalStatus;
67
+ /**
68
+ * Sets up mutation observer to watch for new messages and process approval buttons.
69
+ */
70
+ private _setupMessageProcessing;
71
+ /**
72
+ * Processes text nodes to replace approval button placeholders with actual button elements.
73
+ *
74
+ * @param element - The element to search for approval button placeholders
75
+ */
76
+ private _processApprovalButtons;
77
+ /**
78
+ * Creates an approval button element with appropriate styling and classes.
79
+ *
80
+ * @param text - The button text
81
+ * @param isApprove - Whether this is an approve or reject button
82
+ * @param additionalClasses - Additional CSS classes to add
83
+ * @returns The created button element
84
+ */
85
+ private _createApprovalButton;
86
+ /**
87
+ * Creates and inserts approval buttons for a single tool call.
88
+ *
89
+ * @param textNode - The text node to replace with buttons
90
+ * @param interruptionId - The interruption ID for the tool call
91
+ */
92
+ private _createSingleApprovalButtons;
93
+ /**
94
+ * Creates and inserts approval buttons for grouped tool calls.
95
+ *
96
+ * @param textNode - The text node to replace with buttons
97
+ * @param groupId - The group ID for the tool calls
98
+ * @param interruptionIds - Comma-separated interruption IDs
99
+ */
100
+ private _createGroupedApprovalButtons;
101
+ /**
102
+ * Finds the message ID by traversing up the DOM tree from a text node.
103
+ *
104
+ * @param textNode - The text node to start searching from
105
+ * @returns The message ID if found, null otherwise
106
+ */
107
+ private _findMessageId;
108
+ /**
109
+ * Fixes focus issue by replacing the global click handler with a more selective one.
110
+ * Only focuses the input when clicking empty areas, not on interactive elements.
111
+ */
112
+ private _fixCopyFocusIssue;
113
+ private _chatPanel;
114
+ private _chatModel;
115
+ private _settingsModel;
116
+ private _toolbar;
117
+ private _commands;
118
+ }
119
+ /**
120
+ * Namespace for ChatWrapperWidget statics.
121
+ */
122
+ export declare namespace ChatWrapperWidget {
123
+ /**
124
+ * The options for the constructor of the chat wrapper widget.
125
+ */
126
+ interface IOptions {
127
+ /**
128
+ * The chat panel widget to wrap.
129
+ */
130
+ chatPanel: Widget;
131
+ /**
132
+ * The command registry for the chat wrapper.
133
+ */
134
+ commands: CommandRegistry;
135
+ /**
136
+ * The chat model for the chat wrapper.
137
+ */
138
+ chatModel: AIChatModel;
139
+ /**
140
+ * The settings model for the chat wrapper.
141
+ */
142
+ settingsModel: AISettingsModel;
143
+ }
144
+ }
@@ -0,0 +1,390 @@
1
+ import { settingsIcon, Toolbar, ToolbarButton } from '@jupyterlab/ui-components';
2
+ import { Panel } from '@lumino/widgets';
3
+ import { TokenUsageWidget } from '../components/token-usage-display';
4
+ /**
5
+ * CSS class for the chat toolbar
6
+ */
7
+ const CHAT_TOOLBAR_CLASS = 'jp-AIChatToolbar';
8
+ /**
9
+ * CSS class for the chat panel
10
+ */
11
+ const CHAT_PANEL_CLASS = 'jp-AIChatPanel';
12
+ /**
13
+ * A widget wrapper for the chat panel that provides toolbar functionality
14
+ * and approval button handling for AI tool calls.
15
+ */
16
+ export class ChatWrapperWidget extends Panel {
17
+ /**
18
+ * Constructs a new ChatWrapperWidget.
19
+ *
20
+ * @param options - Configuration options for the widget
21
+ */
22
+ constructor(options) {
23
+ super();
24
+ this._chatPanel = options.chatPanel;
25
+ this._chatModel = options.chatModel;
26
+ this._settingsModel = options.settingsModel;
27
+ this._commands = options.commands;
28
+ this._toolbar = this._createToolbar();
29
+ this.id = '@jupyterlite/ai:chat-wrapper';
30
+ this.title.caption = 'Chat with AI assistant';
31
+ this.title.icon = this._chatPanel.title.icon;
32
+ this.addClass('jp-AIChatWrapper');
33
+ this._toolbar.addClass(CHAT_TOOLBAR_CLASS);
34
+ this._chatPanel.addClass(CHAT_PANEL_CLASS);
35
+ // Add widgets to the panel
36
+ this.addWidget(this._toolbar);
37
+ this.addWidget(this._chatPanel);
38
+ // Set up approval button event handling
39
+ this._setupApprovalHandlers();
40
+ // Set up message processing for approval buttons
41
+ this._setupMessageProcessing();
42
+ // Fix the focus issue: override the global click handler
43
+ // TODO: remove after https://github.com/jupyterlab/jupyter-chat/issues/267
44
+ this._fixCopyFocusIssue();
45
+ }
46
+ /**
47
+ * Creates and configures the toolbar with token usage display and settings button.
48
+ *
49
+ * TODO: integrate with IToolbarRegistry to allow adding custom toolbar items
50
+ *
51
+ * @returns The configured toolbar widget
52
+ */
53
+ _createToolbar() {
54
+ const toolbar = new Toolbar();
55
+ const tokenUsageWidget = new TokenUsageWidget({
56
+ tokenUsageChanged: this._chatModel.tokenUsageChanged,
57
+ settingsModel: this._settingsModel
58
+ });
59
+ toolbar.addItem('token-usage', tokenUsageWidget);
60
+ toolbar.addItem('spacer', Toolbar.createSpacerItem());
61
+ toolbar.addItem('settings', new ToolbarButton({
62
+ icon: settingsIcon,
63
+ onClick: () => {
64
+ this._commands.execute('@jupyterlite/ai:open-settings');
65
+ },
66
+ tooltip: 'Open AI Settings'
67
+ }));
68
+ return toolbar;
69
+ }
70
+ /**
71
+ * Sets up event handlers for existing approval buttons in the chat panel.
72
+ */
73
+ _setupApprovalHandlers() {
74
+ // This method will be called to add handlers to existing buttons
75
+ // New buttons get handlers added in _processApprovalButtons
76
+ const existingButtons = this._chatPanel.node.querySelectorAll('.jp-ai-approval-btn');
77
+ existingButtons.forEach(button => {
78
+ this._addButtonHandler(button);
79
+ });
80
+ }
81
+ /**
82
+ * Adds click event handler to an approval button.
83
+ *
84
+ * @param button - The button element to add handler to
85
+ */
86
+ _addButtonHandler(button) {
87
+ // Remove any existing listeners to avoid duplicates
88
+ button.removeEventListener('click', this._handleButtonClick);
89
+ button.addEventListener('click', this._handleButtonClick);
90
+ }
91
+ /**
92
+ * Handles click events for individual approval buttons.
93
+ *
94
+ * @param event - The click event
95
+ */
96
+ _handleButtonClick = async (event) => {
97
+ const target = event.target;
98
+ event.preventDefault();
99
+ event.stopPropagation();
100
+ const buttonsContainer = target.closest('.jp-ai-tool-approval-buttons');
101
+ if (!buttonsContainer) {
102
+ return;
103
+ }
104
+ const interruptionId = buttonsContainer.getAttribute('data-interruption-id');
105
+ if (!interruptionId) {
106
+ return;
107
+ }
108
+ // Get message ID for updating the tool call box
109
+ const messageId = buttonsContainer.getAttribute('data-message-id');
110
+ // Hide buttons immediately and show status
111
+ const isApprove = target.classList.contains('jp-ai-approval-approve');
112
+ this._showApprovalStatus(buttonsContainer, isApprove);
113
+ if (isApprove) {
114
+ // Execute approval with message ID for updating the tool call box
115
+ await this._chatModel.approveToolCall(interruptionId, messageId || undefined);
116
+ }
117
+ else if (target.classList.contains('jp-ai-approval-reject')) {
118
+ // Execute rejection with message ID for updating the tool call box
119
+ await this._chatModel.rejectToolCall(interruptionId, messageId || undefined);
120
+ }
121
+ };
122
+ /**
123
+ * Adds click event handler to a grouped approval button.
124
+ *
125
+ * @param button - The button element to add handler to
126
+ */
127
+ _addGroupedButtonHandler(button) {
128
+ // Remove any existing listeners to avoid duplicates
129
+ button.removeEventListener('click', this._handleGroupedButtonClick);
130
+ button.addEventListener('click', this._handleGroupedButtonClick);
131
+ }
132
+ /**
133
+ * Handles click events for grouped approval buttons.
134
+ *
135
+ * @param event - The click event
136
+ */
137
+ _handleGroupedButtonClick = async (event) => {
138
+ const target = event.target;
139
+ event.preventDefault();
140
+ event.stopPropagation();
141
+ const buttonsContainer = target.closest('.jp-ai-group-approval-buttons');
142
+ if (!buttonsContainer) {
143
+ return;
144
+ }
145
+ const groupId = buttonsContainer.getAttribute('data-group-id');
146
+ const interruptionIdsStr = buttonsContainer.getAttribute('data-interruption-ids');
147
+ if (!groupId || !interruptionIdsStr) {
148
+ return;
149
+ }
150
+ const interruptionIds = interruptionIdsStr.split(',');
151
+ const messageId = buttonsContainer.getAttribute('data-message-id');
152
+ // Hide buttons immediately and show status
153
+ const isApprove = target.classList.contains('jp-ai-group-approve-all');
154
+ this._showGroupApprovalStatus(buttonsContainer, isApprove);
155
+ if (isApprove) {
156
+ // Execute grouped approval
157
+ await this._chatModel.approveGroupedToolCalls(groupId, interruptionIds, messageId || undefined);
158
+ }
159
+ else if (target.classList.contains('jp-ai-group-reject-all')) {
160
+ // Execute grouped rejection
161
+ await this._chatModel.rejectGroupedToolCalls(groupId, interruptionIds, messageId || undefined);
162
+ }
163
+ };
164
+ /**
165
+ * Shows approval status by replacing buttons with status indicator.
166
+ *
167
+ * @param buttonsContainer - The container element holding the buttons
168
+ * @param isApprove - Whether the action was approval or rejection
169
+ */
170
+ _showApprovalStatus(buttonsContainer, isApprove) {
171
+ // Clear the container and add status indicator
172
+ buttonsContainer.innerHTML = '';
173
+ const statusDiv = document.createElement('div');
174
+ statusDiv.className = `jp-ai-approval-status ${isApprove ? 'jp-ai-approval-status-approved' : 'jp-ai-approval-status-rejected'}`;
175
+ const icon = document.createElement('span');
176
+ icon.className = 'jp-ai-approval-icon';
177
+ icon.textContent = isApprove ? '✅' : '❌';
178
+ const text = document.createElement('span');
179
+ text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
180
+ statusDiv.appendChild(icon);
181
+ statusDiv.appendChild(text);
182
+ buttonsContainer.appendChild(statusDiv);
183
+ }
184
+ /**
185
+ * Shows group approval status by replacing buttons with status indicator.
186
+ *
187
+ * @param buttonsContainer - The container element holding the buttons
188
+ * @param isApprove - Whether the action was approval or rejection
189
+ * @param toolCount - The number of tools that were approved/rejected
190
+ */
191
+ _showGroupApprovalStatus(buttonsContainer, isApprove) {
192
+ // Clear the container and add status indicator
193
+ buttonsContainer.innerHTML = '';
194
+ const statusDiv = document.createElement('div');
195
+ statusDiv.className = `jp-ai-group-approval-status ${isApprove ? 'jp-ai-group-approval-status-approved' : 'jp-ai-group-approval-status-rejected'}`;
196
+ const icon = document.createElement('span');
197
+ icon.className = 'jp-ai-approval-icon';
198
+ icon.textContent = isApprove ? '✅' : '❌';
199
+ const text = document.createElement('span');
200
+ text.textContent = isApprove ? 'Tools approved' : 'Tools rejected';
201
+ statusDiv.appendChild(icon);
202
+ statusDiv.appendChild(text);
203
+ buttonsContainer.appendChild(statusDiv);
204
+ }
205
+ /**
206
+ * Sets up mutation observer to watch for new messages and process approval buttons.
207
+ */
208
+ _setupMessageProcessing() {
209
+ // Use a MutationObserver to watch for new messages and process approval buttons
210
+ const observer = new MutationObserver(mutations => {
211
+ mutations.forEach(mutation => {
212
+ mutation.addedNodes.forEach(node => {
213
+ if (node.nodeType === Node.ELEMENT_NODE) {
214
+ const element = node;
215
+ this._processApprovalButtons(element);
216
+ }
217
+ });
218
+ });
219
+ });
220
+ observer.observe(this._chatPanel.node, {
221
+ childList: true,
222
+ subtree: true
223
+ });
224
+ }
225
+ /**
226
+ * Processes text nodes to replace approval button placeholders with actual button elements.
227
+ *
228
+ * @param element - The element to search for approval button placeholders
229
+ */
230
+ _processApprovalButtons(element) {
231
+ // Find all text nodes that contain approval buttons and replace them with actual buttons
232
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT, null);
233
+ const textNodes = [];
234
+ let node;
235
+ while ((node = walker.nextNode())) {
236
+ textNodes.push(node);
237
+ }
238
+ textNodes.forEach(textNode => {
239
+ const text = textNode.textContent || '';
240
+ // Handle single tool approval buttons [APPROVAL_BUTTONS:id]
241
+ const singleMatch = text.match(/\[APPROVAL_BUTTONS:([^\]]+)\]/);
242
+ if (singleMatch) {
243
+ this._createSingleApprovalButtons(textNode, singleMatch[1]);
244
+ return;
245
+ }
246
+ // Handle grouped tool approval buttons [GROUP_APPROVAL_BUTTONS:groupId:id1,id2,id3]
247
+ const groupMatch = text.match(/\[GROUP_APPROVAL_BUTTONS:([^:]+):([^\]]+)\]/);
248
+ if (groupMatch) {
249
+ this._createGroupedApprovalButtons(textNode, groupMatch[1], groupMatch[2]);
250
+ return;
251
+ }
252
+ });
253
+ }
254
+ /**
255
+ * Creates an approval button element with appropriate styling and classes.
256
+ *
257
+ * @param text - The button text
258
+ * @param isApprove - Whether this is an approve or reject button
259
+ * @param additionalClasses - Additional CSS classes to add
260
+ * @returns The created button element
261
+ */
262
+ _createApprovalButton(text, isApprove, additionalClasses = '') {
263
+ const button = document.createElement('button');
264
+ const baseClass = isApprove
265
+ ? 'jp-ai-approval-approve'
266
+ : 'jp-ai-approval-reject';
267
+ button.className = `jp-ai-approval-btn ${baseClass}${additionalClasses ? ' ' + additionalClasses : ''}`;
268
+ button.textContent = text;
269
+ return button;
270
+ }
271
+ /**
272
+ * Creates and inserts approval buttons for a single tool call.
273
+ *
274
+ * @param textNode - The text node to replace with buttons
275
+ * @param interruptionId - The interruption ID for the tool call
276
+ */
277
+ _createSingleApprovalButtons(textNode, interruptionId) {
278
+ // Create approval buttons for single tool
279
+ const buttonContainer = document.createElement('div');
280
+ buttonContainer.className = 'jp-ai-tool-approval-buttons';
281
+ buttonContainer.setAttribute('data-interruption-id', interruptionId);
282
+ // Try to find the message ID from the closest message container
283
+ const messageId = this._findMessageId(textNode);
284
+ if (messageId) {
285
+ buttonContainer.setAttribute('data-message-id', messageId);
286
+ }
287
+ const approveBtn = this._createApprovalButton('Approve', true);
288
+ const rejectBtn = this._createApprovalButton('Reject', false);
289
+ // Add click handlers directly to the buttons
290
+ this._addButtonHandler(approveBtn);
291
+ this._addButtonHandler(rejectBtn);
292
+ buttonContainer.appendChild(approveBtn);
293
+ buttonContainer.appendChild(rejectBtn);
294
+ // Replace the text node with the button container
295
+ const parent = textNode.parentNode;
296
+ if (parent) {
297
+ parent.replaceChild(buttonContainer, textNode);
298
+ }
299
+ }
300
+ /**
301
+ * Creates and inserts approval buttons for grouped tool calls.
302
+ *
303
+ * @param textNode - The text node to replace with buttons
304
+ * @param groupId - The group ID for the tool calls
305
+ * @param interruptionIds - Comma-separated interruption IDs
306
+ */
307
+ _createGroupedApprovalButtons(textNode, groupId, interruptionIds) {
308
+ // Create approval buttons for grouped tools
309
+ const buttonContainer = document.createElement('div');
310
+ buttonContainer.className = 'jp-ai-group-approval-buttons';
311
+ buttonContainer.setAttribute('data-group-id', groupId);
312
+ buttonContainer.setAttribute('data-interruption-ids', interruptionIds);
313
+ // Try to find the message ID from the closest message container
314
+ const messageId = this._findMessageId(textNode);
315
+ if (messageId) {
316
+ buttonContainer.setAttribute('data-message-id', messageId);
317
+ }
318
+ const approveBtn = this._createApprovalButton('Approve', true, 'jp-ai-group-approve-all');
319
+ const rejectBtn = this._createApprovalButton('Reject', false, 'jp-ai-group-reject-all');
320
+ // Add click handlers for grouped approvals
321
+ this._addGroupedButtonHandler(approveBtn);
322
+ this._addGroupedButtonHandler(rejectBtn);
323
+ buttonContainer.appendChild(approveBtn);
324
+ buttonContainer.appendChild(rejectBtn);
325
+ // Replace the text node with the button container
326
+ const parent = textNode.parentNode;
327
+ if (parent) {
328
+ parent.replaceChild(buttonContainer, textNode);
329
+ }
330
+ }
331
+ /**
332
+ * Finds the message ID by traversing up the DOM tree from a text node.
333
+ *
334
+ * @param textNode - The text node to start searching from
335
+ * @returns The message ID if found, null otherwise
336
+ */
337
+ _findMessageId(textNode) {
338
+ let messageElement = textNode.parentNode;
339
+ while (messageElement && messageElement !== document.body) {
340
+ if (messageElement.nodeType === Node.ELEMENT_NODE) {
341
+ const element = messageElement;
342
+ // Look for common message container attributes or classes
343
+ const messageId = element.getAttribute('data-message-id') ||
344
+ element.getAttribute('id') ||
345
+ element
346
+ .querySelector('[data-message-id]')
347
+ ?.getAttribute('data-message-id');
348
+ if (messageId) {
349
+ return messageId;
350
+ }
351
+ }
352
+ messageElement = messageElement.parentNode;
353
+ }
354
+ return null;
355
+ }
356
+ /**
357
+ * Fixes focus issue by replacing the global click handler with a more selective one.
358
+ * Only focuses the input when clicking empty areas, not on interactive elements.
359
+ */
360
+ _fixCopyFocusIssue() {
361
+ // Remove the global click handler that causes focus on any click
362
+ // The original handler is: this.node.onclick = () => this.model.input.focus();
363
+ if (this._chatPanel.node.onclick) {
364
+ this._chatPanel.node.onclick = null;
365
+ }
366
+ // Add a more selective click handler that only focuses when clicking empty areas
367
+ this._chatPanel.node.addEventListener('click', (event) => {
368
+ const target = event.target;
369
+ // Don't focus if clicking on selectable text elements
370
+ const selection = window.getSelection();
371
+ if (target.closest('pre') ||
372
+ target.closest('code') ||
373
+ target.closest('.jp-RenderedMarkdown') ||
374
+ target.closest('.jp-ai-tool-call') ||
375
+ target.closest('button') ||
376
+ target.closest('.jp-chat-input-container') ||
377
+ (selection && selection.toString().length > 0)) {
378
+ return;
379
+ }
380
+ // Only focus input when clicking empty chat areas
381
+ this._chatModel.input.focus();
382
+ });
383
+ }
384
+ // Private fields
385
+ _chatPanel;
386
+ _chatModel;
387
+ _settingsModel;
388
+ _toolbar;
389
+ _commands;
390
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { IProviderConfig } from '../models/settings-model';
3
+ import type { IChatProviderRegistry } from '../tokens';
4
+ interface IProviderConfigDialogProps {
5
+ open: boolean;
6
+ onClose: () => void;
7
+ onSave: (config: Omit<IProviderConfig, 'id'>) => void;
8
+ initialConfig?: IProviderConfig;
9
+ mode: 'add' | 'edit';
10
+ chatProviderRegistry: IChatProviderRegistry;
11
+ }
12
+ export declare const ProviderConfigDialog: React.FC<IProviderConfigDialogProps>;
13
+ export {};
@@ -0,0 +1,104 @@
1
+ import Visibility from '@mui/icons-material/Visibility';
2
+ import VisibilityOff from '@mui/icons-material/VisibilityOff';
3
+ import { Alert, Box, Button, Chip, Dialog, DialogActions, DialogContent, DialogTitle, FormControl, IconButton, InputAdornment, InputLabel, MenuItem, Select, TextField, Typography } from '@mui/material';
4
+ import React from 'react';
5
+ export const ProviderConfigDialog = ({ open, onClose, onSave, initialConfig, mode, chatProviderRegistry }) => {
6
+ const [name, setName] = React.useState(initialConfig?.name || '');
7
+ const [provider, setProvider] = React.useState(initialConfig?.provider || 'anthropic');
8
+ const [model, setModel] = React.useState(initialConfig?.model || '');
9
+ const [apiKey, setApiKey] = React.useState(initialConfig?.apiKey || '');
10
+ const [baseURL, setBaseURL] = React.useState(initialConfig?.baseURL || '');
11
+ const [showApiKey, setShowApiKey] = React.useState(false);
12
+ // Get provider options from registry
13
+ const providerOptions = React.useMemo(() => {
14
+ const providers = chatProviderRegistry.providers;
15
+ return Object.keys(providers).map(id => {
16
+ const info = providers[id];
17
+ return {
18
+ value: id,
19
+ label: info.name,
20
+ models: info.defaultModels,
21
+ requiresApiKey: info.requiresApiKey,
22
+ allowCustomModel: id === 'ollama' // Only Ollama allows custom models for now
23
+ };
24
+ });
25
+ }, [chatProviderRegistry]);
26
+ const selectedProvider = providerOptions.find(p => p.value === provider);
27
+ React.useEffect(() => {
28
+ if (open) {
29
+ // Reset form when dialog opens
30
+ setName(initialConfig?.name || '');
31
+ setProvider(initialConfig?.provider || 'anthropic');
32
+ setModel(initialConfig?.model || '');
33
+ setApiKey(initialConfig?.apiKey || '');
34
+ setBaseURL(initialConfig?.baseURL || '');
35
+ setShowApiKey(false);
36
+ }
37
+ }, [open, initialConfig]);
38
+ React.useEffect(() => {
39
+ // Auto-select first model when provider changes
40
+ if (selectedProvider && selectedProvider.models.length > 0 && !model) {
41
+ setModel(selectedProvider.models[0]);
42
+ }
43
+ }, [provider, selectedProvider, model]);
44
+ const handleSave = () => {
45
+ if (!name.trim() || !provider || !model) {
46
+ return;
47
+ }
48
+ const config = {
49
+ name: name.trim(),
50
+ provider: provider,
51
+ model,
52
+ ...(selectedProvider?.requiresApiKey && apiKey && { apiKey }),
53
+ ...(baseURL && { baseURL })
54
+ };
55
+ onSave(config);
56
+ onClose();
57
+ };
58
+ const isValid = name.trim() &&
59
+ provider &&
60
+ model &&
61
+ (!selectedProvider?.requiresApiKey || apiKey);
62
+ return (React.createElement(Dialog, { open: open, onClose: onClose, maxWidth: "md", fullWidth: true },
63
+ React.createElement(DialogTitle, null, mode === 'add' ? 'Add New Provider' : 'Edit Provider'),
64
+ React.createElement(DialogContent, null,
65
+ React.createElement(Box, { sx: { display: 'flex', flexDirection: 'column', gap: 2, pt: 1 } },
66
+ React.createElement(TextField, { fullWidth: true, label: "Provider Name", value: name, onChange: e => setName(e.target.value), placeholder: "e.g., My Anthropic Config, Work Provider", helperText: "A friendly name to identify this provider configuration", required: true }),
67
+ React.createElement(FormControl, { fullWidth: true, required: true },
68
+ React.createElement(InputLabel, null, "Provider Type"),
69
+ React.createElement(Select, { value: provider, label: "Provider Type", onChange: e => setProvider(e.target.value) }, providerOptions.map(option => (React.createElement(MenuItem, { key: option.value, value: option.value },
70
+ React.createElement(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1 } },
71
+ option.label,
72
+ option.requiresApiKey && (React.createElement(Chip, { size: "small", label: "API Key", color: "default", variant: "outlined" })))))))),
73
+ selectedProvider?.allowCustomModel ? (React.createElement(TextField, { fullWidth: true, label: "Model", value: model, onChange: e => setModel(e.target.value), placeholder: "Enter model name", helperText: "Enter any compatible model name", required: true })) : (React.createElement(FormControl, { fullWidth: true, required: true },
74
+ React.createElement(InputLabel, null, "Model"),
75
+ React.createElement(Select, { value: model, label: "Model", onChange: e => setModel(e.target.value) }, selectedProvider?.models.map(modelOption => (React.createElement(MenuItem, { key: modelOption, value: modelOption },
76
+ React.createElement(Box, null,
77
+ React.createElement(Typography, { variant: "body1" }, modelOption),
78
+ React.createElement(Typography, { variant: "caption", color: "text.secondary" }, modelOption.includes('sonnet')
79
+ ? 'Balanced performance'
80
+ : modelOption.includes('opus')
81
+ ? 'Advanced reasoning'
82
+ : modelOption.includes('haiku')
83
+ ? 'Fast and lightweight'
84
+ : modelOption.includes('large')
85
+ ? 'Most capable model'
86
+ : modelOption.includes('small')
87
+ ? 'Fast and efficient'
88
+ : modelOption.includes('codestral')
89
+ ? 'Code-specialized'
90
+ : 'General purpose')))))))),
91
+ selectedProvider?.requiresApiKey && (React.createElement(TextField, { fullWidth: true, label: "API Key", type: showApiKey ? 'text' : 'password', value: apiKey, onChange: e => setApiKey(e.target.value), placeholder: "Enter your API key...", required: selectedProvider.requiresApiKey, InputProps: {
92
+ endAdornment: (React.createElement(InputAdornment, { position: "end" },
93
+ React.createElement(IconButton, { onClick: () => setShowApiKey(!showApiKey), edge: "end" }, showApiKey ? React.createElement(VisibilityOff, null) : React.createElement(Visibility, null))))
94
+ } })),
95
+ (provider === 'ollama' || selectedProvider?.allowCustomModel) && (React.createElement(TextField, { fullWidth: true, label: "Base URL (Optional)", value: baseURL, onChange: e => setBaseURL(e.target.value), placeholder: provider === 'ollama'
96
+ ? 'http://localhost:11434/api'
97
+ : 'Custom API endpoint', helperText: provider === 'ollama'
98
+ ? 'Ollama server endpoint'
99
+ : 'Custom API base URL if needed' })),
100
+ !selectedProvider?.requiresApiKey && (React.createElement(Alert, { severity: "info" }, "This provider does not require an API key.")))),
101
+ React.createElement(DialogActions, null,
102
+ React.createElement(Button, { onClick: onClose }, "Cancel"),
103
+ React.createElement(Button, { onClick: handleSave, variant: "contained", disabled: !isValid }, mode === 'add' ? 'Add Provider' : 'Save Changes'))));
104
+ };