@plmbr/notebook-intelligence 5.0.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 (137) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +412 -0
  3. package/lib/api.d.ts +288 -0
  4. package/lib/api.js +927 -0
  5. package/lib/cell-output-bundle.d.ts +25 -0
  6. package/lib/cell-output-bundle.js +129 -0
  7. package/lib/cell-output-toolbar.d.ts +26 -0
  8. package/lib/cell-output-toolbar.js +188 -0
  9. package/lib/chat-progress-feedback.d.ts +3 -0
  10. package/lib/chat-progress-feedback.js +27 -0
  11. package/lib/chat-sidebar.d.ts +92 -0
  12. package/lib/chat-sidebar.js +3452 -0
  13. package/lib/command-ids.d.ts +39 -0
  14. package/lib/command-ids.js +44 -0
  15. package/lib/components/ask-user-question.d.ts +2 -0
  16. package/lib/components/ask-user-question.js +85 -0
  17. package/lib/components/checkbox.d.ts +2 -0
  18. package/lib/components/checkbox.js +30 -0
  19. package/lib/components/claude-mcp-panel.d.ts +2 -0
  20. package/lib/components/claude-mcp-panel.js +275 -0
  21. package/lib/components/claude-mcp-paste.d.ts +7 -0
  22. package/lib/components/claude-mcp-paste.js +104 -0
  23. package/lib/components/claude-session-picker.d.ts +8 -0
  24. package/lib/components/claude-session-picker.js +127 -0
  25. package/lib/components/form-dialog.d.ts +25 -0
  26. package/lib/components/form-dialog.js +35 -0
  27. package/lib/components/launcher-picker.d.ts +6 -0
  28. package/lib/components/launcher-picker.js +135 -0
  29. package/lib/components/mcp-util.d.ts +2 -0
  30. package/lib/components/mcp-util.js +37 -0
  31. package/lib/components/notebook-generation-popover.d.ts +7 -0
  32. package/lib/components/notebook-generation-popover.js +60 -0
  33. package/lib/components/pill.d.ts +2 -0
  34. package/lib/components/pill.js +5 -0
  35. package/lib/components/plugins-panel.d.ts +3 -0
  36. package/lib/components/plugins-panel.js +466 -0
  37. package/lib/components/settings-panel.d.ts +11 -0
  38. package/lib/components/settings-panel.js +742 -0
  39. package/lib/components/skills-panel.d.ts +2 -0
  40. package/lib/components/skills-panel.js +1264 -0
  41. package/lib/handler.d.ts +8 -0
  42. package/lib/handler.js +36 -0
  43. package/lib/icons.d.ts +45 -0
  44. package/lib/icons.js +54 -0
  45. package/lib/index.d.ts +8 -0
  46. package/lib/index.js +2079 -0
  47. package/lib/markdown-renderer.d.ts +10 -0
  48. package/lib/markdown-renderer.js +64 -0
  49. package/lib/notebook-generation-toolbar.d.ts +16 -0
  50. package/lib/notebook-generation-toolbar.js +197 -0
  51. package/lib/notebook-generation.d.ts +8 -0
  52. package/lib/notebook-generation.js +12 -0
  53. package/lib/open-file-refresh-watcher-env.d.ts +4 -0
  54. package/lib/open-file-refresh-watcher-env.js +33 -0
  55. package/lib/open-file-refresh-watcher.d.ts +97 -0
  56. package/lib/open-file-refresh-watcher.js +190 -0
  57. package/lib/shell-utils.d.ts +6 -0
  58. package/lib/shell-utils.js +9 -0
  59. package/lib/task-target-notebook.d.ts +2 -0
  60. package/lib/task-target-notebook.js +28 -0
  61. package/lib/terminal-drag-format.d.ts +9 -0
  62. package/lib/terminal-drag-format.js +23 -0
  63. package/lib/terminal-drag.d.ts +12 -0
  64. package/lib/terminal-drag.js +268 -0
  65. package/lib/tokens.d.ts +149 -0
  66. package/lib/tokens.js +88 -0
  67. package/lib/tour/tour-anchors.d.ts +18 -0
  68. package/lib/tour/tour-anchors.js +18 -0
  69. package/lib/tour/tour-config.d.ts +66 -0
  70. package/lib/tour/tour-config.js +99 -0
  71. package/lib/tour/tour-defaults.json +58 -0
  72. package/lib/tour/tour-events.d.ts +19 -0
  73. package/lib/tour/tour-events.js +30 -0
  74. package/lib/tour/tour-overlay.d.ts +6 -0
  75. package/lib/tour/tour-overlay.js +350 -0
  76. package/lib/tour/tour-state.d.ts +20 -0
  77. package/lib/tour/tour-state.js +81 -0
  78. package/lib/tour/tour-steps.d.ts +33 -0
  79. package/lib/tour/tour-steps.js +216 -0
  80. package/lib/utils.d.ts +53 -0
  81. package/lib/utils.js +385 -0
  82. package/package.json +258 -0
  83. package/schema/plugin.json +42 -0
  84. package/src/api.ts +1424 -0
  85. package/src/cell-output-bundle.ts +176 -0
  86. package/src/cell-output-toolbar.ts +232 -0
  87. package/src/chat-progress-feedback.ts +35 -0
  88. package/src/chat-sidebar.tsx +5147 -0
  89. package/src/command-ids.ts +67 -0
  90. package/src/components/ask-user-question.tsx +151 -0
  91. package/src/components/checkbox.tsx +62 -0
  92. package/src/components/claude-mcp-panel.tsx +543 -0
  93. package/src/components/claude-mcp-paste.ts +132 -0
  94. package/src/components/claude-session-picker.tsx +214 -0
  95. package/src/components/form-dialog.tsx +75 -0
  96. package/src/components/launcher-picker.tsx +237 -0
  97. package/src/components/mcp-util.ts +53 -0
  98. package/src/components/notebook-generation-popover.tsx +127 -0
  99. package/src/components/pill.tsx +15 -0
  100. package/src/components/plugins-panel.tsx +774 -0
  101. package/src/components/settings-panel.tsx +1631 -0
  102. package/src/components/skills-panel.tsx +2084 -0
  103. package/src/handler.ts +51 -0
  104. package/src/icons.ts +71 -0
  105. package/src/index.ts +2583 -0
  106. package/src/markdown-renderer.tsx +153 -0
  107. package/src/notebook-generation-toolbar.tsx +281 -0
  108. package/src/notebook-generation.ts +23 -0
  109. package/src/open-file-refresh-watcher-env.ts +52 -0
  110. package/src/open-file-refresh-watcher.ts +260 -0
  111. package/src/shell-utils.ts +10 -0
  112. package/src/svg.d.ts +4 -0
  113. package/src/task-target-notebook.ts +37 -0
  114. package/src/terminal-drag-format.ts +29 -0
  115. package/src/terminal-drag.ts +382 -0
  116. package/src/tokens.ts +171 -0
  117. package/src/tour/tour-anchors.ts +21 -0
  118. package/src/tour/tour-config.ts +160 -0
  119. package/src/tour/tour-events.ts +34 -0
  120. package/src/tour/tour-overlay.tsx +474 -0
  121. package/src/tour/tour-state.ts +87 -0
  122. package/src/tour/tour-steps.ts +281 -0
  123. package/src/utils.ts +455 -0
  124. package/style/base.css +3238 -0
  125. package/style/icons/cell-toolbar-bug.svg +5 -0
  126. package/style/icons/cell-toolbar-chat.svg +5 -0
  127. package/style/icons/cell-toolbar-sparkle.svg +5 -0
  128. package/style/icons/claude.svg +1 -0
  129. package/style/icons/copilot-warning.svg +1 -0
  130. package/style/icons/copilot.svg +1 -0
  131. package/style/icons/copy.svg +1 -0
  132. package/style/icons/openai.svg +1 -0
  133. package/style/icons/opencode.svg +1 -0
  134. package/style/icons/sparkles-warning.svg +5 -0
  135. package/style/icons/sparkles.svg +1 -0
  136. package/style/index.css +1 -0
  137. package/style/index.js +1 -0
@@ -0,0 +1,3452 @@
1
+ // Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
2
+ import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
3
+ import { Notification, ReactWidget } from '@jupyterlab/apputils';
4
+ import { UUID } from '@lumino/coreutils';
5
+ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js';
6
+ import { NBIAPI, GitHubCopilotLoginStatus } from './api';
7
+ import { injectTaskTargetNotebook } from './task-target-notebook';
8
+ import { formatElapsedSeconds, isHeartbeatStale } from './chat-progress-feedback';
9
+ import { BackendMessageType, BuiltinToolsetType, CLAUDE_CODE_CHAT_PARTICIPANT_ID, ContextType, RequestDataType, ResponseStreamDataType, TelemetryEventType } from './tokens';
10
+ import { MarkdownRenderer as OriginalMarkdownRenderer } from './markdown-renderer';
11
+ const MarkdownRenderer = memo(OriginalMarkdownRenderer);
12
+ import copySvgstr from '../style/icons/copy.svg';
13
+ import copilotSvgstr from '../style/icons/copilot.svg';
14
+ import copilotWarningSvgstr from '../style/icons/copilot-warning.svg';
15
+ import { VscSend, VscStopCircle, VscEye, VscEyeClosed, VscAdd, VscClose, VscHistory, VscTriangleRight, VscTriangleDown, VscSettingsGear, VscPassFilled, VscTools, VscTrash, VscThumbsup, VscThumbsdown, VscThumbsupFilled, VscThumbsdownFilled, VscCloudUpload, VscFile, VscRefresh } from './icons';
16
+ import { extractLLMGeneratedCode, isDarkTheme, safeAnchorUri, writeTextToClipboard } from './utils';
17
+ import { CheckBoxItem } from './components/checkbox';
18
+ import { mcpServerSettingsToEnabledState } from './components/mcp-util';
19
+ import claudeSvgStr from '../style/icons/claude.svg';
20
+ import { AskUserQuestion } from './components/ask-user-question';
21
+ import { ClaudeSessionPicker } from './components/claude-session-picker';
22
+ import { TourOverlay } from './tour/tour-overlay';
23
+ import { TOUR_ANCHOR } from './tour/tour-anchors';
24
+ import { TOUR_START_EVENT, TOUR_STOP_EVENT } from './tour/tour-events';
25
+ import { hasCompletedTour } from './tour/tour-state';
26
+ import { NOTEBOOK_GENERATION_PROGRESS_EVENT } from './notebook-generation';
27
+ export var RunChatCompletionType;
28
+ (function (RunChatCompletionType) {
29
+ RunChatCompletionType[RunChatCompletionType["Chat"] = 0] = "Chat";
30
+ RunChatCompletionType[RunChatCompletionType["ExplainThis"] = 1] = "ExplainThis";
31
+ RunChatCompletionType[RunChatCompletionType["FixThis"] = 2] = "FixThis";
32
+ RunChatCompletionType[RunChatCompletionType["GenerateCode"] = 3] = "GenerateCode";
33
+ RunChatCompletionType[RunChatCompletionType["NotebookGeneration"] = 4] = "NotebookGeneration";
34
+ })(RunChatCompletionType || (RunChatCompletionType = {}));
35
+ export class ChatSidebar extends ReactWidget {
36
+ constructor(options) {
37
+ super();
38
+ this._options = options;
39
+ this.node.style.height = '100%';
40
+ }
41
+ render() {
42
+ return (React.createElement(SidebarComponent, { getCurrentDirectory: this._options.getCurrentDirectory, getActiveDocumentInfo: this._options.getActiveDocumentInfo, getActiveSelectionContent: this._options.getActiveSelectionContent, getCurrentCellContents: this._options.getCurrentCellContents, openFile: this._options.openFile, getApp: this._options.getApp, getTelemetryEmitter: this._options.getTelemetryEmitter }));
43
+ }
44
+ }
45
+ export class InlinePromptWidget extends ReactWidget {
46
+ // Pass `rect` for floating mode (file editor). Pass null for inline mode
47
+ // (notebook cell), where CodeMirror owns the in-flow block placement.
48
+ constructor(rect, options) {
49
+ super();
50
+ this._streamError = null;
51
+ this._floating = false;
52
+ this.node.classList.add('inline-prompt-widget');
53
+ if (rect) {
54
+ this._floating = true;
55
+ this.node.classList.add('inline-prompt-widget-floating');
56
+ this.node.style.top = `${rect.top + 32}px`;
57
+ this.node.style.left = `${rect.left}px`;
58
+ this.node.style.width = rect.width + 'px';
59
+ this.node.style.height = '48px';
60
+ }
61
+ else {
62
+ this.node.classList.add('inline-prompt-widget-inline');
63
+ this.node.style.height = '48px';
64
+ }
65
+ this._options = options;
66
+ if (this._floating) {
67
+ this.node.addEventListener('focusout', (event) => {
68
+ if (this.node.contains(event.relatedTarget)) {
69
+ return;
70
+ }
71
+ window.setTimeout(() => {
72
+ if (this.node.contains(document.activeElement)) {
73
+ return;
74
+ }
75
+ this._options.onRequestCancelled();
76
+ }, 0);
77
+ });
78
+ }
79
+ }
80
+ updatePosition(rect) {
81
+ if (!this._floating) {
82
+ return;
83
+ }
84
+ this.node.style.top = `${rect.top + 32}px`;
85
+ this.node.style.left = `${rect.left}px`;
86
+ this.node.style.width = rect.width + 'px';
87
+ }
88
+ _onResponse(response) {
89
+ var _a, _b, _c, _d, _e, _f;
90
+ if (response.type === BackendMessageType.StreamMessage) {
91
+ // Backend sets nbi_stream_error alongside the [Stream interrupted]
92
+ // marker delta. Capture it so onContentStreamEnd can tell the
93
+ // auto-insert path to skip writing the partial buffer.
94
+ if (typeof ((_a = response.data) === null || _a === void 0 ? void 0 : _a.nbi_stream_error) === 'string') {
95
+ this._streamError = response.data.nbi_stream_error;
96
+ }
97
+ const delta = (_c = (_b = response.data['choices']) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c['delta'];
98
+ if (!delta) {
99
+ return;
100
+ }
101
+ const responseMessage = (_f = (_e = (_d = response.data['choices']) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e['delta']) === null || _f === void 0 ? void 0 : _f['content'];
102
+ if (!responseMessage) {
103
+ return;
104
+ }
105
+ this._options.onContentStream(responseMessage);
106
+ }
107
+ else if (response.type === BackendMessageType.StreamEnd) {
108
+ this._options.onContentStreamEnd(this._streamError);
109
+ this._streamError = null;
110
+ const timeElapsed = (new Date().getTime() - this._requestTime.getTime()) / 1000;
111
+ this._options.telemetryEmitter.emitTelemetryEvent({
112
+ type: TelemetryEventType.InlineChatResponse,
113
+ data: {
114
+ chatModel: {
115
+ provider: NBIAPI.config.chatModel.provider,
116
+ model: NBIAPI.config.chatModel.model
117
+ },
118
+ timeElapsed
119
+ }
120
+ });
121
+ }
122
+ }
123
+ _onRequestSubmitted(prompt) {
124
+ // code update
125
+ if (this._options.existingCode !== '') {
126
+ this.node.style.height = '300px';
127
+ }
128
+ // save the prompt in case of a rerender
129
+ this._options.prompt = prompt;
130
+ this._options.onRequestSubmitted(prompt);
131
+ this._requestTime = new Date();
132
+ this._options.telemetryEmitter.emitTelemetryEvent({
133
+ type: TelemetryEventType.InlineChatRequest,
134
+ data: {
135
+ chatModel: {
136
+ provider: NBIAPI.config.chatModel.provider,
137
+ model: NBIAPI.config.chatModel.model
138
+ },
139
+ prompt: prompt
140
+ }
141
+ });
142
+ }
143
+ render() {
144
+ return (React.createElement(InlinePopoverComponent, { prompt: this._options.prompt, existingCode: this._options.existingCode, onRequestSubmitted: this._onRequestSubmitted.bind(this), onRequestCancelled: this._options.onRequestCancelled, onResponseEmit: this._onResponse.bind(this), prefix: this._options.prefix, suffix: this._options.suffix, onUpdatedCodeChange: this._options.onUpdatedCodeChange, onUpdatedCodeAccepted: this._options.onUpdatedCodeAccepted }));
145
+ }
146
+ }
147
+ export class GitHubCopilotStatusBarItem extends ReactWidget {
148
+ constructor(options) {
149
+ super();
150
+ this._getApp = options.getApp;
151
+ }
152
+ render() {
153
+ return React.createElement(GitHubCopilotStatusComponent, { getApp: this._getApp });
154
+ }
155
+ }
156
+ export class GitHubCopilotLoginDialogBody extends ReactWidget {
157
+ constructor(options) {
158
+ super();
159
+ this._onLoggedIn = options.onLoggedIn;
160
+ }
161
+ render() {
162
+ return (React.createElement(GitHubCopilotLoginDialogBodyComponent, { onLoggedIn: () => this._onLoggedIn() }));
163
+ }
164
+ }
165
+ const MAX_ATTACHED_FILES = 10;
166
+ const TEXT_MIME_PREFIXES = [
167
+ 'text/',
168
+ 'application/json',
169
+ 'application/xml',
170
+ 'application/x-yaml',
171
+ 'application/yaml'
172
+ ];
173
+ const TEXT_EXTENSIONS = new Set([
174
+ '.py',
175
+ '.js',
176
+ '.ts',
177
+ '.tsx',
178
+ '.jsx',
179
+ '.json',
180
+ '.yaml',
181
+ '.yml',
182
+ '.md',
183
+ '.txt',
184
+ '.csv',
185
+ '.html',
186
+ '.css',
187
+ '.sql',
188
+ '.sh',
189
+ '.r',
190
+ '.ipynb',
191
+ '.xml',
192
+ '.toml',
193
+ '.cfg',
194
+ '.ini',
195
+ '.env',
196
+ '.gitignore',
197
+ '.dockerfile',
198
+ '.svg',
199
+ '.rb',
200
+ '.go',
201
+ '.rs',
202
+ '.java',
203
+ '.c',
204
+ '.cpp',
205
+ '.h',
206
+ '.hpp',
207
+ '.swift',
208
+ '.kt',
209
+ '.scala',
210
+ '.lua',
211
+ '.pl',
212
+ '.m',
213
+ '.mm'
214
+ ]);
215
+ function isLikelyTextFile(file) {
216
+ var _a;
217
+ if (TEXT_MIME_PREFIXES.some(prefix => file.type.startsWith(prefix))) {
218
+ return true;
219
+ }
220
+ const ext = '.' + ((_a = file.name.split('.').pop()) === null || _a === void 0 ? void 0 : _a.toLowerCase());
221
+ return TEXT_EXTENSIONS.has(ext);
222
+ }
223
+ function readFileAsText(file) {
224
+ return new Promise((resolve, reject) => {
225
+ const reader = new FileReader();
226
+ reader.onload = () => resolve(reader.result);
227
+ reader.onerror = () => reject(reader.error);
228
+ reader.readAsText(file);
229
+ });
230
+ }
231
+ function readFileAsDataURL(file) {
232
+ return new Promise((resolve, reject) => {
233
+ const reader = new FileReader();
234
+ reader.onload = () => resolve(reader.result);
235
+ reader.onerror = () => reject(reader.error);
236
+ reader.readAsDataURL(file);
237
+ });
238
+ }
239
+ const MAX_VISIBLE_WORKSPACE_FILES = 50;
240
+ const MAX_WORKSPACE_FILE_SCAN_COUNT = 1500;
241
+ const SKIPPED_WORKSPACE_DIRECTORIES = new Set(['__pycache__', 'node_modules']);
242
+ // Bounded parallelism for the workspace tree walk. The Jupyter Contents API
243
+ // is per-directory, so a directory-heavy workspace with strictly serial
244
+ // fetches gets dominated by HTTP roundtrip latency. Eight in flight at a
245
+ // time stays well under the server's default tornado handler pool while
246
+ // recovering most of the easy speedup; further parallelism is bounded by
247
+ // the tree's width and the slowest fetch in each batch.
248
+ const WORKSPACE_SCAN_CONCURRENCY = 8;
249
+ // Coalesce window for the Contents-API `fileChanged` storm that fires when
250
+ // the user (or an agent) creates a directory, drops a folder of files, or
251
+ // renames a tree. Without coalescing, one drag-drop of N items would
252
+ // schedule N rescans; with a 300ms window every realistic bulk operation
253
+ // settles into a single rescan.
254
+ const WORKSPACE_FILE_REFRESH_DEBOUNCE_MS = 300;
255
+ function countContentLines(content) {
256
+ if (content === '') {
257
+ return 1;
258
+ }
259
+ return content.split('\n').length;
260
+ }
261
+ function serializeWorkspaceFileContent(model) {
262
+ if (model.type === 'directory') {
263
+ throw new Error('Directories cannot be attached as chat context.');
264
+ }
265
+ if (model.format === 'base64') {
266
+ throw new Error('Binary files cannot be attached as chat context.');
267
+ }
268
+ if (typeof model.content === 'string') {
269
+ return model.content;
270
+ }
271
+ if (model.content === null || model.content === undefined) {
272
+ return '';
273
+ }
274
+ return JSON.stringify(model.content, null, 2);
275
+ }
276
+ const answeredForms = new Map();
277
+ function ChatResponseHTMLFrame(props) {
278
+ const iframSrc = useMemo(() => URL.createObjectURL(new Blob([props.source], { type: 'text/html' })), []);
279
+ return (React.createElement("div", { className: "chat-response-html-frame", key: `key-${props.index}` },
280
+ React.createElement("iframe", { className: "chat-response-html-frame-iframe", height: props.height, sandbox: "allow-scripts", src: iframSrc })));
281
+ }
282
+ // Memoize ChatResponse for performance
283
+ function ChatResponse(props) {
284
+ var _a, _b, _c;
285
+ const [renderCount, setRenderCount] = useState(0);
286
+ const msg = props.message;
287
+ const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
288
+ const openNotebook = (event) => {
289
+ const notebookPath = event.target.dataset['ref'];
290
+ props.openFile(notebookPath);
291
+ };
292
+ const markFormConfirmed = (contentId) => {
293
+ answeredForms.set(contentId, 'confirmed');
294
+ setRenderCount(prev => prev + 1);
295
+ };
296
+ const markFormCanceled = (contentId) => {
297
+ answeredForms.set(contentId, 'canceled');
298
+ setRenderCount(prev => prev + 1);
299
+ };
300
+ const runCommand = (commandId, args) => {
301
+ props.getApp().commands.execute(commandId, args);
302
+ };
303
+ // group messages by type
304
+ const groupedContents = [];
305
+ let lastItemType;
306
+ const responseDetailTags = [
307
+ '<think>',
308
+ '</think>',
309
+ '<terminal-output>',
310
+ '</terminal-output>'
311
+ ];
312
+ const extractReasoningContent = (item) => {
313
+ let currentContent = item.content;
314
+ if (typeof currentContent !== 'string') {
315
+ return item.reasoningContent && !item.reasoningFinished;
316
+ }
317
+ let reasoningContent = '';
318
+ const reasoningStartTime = new Date(item.created);
319
+ const reasoningEndTime = new Date();
320
+ let startPos = -1;
321
+ let startTag = '';
322
+ for (const tag of responseDetailTags) {
323
+ startPos = currentContent.indexOf(tag);
324
+ if (startPos >= 0) {
325
+ startTag = tag;
326
+ break;
327
+ }
328
+ }
329
+ const hasStart = startPos >= 0;
330
+ if (hasStart) {
331
+ currentContent = currentContent.substring(startPos + startTag.length);
332
+ }
333
+ let endPos = -1;
334
+ let endTag = '';
335
+ for (const tag of responseDetailTags) {
336
+ endPos = currentContent.indexOf(tag);
337
+ if (endPos >= 0) {
338
+ endTag = tag;
339
+ break;
340
+ }
341
+ }
342
+ const hasEnd = endPos >= 0;
343
+ if (hasEnd) {
344
+ reasoningContent += currentContent.substring(0, endPos);
345
+ currentContent = currentContent.substring(endPos + endTag.length);
346
+ }
347
+ else {
348
+ if (hasStart) {
349
+ reasoningContent += currentContent;
350
+ currentContent = '';
351
+ }
352
+ }
353
+ if (hasStart) {
354
+ item.content = currentContent;
355
+ item.reasoningTag = startTag;
356
+ item.reasoningContent = (item.reasoningContent || '') + reasoningContent;
357
+ item.reasoningFinished = hasEnd;
358
+ }
359
+ if (item.reasoningContent) {
360
+ item.reasoningTime =
361
+ (reasoningEndTime.getTime() - reasoningStartTime.getTime()) / 1000;
362
+ }
363
+ return hasStart && !hasEnd; // is thinking extracted now
364
+ };
365
+ for (let i = 0; i < msg.contents.length; i++) {
366
+ const item = msg.contents[i];
367
+ if (item.type === lastItemType &&
368
+ lastItemType === ResponseStreamDataType.MarkdownPart) {
369
+ const lastItem = groupedContents[groupedContents.length - 1];
370
+ lastItem.content += item.content || '';
371
+ if (item.reasoningContent) {
372
+ lastItem.reasoningContent =
373
+ (lastItem.reasoningContent || '') + item.reasoningContent;
374
+ }
375
+ if (item.reasoningFinished) {
376
+ lastItem.reasoningFinished = true;
377
+ }
378
+ }
379
+ else {
380
+ groupedContents.push(structuredClone(item));
381
+ lastItemType = item.type;
382
+ }
383
+ }
384
+ const [thinkingInProgress, setThinkingInProgress] = useState(false);
385
+ for (const item of groupedContents) {
386
+ const isThinking = extractReasoningContent(item);
387
+ if (isThinking && !thinkingInProgress) {
388
+ setThinkingInProgress(true);
389
+ }
390
+ }
391
+ useEffect(() => {
392
+ let intervalId = undefined;
393
+ if (thinkingInProgress) {
394
+ intervalId = setInterval(() => {
395
+ setRenderCount(prev => prev + 1);
396
+ setThinkingInProgress(false);
397
+ }, 1000);
398
+ }
399
+ return () => clearInterval(intervalId);
400
+ }, [thinkingInProgress]);
401
+ const onExpandCollapseClick = (event) => {
402
+ const parent = event.currentTarget.parentElement;
403
+ if (parent.classList.contains('expanded')) {
404
+ parent.classList.remove('expanded');
405
+ }
406
+ else {
407
+ parent.classList.add('expanded');
408
+ }
409
+ };
410
+ const getReasoningTitle = (item) => {
411
+ if (item.reasoningTag === '<terminal-output>') {
412
+ return item.reasoningFinished
413
+ ? 'Output'
414
+ : `Running (${Math.floor(item.reasoningTime)} s)`;
415
+ }
416
+ return item.reasoningFinished
417
+ ? 'Thought'
418
+ : `Thinking (${Math.floor(item.reasoningTime)} s)`;
419
+ };
420
+ const chatParticipantId = ((_a = msg.participant) === null || _a === void 0 ? void 0 : _a.id) || 'default';
421
+ return (React.createElement("div", { className: `chat-message chat-message-${msg.from}`, "data-render-count": renderCount },
422
+ React.createElement("div", { className: "chat-message-header" },
423
+ React.createElement("div", { className: "chat-message-from" },
424
+ ((_b = msg.participant) === null || _b === void 0 ? void 0 : _b.iconPath) && (React.createElement("div", { className: `chat-message-from-icon chat-message-from-icon-${chatParticipantId} ${isDarkTheme() ? 'dark' : ''}` },
425
+ React.createElement("img", { src: msg.participant.iconPath, alt: "" }))),
426
+ React.createElement("div", { className: "chat-message-from-title" }, msg.from === 'user'
427
+ ? 'User'
428
+ : ((_c = msg.participant) === null || _c === void 0 ? void 0 : _c.name) || 'AI Assistant'),
429
+ React.createElement("div", { className: "chat-message-from-progress", style: { display: `${props.showGenerating ? 'visible' : 'none'}` } },
430
+ React.createElement("span", {
431
+ // Key on the heartbeat tick so React re-mounts the dot on
432
+ // every beat; CSS-animation restart from an attribute-only
433
+ // change is not reliable across browsers.
434
+ key: props.heartbeatTick, className: `generating-pulse${props.isStalled ? ' is-stalled' : ''}`, "aria-hidden": "true" }),
435
+ React.createElement("div", { className: "generating-label", "aria-live": "polite", "aria-atomic": "true" },
436
+ props.isStalled
437
+ ? 'Still working, server may be slow'
438
+ : 'Generating',
439
+ props.showGenerating && props.elapsedSeconds > 0
440
+ ? ` (${formatElapsedSeconds(props.elapsedSeconds)})`
441
+ : ''))),
442
+ React.createElement("div", { className: "chat-message-timestamp" }, timestamp)),
443
+ React.createElement("div", { className: "chat-message-content" },
444
+ groupedContents.map((item, index) => {
445
+ switch (item.type) {
446
+ case ResponseStreamDataType.Markdown:
447
+ case ResponseStreamDataType.MarkdownPart:
448
+ return (React.createElement(React.Fragment, null,
449
+ item.reasoningContent &&
450
+ typeof item.reasoningContent === 'string' && (React.createElement("div", { className: `expandable-content ${!item.reasoningFinished ? 'expanded' : ''}` },
451
+ React.createElement("button", { type: "button", className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event), "aria-expanded": !item.reasoningFinished },
452
+ React.createElement(VscTriangleRight, { className: "collapsed-icon", "aria-hidden": "true" }),
453
+ React.createElement(VscTriangleDown, { className: "expanded-icon", "aria-hidden": "true" }),
454
+ ' ',
455
+ getReasoningTitle(item)),
456
+ React.createElement("div", { className: "expandable-content-text" },
457
+ React.createElement(MarkdownRenderer, { key: `reasoning-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.reasoningContent)))),
458
+ React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.content.replace(/\n/gi, ' \n')),
459
+ item.contentDetail ? (React.createElement("div", { className: "expandable-content expanded" },
460
+ React.createElement("button", { type: "button", className: "expandable-content-title", onClick: (event) => onExpandCollapseClick(event), "aria-expanded": true },
461
+ React.createElement(VscTriangleRight, { className: "collapsed-icon", "aria-hidden": "true" }),
462
+ React.createElement(VscTriangleDown, { className: "expanded-icon", "aria-hidden": "true" }),
463
+ ' ',
464
+ item.contentDetail.title),
465
+ React.createElement("div", { className: "expandable-content-text" },
466
+ React.createElement(MarkdownRenderer, { key: `key-${index}`, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo }, item.contentDetail.content)))) : null));
467
+ case ResponseStreamDataType.Image:
468
+ return (React.createElement("div", { className: "chat-response-img", key: `key-${index}` },
469
+ React.createElement("img", { src: item.content, alt: "Chat response image" })));
470
+ case ResponseStreamDataType.HTMLFrame:
471
+ return (React.createElement(ChatResponseHTMLFrame, { index: index, source: item.content.source, height: item.content.height }));
472
+ case ResponseStreamDataType.Button:
473
+ return (React.createElement("div", { className: "chat-response-button", key: `key-${index}` },
474
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => runCommand(item.content.commandId, item.content.args) },
475
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.title))));
476
+ case ResponseStreamDataType.Anchor: {
477
+ const safeUri = safeAnchorUri(item.content.uri);
478
+ if (!safeUri) {
479
+ return (React.createElement("div", { className: "chat-response-anchor", key: `key-${index}` },
480
+ React.createElement("span", null,
481
+ item.content.title,
482
+ React.createElement("span", { className: "nbi-sr-only" }, " (link blocked)"))));
483
+ }
484
+ return (React.createElement("div", { className: "chat-response-anchor", key: `key-${index}` },
485
+ React.createElement("a", { href: safeUri, target: "_blank", rel: "noopener noreferrer" },
486
+ item.content.title,
487
+ React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)"))));
488
+ }
489
+ case ResponseStreamDataType.Progress:
490
+ // Render only the most recent progress entry, and only while
491
+ // the request is still in flight — once the assistant has
492
+ // finished, transient activity markers disappear. The icon
493
+ // is part of the streamed text so backend callers can pick
494
+ // an appropriate symbol (e.g. ↻ for in-progress, ✓ for done,
495
+ // ✗ for error) rather than forcing a single rendering here.
496
+ return index === groupedContents.length - 1 &&
497
+ props.showGenerating ? (React.createElement("div", { className: "chat-response-progress", key: `key-${index}` }, item.content)) : null;
498
+ case ResponseStreamDataType.Confirmation:
499
+ return answeredForms.get(item.id) ===
500
+ 'confirmed' ? null : answeredForms.get(item.id) ===
501
+ 'canceled' ? (React.createElement("div", null, "\u2716 Canceled")) : (React.createElement("div", { className: "chat-confirmation-form", key: `key-${index}` },
502
+ item.content.title ? (React.createElement("div", null,
503
+ React.createElement("b", null, item.content.title))) : null,
504
+ item.content.message ? (React.createElement("div", null, item.content.message)) : null,
505
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => {
506
+ markFormConfirmed(item.id);
507
+ runCommand('notebook-intelligence:chat-user-input', item.content.confirmArgs);
508
+ } },
509
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.confirmLabel)),
510
+ item.content.confirmSessionArgs ? (React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => {
511
+ markFormConfirmed(item.id);
512
+ runCommand('notebook-intelligence:chat-user-input', item.content.confirmSessionArgs);
513
+ } },
514
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.confirmSessionLabel))) : null,
515
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: () => {
516
+ markFormCanceled(item.id);
517
+ runCommand('notebook-intelligence:chat-user-input', item.content.cancelArgs);
518
+ } },
519
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, item.content.cancelLabel))));
520
+ case ResponseStreamDataType.AskUserQuestion:
521
+ return answeredForms.get(item.id) ===
522
+ 'confirmed' ? null : answeredForms.get(item.id) ===
523
+ 'canceled' ? (React.createElement("div", null, "\u2716 Canceled")) : (React.createElement("div", { className: "chat-confirmation-form ask-user-question", key: `key-${index}` },
524
+ React.createElement(AskUserQuestion, { userQuestions: item, onSubmit: (selectedAnswers) => {
525
+ markFormConfirmed(item.id);
526
+ runCommand('notebook-intelligence:chat-user-input', {
527
+ id: item.content.identifier.id,
528
+ data: {
529
+ callback_id: item.content.identifier.callback_id,
530
+ data: {
531
+ confirmed: true,
532
+ selectedAnswers
533
+ }
534
+ }
535
+ });
536
+ }, onCancel: () => {
537
+ markFormCanceled(item.id);
538
+ runCommand('notebook-intelligence:chat-user-input', {
539
+ id: item.content.identifier.id,
540
+ data: {
541
+ callback_id: item.content.identifier.callback_id,
542
+ data: { confirmed: false }
543
+ }
544
+ });
545
+ } })));
546
+ }
547
+ return null;
548
+ }),
549
+ msg.notebookLink && (React.createElement("button", { type: "button", className: "copilot-generated-notebook-link", "data-ref": msg.notebookLink, "aria-label": `Open notebook ${msg.notebookLink}`, onClick: openNotebook }, "open notebook"))),
550
+ msg.from === 'copilot' &&
551
+ !props.showGenerating &&
552
+ NBIAPI.config.chatFeedbackEnabled && (React.createElement("div", { className: "chat-message-feedback" },
553
+ React.createElement("button", { className: `chat-feedback-btn ${msg.feedback === 'positive' ? 'selected' : ''}`, onClick: () => {
554
+ var _a;
555
+ props.onFeedback(msg.id, 'positive');
556
+ if (msg.feedback !== 'positive') {
557
+ props.telemetryEmitter.emitTelemetryEvent({
558
+ type: TelemetryEventType.Feedback,
559
+ data: {
560
+ sentiment: 'positive',
561
+ chatId: props.chatId,
562
+ messageId: msg.id,
563
+ model: msg.chatModel,
564
+ participant: (_a = msg.participant) === null || _a === void 0 ? void 0 : _a.id,
565
+ timestamp: new Date().toISOString()
566
+ }
567
+ });
568
+ }
569
+ }, "aria-label": "Rate as helpful", "aria-pressed": msg.feedback === 'positive', title: "Helpful" }, msg.feedback === 'positive' ? (React.createElement(VscThumbsupFilled, null)) : (React.createElement(VscThumbsup, null))),
570
+ React.createElement("button", { className: `chat-feedback-btn ${msg.feedback === 'negative' ? 'selected' : ''}`, onClick: () => {
571
+ var _a;
572
+ props.onFeedback(msg.id, 'negative');
573
+ if (msg.feedback !== 'negative') {
574
+ props.telemetryEmitter.emitTelemetryEvent({
575
+ type: TelemetryEventType.Feedback,
576
+ data: {
577
+ sentiment: 'negative',
578
+ chatId: props.chatId,
579
+ messageId: msg.id,
580
+ model: msg.chatModel,
581
+ participant: (_a = msg.participant) === null || _a === void 0 ? void 0 : _a.id,
582
+ timestamp: new Date().toISOString()
583
+ }
584
+ });
585
+ }
586
+ }, "aria-label": "Rate as unhelpful", "aria-pressed": msg.feedback === 'negative', title: "Not helpful" }, msg.feedback === 'negative' ? (React.createElement(VscThumbsdownFilled, null)) : (React.createElement(VscThumbsdown, null)))))));
587
+ }
588
+ const MemoizedChatResponse = memo(ChatResponse);
589
+ async function submitCompletionRequest(request, responseEmitter) {
590
+ switch (request.type) {
591
+ case RunChatCompletionType.Chat:
592
+ case RunChatCompletionType.NotebookGeneration:
593
+ return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || '', request.additionalContext || [], request.chatMode, request.toolSelections || {}, responseEmitter);
594
+ case RunChatCompletionType.ExplainThis:
595
+ case RunChatCompletionType.FixThis: {
596
+ return NBIAPI.chatRequest(request.messageId, request.chatId, request.content, request.language || 'python', request.currentDirectory || '', request.filename || '', [], 'ask', {}, responseEmitter);
597
+ }
598
+ case RunChatCompletionType.GenerateCode:
599
+ return NBIAPI.generateCode(request.messageId, request.chatId, request.content, request.prefix || '', request.suffix || '', request.existingCode || '', request.language || 'python', request.filename || '', responseEmitter);
600
+ }
601
+ }
602
+ function getActiveChatModel() {
603
+ var _a, _b;
604
+ if (NBIAPI.config.isInClaudeCodeMode) {
605
+ return {
606
+ provider: 'anthropic',
607
+ model: ((_b = (_a = NBIAPI.config.claudeSettings) === null || _a === void 0 ? void 0 : _a.chat_model) === null || _b === void 0 ? void 0 : _b.trim()) || 'default'
608
+ };
609
+ }
610
+ return {
611
+ provider: NBIAPI.config.chatModel.provider,
612
+ model: NBIAPI.config.chatModel.model
613
+ };
614
+ }
615
+ function SidebarComponent(props) {
616
+ const [chatMessages, setChatMessages] = useState([]);
617
+ const [prompt, setPrompt] = useState('');
618
+ const [draftPrompt, setDraftPrompt] = useState('');
619
+ // The first-run tour is rendered inside the sidebar so it can anchor
620
+ // to DOM elements that only exist when the sidebar is mounted. Auto-
621
+ // show once per browser (state in localStorage); the JupyterLab
622
+ // command `notebook-intelligence:show-tour` dispatches the start
623
+ // event so the same component handles both flows.
624
+ const [tourVisible, setTourVisible] = useState(false);
625
+ const sidebarRootRef = useRef(null);
626
+ useEffect(() => {
627
+ if (hasCompletedTour()) {
628
+ return;
629
+ }
630
+ // The sidebar React tree mounts even when the Lumino panel is
631
+ // hidden (lm-mod-hidden). Firing the tour while hidden anchors
632
+ // resolve to 0x0 rects which clamps the tooltip into the viewport
633
+ // corner over unrelated UI. Poll on each animation frame until the
634
+ // root is actually laid out, then fire. Cap the poll so a sidebar
635
+ // that's never opened doesn't keep an rAF tick warm for the whole
636
+ // session.
637
+ let cancelled = false;
638
+ let rafId = 0;
639
+ let attempts = 0;
640
+ const MAX_ATTEMPTS = 1800; // ~30s at 60Hz
641
+ const tick = () => {
642
+ if (cancelled) {
643
+ return;
644
+ }
645
+ if (hasCompletedTour()) {
646
+ // The command palette replay path may have completed the tour
647
+ // out of band; stop polling.
648
+ return;
649
+ }
650
+ const root = sidebarRootRef.current;
651
+ if (root && root.offsetParent !== null && root.offsetWidth > 0) {
652
+ setTourVisible(true);
653
+ return;
654
+ }
655
+ attempts += 1;
656
+ if (attempts >= MAX_ATTEMPTS) {
657
+ return;
658
+ }
659
+ rafId = requestAnimationFrame(tick);
660
+ };
661
+ rafId = requestAnimationFrame(tick);
662
+ return () => {
663
+ cancelled = true;
664
+ cancelAnimationFrame(rafId);
665
+ };
666
+ }, []);
667
+ useEffect(() => {
668
+ const start = () => setTourVisible(true);
669
+ const stop = () => setTourVisible(false);
670
+ document.addEventListener(TOUR_START_EVENT, start);
671
+ document.addEventListener(TOUR_STOP_EVENT, stop);
672
+ return () => {
673
+ document.removeEventListener(TOUR_START_EVENT, start);
674
+ document.removeEventListener(TOUR_STOP_EVENT, stop);
675
+ };
676
+ }, []);
677
+ const messagesEndRef = useRef(null);
678
+ const [ghLoginStatus, setGHLoginStatus] = useState(GitHubCopilotLoginStatus.NotLoggedIn);
679
+ const [loginClickCount, _setLoginClickCount] = useState(0);
680
+ const [copilotRequestInProgress, setCopilotRequestInProgress] = useState(false);
681
+ // sr-only announcement string driven by request-in-progress transitions.
682
+ // Wrapping the whole transcript in aria-live would queue a polite
683
+ // announcement per streamed token — hostile for screen reader users.
684
+ // Instead announce at request boundaries: "Generating response" when
685
+ // a request starts, "Response complete" when it ends.
686
+ const [chatStatusAnnouncement, setChatStatusAnnouncement] = useState('');
687
+ const prevCopilotRequestInProgressRef = useRef(false);
688
+ useEffect(() => {
689
+ const prev = prevCopilotRequestInProgressRef.current;
690
+ if (!prev && copilotRequestInProgress) {
691
+ setChatStatusAnnouncement('Generating response.');
692
+ }
693
+ else if (prev && !copilotRequestInProgress) {
694
+ setChatStatusAnnouncement('Response complete.');
695
+ }
696
+ prevCopilotRequestInProgressRef.current = copilotRequestInProgress;
697
+ }, [copilotRequestInProgress]);
698
+ const [showPopover, setShowPopover] = useState(false);
699
+ const [originalPrefixes, setOriginalPrefixes] = useState([]);
700
+ const [prefixSuggestions, setPrefixSuggestions] = useState([]);
701
+ const [selectedPrefixSuggestionIndex, setSelectedPrefixSuggestionIndex] = useState(0);
702
+ const promptInputRef = useRef(null);
703
+ const autocompleteRef = useRef(null);
704
+ const atButtonRef = useRef(null);
705
+ // Refs on the popover wrappers so we can move focus into the dialog
706
+ // when it opens (replaces the broken autoFocus={true} pattern, which
707
+ // only works on form controls). Paired with focus-restore refs that
708
+ // remember which element triggered the open so we can put focus back
709
+ // on close, regardless of which exit path the user took.
710
+ const workspaceFilePopoverRef = useRef(null);
711
+ const modeToolsPopoverRef = useRef(null);
712
+ const workspaceFilePickerOpenerRef = useRef(null);
713
+ const modeToolsOpenerRef = useRef(null);
714
+ const slashPopoverOpenerRef = useRef(null);
715
+ const [promptHistory, setPromptHistory] = useState([]);
716
+ // position on prompt history stack
717
+ const [promptHistoryIndex, setPromptHistoryIndex] = useState(0);
718
+ const [chatId, setChatId] = useState(UUID.uuid4());
719
+ const lastMessageId = useRef('');
720
+ const lastRequestTime = useRef(new Date());
721
+ const [contextOn, setContextOn] = useState(false);
722
+ const [activeDocumentInfo, setActiveDocumentInfo] = useState(null);
723
+ const [currentFileContextTitle, setCurrentFileContextTitle] = useState('');
724
+ const [selectedContextFiles, setSelectedContextFiles] = useState([]);
725
+ const [showWorkspaceFilePicker, setShowWorkspaceFilePicker] = useState(false);
726
+ const [workspaceFiles, setWorkspaceFiles] = useState([]);
727
+ const [workspaceFileSearch, setWorkspaceFileSearch] = useState('');
728
+ const [workspaceFilesLoaded, setWorkspaceFilesLoaded] = useState(false);
729
+ const [workspaceFilesLoading, setWorkspaceFilesLoading] = useState(false);
730
+ const [showClaudeSessionPicker, setShowClaudeSessionPicker] = useState(false);
731
+ const [workspaceFilesError, setWorkspaceFilesError] = useState('');
732
+ const [workspaceScanLimitReached, setWorkspaceScanLimitReached] = useState(false);
733
+ const [workspaceFileActionPath, setWorkspaceFileActionPath] = useState('');
734
+ // Scan-generation counter — incremented when the picker closes, on
735
+ // unmount, or when a fresh scan starts. The in-flight BFS reads this
736
+ // before each batch and before its terminal `setState` calls; if the
737
+ // generation has changed, the scan abandons silently so a slow tree
738
+ // walk can't land stale results on a reopened picker.
739
+ const workspaceFilesLoadingRef = useRef(false);
740
+ const workspaceScanGenerationRef = useRef(0);
741
+ // Path of the notebook that was active when the current agent task
742
+ // started. Threaded into every notebook-cell RunUICommand so tools keep
743
+ // targeting the right notebook after the user switches tabs mid-task
744
+ // (issue #252). Cleared / reset on every new chat submission.
745
+ const taskTargetNotebookPathRef = useRef(null);
746
+ // Progress-feedback state for the "Generating" indicator.
747
+ // `elapsedSeconds` ticks every 1s while a request is in flight (so the
748
+ // user can see at a glance how long they've been waiting).
749
+ // `lastHeartbeatAtRef` tracks the most recent ClaudeCodeHeartbeat from
750
+ // the server; when the gap exceeds HEARTBEAT_STALE_MS the indicator
751
+ // copy flips to a "may be slow" variant. `heartbeatTick` increments on
752
+ // each heartbeat to drive a brief CSS pulse on the indicator dot. None
753
+ // of these matter outside Claude mode because heartbeats only fire
754
+ // there, but the elapsed counter is a useful signal regardless of
755
+ // provider.
756
+ const [elapsedSeconds, setElapsedSeconds] = useState(0);
757
+ const requestStartedAtRef = useRef(null);
758
+ const lastHeartbeatAtRef = useRef(null);
759
+ const [heartbeatTick, setHeartbeatTick] = useState(0);
760
+ const [isStalled, setIsStalled] = useState(false);
761
+ const [isDragOver, setIsDragOver] = useState(false);
762
+ const [isUploadingFiles, setIsUploadingFiles] = useState(false);
763
+ const fileInputRef = useRef(null);
764
+ const telemetryEmitter = props.getTelemetryEmitter();
765
+ const [chatMode, setChatMode] = useState(NBIAPI.config.defaultChatMode);
766
+ const [toolSelectionTitle, setToolSelectionTitle] = useState('Tool selection');
767
+ const [selectedToolCount, setSelectedToolCount] = useState(0);
768
+ const [unsafeToolSelected, setUnsafeToolSelected] = useState(false);
769
+ const [renderCount, setRenderCount] = useState(1);
770
+ const toolConfigRef = useRef({
771
+ builtinToolsets: [
772
+ { id: BuiltinToolsetType.NotebookEdit, name: 'Notebook edit' },
773
+ { id: BuiltinToolsetType.NotebookExecute, name: 'Notebook execute' }
774
+ ],
775
+ mcpServers: [],
776
+ extensions: []
777
+ });
778
+ const mcpServerSettingsRef = useRef(NBIAPI.config.mcpServerSettings);
779
+ const [mcpServerEnabledState, setMCPServerEnabledState] = useState(new Map(mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current)));
780
+ const [showModeTools, setShowModeTools] = useState(false);
781
+ const toolSelectionsInitial = {
782
+ builtinToolsets: [],
783
+ mcpServers: {},
784
+ extensions: {}
785
+ };
786
+ const toolSelectionsEmpty = {
787
+ builtinToolsets: [],
788
+ mcpServers: {},
789
+ extensions: {}
790
+ };
791
+ const [toolSelections, setToolSelections] = useState(structuredClone(toolSelectionsInitial));
792
+ const [hasExtensionTools, setHasExtensionTools] = useState(false);
793
+ const [lastScrollTime, setLastScrollTime] = useState(0);
794
+ const [scrollPending, setScrollPending] = useState(false);
795
+ const selectedContextFilePaths = useMemo(() => new Set(selectedContextFiles.map(file => file.path)), [selectedContextFiles]);
796
+ const visibleWorkspaceFiles = useMemo(() => {
797
+ const search = workspaceFileSearch.trim().toLowerCase();
798
+ const filteredFiles = search === ''
799
+ ? workspaceFiles
800
+ : workspaceFiles.filter(file => file.path.toLowerCase().includes(search));
801
+ return filteredFiles.slice(0, MAX_VISIBLE_WORKSPACE_FILES);
802
+ }, [workspaceFileSearch, workspaceFiles]);
803
+ const loadWorkspaceFiles = useCallback(async () => {
804
+ if (workspaceFilesLoadingRef.current) {
805
+ return;
806
+ }
807
+ workspaceFilesLoadingRef.current = true;
808
+ const generation = ++workspaceScanGenerationRef.current;
809
+ const isCanceled = () => workspaceScanGenerationRef.current !== generation;
810
+ setWorkspaceFilesLoading(true);
811
+ setWorkspaceFilesError('');
812
+ const discoveredFiles = [];
813
+ const directoriesToScan = [''];
814
+ // Path-string dedupe: prevents a directory enqueued under the same
815
+ // logical path from being fetched twice. Symlinks reached via two
816
+ // different parent paths (e.g., `a/link` and `b/link` both pointing
817
+ // at `foo/`) have distinct path strings and will still be walked
818
+ // twice — the Contents API doesn't expose the resolved inode for
819
+ // a true canonical dedupe.
820
+ const visitedDirectories = new Set(['']);
821
+ let limitReached = false;
822
+ let totalFulfilled = 0;
823
+ // Boolean rather than `lastRejection !== undefined`: a rejection
824
+ // whose reason is itself `undefined` should still surface as an
825
+ // error, not be silently treated as success.
826
+ let sawRejection = false;
827
+ let lastRejection;
828
+ try {
829
+ const contentsManager = props.getApp().serviceManager.contents;
830
+ // Merge built-in skips with the admin-configured
831
+ // `additional_skipped_workspace_directories` traitlet so both layers
832
+ // gate enqueueing before we issue an HTTP request for the subdir.
833
+ const skipDirectoryNames = new Set([
834
+ ...SKIPPED_WORKSPACE_DIRECTORIES,
835
+ ...NBIAPI.config.additionalSkippedWorkspaceDirectories
836
+ ]);
837
+ // BFS the tree in bounded-parallel batches. Per-directory failures
838
+ // (deleted mid-scan, permission-denied mount) are skipped so one bad
839
+ // directory doesn't kill the whole picker — but if no fetch in the
840
+ // entire walk succeeds (offline, server down), the all-rejected case
841
+ // bubbles to the catch below as a real error.
842
+ while (directoriesToScan.length > 0 &&
843
+ discoveredFiles.length < MAX_WORKSPACE_FILE_SCAN_COUNT) {
844
+ if (isCanceled()) {
845
+ return;
846
+ }
847
+ // Sort the queue before slicing so cap-truncated walks pick a
848
+ // stable, alphabetical subset across runs. Without this the
849
+ // "first 1500 files" the user sees depends on which HTTP
850
+ // responses happened to resolve first.
851
+ directoriesToScan.sort();
852
+ const batch = directoriesToScan.splice(0, WORKSPACE_SCAN_CONCURRENCY);
853
+ const results = await Promise.allSettled(batch.map(dir => contentsManager.get(dir, { content: true })));
854
+ if (isCanceled()) {
855
+ return;
856
+ }
857
+ for (const result of results) {
858
+ if (limitReached) {
859
+ break;
860
+ }
861
+ if (result.status !== 'fulfilled') {
862
+ sawRejection = true;
863
+ lastRejection = result.reason;
864
+ continue;
865
+ }
866
+ totalFulfilled += 1;
867
+ const model = result.value;
868
+ if (model.type !== 'directory' || !Array.isArray(model.content)) {
869
+ continue;
870
+ }
871
+ // Sort entries within a directory so cap-truncated picks remain
872
+ // deterministic when the cap fires mid-directory.
873
+ const entries = [...model.content].sort((lhs, rhs) => lhs.path.localeCompare(rhs.path));
874
+ for (const entry of entries) {
875
+ if (!(entry === null || entry === void 0 ? void 0 : entry.path) || !(entry === null || entry === void 0 ? void 0 : entry.name)) {
876
+ continue;
877
+ }
878
+ if (entry.name.startsWith('.')) {
879
+ continue;
880
+ }
881
+ if (entry.type === 'directory') {
882
+ if (!skipDirectoryNames.has(entry.name) &&
883
+ !visitedDirectories.has(entry.path)) {
884
+ visitedDirectories.add(entry.path);
885
+ directoriesToScan.push(entry.path);
886
+ }
887
+ continue;
888
+ }
889
+ if (entry.type === 'file' || entry.type === 'notebook') {
890
+ discoveredFiles.push({
891
+ name: entry.name,
892
+ path: entry.path,
893
+ type: entry.type
894
+ });
895
+ if (discoveredFiles.length >= MAX_WORKSPACE_FILE_SCAN_COUNT) {
896
+ limitReached = true;
897
+ break;
898
+ }
899
+ }
900
+ }
901
+ }
902
+ }
903
+ if (isCanceled()) {
904
+ return;
905
+ }
906
+ if (totalFulfilled === 0 && sawRejection) {
907
+ // Every fetch rejected — bubble out the first failure so the
908
+ // popover surfaces an error instead of an empty list.
909
+ throw lastRejection;
910
+ }
911
+ discoveredFiles.sort((lhs, rhs) => lhs.path.localeCompare(rhs.path));
912
+ setWorkspaceFiles(discoveredFiles);
913
+ setWorkspaceFilesLoaded(true);
914
+ setWorkspaceScanLimitReached(limitReached);
915
+ }
916
+ catch (error) {
917
+ // Log before the cancel guard so a fully-failed scan that the user
918
+ // then closed/unmounted still leaves a diagnostic trail.
919
+ console.error('Failed to load workspace files.', error);
920
+ if (isCanceled()) {
921
+ return;
922
+ }
923
+ setWorkspaceFilesError((error === null || error === void 0 ? void 0 : error.message) || 'Failed to load workspace files.');
924
+ }
925
+ finally {
926
+ workspaceFilesLoadingRef.current = false;
927
+ if (!isCanceled()) {
928
+ setWorkspaceFilesLoading(false);
929
+ }
930
+ }
931
+ }, [props]);
932
+ // Latest references for the fileChanged subscription handler to read
933
+ // without re-binding the effect on every render.
934
+ const loadWorkspaceFilesRef = useRef(loadWorkspaceFiles);
935
+ useEffect(() => {
936
+ loadWorkspaceFilesRef.current = loadWorkspaceFiles;
937
+ }, [loadWorkspaceFiles]);
938
+ const showWorkspaceFilePickerRef = useRef(showWorkspaceFilePicker);
939
+ useEffect(() => {
940
+ showWorkspaceFilePickerRef.current = showWorkspaceFilePicker;
941
+ }, [showWorkspaceFilePicker]);
942
+ // Focus management for the popovers: move focus into the popover on
943
+ // open so keyboard users land inside it, then restore focus to the
944
+ // trigger on close. Restoration runs on the close transition
945
+ // regardless of which exit path (close button, Escape, outside click)
946
+ // dismissed the popover.
947
+ //
948
+ // Restoration is guarded by:
949
+ // (a) `document.contains(opener)` so a trigger that was unmounted
950
+ // between open and close (e.g., chat-mode flipped) doesn't
951
+ // throw; and
952
+ // (b) "the user hasn't moved focus elsewhere intentionally" — we
953
+ // only steal focus back when the activeElement is still inside
954
+ // the popover that's closing (i.e., the close came from the
955
+ // popover itself, not from the user clicking a sibling control).
956
+ // Without this guard, dismissing one popover by clicking the
957
+ // trigger of another would yank focus to the first popover's
958
+ // trigger right after the second popover focused itself.
959
+ const prevWorkspaceFilePickerRef = useRef(false);
960
+ useEffect(() => {
961
+ var _a;
962
+ if (showWorkspaceFilePicker && !prevWorkspaceFilePickerRef.current) {
963
+ (_a = workspaceFilePopoverRef.current) === null || _a === void 0 ? void 0 : _a.focus();
964
+ }
965
+ else if (!showWorkspaceFilePicker && prevWorkspaceFilePickerRef.current) {
966
+ const opener = workspaceFilePickerOpenerRef.current;
967
+ const popover = workspaceFilePopoverRef.current;
968
+ workspaceFilePickerOpenerRef.current = null;
969
+ const active = document.activeElement;
970
+ const focusInsidePopover = popover ? popover.contains(active) : false;
971
+ const focusOnBody = active === document.body || active === null;
972
+ if ((focusInsidePopover || focusOnBody) &&
973
+ opener &&
974
+ document.contains(opener)) {
975
+ opener.focus();
976
+ }
977
+ }
978
+ prevWorkspaceFilePickerRef.current = showWorkspaceFilePicker;
979
+ }, [showWorkspaceFilePicker]);
980
+ const prevModeToolsRef = useRef(false);
981
+ useEffect(() => {
982
+ var _a;
983
+ if (showModeTools && !prevModeToolsRef.current) {
984
+ (_a = modeToolsPopoverRef.current) === null || _a === void 0 ? void 0 : _a.focus();
985
+ }
986
+ else if (!showModeTools && prevModeToolsRef.current) {
987
+ const opener = modeToolsOpenerRef.current;
988
+ const popover = modeToolsPopoverRef.current;
989
+ modeToolsOpenerRef.current = null;
990
+ const active = document.activeElement;
991
+ const focusInsidePopover = popover ? popover.contains(active) : false;
992
+ const focusOnBody = active === document.body || active === null;
993
+ if ((focusInsidePopover || focusOnBody) &&
994
+ opener &&
995
+ document.contains(opener)) {
996
+ opener.focus();
997
+ }
998
+ }
999
+ prevModeToolsRef.current = showModeTools;
1000
+ }, [showModeTools]);
1001
+ // Slash-popover restoration: only the close transition matters; the
1002
+ // popover lives next to the textarea and the textarea retains focus
1003
+ // while it's open, so there's nothing to focus *into* on open.
1004
+ const prevShowPopoverRef = useRef(false);
1005
+ useEffect(() => {
1006
+ if (!showPopover && prevShowPopoverRef.current) {
1007
+ const opener = slashPopoverOpenerRef.current;
1008
+ const popover = autocompleteRef.current;
1009
+ slashPopoverOpenerRef.current = null;
1010
+ const active = document.activeElement;
1011
+ const focusInsidePopover = popover ? popover.contains(active) : false;
1012
+ // The textarea always stays focusable next to the popover and the
1013
+ // user may have been typing inside it the whole time, so leave
1014
+ // focus alone if it's on the textarea. Same "user moved focus
1015
+ // elsewhere intentionally" guard as the other popovers.
1016
+ const focusOnTextarea = active === promptInputRef.current;
1017
+ const focusOnBody = active === document.body || active === null;
1018
+ if ((focusInsidePopover || focusOnBody) &&
1019
+ !focusOnTextarea &&
1020
+ opener &&
1021
+ document.contains(opener)) {
1022
+ opener.focus();
1023
+ }
1024
+ }
1025
+ prevShowPopoverRef.current = showPopover;
1026
+ }, [showPopover]);
1027
+ // Set when a refresh arrives mid-scan. The in-flight scan's drain loop
1028
+ // honors one more pass at completion, bounding the storm to "at most one
1029
+ // scan running + one queued" instead of cascading cancellations.
1030
+ const pendingRescanRef = useRef(false);
1031
+ const runWorkspaceFileScan = useCallback(async () => {
1032
+ if (workspaceFilesLoadingRef.current) {
1033
+ pendingRescanRef.current = true;
1034
+ return;
1035
+ }
1036
+ do {
1037
+ pendingRescanRef.current = false;
1038
+ await loadWorkspaceFilesRef.current();
1039
+ } while (pendingRescanRef.current && showWorkspaceFilePickerRef.current);
1040
+ }, []);
1041
+ const runWorkspaceFileScanRef = useRef(runWorkspaceFileScan);
1042
+ useEffect(() => {
1043
+ runWorkspaceFileScanRef.current = runWorkspaceFileScan;
1044
+ }, [runWorkspaceFileScan]);
1045
+ // Subscribe to Contents-API changes so a notebook or file created outside
1046
+ // the picker (manually in the file browser, via terminal, or by a Claude
1047
+ // tool that round-trips through the Contents API) shows up without
1048
+ // requiring a full lab reload. The contents manager is a singleton for
1049
+ // the app's lifetime; depending on `[]` avoids `props`-identity churn that
1050
+ // would otherwise reconnect the signal on every parent render and reset
1051
+ // the in-flight debounce timer.
1052
+ const appRef = useRef(props.getApp());
1053
+ useEffect(() => {
1054
+ const contents = appRef.current.serviceManager.contents;
1055
+ let timer = null;
1056
+ const onFileChanged = (_sender, change) => {
1057
+ // 'save' fires on every edit-and-save; the picker doesn't care about
1058
+ // content changes, only the file set.
1059
+ if (change.type === 'save') {
1060
+ return;
1061
+ }
1062
+ if (timer !== null) {
1063
+ clearTimeout(timer);
1064
+ }
1065
+ timer = setTimeout(() => {
1066
+ timer = null;
1067
+ if (showWorkspaceFilePickerRef.current) {
1068
+ runWorkspaceFileScanRef.current();
1069
+ }
1070
+ else {
1071
+ // Picker is closed — mark stale so the next open re-scans.
1072
+ setWorkspaceFilesLoaded(false);
1073
+ }
1074
+ }, WORKSPACE_FILE_REFRESH_DEBOUNCE_MS);
1075
+ };
1076
+ contents.fileChanged.connect(onFileChanged);
1077
+ return () => {
1078
+ contents.fileChanged.disconnect(onFileChanged);
1079
+ if (timer !== null) {
1080
+ clearTimeout(timer);
1081
+ }
1082
+ };
1083
+ }, []);
1084
+ const refreshWorkspaceFiles = useCallback(() => {
1085
+ runWorkspaceFileScanRef.current();
1086
+ }, []);
1087
+ const handleWorkspaceFilePickerClick = async () => {
1088
+ var _a;
1089
+ setShowPopover(false);
1090
+ setShowModeTools(false);
1091
+ const nextState = !showWorkspaceFilePicker;
1092
+ // Capture the trigger element before re-render so we can restore
1093
+ // focus to it when the popover closes (D030).
1094
+ if (nextState) {
1095
+ workspaceFilePickerOpenerRef.current =
1096
+ (_a = document.activeElement) !== null && _a !== void 0 ? _a : null;
1097
+ }
1098
+ setShowWorkspaceFilePicker(nextState);
1099
+ // Sync the ref synchronously so a debounced refresh that fires during
1100
+ // the awaited scan below honors the now-open picker state immediately,
1101
+ // rather than waiting for the post-render useEffect to catch up.
1102
+ showWorkspaceFilePickerRef.current = nextState;
1103
+ if (nextState && !workspaceFilesLoaded) {
1104
+ await runWorkspaceFileScan();
1105
+ }
1106
+ };
1107
+ const handleWorkspaceFileSelection = async (file) => {
1108
+ if (selectedContextFilePaths.has(file.path)) {
1109
+ setSelectedContextFiles(previousFiles => previousFiles.filter(selectedFile => selectedFile.source === 'upload' || selectedFile.path !== file.path));
1110
+ return;
1111
+ }
1112
+ setWorkspaceFilesError('');
1113
+ setWorkspaceFileActionPath(file.path);
1114
+ try {
1115
+ // In Claude Code mode, the agent reads the file itself via the
1116
+ // server's @-mention path. Skip the contents fetch so binary files
1117
+ // (images, PDFs, notebooks) are picker-eligible and large files
1118
+ // aren't truncated by the client-side content-injection budget.
1119
+ let content = '';
1120
+ let lineCount = 0;
1121
+ if (!NBIAPI.config.isInClaudeCodeMode) {
1122
+ const contentsManager = props.getApp().serviceManager.contents;
1123
+ const model = await contentsManager.get(file.path, {
1124
+ content: true
1125
+ });
1126
+ content = serializeWorkspaceFileContent(model);
1127
+ if (content.trim() === '') {
1128
+ throw new Error('Empty files do not provide useful context.');
1129
+ }
1130
+ lineCount = countContentLines(content);
1131
+ }
1132
+ const nextSelectedFile = {
1133
+ content,
1134
+ lineCount,
1135
+ path: file.path,
1136
+ type: file.type
1137
+ };
1138
+ setSelectedContextFiles(previousFiles => [...previousFiles, nextSelectedFile].sort((lhs, rhs) => lhs.path.localeCompare(rhs.path)));
1139
+ }
1140
+ catch (error) {
1141
+ console.error(`Failed to attach workspace file '${file.path}'.`, error);
1142
+ setWorkspaceFilesError((error === null || error === void 0 ? void 0 : error.message) || `Failed to attach workspace file '${file.path}'.`);
1143
+ }
1144
+ finally {
1145
+ setWorkspaceFileActionPath('');
1146
+ }
1147
+ };
1148
+ const removeSelectedContextFile = (fileKey) => {
1149
+ setSelectedContextFiles(previousFiles => previousFiles.filter(file => { var _a; return ((_a = file.serverPath) !== null && _a !== void 0 ? _a : file.path) !== fileKey; }));
1150
+ };
1151
+ const handleDragOver = (event) => {
1152
+ event.preventDefault();
1153
+ event.stopPropagation();
1154
+ if (!isDragOver &&
1155
+ chatEnabled &&
1156
+ event.dataTransfer.types.includes('Files')) {
1157
+ setIsDragOver(true);
1158
+ }
1159
+ };
1160
+ const handleDragLeave = (event) => {
1161
+ event.preventDefault();
1162
+ event.stopPropagation();
1163
+ if (!event.currentTarget.contains(event.relatedTarget)) {
1164
+ setIsDragOver(false);
1165
+ }
1166
+ };
1167
+ const processDroppedFile = async (file) => {
1168
+ if (isLikelyTextFile(file)) {
1169
+ const content = await readFileAsText(file);
1170
+ if (content.trim() === '') {
1171
+ throw new Error(`'${file.name}' is empty`);
1172
+ }
1173
+ return {
1174
+ content,
1175
+ lineCount: countContentLines(content),
1176
+ path: file.name,
1177
+ type: 'file',
1178
+ source: 'upload'
1179
+ };
1180
+ }
1181
+ if (file.type.startsWith('image/')) {
1182
+ const [imageDataUrl, { serverPath, filename }] = await Promise.all([
1183
+ readFileAsDataURL(file),
1184
+ NBIAPI.uploadFile(file)
1185
+ ]);
1186
+ return {
1187
+ content: '',
1188
+ lineCount: 0,
1189
+ path: filename,
1190
+ type: 'file',
1191
+ source: 'upload',
1192
+ serverPath,
1193
+ isImage: true,
1194
+ imageDataUrl,
1195
+ mimeType: file.type
1196
+ };
1197
+ }
1198
+ const { serverPath, filename } = await NBIAPI.uploadFile(file);
1199
+ return {
1200
+ content: '',
1201
+ lineCount: 0,
1202
+ path: filename,
1203
+ type: 'file',
1204
+ source: 'upload',
1205
+ serverPath
1206
+ };
1207
+ };
1208
+ const addSystemNotice = (message) => {
1209
+ setChatMessages(prev => [
1210
+ ...prev,
1211
+ {
1212
+ id: UUID.uuid4(),
1213
+ date: new Date(),
1214
+ from: 'notice',
1215
+ participant: { name: 'Notice' },
1216
+ contents: [
1217
+ {
1218
+ id: UUID.uuid4(),
1219
+ type: ResponseStreamDataType.Markdown,
1220
+ content: message,
1221
+ created: new Date()
1222
+ }
1223
+ ]
1224
+ }
1225
+ ]);
1226
+ };
1227
+ const processAndAttachFiles = async (files) => {
1228
+ var _a, _b, _c;
1229
+ if (files.length === 0) {
1230
+ return;
1231
+ }
1232
+ const uploadedFiles = selectedContextFiles.filter(f => f.source === 'upload');
1233
+ // Duplicate detection: skip files already attached
1234
+ const existingNames = new Set(uploadedFiles.map(f => f.path));
1235
+ const uniqueFiles = files.filter(f => !existingNames.has(f.name));
1236
+ const duplicateCount = files.length - uniqueFiles.length;
1237
+ // Enforce file count limit
1238
+ const currentUploadCount = uploadedFiles.length;
1239
+ const available = MAX_ATTACHED_FILES - currentUploadCount;
1240
+ const filesToProcess = uniqueFiles.slice(0, Math.max(0, available));
1241
+ const skippedCount = uniqueFiles.length - filesToProcess.length;
1242
+ if (filesToProcess.length === 0) {
1243
+ const parts = [];
1244
+ if (duplicateCount > 0) {
1245
+ parts.push(`${duplicateCount} already attached`);
1246
+ }
1247
+ if (skippedCount > 0) {
1248
+ parts.push(`limit of ${MAX_ATTACHED_FILES} files reached`);
1249
+ }
1250
+ addSystemNotice(`No files added: ${parts.join('; ')}.`);
1251
+ return;
1252
+ }
1253
+ setIsUploadingFiles(true);
1254
+ try {
1255
+ const results = await Promise.allSettled(filesToProcess.map(file => processDroppedFile(file)));
1256
+ const newContextFiles = [];
1257
+ const errors = [];
1258
+ for (const result of results) {
1259
+ if (result.status === 'fulfilled' && result.value) {
1260
+ newContextFiles.push(result.value);
1261
+ }
1262
+ else if (result.status === 'rejected') {
1263
+ errors.push(String((_b = (_a = result.reason) === null || _a === void 0 ? void 0 : _a.message) !== null && _b !== void 0 ? _b : result.reason));
1264
+ }
1265
+ }
1266
+ const notices = [];
1267
+ if (errors.length > 0) {
1268
+ notices.push(`Could not attach: ${errors.join('; ')}`);
1269
+ }
1270
+ if (duplicateCount > 0) {
1271
+ notices.push(`${duplicateCount} duplicate${duplicateCount > 1 ? 's' : ''} skipped`);
1272
+ }
1273
+ if (skippedCount > 0) {
1274
+ notices.push(`${skippedCount} file${skippedCount > 1 ? 's' : ''} skipped (limit of ${MAX_ATTACHED_FILES})`);
1275
+ }
1276
+ if (notices.length > 0) {
1277
+ addSystemNotice(notices.join('. ') + '.');
1278
+ }
1279
+ if (newContextFiles.length > 0) {
1280
+ setSelectedContextFiles(prev => [...prev, ...newContextFiles]);
1281
+ // Same reason as the lm-drop path: the gesture that initiated this
1282
+ // (HTML5 drop, file dialog, image paste) often leaves focus outside
1283
+ // the chat composer, so the next Enter goes somewhere unintended.
1284
+ (_c = promptInputRef.current) === null || _c === void 0 ? void 0 : _c.focus();
1285
+ }
1286
+ }
1287
+ finally {
1288
+ setIsUploadingFiles(false);
1289
+ }
1290
+ };
1291
+ const handleDrop = async (event) => {
1292
+ event.preventDefault();
1293
+ event.stopPropagation();
1294
+ setIsDragOver(false);
1295
+ if (!chatEnabled) {
1296
+ return;
1297
+ }
1298
+ await processAndAttachFiles(Array.from(event.dataTransfer.files));
1299
+ };
1300
+ const handleFileInputChange = async (event) => {
1301
+ var _a;
1302
+ const files = Array.from((_a = event.target.files) !== null && _a !== void 0 ? _a : []);
1303
+ event.target.value = '';
1304
+ await processAndAttachFiles(files);
1305
+ };
1306
+ const handlePaste = async (event) => {
1307
+ var _a;
1308
+ const items = Array.from(event.clipboardData.items);
1309
+ const imageItem = items.find(item => item.type.startsWith('image/'));
1310
+ if (!imageItem || !chatEnabled) {
1311
+ return;
1312
+ }
1313
+ const file = imageItem.getAsFile();
1314
+ if (!file) {
1315
+ return;
1316
+ }
1317
+ event.preventDefault();
1318
+ const ext = ((_a = imageItem.type.split('/')[1]) !== null && _a !== void 0 ? _a : 'png').split('+')[0];
1319
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
1320
+ const namedFile = new File([file], `screenshot-${timestamp}.${ext}`, {
1321
+ type: imageItem.type
1322
+ });
1323
+ await processAndAttachFiles([namedFile]);
1324
+ };
1325
+ const cleanupRemovedToolsFromToolSelections = () => {
1326
+ const newToolSelections = { ...toolSelections };
1327
+ // if servers or tool is not in mcpServerEnabledState, remove it from newToolSelections
1328
+ for (const serverId in newToolSelections.mcpServers) {
1329
+ if (!mcpServerEnabledState.has(serverId)) {
1330
+ delete newToolSelections.mcpServers[serverId];
1331
+ }
1332
+ else {
1333
+ for (const tool of newToolSelections.mcpServers[serverId]) {
1334
+ if (!mcpServerEnabledState.get(serverId).has(tool)) {
1335
+ newToolSelections.mcpServers[serverId].splice(newToolSelections.mcpServers[serverId].indexOf(tool), 1);
1336
+ }
1337
+ }
1338
+ }
1339
+ }
1340
+ for (const extensionId in newToolSelections.extensions) {
1341
+ if (!mcpServerEnabledState.has(extensionId)) {
1342
+ delete newToolSelections.extensions[extensionId];
1343
+ }
1344
+ else {
1345
+ for (const toolsetId in newToolSelections.extensions[extensionId]) {
1346
+ for (const tool of newToolSelections.extensions[extensionId][toolsetId]) {
1347
+ if (!mcpServerEnabledState.get(extensionId).has(tool)) {
1348
+ newToolSelections.extensions[extensionId][toolsetId].splice(newToolSelections.extensions[extensionId][toolsetId].indexOf(tool), 1);
1349
+ }
1350
+ }
1351
+ }
1352
+ }
1353
+ }
1354
+ setToolSelections(newToolSelections);
1355
+ setRenderCount(renderCount => renderCount + 1);
1356
+ };
1357
+ useEffect(() => {
1358
+ cleanupRemovedToolsFromToolSelections();
1359
+ }, [mcpServerEnabledState]);
1360
+ // JupyterLab file-browser drag uses Lumino's lm-* CustomEvents (mime
1361
+ // 'application/x-jupyter-icontents' carrying workspace-relative paths),
1362
+ // not native HTML5 drag. We listen at the document level with capture
1363
+ // phase + a containment check so intermediate widgets that
1364
+ // stopPropagation in target phase can't swallow the event.
1365
+ useEffect(() => {
1366
+ const FILE_BROWSER_MIME = 'application/x-jupyter-icontents';
1367
+ const isInsideSidebar = (event) => {
1368
+ const root = sidebarRootRef.current;
1369
+ if (!root) {
1370
+ return false;
1371
+ }
1372
+ const target = event.target;
1373
+ return target instanceof Node && root.contains(target);
1374
+ };
1375
+ const hasPaths = (event) => {
1376
+ var _a;
1377
+ const mimeData = event.mimeData;
1378
+ return ((_a = mimeData === null || mimeData === void 0 ? void 0 : mimeData.hasData) === null || _a === void 0 ? void 0 : _a.call(mimeData, FILE_BROWSER_MIME)) === true;
1379
+ };
1380
+ const dragEnter = (event) => {
1381
+ if (!NBIAPI.getChatEnabled() ||
1382
+ !hasPaths(event) ||
1383
+ !isInsideSidebar(event)) {
1384
+ return;
1385
+ }
1386
+ setIsDragOver(true);
1387
+ event.preventDefault();
1388
+ event.stopPropagation();
1389
+ };
1390
+ const dragOver = (event) => {
1391
+ if (!NBIAPI.getChatEnabled() ||
1392
+ !hasPaths(event) ||
1393
+ !isInsideSidebar(event)) {
1394
+ return;
1395
+ }
1396
+ event.preventDefault();
1397
+ event.stopPropagation();
1398
+ // Mirror the source's proposedAction. The JL file browser starts
1399
+ // its Drag with supportedActions: 'move'; setting dropAction to a
1400
+ // value outside that set falls through validateAction to 'none'
1401
+ // and Lumino skips lm-drop on pointerup.
1402
+ const drag = event;
1403
+ drag.dropAction = drag.proposedAction || 'move';
1404
+ };
1405
+ const dragLeave = (event) => {
1406
+ var _a;
1407
+ if (!NBIAPI.getChatEnabled() || !isInsideSidebar(event)) {
1408
+ return;
1409
+ }
1410
+ // Only clear the overlay when the drag genuinely leaves the
1411
+ // sidebar; ignore leaves that cross internal child boundaries.
1412
+ const related = event
1413
+ .relatedTarget;
1414
+ if (related && ((_a = sidebarRootRef.current) === null || _a === void 0 ? void 0 : _a.contains(related))) {
1415
+ return;
1416
+ }
1417
+ setIsDragOver(false);
1418
+ };
1419
+ const drop = (event) => {
1420
+ if (!NBIAPI.getChatEnabled() ||
1421
+ !hasPaths(event) ||
1422
+ !isInsideSidebar(event)) {
1423
+ return;
1424
+ }
1425
+ const dragEvent = event;
1426
+ const raw = dragEvent.mimeData.getData(FILE_BROWSER_MIME);
1427
+ if (!Array.isArray(raw) || raw.length === 0) {
1428
+ return;
1429
+ }
1430
+ event.preventDefault();
1431
+ event.stopPropagation();
1432
+ dragEvent.dropAction = dragEvent.proposedAction || 'move';
1433
+ setIsDragOver(false);
1434
+ const paths = raw.filter((p) => typeof p === 'string');
1435
+ void attachWorkspacePaths(paths);
1436
+ };
1437
+ document.addEventListener('lm-dragenter', dragEnter, true);
1438
+ document.addEventListener('lm-dragover', dragOver, true);
1439
+ document.addEventListener('lm-dragleave', dragLeave, true);
1440
+ document.addEventListener('lm-drop', drop, true);
1441
+ return () => {
1442
+ document.removeEventListener('lm-dragenter', dragEnter, true);
1443
+ document.removeEventListener('lm-dragover', dragOver, true);
1444
+ document.removeEventListener('lm-dragleave', dragLeave, true);
1445
+ document.removeEventListener('lm-drop', drop, true);
1446
+ };
1447
+ }, []);
1448
+ // Attach a list of workspace-relative paths (from JL file browser
1449
+ // drag) as chat context. Images are attached as image context (with a
1450
+ // base64 dataURL thumbnail) so the model can see them; text and
1451
+ // notebook files are attached as content context.
1452
+ const attachWorkspacePaths = async (paths) => {
1453
+ var _a;
1454
+ if (paths.length === 0) {
1455
+ return;
1456
+ }
1457
+ const contentsManager = props.getApp().serviceManager.contents;
1458
+ const additions = [];
1459
+ const errors = [];
1460
+ await Promise.all(paths.map(async (path) => {
1461
+ try {
1462
+ const model = await contentsManager.get(path, { content: true });
1463
+ const mimetype = model.mimetype || '';
1464
+ // Image branch: file is already on the server, so build a
1465
+ // data URL from the base64 content for the thumbnail and let
1466
+ // the backend resolve the workspace path. No upload needed.
1467
+ if (model.format === 'base64' && mimetype.startsWith('image/')) {
1468
+ additions.push({
1469
+ content: '',
1470
+ lineCount: 0,
1471
+ path,
1472
+ type: 'file',
1473
+ isImage: true,
1474
+ imageDataUrl: `data:${mimetype};base64,${model.content}`,
1475
+ mimeType: mimetype
1476
+ });
1477
+ return;
1478
+ }
1479
+ const content = serializeWorkspaceFileContent(model);
1480
+ if (content.trim() === '') {
1481
+ throw new Error(`'${path}' has no content to attach.`);
1482
+ }
1483
+ additions.push({
1484
+ content,
1485
+ lineCount: countContentLines(content),
1486
+ path,
1487
+ type: model.type === 'notebook' ? 'notebook' : 'file'
1488
+ });
1489
+ }
1490
+ catch (error) {
1491
+ errors.push((error === null || error === void 0 ? void 0 : error.message) || `Failed to attach '${path}'.`);
1492
+ }
1493
+ }));
1494
+ if (additions.length > 0) {
1495
+ // De-dupe inside the functional updater so it reads the freshest
1496
+ // state. A stale closure on `selectedContextFiles` here would let
1497
+ // the same path land twice when the user drops it a second time.
1498
+ setSelectedContextFiles(previous => {
1499
+ const existing = new Set(previous.map(f => f.path));
1500
+ const fresh = additions.filter(a => !existing.has(a.path));
1501
+ if (fresh.length === 0) {
1502
+ return previous;
1503
+ }
1504
+ return [...previous, ...fresh].sort((lhs, rhs) => lhs.path.localeCompare(rhs.path));
1505
+ });
1506
+ // Move keyboard focus into the prompt so the user can immediately
1507
+ // describe what they want done with the attached files.
1508
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1509
+ }
1510
+ if (errors.length > 0) {
1511
+ // Match the terminal-drag error path (Notification toast) instead
1512
+ // of slipping a chat-message-notice into the transcript, which
1513
+ // can scroll out of view.
1514
+ Notification.warning(`Could not attach: ${errors.join('; ')}`);
1515
+ }
1516
+ };
1517
+ useEffect(() => {
1518
+ const handler = () => {
1519
+ toolConfigRef.current = NBIAPI.config.toolConfig;
1520
+ mcpServerSettingsRef.current = NBIAPI.config.mcpServerSettings;
1521
+ const newMcpServerEnabledState = mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current);
1522
+ setMCPServerEnabledState(newMcpServerEnabledState);
1523
+ setRenderCount(renderCount => renderCount + 1);
1524
+ };
1525
+ NBIAPI.configChanged.connect(handler);
1526
+ return () => {
1527
+ NBIAPI.configChanged.disconnect(handler);
1528
+ };
1529
+ }, []);
1530
+ useEffect(() => {
1531
+ let hasTools = false;
1532
+ for (const extension of toolConfigRef.current.extensions) {
1533
+ if (extension.toolsets.length > 0) {
1534
+ hasTools = true;
1535
+ break;
1536
+ }
1537
+ }
1538
+ setHasExtensionTools(hasTools);
1539
+ }, [toolConfigRef.current]);
1540
+ // Subscribe to the Claude agent's 20s keepalive. Each beat resets the
1541
+ // staleness window, kicks a pulse on the indicator dot, and clears the
1542
+ // "server may be slow" copy.
1543
+ useEffect(() => {
1544
+ const handler = () => {
1545
+ lastHeartbeatAtRef.current = Date.now();
1546
+ setHeartbeatTick(tick => tick + 1);
1547
+ setIsStalled(false);
1548
+ };
1549
+ NBIAPI.claudeCodeHeartbeat.connect(handler);
1550
+ return () => {
1551
+ NBIAPI.claudeCodeHeartbeat.disconnect(handler);
1552
+ };
1553
+ }, []);
1554
+ // Drive the elapsed-time counter while a request is in flight. The same
1555
+ // interval re-evaluates whether the heartbeat has gone stale so the
1556
+ // indicator copy can swap to "Still working..." without a second timer.
1557
+ useEffect(() => {
1558
+ if (!copilotRequestInProgress) {
1559
+ requestStartedAtRef.current = null;
1560
+ setElapsedSeconds(0);
1561
+ setIsStalled(false);
1562
+ return;
1563
+ }
1564
+ requestStartedAtRef.current = Date.now();
1565
+ lastHeartbeatAtRef.current = Date.now();
1566
+ setElapsedSeconds(0);
1567
+ setIsStalled(false);
1568
+ const tick = () => {
1569
+ const started = requestStartedAtRef.current;
1570
+ if (started === null) {
1571
+ return;
1572
+ }
1573
+ setElapsedSeconds(Math.floor((Date.now() - started) / 1000));
1574
+ // Heartbeats only fire in Claude mode; suppress the staleness check
1575
+ // for other providers so they don't get a permanent "may be slow"
1576
+ // banner just because no heartbeats arrive there.
1577
+ if (NBIAPI.config.isInClaudeCodeMode) {
1578
+ setIsStalled(isHeartbeatStale(lastHeartbeatAtRef.current, Date.now()));
1579
+ }
1580
+ };
1581
+ tick();
1582
+ const intervalId = window.setInterval(tick, 1000);
1583
+ return () => {
1584
+ window.clearInterval(intervalId);
1585
+ };
1586
+ }, [copilotRequestInProgress]);
1587
+ useEffect(() => {
1588
+ const builtinToolSelCount = toolSelections.builtinToolsets.length;
1589
+ let mcpServerToolSelCount = 0;
1590
+ let extensionToolSelCount = 0;
1591
+ for (const serverId in toolSelections.mcpServers) {
1592
+ const mcpServerTools = toolSelections.mcpServers[serverId];
1593
+ mcpServerToolSelCount += mcpServerTools.length;
1594
+ }
1595
+ for (const extensionId in toolSelections.extensions) {
1596
+ const extensionToolsets = toolSelections.extensions[extensionId];
1597
+ for (const toolsetId in extensionToolsets) {
1598
+ const toolsetTools = extensionToolsets[toolsetId];
1599
+ extensionToolSelCount += toolsetTools.length;
1600
+ }
1601
+ }
1602
+ const typeCounts = [];
1603
+ if (builtinToolSelCount > 0) {
1604
+ typeCounts.push(`${builtinToolSelCount} built-in`);
1605
+ }
1606
+ if (mcpServerToolSelCount > 0) {
1607
+ typeCounts.push(`${mcpServerToolSelCount} mcp`);
1608
+ }
1609
+ if (extensionToolSelCount > 0) {
1610
+ typeCounts.push(`${extensionToolSelCount} ext`);
1611
+ }
1612
+ setSelectedToolCount(builtinToolSelCount + mcpServerToolSelCount + extensionToolSelCount);
1613
+ setUnsafeToolSelected(toolSelections.builtinToolsets.some((toolsetName) => [
1614
+ BuiltinToolsetType.NotebookEdit,
1615
+ BuiltinToolsetType.NotebookExecute,
1616
+ BuiltinToolsetType.PythonFileEdit,
1617
+ BuiltinToolsetType.FileEdit,
1618
+ BuiltinToolsetType.CommandExecute
1619
+ ].includes(toolsetName)));
1620
+ setToolSelectionTitle(typeCounts.length === 0
1621
+ ? 'Tool selection'
1622
+ : `Tool selection (${typeCounts.join(', ')})`);
1623
+ }, [toolSelections]);
1624
+ const onClearToolsButtonClicked = () => {
1625
+ setToolSelections(toolSelectionsEmpty);
1626
+ };
1627
+ const getBuiltinToolsetState = (toolsetName) => {
1628
+ return toolSelections.builtinToolsets.includes(toolsetName);
1629
+ };
1630
+ const setBuiltinToolsetState = (toolsetName, enabled) => {
1631
+ const newConfig = { ...toolSelections };
1632
+ if (enabled) {
1633
+ if (!toolSelections.builtinToolsets.includes(toolsetName)) {
1634
+ newConfig.builtinToolsets.push(toolsetName);
1635
+ }
1636
+ }
1637
+ else {
1638
+ const index = newConfig.builtinToolsets.indexOf(toolsetName);
1639
+ if (index !== -1) {
1640
+ newConfig.builtinToolsets.splice(index, 1);
1641
+ }
1642
+ }
1643
+ setToolSelections(newConfig);
1644
+ };
1645
+ const anyMCPServerToolSelected = (id) => {
1646
+ if (!(id in toolSelections.mcpServers)) {
1647
+ return false;
1648
+ }
1649
+ return toolSelections.mcpServers[id].length > 0;
1650
+ };
1651
+ const getMCPServerState = (id) => {
1652
+ if (!(id in toolSelections.mcpServers)) {
1653
+ return false;
1654
+ }
1655
+ const mcpServer = toolConfigRef.current.mcpServers.find(server => server.id === id);
1656
+ const selectedServerTools = toolSelections.mcpServers[id];
1657
+ for (const tool of mcpServer.tools) {
1658
+ if (!selectedServerTools.includes(tool.name)) {
1659
+ return false;
1660
+ }
1661
+ }
1662
+ return true;
1663
+ };
1664
+ const onMCPServerClicked = (id) => {
1665
+ if (anyMCPServerToolSelected(id)) {
1666
+ const newConfig = { ...toolSelections };
1667
+ delete newConfig.mcpServers[id];
1668
+ setToolSelections(newConfig);
1669
+ }
1670
+ else {
1671
+ const mcpServer = toolConfigRef.current.mcpServers.find(server => server.id === id);
1672
+ const newConfig = { ...toolSelections };
1673
+ newConfig.mcpServers[id] = structuredClone(mcpServer.tools
1674
+ .filter((tool) => mcpServerEnabledState.get(mcpServer.id).has(tool.name))
1675
+ .map((tool) => tool.name));
1676
+ setToolSelections(newConfig);
1677
+ }
1678
+ };
1679
+ const getMCPServerToolState = (serverId, toolId) => {
1680
+ if (!(serverId in toolSelections.mcpServers)) {
1681
+ return false;
1682
+ }
1683
+ const selectedServerTools = toolSelections.mcpServers[serverId];
1684
+ return selectedServerTools.includes(toolId);
1685
+ };
1686
+ const setMCPServerToolState = (serverId, toolId, checked) => {
1687
+ const newConfig = { ...toolSelections };
1688
+ if (checked && !(serverId in newConfig.mcpServers)) {
1689
+ newConfig.mcpServers[serverId] = [];
1690
+ }
1691
+ const selectedServerTools = newConfig.mcpServers[serverId];
1692
+ if (checked) {
1693
+ selectedServerTools.push(toolId);
1694
+ }
1695
+ else {
1696
+ const index = selectedServerTools.indexOf(toolId);
1697
+ if (index !== -1) {
1698
+ selectedServerTools.splice(index, 1);
1699
+ }
1700
+ }
1701
+ setToolSelections(newConfig);
1702
+ };
1703
+ // all toolsets and tools of the extension are selected
1704
+ const getExtensionState = (extensionId) => {
1705
+ if (!(extensionId in toolSelections.extensions)) {
1706
+ return false;
1707
+ }
1708
+ const extension = toolConfigRef.current.extensions.find(extension => extension.id === extensionId);
1709
+ for (const toolset of extension.toolsets) {
1710
+ if (!getExtensionToolsetState(extensionId, toolset.id)) {
1711
+ return false;
1712
+ }
1713
+ }
1714
+ return true;
1715
+ };
1716
+ const getExtensionToolsetState = (extensionId, toolsetId) => {
1717
+ if (!(extensionId in toolSelections.extensions)) {
1718
+ return false;
1719
+ }
1720
+ if (!(toolsetId in toolSelections.extensions[extensionId])) {
1721
+ return false;
1722
+ }
1723
+ const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
1724
+ const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
1725
+ const selectedToolsetTools = toolSelections.extensions[extensionId][toolsetId];
1726
+ for (const tool of extensionToolset.tools) {
1727
+ if (!selectedToolsetTools.includes(tool.name)) {
1728
+ return false;
1729
+ }
1730
+ }
1731
+ return true;
1732
+ };
1733
+ const anyExtensionToolsetSelected = (extensionId) => {
1734
+ if (!(extensionId in toolSelections.extensions)) {
1735
+ return false;
1736
+ }
1737
+ return Object.keys(toolSelections.extensions[extensionId]).length > 0;
1738
+ };
1739
+ const onExtensionClicked = (extensionId) => {
1740
+ if (anyExtensionToolsetSelected(extensionId)) {
1741
+ const newConfig = { ...toolSelections };
1742
+ delete newConfig.extensions[extensionId];
1743
+ setToolSelections(newConfig);
1744
+ }
1745
+ else {
1746
+ const newConfig = { ...toolSelections };
1747
+ const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
1748
+ if (extensionId in newConfig.extensions) {
1749
+ delete newConfig.extensions[extensionId];
1750
+ }
1751
+ newConfig.extensions[extensionId] = {};
1752
+ for (const toolset of extension.toolsets) {
1753
+ newConfig.extensions[extensionId][toolset.id] = structuredClone(toolset.tools.map((tool) => tool.name));
1754
+ }
1755
+ setToolSelections(newConfig);
1756
+ }
1757
+ };
1758
+ const anyExtensionToolsetToolSelected = (extensionId, toolsetId) => {
1759
+ if (!(extensionId in toolSelections.extensions)) {
1760
+ return false;
1761
+ }
1762
+ if (!(toolsetId in toolSelections.extensions[extensionId])) {
1763
+ return false;
1764
+ }
1765
+ return toolSelections.extensions[extensionId][toolsetId].length > 0;
1766
+ };
1767
+ const onExtensionToolsetClicked = (extensionId, toolsetId) => {
1768
+ if (anyExtensionToolsetToolSelected(extensionId, toolsetId)) {
1769
+ const newConfig = { ...toolSelections };
1770
+ if (toolsetId in newConfig.extensions[extensionId]) {
1771
+ delete newConfig.extensions[extensionId][toolsetId];
1772
+ }
1773
+ setToolSelections(newConfig);
1774
+ }
1775
+ else {
1776
+ const extension = toolConfigRef.current.extensions.find(ext => ext.id === extensionId);
1777
+ const extensionToolset = extension.toolsets.find((toolset) => toolset.id === toolsetId);
1778
+ const newConfig = { ...toolSelections };
1779
+ if (!(extensionId in newConfig.extensions)) {
1780
+ newConfig.extensions[extensionId] = {};
1781
+ }
1782
+ newConfig.extensions[extensionId][toolsetId] = structuredClone(extensionToolset.tools.map((tool) => tool.name));
1783
+ setToolSelections(newConfig);
1784
+ }
1785
+ };
1786
+ const getExtensionToolsetToolState = (extensionId, toolsetId, toolId) => {
1787
+ if (!(extensionId in toolSelections.extensions)) {
1788
+ return false;
1789
+ }
1790
+ const selectedExtensionToolsets = toolSelections.extensions[extensionId];
1791
+ if (!(toolsetId in selectedExtensionToolsets)) {
1792
+ return false;
1793
+ }
1794
+ const selectedServerTools = selectedExtensionToolsets[toolsetId];
1795
+ return selectedServerTools.includes(toolId);
1796
+ };
1797
+ const setExtensionToolsetToolState = (extensionId, toolsetId, toolId, checked) => {
1798
+ const newConfig = { ...toolSelections };
1799
+ if (checked && !(extensionId in newConfig.extensions)) {
1800
+ newConfig.extensions[extensionId] = {};
1801
+ }
1802
+ if (checked && !(toolsetId in newConfig.extensions[extensionId])) {
1803
+ newConfig.extensions[extensionId][toolsetId] = [];
1804
+ }
1805
+ const selectedTools = newConfig.extensions[extensionId][toolsetId];
1806
+ if (checked) {
1807
+ selectedTools.push(toolId);
1808
+ }
1809
+ else {
1810
+ const index = selectedTools.indexOf(toolId);
1811
+ if (index !== -1) {
1812
+ selectedTools.splice(index, 1);
1813
+ }
1814
+ }
1815
+ setToolSelections(newConfig);
1816
+ };
1817
+ useEffect(() => {
1818
+ var _a;
1819
+ const prefixes = [];
1820
+ if (NBIAPI.config.isInClaudeCodeMode) {
1821
+ const claudeChatParticipant = NBIAPI.config.chatParticipants.find(participant => participant.id === CLAUDE_CODE_CHAT_PARTICIPANT_ID);
1822
+ if (claudeChatParticipant) {
1823
+ const commands = claudeChatParticipant.commands;
1824
+ for (const command of commands) {
1825
+ prefixes.push(`/${command}`);
1826
+ }
1827
+ }
1828
+ prefixes.push('/enter-plan-mode');
1829
+ prefixes.push('/exit-plan-mode');
1830
+ }
1831
+ else {
1832
+ if (chatMode === 'ask') {
1833
+ const chatParticipants = NBIAPI.config.chatParticipants;
1834
+ for (const participant of chatParticipants) {
1835
+ const id = participant.id;
1836
+ const commands = participant.commands;
1837
+ const participantPrefix = id === 'default' ? '' : `@${id}`;
1838
+ if (participantPrefix !== '') {
1839
+ prefixes.push(participantPrefix);
1840
+ }
1841
+ const commandPrefix = participantPrefix === '' ? '' : `${participantPrefix} `;
1842
+ for (const command of commands) {
1843
+ prefixes.push(`${commandPrefix}/${command}`);
1844
+ }
1845
+ }
1846
+ }
1847
+ else {
1848
+ prefixes.push('/clear');
1849
+ }
1850
+ }
1851
+ const mcpServers = NBIAPI.config.toolConfig.mcpServers;
1852
+ const mcpServerSettings = NBIAPI.config.mcpServerSettings;
1853
+ for (const mcpServer of mcpServers) {
1854
+ if (((_a = mcpServerSettings[mcpServer.id]) === null || _a === void 0 ? void 0 : _a.disabled) !== true) {
1855
+ for (const prompt of mcpServer.prompts) {
1856
+ prefixes.push(`/mcp:${mcpServer.id}:${prompt.name}`);
1857
+ }
1858
+ }
1859
+ }
1860
+ setOriginalPrefixes(prefixes);
1861
+ setPrefixSuggestions(prefixes);
1862
+ }, [chatMode, renderCount]);
1863
+ useEffect(() => {
1864
+ const fetchData = () => {
1865
+ setGHLoginStatus(NBIAPI.getLoginStatus());
1866
+ };
1867
+ fetchData();
1868
+ const intervalId = setInterval(fetchData, 1000);
1869
+ return () => clearInterval(intervalId);
1870
+ }, [loginClickCount]);
1871
+ useEffect(() => {
1872
+ setSelectedPrefixSuggestionIndex(0);
1873
+ }, [prefixSuggestions]);
1874
+ useEffect(() => {
1875
+ if (!showPopover) {
1876
+ return;
1877
+ }
1878
+ const handleClickOutside = (event) => {
1879
+ var _a, _b, _c;
1880
+ if (!((_a = autocompleteRef.current) === null || _a === void 0 ? void 0 : _a.contains(event.target)) &&
1881
+ !((_b = promptInputRef.current) === null || _b === void 0 ? void 0 : _b.contains(event.target)) &&
1882
+ !((_c = atButtonRef.current) === null || _c === void 0 ? void 0 : _c.contains(event.target))) {
1883
+ setShowPopover(false);
1884
+ }
1885
+ };
1886
+ document.addEventListener('mousedown', handleClickOutside);
1887
+ return () => document.removeEventListener('mousedown', handleClickOutside);
1888
+ }, [showPopover]);
1889
+ const onPromptChange = (event) => {
1890
+ var _a;
1891
+ const newPrompt = event.target.value;
1892
+ setPrompt(newPrompt);
1893
+ const trimmedPrompt = newPrompt.trimStart();
1894
+ if (trimmedPrompt === '@' || trimmedPrompt === '/') {
1895
+ // D030: remember which element opened the popover so focus returns
1896
+ // there on close. For the typing path that's the textarea itself.
1897
+ slashPopoverOpenerRef.current =
1898
+ (_a = document.activeElement) !== null && _a !== void 0 ? _a : null;
1899
+ setShowPopover(true);
1900
+ filterPrefixSuggestions(trimmedPrompt);
1901
+ }
1902
+ else if (trimmedPrompt.startsWith('@') || trimmedPrompt.startsWith('/')) {
1903
+ filterPrefixSuggestions(trimmedPrompt);
1904
+ }
1905
+ else {
1906
+ setShowPopover(false);
1907
+ }
1908
+ };
1909
+ const applyPrefixSuggestion = async (prefix) => {
1910
+ var _a;
1911
+ let mcpArguments = '';
1912
+ if (prefix.startsWith('/mcp:')) {
1913
+ mcpArguments = ':';
1914
+ const serverId = prefix.split(':')[1];
1915
+ const promptName = prefix.split(':')[2];
1916
+ const promptConfig = NBIAPI.config.getMCPServerPrompt(serverId, promptName);
1917
+ if (promptConfig &&
1918
+ promptConfig.arguments &&
1919
+ promptConfig.arguments.length > 0) {
1920
+ const result = await props
1921
+ .getApp()
1922
+ .commands.execute('notebook-intelligence:show-form-input-dialog', {
1923
+ title: 'Input Parameters',
1924
+ fields: promptConfig.arguments
1925
+ });
1926
+ const argumentValues = [];
1927
+ for (const argument of promptConfig.arguments) {
1928
+ if (result[argument.name] !== undefined) {
1929
+ argumentValues.push(`${argument.name}=${result[argument.name]}`);
1930
+ }
1931
+ }
1932
+ mcpArguments = `(${argumentValues.join(', ')}):`;
1933
+ }
1934
+ }
1935
+ if (prefix.includes(prompt)) {
1936
+ setPrompt(`${prefix}${mcpArguments} `);
1937
+ }
1938
+ else {
1939
+ setPrompt(`${prefix} ${prompt}${mcpArguments} `);
1940
+ }
1941
+ setShowPopover(false);
1942
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
1943
+ setSelectedPrefixSuggestionIndex(0);
1944
+ };
1945
+ const prefixSuggestionSelected = (event) => {
1946
+ const prefix = event.target.dataset['value'];
1947
+ applyPrefixSuggestion(prefix);
1948
+ };
1949
+ const handleSubmitStopChatButtonClick = async () => {
1950
+ setShowModeTools(false);
1951
+ if (!copilotRequestInProgress) {
1952
+ handleUserInputSubmit();
1953
+ }
1954
+ else {
1955
+ handleUserInputCancel();
1956
+ }
1957
+ };
1958
+ const handleSettingsButtonClick = async () => {
1959
+ setShowModeTools(false);
1960
+ setShowWorkspaceFilePicker(false);
1961
+ props
1962
+ .getApp()
1963
+ .commands.execute('notebook-intelligence:open-configuration-dialog');
1964
+ };
1965
+ const handleChatToolsButtonClick = async () => {
1966
+ var _a;
1967
+ setShowWorkspaceFilePicker(false);
1968
+ if (!showModeTools) {
1969
+ // D030: remember the trigger so focus returns there on close.
1970
+ modeToolsOpenerRef.current =
1971
+ (_a = document.activeElement) !== null && _a !== void 0 ? _a : null;
1972
+ NBIAPI.fetchCapabilities().then(() => {
1973
+ toolConfigRef.current = NBIAPI.config.toolConfig;
1974
+ mcpServerSettingsRef.current = NBIAPI.config.mcpServerSettings;
1975
+ const newMcpServerEnabledState = mcpServerSettingsToEnabledState(toolConfigRef.current.mcpServers, mcpServerSettingsRef.current);
1976
+ setMCPServerEnabledState(newMcpServerEnabledState);
1977
+ setRenderCount(renderCount => renderCount + 1);
1978
+ });
1979
+ }
1980
+ setShowModeTools(!showModeTools);
1981
+ };
1982
+ const handleUserInputSubmit = async (options) => {
1983
+ var _a, _b, _c, _d;
1984
+ const submitPrompt = (_a = options === null || options === void 0 ? void 0 : options.promptOverride) !== null && _a !== void 0 ? _a : prompt;
1985
+ const isAutoSubmit = (options === null || options === void 0 ? void 0 : options.promptOverride) !== undefined;
1986
+ if (!isAutoSubmit) {
1987
+ setPromptHistoryIndex(promptHistory.length + 1);
1988
+ setPromptHistory([...promptHistory, prompt]);
1989
+ }
1990
+ setShowPopover(false);
1991
+ const promptPrefixParts = [];
1992
+ const promptParts = submitPrompt.split(' ');
1993
+ if (promptParts.length > 1) {
1994
+ for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
1995
+ const part = promptParts[i];
1996
+ if (part.startsWith('@') || part.startsWith('/')) {
1997
+ promptPrefixParts.push(part);
1998
+ }
1999
+ }
2000
+ }
2001
+ lastMessageId.current = UUID.uuid4();
2002
+ lastRequestTime.current = new Date();
2003
+ const newList = [
2004
+ ...chatMessages,
2005
+ {
2006
+ id: lastMessageId.current,
2007
+ date: new Date(),
2008
+ from: 'user',
2009
+ contents: [
2010
+ {
2011
+ id: UUID.uuid4(),
2012
+ type: ResponseStreamDataType.Markdown,
2013
+ content: submitPrompt,
2014
+ created: new Date()
2015
+ }
2016
+ ]
2017
+ }
2018
+ ];
2019
+ setChatMessages(newList);
2020
+ if (submitPrompt.startsWith('/clear')) {
2021
+ startNewChatSession();
2022
+ return;
2023
+ }
2024
+ setCopilotRequestInProgress(true);
2025
+ const activeDocInfo = props.getActiveDocumentInfo();
2026
+ // Snapshot the active notebook so cell-targeting tools the agent fires
2027
+ // later in this run keep hitting the right notebook even after the
2028
+ // user switches tabs (issue #252). Non-notebook contexts clear the
2029
+ // ref so a stale path from a previous run doesn't bleed through.
2030
+ taskTargetNotebookPathRef.current = ((_b = activeDocInfo === null || activeDocInfo === void 0 ? void 0 : activeDocInfo.filePath) === null || _b === void 0 ? void 0 : _b.endsWith('.ipynb'))
2031
+ ? activeDocInfo.filePath
2032
+ : null;
2033
+ const extractedPrompt = submitPrompt;
2034
+ const contents = [];
2035
+ const app = props.getApp();
2036
+ const additionalContext = [];
2037
+ let currentFileUsesWholeDocument = false;
2038
+ if (contextOn && (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename)) {
2039
+ const selection = activeDocumentInfo.selection;
2040
+ const textSelected = selection &&
2041
+ !(selection.start.line === selection.end.line &&
2042
+ selection.start.column === selection.end.column);
2043
+ currentFileUsesWholeDocument = !textSelected;
2044
+ additionalContext.push({
2045
+ type: ContextType.CurrentFile,
2046
+ content: props.getActiveSelectionContent(),
2047
+ currentCellContents: textSelected
2048
+ ? null
2049
+ : props.getCurrentCellContents(),
2050
+ filePath: activeDocumentInfo.filePath,
2051
+ cellIndex: activeDocumentInfo.activeCellIndex,
2052
+ startLine: selection ? selection.start.line + 1 : 1,
2053
+ endLine: selection ? selection.end.line + 1 : 1
2054
+ });
2055
+ }
2056
+ for (const file of selectedContextFiles) {
2057
+ if (file.outputContext) {
2058
+ additionalContext.push({
2059
+ type: ContextType.OutputContext,
2060
+ content: '',
2061
+ currentCellContents: null,
2062
+ filePath: file.path,
2063
+ cellIndex: file.cellIndex,
2064
+ outputContext: file.outputContext
2065
+ });
2066
+ continue;
2067
+ }
2068
+ if (currentFileUsesWholeDocument &&
2069
+ (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filePath) === file.path) {
2070
+ continue;
2071
+ }
2072
+ const contextItem = {
2073
+ type: ContextType.Custom,
2074
+ content: file.content,
2075
+ currentCellContents: null,
2076
+ filePath: file.source === 'upload' ? ((_c = file.serverPath) !== null && _c !== void 0 ? _c : file.path) : file.path,
2077
+ startLine: 1,
2078
+ endLine: file.lineCount
2079
+ };
2080
+ if (file.source === 'upload') {
2081
+ contextItem.isUpload = true;
2082
+ }
2083
+ if (file.isImage) {
2084
+ contextItem.isImage = true;
2085
+ contextItem.mimeType = (_d = file.mimeType) !== null && _d !== void 0 ? _d : 'image/png';
2086
+ }
2087
+ additionalContext.push(contextItem);
2088
+ }
2089
+ // Auto-submit caller (e.g. Explain/Troubleshoot menu items) passes the
2090
+ // freshly-attached output bundle directly: the matching pill is queued
2091
+ // via setSelectedContextFiles in the same tick, so reading it from state
2092
+ // here would still be empty. Dedup against any pre-existing pill for the
2093
+ // same cell to avoid double-bundling.
2094
+ if (options === null || options === void 0 ? void 0 : options.extraOutputContext) {
2095
+ const extra = options.extraOutputContext;
2096
+ const alreadyAttached = selectedContextFiles.some(f => f.path === extra.path && f.outputContext);
2097
+ if (!alreadyAttached && extra.outputContext) {
2098
+ additionalContext.push({
2099
+ type: ContextType.OutputContext,
2100
+ content: '',
2101
+ currentCellContents: null,
2102
+ filePath: extra.path,
2103
+ cellIndex: extra.cellIndex,
2104
+ outputContext: extra.outputContext
2105
+ });
2106
+ }
2107
+ }
2108
+ setShowWorkspaceFilePicker(false);
2109
+ submitCompletionRequest({
2110
+ messageId: lastMessageId.current,
2111
+ chatId,
2112
+ type: RunChatCompletionType.Chat,
2113
+ content: extractedPrompt,
2114
+ language: activeDocInfo.language,
2115
+ currentDirectory: props.getCurrentDirectory(),
2116
+ filename: activeDocInfo.filePath,
2117
+ additionalContext,
2118
+ chatMode,
2119
+ toolSelections: toolSelections
2120
+ }, {
2121
+ emit: async (response) => {
2122
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2123
+ if (response.id !== lastMessageId.current) {
2124
+ return;
2125
+ }
2126
+ let responseMessage = '';
2127
+ if (response.type === BackendMessageType.StreamMessage) {
2128
+ const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
2129
+ if (!delta) {
2130
+ return;
2131
+ }
2132
+ if (delta['nbiContent']) {
2133
+ const nbiContent = delta['nbiContent'];
2134
+ contents.push({
2135
+ id: UUID.uuid4(),
2136
+ type: nbiContent.type,
2137
+ content: nbiContent.content || '',
2138
+ reasoningContent: nbiContent.reasoning_content || '',
2139
+ reasoningTag: nbiContent.reasoning_content
2140
+ ? '<think>'
2141
+ : undefined,
2142
+ reasoningFinished: nbiContent.type === ResponseStreamDataType.Markdown &&
2143
+ nbiContent.reasoning_content
2144
+ ? true
2145
+ : false,
2146
+ contentDetail: nbiContent.detail,
2147
+ created: new Date(response.created)
2148
+ });
2149
+ }
2150
+ else {
2151
+ responseMessage =
2152
+ (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
2153
+ const reasoningContent = (_h = (_g = (_f = response.data['choices']) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g['delta']) === null || _h === void 0 ? void 0 : _h['reasoning_content'];
2154
+ if (!responseMessage && !reasoningContent) {
2155
+ return;
2156
+ }
2157
+ // If we have existing reasoning content and now we get normal content, mark reasoning as finished
2158
+ const lastMarkdownItem = contents
2159
+ .filter(c => c.type === ResponseStreamDataType.MarkdownPart)
2160
+ .pop();
2161
+ if (lastMarkdownItem &&
2162
+ lastMarkdownItem.reasoningContent &&
2163
+ responseMessage &&
2164
+ !lastMarkdownItem.reasoningFinished) {
2165
+ lastMarkdownItem.reasoningFinished = true;
2166
+ }
2167
+ contents.push({
2168
+ id: UUID.uuid4(),
2169
+ type: ResponseStreamDataType.MarkdownPart,
2170
+ content: responseMessage || '',
2171
+ reasoningContent: reasoningContent || '',
2172
+ created: new Date(response.created)
2173
+ });
2174
+ }
2175
+ }
2176
+ else if (response.type === BackendMessageType.StreamEnd) {
2177
+ setCopilotRequestInProgress(false);
2178
+ const timeElapsed = (new Date().getTime() - lastRequestTime.current.getTime()) / 1000;
2179
+ telemetryEmitter.emitTelemetryEvent({
2180
+ type: TelemetryEventType.ChatResponse,
2181
+ data: {
2182
+ chatModel: {
2183
+ provider: NBIAPI.config.chatModel.provider,
2184
+ model: NBIAPI.config.chatModel.model
2185
+ },
2186
+ timeElapsed
2187
+ }
2188
+ });
2189
+ }
2190
+ else if (response.type === BackendMessageType.RunUICommand) {
2191
+ const messageId = response.id;
2192
+ const patchedArgs = injectTaskTargetNotebook(response.data.commandId, response.data.args, taskTargetNotebookPathRef.current);
2193
+ let result = 'void';
2194
+ try {
2195
+ result = await app.commands.execute(response.data.commandId, patchedArgs);
2196
+ }
2197
+ catch (error) {
2198
+ result = `Error executing command: ${error}`;
2199
+ }
2200
+ const data = {
2201
+ callback_id: response.data.callback_id,
2202
+ result: result || 'void'
2203
+ };
2204
+ try {
2205
+ JSON.stringify(data);
2206
+ }
2207
+ catch (error) {
2208
+ data.result = 'Could not serialize the result';
2209
+ }
2210
+ NBIAPI.sendWebSocketMessage(messageId, RequestDataType.RunUICommandResponse, data);
2211
+ }
2212
+ setChatMessages([
2213
+ ...newList,
2214
+ {
2215
+ id: UUID.uuid4(),
2216
+ date: new Date(),
2217
+ from: 'copilot',
2218
+ contents: contents,
2219
+ participant: NBIAPI.config.chatParticipants.find(participant => {
2220
+ return participant.id === response.participant;
2221
+ }),
2222
+ chatModel: getActiveChatModel()
2223
+ }
2224
+ ]);
2225
+ }
2226
+ });
2227
+ if (!isAutoSubmit) {
2228
+ const newPrompt = '';
2229
+ setPrompt(newPrompt);
2230
+ filterPrefixSuggestions(newPrompt);
2231
+ }
2232
+ telemetryEmitter.emitTelemetryEvent({
2233
+ type: TelemetryEventType.ChatRequest,
2234
+ data: {
2235
+ chatMode,
2236
+ chatModel: {
2237
+ provider: NBIAPI.config.chatModel.provider,
2238
+ model: NBIAPI.config.chatModel.model
2239
+ },
2240
+ prompt: extractedPrompt
2241
+ }
2242
+ });
2243
+ };
2244
+ // Refresh the ref so listeners registered with stable identity (e.g.
2245
+ // addOutputContextHandler) always invoke the latest closure of submit
2246
+ // and see current chat state.
2247
+ const handleUserInputSubmitRef = useRef(handleUserInputSubmit);
2248
+ handleUserInputSubmitRef.current = handleUserInputSubmit;
2249
+ const handleUserInputCancel = async () => {
2250
+ NBIAPI.sendWebSocketMessage(lastMessageId.current, RequestDataType.CancelChatRequest, { chatId });
2251
+ lastMessageId.current = '';
2252
+ setCopilotRequestInProgress(false);
2253
+ };
2254
+ const handleFeedback = useCallback((messageId, sentiment) => {
2255
+ setChatMessages(prev => prev.map(m => {
2256
+ if (m.id !== messageId) {
2257
+ return m;
2258
+ }
2259
+ const newFeedback = m.feedback === sentiment ? undefined : sentiment;
2260
+ return { ...m, feedback: newFeedback };
2261
+ }));
2262
+ }, []);
2263
+ const filterPrefixSuggestions = (prmpt) => {
2264
+ const userInput = prmpt.trimStart();
2265
+ if (userInput === '') {
2266
+ setPrefixSuggestions(originalPrefixes);
2267
+ }
2268
+ else {
2269
+ setPrefixSuggestions(originalPrefixes.filter(prefix => prefix.includes(userInput)));
2270
+ }
2271
+ };
2272
+ const resetPrefixSuggestions = () => {
2273
+ setPrefixSuggestions(originalPrefixes);
2274
+ setSelectedPrefixSuggestionIndex(0);
2275
+ };
2276
+ const resetChatId = () => {
2277
+ setChatId(UUID.uuid4());
2278
+ };
2279
+ const handleClaudeSessionResumed = (session) => {
2280
+ setShowClaudeSessionPicker(false);
2281
+ // Reset local chat view so the user starts from a clean slate in the
2282
+ // UI; the Claude Code backend retains the resumed transcript and will
2283
+ // answer subsequent prompts with full prior context.
2284
+ setChatMessages([
2285
+ {
2286
+ id: UUID.uuid4(),
2287
+ date: new Date(),
2288
+ from: 'copilot',
2289
+ contents: [
2290
+ {
2291
+ id: UUID.uuid4(),
2292
+ type: ResponseStreamDataType.Markdown,
2293
+ content: `Resumed Claude session \`${session.session_id.slice(0, 8)}\`${session.preview ? ` \u2014 _${session.preview}_` : ''}.`,
2294
+ created: new Date()
2295
+ }
2296
+ ]
2297
+ }
2298
+ ]);
2299
+ setPrompt('');
2300
+ setSelectedContextFiles([]);
2301
+ setShowWorkspaceFilePicker(false);
2302
+ resetChatId();
2303
+ resetPrefixSuggestions();
2304
+ setPromptHistory([]);
2305
+ setPromptHistoryIndex(0);
2306
+ };
2307
+ const onPromptKeyDown = async (event) => {
2308
+ if (event.key === 'Enter' && !event.shiftKey) {
2309
+ event.stopPropagation();
2310
+ event.preventDefault();
2311
+ if (showPopover) {
2312
+ applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
2313
+ return;
2314
+ }
2315
+ setSelectedPrefixSuggestionIndex(0);
2316
+ handleSubmitStopChatButtonClick();
2317
+ }
2318
+ else if (event.key === 'Tab') {
2319
+ if (showPopover) {
2320
+ event.stopPropagation();
2321
+ event.preventDefault();
2322
+ applyPrefixSuggestion(prefixSuggestions[selectedPrefixSuggestionIndex]);
2323
+ return;
2324
+ }
2325
+ }
2326
+ else if (event.key === 'Escape') {
2327
+ event.stopPropagation();
2328
+ event.preventDefault();
2329
+ setShowPopover(false);
2330
+ setShowModeTools(false);
2331
+ setShowWorkspaceFilePicker(false);
2332
+ setSelectedPrefixSuggestionIndex(0);
2333
+ }
2334
+ else if (event.key === 'ArrowUp') {
2335
+ event.stopPropagation();
2336
+ event.preventDefault();
2337
+ if (showPopover) {
2338
+ setSelectedPrefixSuggestionIndex((selectedPrefixSuggestionIndex - 1 + prefixSuggestions.length) %
2339
+ prefixSuggestions.length);
2340
+ return;
2341
+ }
2342
+ setShowPopover(false);
2343
+ // first time up key press
2344
+ if (promptHistory.length > 0 &&
2345
+ promptHistoryIndex === promptHistory.length) {
2346
+ setDraftPrompt(prompt);
2347
+ }
2348
+ if (promptHistory.length > 0 &&
2349
+ promptHistoryIndex > 0 &&
2350
+ promptHistoryIndex <= promptHistory.length) {
2351
+ const prevPrompt = promptHistory[promptHistoryIndex - 1];
2352
+ const newIndex = promptHistoryIndex - 1;
2353
+ setPrompt(prevPrompt);
2354
+ setPromptHistoryIndex(newIndex);
2355
+ }
2356
+ }
2357
+ else if (event.key === 'ArrowDown') {
2358
+ event.stopPropagation();
2359
+ event.preventDefault();
2360
+ if (showPopover) {
2361
+ setSelectedPrefixSuggestionIndex((selectedPrefixSuggestionIndex + 1 + prefixSuggestions.length) %
2362
+ prefixSuggestions.length);
2363
+ return;
2364
+ }
2365
+ setShowPopover(false);
2366
+ if (promptHistory.length > 0 &&
2367
+ promptHistoryIndex >= 0 &&
2368
+ promptHistoryIndex < promptHistory.length) {
2369
+ if (promptHistoryIndex === promptHistory.length - 1) {
2370
+ setPrompt(draftPrompt);
2371
+ setPromptHistoryIndex(promptHistory.length);
2372
+ return;
2373
+ }
2374
+ const prevPrompt = promptHistory[promptHistoryIndex + 1];
2375
+ const newIndex = promptHistoryIndex + 1;
2376
+ setPrompt(prevPrompt);
2377
+ setPromptHistoryIndex(newIndex);
2378
+ }
2379
+ }
2380
+ };
2381
+ // Throttle scrollMessagesToBottom to only scroll every 500ms
2382
+ const SCROLL_THROTTLE_TIME = 1000;
2383
+ const scrollMessagesToBottom = () => {
2384
+ var _a;
2385
+ const now = Date.now();
2386
+ if (now - lastScrollTime >= SCROLL_THROTTLE_TIME) {
2387
+ (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
2388
+ setLastScrollTime(now);
2389
+ }
2390
+ else if (!scrollPending) {
2391
+ setScrollPending(true);
2392
+ setTimeout(() => {
2393
+ var _a;
2394
+ (_a = messagesEndRef.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: 'smooth' });
2395
+ setLastScrollTime(Date.now());
2396
+ setScrollPending(false);
2397
+ }, SCROLL_THROTTLE_TIME - (now - lastScrollTime));
2398
+ }
2399
+ };
2400
+ const handleConfigurationClick = async () => {
2401
+ setShowWorkspaceFilePicker(false);
2402
+ props
2403
+ .getApp()
2404
+ .commands.execute('notebook-intelligence:open-configuration-dialog');
2405
+ };
2406
+ const handleLoginClick = async () => {
2407
+ props
2408
+ .getApp()
2409
+ .commands.execute('notebook-intelligence:open-github-copilot-login-dialog');
2410
+ };
2411
+ useEffect(() => {
2412
+ scrollMessagesToBottom();
2413
+ }, [chatMessages]);
2414
+ const promptRequestHandler = useCallback((eventData) => {
2415
+ var _a;
2416
+ const request = eventData.detail;
2417
+ request.chatId = chatId;
2418
+ let message = '';
2419
+ switch (request.type) {
2420
+ case RunChatCompletionType.ExplainThis:
2421
+ message = `Explain this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
2422
+ break;
2423
+ case RunChatCompletionType.FixThis:
2424
+ message = `Fix this code:\n\`\`\`\n${request.content}\n\`\`\`\n`;
2425
+ break;
2426
+ case RunChatCompletionType.NotebookGeneration:
2427
+ // The notebook-toolbar popover already prefixed the prompt; pass it
2428
+ // through verbatim so the message displayed in chat matches what
2429
+ // was sent to the backend.
2430
+ message = request.content;
2431
+ // Notebook generation requires agent mode with the notebook-edit
2432
+ // and notebook-execute toolsets — without them the LLM can't
2433
+ // actually mutate cells regardless of how it answers. Override
2434
+ // whatever the user has in the sidebar so the toolbar button
2435
+ // works out-of-the-box in non-Claude modes too (issue #229).
2436
+ request.chatMode = 'agent';
2437
+ request.toolSelections = {
2438
+ builtinToolsets: [
2439
+ BuiltinToolsetType.NotebookEdit,
2440
+ BuiltinToolsetType.NotebookExecute
2441
+ ],
2442
+ mcpServers: {},
2443
+ extensions: {}
2444
+ };
2445
+ break;
2446
+ }
2447
+ const messageId = UUID.uuid4();
2448
+ request.messageId = messageId;
2449
+ request.content = message;
2450
+ const externalRequestId = request.externalRequestId;
2451
+ const emitProgress = (inProgress, error) => {
2452
+ if (!externalRequestId) {
2453
+ return;
2454
+ }
2455
+ const detail = {
2456
+ requestId: externalRequestId,
2457
+ inProgress
2458
+ };
2459
+ if (error) {
2460
+ detail.error = error;
2461
+ }
2462
+ document.dispatchEvent(new CustomEvent(NOTEBOOK_GENERATION_PROGRESS_EVENT, { detail }));
2463
+ };
2464
+ emitProgress(true);
2465
+ // Snapshot the notebook the agent should target for this externally-
2466
+ // triggered request (e.g., notebook-toolbar generation). The
2467
+ // `RunUICommand` handler below uses this for the same tab-switch
2468
+ // resilience covered by the main submit flow (issue #252).
2469
+ const externalActiveDocInfo = props.getActiveDocumentInfo();
2470
+ taskTargetNotebookPathRef.current =
2471
+ ((_a = externalActiveDocInfo === null || externalActiveDocInfo === void 0 ? void 0 : externalActiveDocInfo.filePath) === null || _a === void 0 ? void 0 : _a.endsWith('.ipynb'))
2472
+ ? externalActiveDocInfo.filePath
2473
+ : null;
2474
+ const hideInChat = !!request.hideInChat;
2475
+ const newList = hideInChat
2476
+ ? chatMessages
2477
+ : [
2478
+ ...chatMessages,
2479
+ {
2480
+ id: messageId,
2481
+ date: new Date(),
2482
+ from: 'user',
2483
+ contents: [
2484
+ {
2485
+ id: messageId,
2486
+ type: ResponseStreamDataType.Markdown,
2487
+ content: message,
2488
+ created: new Date()
2489
+ }
2490
+ ]
2491
+ }
2492
+ ];
2493
+ if (!hideInChat) {
2494
+ setChatMessages(newList);
2495
+ setCopilotRequestInProgress(true);
2496
+ }
2497
+ const contents = [];
2498
+ submitCompletionRequest(request, {
2499
+ emit: async (response) => {
2500
+ var _a, _b, _c, _d, _e, _f, _g, _h;
2501
+ if (response.type === BackendMessageType.StreamMessage) {
2502
+ const delta = (_b = (_a = response.data['choices']) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b['delta'];
2503
+ if (!delta) {
2504
+ return;
2505
+ }
2506
+ if (delta['nbiContent']) {
2507
+ const nbiContent = delta['nbiContent'];
2508
+ contents.push({
2509
+ id: UUID.uuid4(),
2510
+ type: nbiContent.type,
2511
+ content: nbiContent.content || '',
2512
+ reasoningContent: nbiContent.reasoning_content || '',
2513
+ reasoningTag: nbiContent.reasoning_content
2514
+ ? '<think>'
2515
+ : undefined,
2516
+ reasoningFinished: nbiContent.type === ResponseStreamDataType.Markdown &&
2517
+ nbiContent.reasoning_content
2518
+ ? true
2519
+ : false,
2520
+ contentDetail: nbiContent.detail,
2521
+ created: new Date(response.created)
2522
+ });
2523
+ }
2524
+ else {
2525
+ const responseMessage = (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
2526
+ const reasoningContent = (_h = (_g = (_f = response.data['choices']) === null || _f === void 0 ? void 0 : _f[0]) === null || _g === void 0 ? void 0 : _g['delta']) === null || _h === void 0 ? void 0 : _h['reasoning_content'];
2527
+ if (!responseMessage && !reasoningContent) {
2528
+ return;
2529
+ }
2530
+ // If we have existing reasoning content and now we get normal content, mark reasoning as finished
2531
+ const lastMarkdownItem = contents
2532
+ .filter(c => c.type === ResponseStreamDataType.MarkdownPart)
2533
+ .pop();
2534
+ if (lastMarkdownItem &&
2535
+ lastMarkdownItem.reasoningContent &&
2536
+ responseMessage &&
2537
+ !lastMarkdownItem.reasoningFinished) {
2538
+ lastMarkdownItem.reasoningFinished = true;
2539
+ }
2540
+ contents.push({
2541
+ id: response.id,
2542
+ type: ResponseStreamDataType.MarkdownPart,
2543
+ content: responseMessage || '',
2544
+ reasoningContent: reasoningContent || '',
2545
+ created: new Date(response.created)
2546
+ });
2547
+ }
2548
+ }
2549
+ else if (response.type === BackendMessageType.StreamEnd) {
2550
+ if (!hideInChat) {
2551
+ setCopilotRequestInProgress(false);
2552
+ }
2553
+ emitProgress(false);
2554
+ }
2555
+ else if (response.type === BackendMessageType.RunUICommand) {
2556
+ const runUiMessageId = response.id;
2557
+ const patchedArgs = injectTaskTargetNotebook(response.data.commandId, response.data.args, taskTargetNotebookPathRef.current);
2558
+ let result = 'void';
2559
+ try {
2560
+ result = await props
2561
+ .getApp()
2562
+ .commands.execute(response.data.commandId, patchedArgs);
2563
+ }
2564
+ catch (error) {
2565
+ result = `Error executing command: ${error}`;
2566
+ }
2567
+ const data = {
2568
+ callback_id: response.data.callback_id,
2569
+ result: result || 'void'
2570
+ };
2571
+ try {
2572
+ JSON.stringify(data);
2573
+ }
2574
+ catch (error) {
2575
+ data.result = 'Could not serialize the result';
2576
+ }
2577
+ NBIAPI.sendWebSocketMessage(runUiMessageId, RequestDataType.RunUICommandResponse, data);
2578
+ }
2579
+ if (hideInChat) {
2580
+ return;
2581
+ }
2582
+ const messageId = UUID.uuid4();
2583
+ setChatMessages([
2584
+ ...newList,
2585
+ {
2586
+ id: messageId,
2587
+ date: new Date(),
2588
+ from: 'copilot',
2589
+ contents: contents,
2590
+ participant: NBIAPI.config.chatParticipants.find(participant => {
2591
+ return participant.id === response.participant;
2592
+ }),
2593
+ chatModel: getActiveChatModel()
2594
+ }
2595
+ ]);
2596
+ }
2597
+ });
2598
+ }, [chatMessages, chatMode]);
2599
+ useEffect(() => {
2600
+ document.addEventListener('copilotSidebar:runPrompt', promptRequestHandler);
2601
+ return () => {
2602
+ document.removeEventListener('copilotSidebar:runPrompt', promptRequestHandler);
2603
+ };
2604
+ }, [chatMessages]);
2605
+ // copilotSidebar:focusPrompt is dispatched from the global keybinding
2606
+ // (Ctrl/Cmd+Shift+L) registered in index.ts. activateById can take
2607
+ // several frames to mount the sidebar when it was collapsed, so the
2608
+ // handler retries up to ~10 frames waiting for the textarea ref to be
2609
+ // populated and on-screen before calling focus(). One frame isn't
2610
+ // enough on a cold-open of the left rail.
2611
+ useEffect(() => {
2612
+ const handler = () => {
2613
+ let attempts = 0;
2614
+ const MAX_ATTEMPTS = 10;
2615
+ const tryFocus = () => {
2616
+ const el = promptInputRef.current;
2617
+ if (el && el.offsetParent !== null) {
2618
+ el.focus();
2619
+ return;
2620
+ }
2621
+ attempts += 1;
2622
+ if (attempts < MAX_ATTEMPTS) {
2623
+ requestAnimationFrame(tryFocus);
2624
+ }
2625
+ };
2626
+ tryFocus();
2627
+ };
2628
+ document.addEventListener('copilotSidebar:focusPrompt', handler);
2629
+ return () => document.removeEventListener('copilotSidebar:focusPrompt', handler);
2630
+ }, []);
2631
+ const addOutputContextHandler = useCallback((eventData) => {
2632
+ const detail = eventData === null || eventData === void 0 ? void 0 : eventData.detail;
2633
+ if (!detail || !detail.outputContext) {
2634
+ return;
2635
+ }
2636
+ const cellIndex = detail.cellIndex;
2637
+ const notebookFilename = detail.notebookFilename;
2638
+ const cellId = detail.cellId;
2639
+ const autoSubmitPrompt = detail.autoSubmitPrompt;
2640
+ // Cell IDs are stable across cell moves/renames, so two right-clicks on
2641
+ // the same cell collapse to one attachment. Fall back to the (notebook,
2642
+ // index) tuple only when the platform doesn't expose an ID.
2643
+ const path = cellId
2644
+ ? `nbi://output/cell/${cellId}`
2645
+ : `nbi://output/${notebookFilename !== null && notebookFilename !== void 0 ? notebookFilename : 'notebook'}/${cellIndex !== null && cellIndex !== void 0 ? cellIndex : 0}`;
2646
+ const attached = {
2647
+ content: '',
2648
+ lineCount: 0,
2649
+ path,
2650
+ type: 'output',
2651
+ outputContext: detail.outputContext,
2652
+ cellIndex,
2653
+ notebookFilename
2654
+ };
2655
+ setSelectedContextFiles(prev => {
2656
+ if (prev.some(file => file.path === path)) {
2657
+ return prev;
2658
+ }
2659
+ if (prev.length >= MAX_ATTACHED_FILES) {
2660
+ return prev;
2661
+ }
2662
+ return [...prev, attached];
2663
+ });
2664
+ if (autoSubmitPrompt) {
2665
+ handleUserInputSubmitRef.current({
2666
+ promptOverride: autoSubmitPrompt,
2667
+ extraOutputContext: attached
2668
+ });
2669
+ }
2670
+ }, []);
2671
+ useEffect(() => {
2672
+ document.addEventListener('copilotSidebar:addOutputContext', addOutputContextHandler);
2673
+ return () => {
2674
+ document.removeEventListener('copilotSidebar:addOutputContext', addOutputContextHandler);
2675
+ };
2676
+ }, [addOutputContextHandler]);
2677
+ const activeDocumentChangeHandler = (eventData) => {
2678
+ var _a;
2679
+ // if file changes reset the context toggle
2680
+ if (((_a = eventData.detail.activeDocumentInfo) === null || _a === void 0 ? void 0 : _a.filePath) !==
2681
+ (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filePath)) {
2682
+ setContextOn(false);
2683
+ }
2684
+ setActiveDocumentInfo({
2685
+ ...eventData.detail.activeDocumentInfo,
2686
+ ...{ activeWidget: null }
2687
+ });
2688
+ setCurrentFileContextTitle(getActiveDocumentContextTitle(eventData.detail.activeDocumentInfo));
2689
+ };
2690
+ useEffect(() => {
2691
+ document.addEventListener('copilotSidebar:activeDocumentChanged', activeDocumentChangeHandler);
2692
+ return () => {
2693
+ document.removeEventListener('copilotSidebar:activeDocumentChanged', activeDocumentChangeHandler);
2694
+ };
2695
+ }, [activeDocumentInfo]);
2696
+ useEffect(() => {
2697
+ if (!showWorkspaceFilePicker) {
2698
+ // Abandon any in-flight scan so its terminal setState calls don't
2699
+ // land on a closed picker (or, if the user re-opens, on a new
2700
+ // generation's render).
2701
+ workspaceScanGenerationRef.current += 1;
2702
+ workspaceFilesLoadingRef.current = false;
2703
+ setWorkspaceFilesLoading(false);
2704
+ setWorkspaceFilesError('');
2705
+ setWorkspaceFileSearch('');
2706
+ }
2707
+ }, [showWorkspaceFilePicker]);
2708
+ // Abandon any in-flight scan on unmount; setState after unmount is a
2709
+ // React anti-pattern and the parallel BFS makes the race window wider.
2710
+ useEffect(() => () => {
2711
+ workspaceScanGenerationRef.current += 1;
2712
+ }, []);
2713
+ const getActiveDocumentContextTitle = (activeDocumentInfo) => {
2714
+ if (!(activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename)) {
2715
+ return '';
2716
+ }
2717
+ const wholeFile = !activeDocumentInfo.selection ||
2718
+ (activeDocumentInfo.selection.start.line ===
2719
+ activeDocumentInfo.selection.end.line &&
2720
+ activeDocumentInfo.selection.start.column ===
2721
+ activeDocumentInfo.selection.end.column);
2722
+ let cellAndLineIndicator = '';
2723
+ if (!wholeFile) {
2724
+ if (activeDocumentInfo.filename.endsWith('.ipynb')) {
2725
+ cellAndLineIndicator = ` · Cell ${activeDocumentInfo.activeCellIndex + 1}`;
2726
+ }
2727
+ if (activeDocumentInfo.selection.start.line ===
2728
+ activeDocumentInfo.selection.end.line) {
2729
+ cellAndLineIndicator += `:${activeDocumentInfo.selection.start.line + 1}`;
2730
+ }
2731
+ else {
2732
+ cellAndLineIndicator += `:${activeDocumentInfo.selection.start.line + 1}-${activeDocumentInfo.selection.end.line + 1}`;
2733
+ }
2734
+ }
2735
+ return `${activeDocumentInfo.filename}${cellAndLineIndicator}`;
2736
+ };
2737
+ const [ghLoginRequired, setGHLoginRequired] = useState(NBIAPI.getGHLoginRequired());
2738
+ const [chatEnabled, setChatEnabled] = useState(NBIAPI.getChatEnabled());
2739
+ const [skillsReloadedVisible, setSkillsReloadedVisible] = useState(false);
2740
+ // Visible for a few seconds after the user starts a new chat session
2741
+ // (either via the header button or `/clear`). The aria-live region
2742
+ // below announces it to assistive tech.
2743
+ const [newChatNoticeVisible, setNewChatNoticeVisible] = useState(false);
2744
+ const newChatNoticeTimerRef = useRef(null);
2745
+ useEffect(() => {
2746
+ return () => {
2747
+ if (newChatNoticeTimerRef.current) {
2748
+ clearTimeout(newChatNoticeTimerRef.current);
2749
+ }
2750
+ };
2751
+ }, []);
2752
+ const startNewChatSession = useCallback(() => {
2753
+ // Reset every piece of per-conversation UI state and tell the server
2754
+ // to drop its conversation history. Functionally equivalent to typing
2755
+ // `/clear`, but reachable from the header button so the user doesn't
2756
+ // have to remember the slash command (issue #237). Also useful when
2757
+ // the Claude SDK client is wedged — restarting the session reconnects
2758
+ // the agent.
2759
+ if (copilotRequestInProgress) {
2760
+ // Cancel any in-flight response before clearing local state. Without
2761
+ // this, stream deltas tied to the old messageId keep arriving against
2762
+ // an empty chat-messages list and silently re-populate it from the
2763
+ // old conversation.
2764
+ NBIAPI.sendWebSocketMessage(lastMessageId.current, RequestDataType.CancelChatRequest, { chatId });
2765
+ lastMessageId.current = '';
2766
+ setCopilotRequestInProgress(false);
2767
+ }
2768
+ setChatMessages([]);
2769
+ setPrompt('');
2770
+ setSelectedContextFiles([]);
2771
+ setShowWorkspaceFilePicker(false);
2772
+ resetChatId();
2773
+ resetPrefixSuggestions();
2774
+ setPromptHistory([]);
2775
+ setPromptHistoryIndex(0);
2776
+ NBIAPI.sendWebSocketMessage(UUID.uuid4(), RequestDataType.ClearChatHistory, {
2777
+ chatId
2778
+ });
2779
+ setNewChatNoticeVisible(true);
2780
+ if (newChatNoticeTimerRef.current) {
2781
+ clearTimeout(newChatNoticeTimerRef.current);
2782
+ }
2783
+ newChatNoticeTimerRef.current = setTimeout(() => {
2784
+ setNewChatNoticeVisible(false);
2785
+ newChatNoticeTimerRef.current = null;
2786
+ }, 3000);
2787
+ // Move focus to the prompt textarea so the user can immediately type
2788
+ // their first message in the fresh session. Defer past the React
2789
+ // commit so the input has re-rendered with the cleared prompt value.
2790
+ window.requestAnimationFrame(() => {
2791
+ var _a;
2792
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
2793
+ });
2794
+ }, [chatId, copilotRequestInProgress, resetChatId, resetPrefixSuggestions]);
2795
+ useEffect(() => {
2796
+ const handler = () => {
2797
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
2798
+ setChatEnabled(NBIAPI.getChatEnabled());
2799
+ };
2800
+ NBIAPI.configChanged.connect(handler);
2801
+ return () => {
2802
+ NBIAPI.configChanged.disconnect(handler);
2803
+ };
2804
+ }, []);
2805
+ useEffect(() => {
2806
+ let timeout = null;
2807
+ const listener = () => {
2808
+ setSkillsReloadedVisible(true);
2809
+ if (timeout) {
2810
+ clearTimeout(timeout);
2811
+ }
2812
+ timeout = setTimeout(() => {
2813
+ setSkillsReloadedVisible(false);
2814
+ }, 4000);
2815
+ };
2816
+ NBIAPI.skillsReloaded.connect(listener);
2817
+ return () => {
2818
+ NBIAPI.skillsReloaded.disconnect(listener);
2819
+ if (timeout) {
2820
+ clearTimeout(timeout);
2821
+ }
2822
+ };
2823
+ }, []);
2824
+ useEffect(() => {
2825
+ setGHLoginRequired(NBIAPI.getGHLoginRequired());
2826
+ setChatEnabled(NBIAPI.getChatEnabled());
2827
+ }, [ghLoginStatus]);
2828
+ return (React.createElement("div", { ref: sidebarRootRef, className: `sidebar${isDragOver ? ' drag-over' : ''}`, onDragOver: handleDragOver, onDragLeave: handleDragLeave, onDrop: handleDrop },
2829
+ chatEnabled && (React.createElement("a", { href: "#sidebar-user-input", className: "nbi-sr-only nbi-skip-link", onClick: event => {
2830
+ var _a;
2831
+ event.preventDefault();
2832
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
2833
+ } }, "Skip to message input")),
2834
+ isDragOver && (React.createElement("div", { className: "drop-zone-overlay" },
2835
+ React.createElement("span", null, "Drop files to attach as context"))),
2836
+ tourVisible && React.createElement(TourOverlay, { onClose: () => setTourVisible(false) }),
2837
+ React.createElement("div", { className: "sidebar-header" },
2838
+ React.createElement("div", { className: "sidebar-title" }, "Notebook Intelligence"),
2839
+ NBIAPI.config.isInClaudeCodeMode && (React.createElement(React.Fragment, null,
2840
+ React.createElement("button", { type: "button", className: "user-input-footer-button", "data-tour-id": TOUR_ANCHOR.newChat, onClick: () => startNewChatSession(), title: "Start a new chat session (restarts the Claude client)", "aria-label": "Start a new chat session" },
2841
+ React.createElement(VscAdd, null)),
2842
+ React.createElement("button", { type: "button", className: "user-input-footer-button", "data-tour-id": TOUR_ANCHOR.claudeHistory, onClick: () => setShowClaudeSessionPicker(true), "aria-label": "Resume previous Claude session", title: "Resume a Claude session you started earlier in this workspace" },
2843
+ React.createElement(VscHistory, null)))),
2844
+ React.createElement("button", { type: "button", className: "user-input-footer-button", "data-tour-id": TOUR_ANCHOR.settingsGear, onClick: () => handleSettingsButtonClick(), "aria-label": "Open Notebook Intelligence settings", title: "Configure providers, API keys, MCP servers, and skills" },
2845
+ React.createElement(VscSettingsGear, null))),
2846
+ React.createElement("div", { className: "nbi-status-banner-live", "aria-live": "polite" },
2847
+ skillsReloadedVisible && (React.createElement("div", { className: "nbi-status-banner" }, "Skills reloaded \u2014 applied to the current session.")),
2848
+ newChatNoticeVisible && (React.createElement("div", { className: "nbi-status-banner" }, "New chat session started."))),
2849
+ React.createElement("div", { className: "nbi-sr-only", role: "status", "aria-live": "polite" }, chatStatusAnnouncement),
2850
+ !chatEnabled && !ghLoginRequired && (React.createElement("div", { className: "sidebar-login-info" },
2851
+ "Chat is disabled as you don't have a model configured.",
2852
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: handleConfigurationClick },
2853
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Configure models")))),
2854
+ !NBIAPI.config.isInClaudeCodeMode && ghLoginRequired && (React.createElement("div", { className: "sidebar-login-info" },
2855
+ React.createElement("div", null, "You are not logged in to GitHub Copilot. Please login now to activate chat."),
2856
+ React.createElement("div", { className: "sidebar-login-buttons" },
2857
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: handleLoginClick },
2858
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Login to GitHub Copilot")),
2859
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleConfigurationClick },
2860
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Change provider"))))),
2861
+ chatEnabled &&
2862
+ (chatMessages.length === 0 ? (React.createElement("div", { className: "sidebar-messages", role: "log", "aria-label": "Chat transcript" },
2863
+ React.createElement("div", { className: "sidebar-greeting" }, "Welcome! How can I assist you today?"))) : (React.createElement("div", { className: "sidebar-messages", role: "log", "aria-label": "Chat transcript" },
2864
+ chatMessages.map((msg, index) => {
2865
+ // Only the most recent copilot message owns the live
2866
+ // progress-feedback state. Non-active messages receive
2867
+ // stable primitives so React.memo can prune them; otherwise
2868
+ // the 1Hz elapsed-time tick would re-render every message
2869
+ // in the chat history every second.
2870
+ const isActiveCopilotMessage = index === chatMessages.length - 1 &&
2871
+ msg.from === 'copilot' &&
2872
+ copilotRequestInProgress;
2873
+ return (React.createElement(MemoizedChatResponse, { key: msg.id, message: msg, openFile: props.openFile, getApp: props.getApp, getActiveDocumentInfo: props.getActiveDocumentInfo, showGenerating: isActiveCopilotMessage, elapsedSeconds: isActiveCopilotMessage ? elapsedSeconds : 0, heartbeatTick: isActiveCopilotMessage ? heartbeatTick : 0, isStalled: isActiveCopilotMessage ? isStalled : false, onFeedback: handleFeedback, chatId: chatId, telemetryEmitter: telemetryEmitter }));
2874
+ }),
2875
+ React.createElement("div", { ref: messagesEndRef })))),
2876
+ chatEnabled && (React.createElement("div", { id: "sidebar-user-input", "data-tour-id": TOUR_ANCHOR.promptInput, className: `sidebar-user-input ${copilotRequestInProgress ? 'generating' : ''}` },
2877
+ React.createElement("textarea", { ref: promptInputRef, rows: 3, onChange: onPromptChange, onKeyDown: onPromptKeyDown, onPaste: handlePaste, placeholder: "Ask Notebook Intelligence...", spellCheck: false, value: prompt }),
2878
+ ((activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename) ||
2879
+ selectedContextFiles.length > 0 ||
2880
+ isUploadingFiles) && (React.createElement("div", { className: "user-input-context-row" },
2881
+ (activeDocumentInfo === null || activeDocumentInfo === void 0 ? void 0 : activeDocumentInfo.filename) && (React.createElement("div", { className: `user-input-context user-input-context-active-file ${contextOn ? 'on' : 'off'}` },
2882
+ React.createElement("div", null, currentFileContextTitle),
2883
+ React.createElement("button", { type: "button", className: "user-input-context-toggle", onClick: () => setContextOn(!contextOn), "aria-label": contextOn
2884
+ ? 'Stop using current file as context'
2885
+ : 'Use current file as context', "aria-pressed": contextOn, title: contextOn ? 'Use as context' : "Don't use as context" }, contextOn ? (React.createElement(VscEye, { "aria-hidden": "true" })) : (React.createElement(VscEyeClosed, { "aria-hidden": "true" }))))),
2886
+ selectedContextFiles.map(file => {
2887
+ var _a;
2888
+ const isOutput = !!file.outputContext;
2889
+ const cellLabel = typeof file.cellIndex === 'number'
2890
+ ? `Cell ${file.cellIndex + 1} output`
2891
+ : 'Cell output';
2892
+ const label = isOutput
2893
+ ? file.notebookFilename
2894
+ ? `${cellLabel} (${file.notebookFilename})`
2895
+ : cellLabel
2896
+ : file.path;
2897
+ const titleText = isOutput
2898
+ ? label
2899
+ : file.source === 'upload'
2900
+ ? `Uploaded: ${file.path}`
2901
+ : file.path;
2902
+ return (React.createElement("div", { key: (_a = file.serverPath) !== null && _a !== void 0 ? _a : file.path, className: `user-input-context user-input-context-selected-file on${file.source === 'upload' ? ' uploaded-file' : ''}${file.isImage ? ' image-file' : ''}${isOutput ? ' output-context' : ''}`, title: titleText },
2903
+ React.createElement("div", null, file.isImage && file.imageDataUrl ? (React.createElement(React.Fragment, null,
2904
+ React.createElement("span", { className: "context-pill-thumbnail-wrap" },
2905
+ React.createElement("img", { src: file.imageDataUrl, className: "context-pill-thumbnail", alt: file.path }),
2906
+ React.createElement("img", { src: file.imageDataUrl, className: "context-pill-thumbnail-preview", alt: "", "aria-hidden": "true" })),
2907
+ file.path)) : file.source === 'upload' ? (React.createElement(React.Fragment, null,
2908
+ React.createElement(VscCloudUpload, null),
2909
+ " ",
2910
+ file.path)) : (label)),
2911
+ React.createElement("button", { type: "button", className: "user-input-context-toggle", onClick: event => {
2912
+ var _a, _b, _c;
2913
+ // The row this button lives in is about to disappear
2914
+ // from the DOM, which would otherwise drop focus to
2915
+ // ``<body>``. Hand focus to the next remove button
2916
+ // in the row (preferred) or back to the textarea so
2917
+ // keyboard users keep their place.
2918
+ const target = event.currentTarget;
2919
+ const row = (_a = target.closest('.user-input-context-row')) !== null && _a !== void 0 ? _a : null;
2920
+ const buttons = row
2921
+ ? Array.from(row.querySelectorAll('button.user-input-context-toggle'))
2922
+ : [];
2923
+ const idx = buttons.indexOf(target);
2924
+ const next = (_b = buttons[idx + 1]) !== null && _b !== void 0 ? _b : buttons[idx - 1];
2925
+ removeSelectedContextFile((_c = file.serverPath) !== null && _c !== void 0 ? _c : file.path);
2926
+ // Defer the focus move past the React re-render that
2927
+ // unmounts ``target``.
2928
+ window.requestAnimationFrame(() => {
2929
+ var _a;
2930
+ if (next && document.contains(next)) {
2931
+ next.focus();
2932
+ }
2933
+ else {
2934
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
2935
+ }
2936
+ });
2937
+ }, "aria-label": `Remove attached file ${label}`, title: "Remove attached file" },
2938
+ React.createElement(VscClose, { "aria-hidden": "true" }))));
2939
+ }),
2940
+ isUploadingFiles && (
2941
+ // The trailing-dots animation runs entirely in CSS
2942
+ // (`.loading-ellipsis::after`), so the live region's DOM
2943
+ // text stays the literal string "Uploading" and screen
2944
+ // readers announce it exactly once on insertion — not on
2945
+ // every dot tick. If a future change moves the dots into
2946
+ // React state, restore the once-announce behavior with a
2947
+ // separate sr-only label.
2948
+ React.createElement("div", { className: "user-input-context uploading-indicator", role: "status", "aria-live": "polite", "aria-atomic": "true", "aria-busy": "true" },
2949
+ React.createElement("div", { className: "loading-ellipsis" }, "Uploading"))))),
2950
+ React.createElement("div", { className: "user-input-footer" },
2951
+ chatMode === 'ask' && (React.createElement("button", { type: "button", ref: atButtonRef, "data-tour-id": TOUR_ANCHOR.slashCommands, className: "user-input-footer-button user-input-footer-slash-button", onClick: () => {
2952
+ var _a;
2953
+ if (!showPopover) {
2954
+ // D030: remember the button so focus returns to it
2955
+ // on close. Capture before the state flip so we
2956
+ // record the click target, not whatever the focus
2957
+ // shift below leaves behind. (Pure: side effects
2958
+ // belong outside the state updater.)
2959
+ slashPopoverOpenerRef.current = atButtonRef.current;
2960
+ }
2961
+ setShowPopover(prev => !prev);
2962
+ (_a = promptInputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
2963
+ }, title: "Slash commands", "aria-label": "Open slash commands" }, "/")),
2964
+ React.createElement("button", { type: "button", "data-tour-id": TOUR_ANCHOR.addContext, className: `user-input-footer-button ${selectedContextFiles.length > 0 ? 'tools-button tools-button-active' : ''}`, onClick: () => handleWorkspaceFilePickerClick(), title: "Add workspace file as context", "aria-label": "Add workspace file as context" },
2965
+ React.createElement(VscFile, null),
2966
+ selectedContextFiles.length > 0 && (React.createElement(React.Fragment, null, selectedContextFiles.length))),
2967
+ React.createElement("button", { type: "button", "data-tour-id": TOUR_ANCHOR.uploadFile, className: "user-input-footer-button", onClick: () => { var _a; return (_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, title: "Upload file from computer", "aria-label": "Upload file from computer" },
2968
+ React.createElement(VscCloudUpload, null)),
2969
+ React.createElement("input", { ref: fileInputRef, type: "file", multiple: true, style: { display: 'none' }, onChange: handleFileInputChange }),
2970
+ React.createElement("div", { style: { flexGrow: 1 } }),
2971
+ React.createElement("div", { className: "chat-mode-widgets-container" },
2972
+ !NBIAPI.config.isInClaudeCodeMode && (React.createElement("div", { "data-tour-id": TOUR_ANCHOR.chatMode },
2973
+ React.createElement("select", { className: "chat-mode-select", title: "Chat mode", value: chatMode, onChange: event => {
2974
+ if (event.target.value === 'ask') {
2975
+ setToolSelections(toolSelectionsEmpty);
2976
+ }
2977
+ setShowModeTools(false);
2978
+ setChatMode(event.target.value);
2979
+ } },
2980
+ React.createElement("option", { value: "ask" }, "Ask"),
2981
+ React.createElement("option", { value: "agent" }, "Agent")))),
2982
+ chatMode !== 'ask' && !NBIAPI.config.isInClaudeCodeMode && (React.createElement("button", { type: "button", className: `user-input-footer-button tools-button ${unsafeToolSelected ? 'tools-button-warning' : selectedToolCount > 0 ? 'tools-button-active' : ''}`, onClick: () => handleChatToolsButtonClick(), title: unsafeToolSelected
2983
+ ? `Tool selection can cause irreversible changes! Review each tool execution carefully.\n${toolSelectionTitle}`
2984
+ : toolSelectionTitle, "aria-label": unsafeToolSelected
2985
+ ? 'Configure tools (warning: irreversible tools selected)'
2986
+ : 'Configure tools' },
2987
+ React.createElement(VscTools, null),
2988
+ selectedToolCount > 0 && React.createElement(React.Fragment, null, selectedToolCount))),
2989
+ NBIAPI.config.isInClaudeCodeMode && (React.createElement("span", { title: "Claude mode", className: "claude-icon", dangerouslySetInnerHTML: { __html: claudeSvgStr } }))),
2990
+ React.createElement("div", null,
2991
+ React.createElement("button", { type: "button", className: `jp-Dialog-button jp-mod-styled send-button${copilotRequestInProgress
2992
+ ? ' jp-mod-warn send-button-stop'
2993
+ : ' jp-mod-accept'}`, onClick: () => handleSubmitStopChatButtonClick(), disabled: prompt.length === 0 && !copilotRequestInProgress, "aria-label": copilotRequestInProgress ? 'Stop generating' : 'Send message', title: copilotRequestInProgress ? 'Stop generating' : 'Send message' }, copilotRequestInProgress ? (React.createElement(VscStopCircle, { "aria-hidden": "true" })) : (React.createElement(VscSend, { "aria-hidden": "true" }))))),
2994
+ showPopover && prefixSuggestions.length > 0 && (React.createElement("div", { className: "user-input-autocomplete", ref: autocompleteRef }, prefixSuggestions.map((prefix, index) => (React.createElement("div", { key: `key-${index}`, className: `user-input-autocomplete-item ${index === selectedPrefixSuggestionIndex ? 'selected' : ''}`, "data-value": prefix, onClick: event => prefixSuggestionSelected(event) }, prefix))))),
2995
+ showClaudeSessionPicker && (React.createElement(ClaudeSessionPicker, { onResume: handleClaudeSessionResumed, onClose: () => setShowClaudeSessionPicker(false) })),
2996
+ showWorkspaceFilePicker && (React.createElement("div", { ref: workspaceFilePopoverRef, className: "workspace-file-popover", tabIndex: -1, role: "dialog", "aria-labelledby": "nbi-workspace-popover-title", onKeyDown: (event) => {
2997
+ if (event.key === 'Escape') {
2998
+ event.stopPropagation();
2999
+ event.preventDefault();
3000
+ setShowWorkspaceFilePicker(false);
3001
+ }
3002
+ } },
3003
+ React.createElement("div", { className: "mode-tools-popover-header" },
3004
+ React.createElement("div", { className: "mode-tools-popover-header-icon" },
3005
+ React.createElement(VscAdd, null)),
3006
+ React.createElement("div", { className: "mode-tools-popover-title", id: "nbi-workspace-popover-title" }, "Add files as context"),
3007
+ React.createElement("div", { style: { flexGrow: 1 } }),
3008
+ React.createElement("button", { type: "button", className: 'mode-tools-popover-button mode-tools-popover-refresh-button' +
3009
+ (workspaceFilesLoading ? ' is-loading' : ''), title: "Refresh file list", "aria-label": "Refresh workspace file list", "aria-busy": workspaceFilesLoading, "aria-disabled": workspaceFilesLoading, onClick: () => {
3010
+ if (!workspaceFilesLoading) {
3011
+ refreshWorkspaceFiles();
3012
+ }
3013
+ } },
3014
+ React.createElement(VscRefresh, null)),
3015
+ React.createElement("button", { type: "button", className: "mode-tools-popover-button mode-tools-popover-close-button", title: "Close", "aria-label": "Close file picker", onClick: () => setShowWorkspaceFilePicker(false) },
3016
+ React.createElement(VscClose, null))),
3017
+ React.createElement("div", { className: "workspace-file-popover-body" },
3018
+ React.createElement("input", { className: "workspace-file-search-input", type: "text", placeholder: "Search files by path", value: workspaceFileSearch, onChange: event => setWorkspaceFileSearch(event.target.value), onKeyDown: (event) => {
3019
+ // Let Escape bubble to the popover's Escape handler so
3020
+ // the dialog closes even when focus is in the search
3021
+ // input (issue #262). Other keys still stop here so the
3022
+ // chat sidebar's keyboard shortcuts don't fire while
3023
+ // the user is typing a filter.
3024
+ if (event.key !== 'Escape') {
3025
+ event.stopPropagation();
3026
+ }
3027
+ } }),
3028
+ workspaceFilesError && (React.createElement("div", { className: "workspace-file-popover-status error" }, workspaceFilesError)),
3029
+ workspaceScanLimitReached && (React.createElement("div", { className: "workspace-file-popover-status" },
3030
+ "Showing the first ",
3031
+ MAX_WORKSPACE_FILE_SCAN_COUNT,
3032
+ " files found in the workspace.")),
3033
+ workspaceFilesLoading ? (React.createElement("div", { className: "workspace-file-popover-status" }, "Loading workspace files...")) : visibleWorkspaceFiles.length > 0 ? (React.createElement("div", { className: "mode-tools-popover-tool-list" }, visibleWorkspaceFiles.map(file => (React.createElement(CheckBoxItem, { key: file.path, checked: selectedContextFilePaths.has(file.path), disabled: workspaceFileActionPath === file.path, label: file.path, onClick: () => handleWorkspaceFileSelection(file), tooltip: file.type === 'notebook'
3034
+ ? 'Notebook file'
3035
+ : 'Text file' }))))) : (React.createElement("div", { className: "workspace-file-popover-status" }, workspaceFilesLoaded
3036
+ ? 'No matching files found.'
3037
+ : 'No workspace files available.'))))),
3038
+ showModeTools && (React.createElement("div", { ref: modeToolsPopoverRef, className: "mode-tools-popover", tabIndex: -1, role: "dialog", "aria-labelledby": "nbi-mode-tools-popover-title", onKeyDown: (event) => {
3039
+ if (event.key === 'Escape' || event.key === 'Enter') {
3040
+ event.stopPropagation();
3041
+ event.preventDefault();
3042
+ setShowModeTools(false);
3043
+ }
3044
+ } },
3045
+ React.createElement("div", { className: "mode-tools-popover-header" },
3046
+ React.createElement("div", { className: "mode-tools-popover-header-icon" },
3047
+ React.createElement(VscTools, null)),
3048
+ React.createElement("div", { className: "mode-tools-popover-title", id: "nbi-mode-tools-popover-title" }, toolSelectionTitle),
3049
+ React.createElement("div", { className: "mode-tools-popover-clear-tools-button", style: {
3050
+ visibility: selectedToolCount > 0 ? 'visible' : 'hidden'
3051
+ } },
3052
+ React.createElement("div", null,
3053
+ React.createElement(VscTrash, null)),
3054
+ React.createElement("div", null,
3055
+ React.createElement("button", { type: "button", className: "link-button", onClick: onClearToolsButtonClicked }, "clear"))),
3056
+ React.createElement("button", { type: "button", className: "mode-tools-popover-button mode-tools-popover-done-button", "aria-label": "Close tools picker", onClick: () => setShowModeTools(false) },
3057
+ React.createElement("div", null,
3058
+ React.createElement(VscPassFilled, null)),
3059
+ React.createElement("div", null, "Done"))),
3060
+ React.createElement("div", { className: "mode-tools-popover-tool-list" },
3061
+ React.createElement("div", { className: "mode-tools-group-header" }, "Built-in"),
3062
+ React.createElement("div", { className: "mode-tools-group mode-tools-group-built-in" }, toolConfigRef.current.builtinToolsets.map((toolset) => (React.createElement(CheckBoxItem, { key: toolset.id, label: toolset.name, checked: getBuiltinToolsetState(toolset.id), tooltip: toolset.description, header: true, onClick: () => {
3063
+ setBuiltinToolsetState(toolset.id, !getBuiltinToolsetState(toolset.id));
3064
+ } })))),
3065
+ renderCount > 0 &&
3066
+ mcpServerEnabledState.size > 0 &&
3067
+ toolConfigRef.current.mcpServers.length > 0 && (React.createElement("div", { className: "mode-tools-group-header" }, "MCP Server Tools")),
3068
+ renderCount > 0 &&
3069
+ toolConfigRef.current.mcpServers
3070
+ .filter(mcpServer => mcpServerEnabledState.has(mcpServer.id))
3071
+ .map((mcpServer, index) => (React.createElement("div", { className: "mode-tools-group" },
3072
+ React.createElement(CheckBoxItem, { label: mcpServer.id, header: true, checked: getMCPServerState(mcpServer.id), onClick: () => onMCPServerClicked(mcpServer.id) }),
3073
+ mcpServer.tools
3074
+ .filter((tool) => mcpServerEnabledState
3075
+ .get(mcpServer.id)
3076
+ .has(tool.name))
3077
+ .map((tool, index) => (React.createElement(CheckBoxItem, { label: tool.name, title: tool.description, indent: 1, checked: getMCPServerToolState(mcpServer.id, tool.name), onClick: () => setMCPServerToolState(mcpServer.id, tool.name, !getMCPServerToolState(mcpServer.id, tool.name)) })))))),
3078
+ hasExtensionTools && (React.createElement("div", { className: "mode-tools-group-header" }, "Extension tools")),
3079
+ toolConfigRef.current.extensions.map((extension, index) => (React.createElement("div", { className: "mode-tools-group" },
3080
+ React.createElement(CheckBoxItem, { label: `${extension.name} (${extension.id})`, header: true, checked: getExtensionState(extension.id), onClick: () => onExtensionClicked(extension.id) }),
3081
+ extension.toolsets.map((toolset, index) => (React.createElement(React.Fragment, null,
3082
+ React.createElement(CheckBoxItem, { label: `${toolset.name} (${toolset.id})`, title: toolset.description, indent: 1, checked: getExtensionToolsetState(extension.id, toolset.id), onClick: () => onExtensionToolsetClicked(extension.id, toolset.id) }),
3083
+ toolset.tools.map((tool, index) => (React.createElement(CheckBoxItem, { label: tool.name, title: tool.description, indent: 2, checked: getExtensionToolsetToolState(extension.id, toolset.id, tool.name), onClick: () => setExtensionToolsetToolState(extension.id, toolset.id, tool.name, !getExtensionToolsetToolState(extension.id, toolset.id, tool.name)) }))))))))))))))));
3084
+ }
3085
+ export function InlinePopoverComponent(props) {
3086
+ const [modifiedCode, setModifiedCode] = useState('');
3087
+ const [promptSubmitted, setPromptSubmitted] = useState(false);
3088
+ // Tracks the in-flight backend request so Escape / Cancel / Accept can
3089
+ // stop it via CancelChatRequest. Cleared on StreamEnd so a stale cancel
3090
+ // doesn't fire after the response already finished.
3091
+ const inflightMessageIdRef = useRef(null);
3092
+ // Mirrors InlinePromptWidget._streamError so this component can branch
3093
+ // on it independently. Set when the backend tags a delta with
3094
+ // nbi_stream_error; cleared on the next submit / StreamEnd.
3095
+ const streamErrorRef = useRef(null);
3096
+ const originalOnRequestSubmitted = props.onRequestSubmitted;
3097
+ const originalOnResponseEmit = props.onResponseEmit;
3098
+ const originalOnRequestCancelled = props.onRequestCancelled;
3099
+ const originalOnUpdatedCodeAccepted = props.onUpdatedCodeAccepted;
3100
+ const cancelInflightRequest = () => {
3101
+ const messageId = inflightMessageIdRef.current;
3102
+ if (!messageId) {
3103
+ return;
3104
+ }
3105
+ // Backend matches cancellations by websocket message id (see
3106
+ // WebsocketCopilotHandler.on_message). Without this send the request
3107
+ // keeps streaming server-side after the popover dismisses, burning
3108
+ // tokens and wasting an inference.
3109
+ NBIAPI.sendWebSocketMessage(messageId, RequestDataType.CancelChatRequest, {});
3110
+ inflightMessageIdRef.current = null;
3111
+ };
3112
+ // Hand the cancel function up to the host so non-React dismissal paths
3113
+ // (e.g. outside-click on the block widget) can cancel without invoking
3114
+ // onRequestCancelled — that path also restores editor focus, which is
3115
+ // wrong when the user is mid-click on a different target.
3116
+ useEffect(() => {
3117
+ var _a;
3118
+ (_a = props.registerCancel) === null || _a === void 0 ? void 0 : _a.call(props, cancelInflightRequest);
3119
+ return () => { var _a; return (_a = props.registerCancel) === null || _a === void 0 ? void 0 : _a.call(props, null); };
3120
+ }, []);
3121
+ const onRequestSubmitted = (prompt) => {
3122
+ setModifiedCode('');
3123
+ setPromptSubmitted(true);
3124
+ streamErrorRef.current = null;
3125
+ originalOnRequestSubmitted(prompt);
3126
+ };
3127
+ const onResponseEmit = (response) => {
3128
+ var _a, _b, _c, _d, _e, _f;
3129
+ if (response.type === BackendMessageType.StreamMessage) {
3130
+ if (typeof ((_a = response.data) === null || _a === void 0 ? void 0 : _a.nbi_stream_error) === 'string') {
3131
+ streamErrorRef.current = response.data.nbi_stream_error;
3132
+ }
3133
+ const delta = (_c = (_b = response.data['choices']) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c['delta'];
3134
+ if (!delta) {
3135
+ return;
3136
+ }
3137
+ const responseMessage = (_f = (_e = (_d = response.data['choices']) === null || _d === void 0 ? void 0 : _d[0]) === null || _e === void 0 ? void 0 : _e['delta']) === null || _f === void 0 ? void 0 : _f['content'];
3138
+ if (!responseMessage) {
3139
+ return;
3140
+ }
3141
+ setModifiedCode((modifiedCode) => modifiedCode + responseMessage);
3142
+ }
3143
+ else if (response.type === BackendMessageType.StreamEnd) {
3144
+ // Only fence-strip on a clean stream. On error the marker has to
3145
+ // stay visible in the diff pane so the modify-existing user has a
3146
+ // persistent failure signal before deciding to Accept the
3147
+ // truncated result.
3148
+ if (!streamErrorRef.current) {
3149
+ setModifiedCode((modifiedCode) => extractLLMGeneratedCode(modifiedCode));
3150
+ }
3151
+ // streamErrorRef intentionally outlives StreamEnd: Accept fires
3152
+ // afterwards and needs to know the stream errored so it can
3153
+ // dismiss instead of writing an empty buffer over the user's
3154
+ // selection. Cleared on the next submit (see onRequestSubmitted).
3155
+ inflightMessageIdRef.current = null;
3156
+ }
3157
+ originalOnResponseEmit(response);
3158
+ };
3159
+ const onRequestCancelled = () => {
3160
+ cancelInflightRequest();
3161
+ originalOnRequestCancelled();
3162
+ };
3163
+ // Accept on a partial diff used to leave the backend stream running
3164
+ // off-screen, spending tokens on output the UI no longer used. Cancel
3165
+ // the in-flight request before applying so a mid-stream Accept
3166
+ // releases the upstream call too. When the stream errored the buffer
3167
+ // is at best truncated and at worst marker-only, which would write an
3168
+ // empty string over the user's selection — extractLLMGeneratedCode
3169
+ // strips the marker and the remainder is just whitespace. Treat
3170
+ // Accept as cancel in that case so the selection survives.
3171
+ const onUpdatedCodeAccepted = () => {
3172
+ if (streamErrorRef.current) {
3173
+ onRequestCancelled();
3174
+ return;
3175
+ }
3176
+ cancelInflightRequest();
3177
+ originalOnUpdatedCodeAccepted();
3178
+ };
3179
+ return (React.createElement("div", { className: "inline-popover" },
3180
+ React.createElement(InlinePromptComponent, { ...props, onRequestSubmitted: onRequestSubmitted, onResponseEmit: onResponseEmit, onRequestCancelled: onRequestCancelled, onMessageIdChange: (id) => {
3181
+ inflightMessageIdRef.current = id;
3182
+ }, onUpdatedCodeAccepted: onUpdatedCodeAccepted, limitHeight: props.existingCode !== '' && promptSubmitted }),
3183
+ props.existingCode !== '' && promptSubmitted && (React.createElement(React.Fragment, null,
3184
+ React.createElement(InlineDiffViewerComponent, { ...props, modifiedCode: modifiedCode }),
3185
+ React.createElement("div", { className: "inline-popover-footer" },
3186
+ React.createElement("div", null,
3187
+ React.createElement("button", { className: "jp-Button jp-mod-accept jp-mod-styled jp-mod-small", onClick: () => onUpdatedCodeAccepted() }, "Accept")),
3188
+ React.createElement("div", null,
3189
+ React.createElement("button", { className: "jp-Button jp-mod-reject jp-mod-styled jp-mod-small", onClick: () => onRequestCancelled() }, "Cancel")))))));
3190
+ }
3191
+ function InlineDiffViewerComponent(props) {
3192
+ const editorContainerRef = useRef(null);
3193
+ const [diffEditor, setDiffEditor] = useState(null);
3194
+ useEffect(() => {
3195
+ const editorEl = editorContainerRef.current;
3196
+ editorEl.className = 'monaco-editor-container';
3197
+ const existingModel = monaco.editor.createModel(props.existingCode, 'text/plain');
3198
+ const modifiedModel = monaco.editor.createModel(props.modifiedCode, 'text/plain');
3199
+ const editor = monaco.editor.createDiffEditor(editorEl, {
3200
+ originalEditable: false,
3201
+ automaticLayout: true,
3202
+ theme: isDarkTheme() ? 'vs-dark' : 'vs'
3203
+ });
3204
+ editor.setModel({
3205
+ original: existingModel,
3206
+ modified: modifiedModel
3207
+ });
3208
+ modifiedModel.onDidChangeContent(() => {
3209
+ props.onUpdatedCodeChange(modifiedModel.getValue());
3210
+ });
3211
+ setDiffEditor(editor);
3212
+ }, []);
3213
+ useEffect(() => {
3214
+ var _a;
3215
+ (_a = diffEditor === null || diffEditor === void 0 ? void 0 : diffEditor.getModifiedEditor().getModel()) === null || _a === void 0 ? void 0 : _a.setValue(props.modifiedCode);
3216
+ }, [props.modifiedCode]);
3217
+ return (React.createElement("div", { ref: editorContainerRef, className: "monaco-editor-container" }));
3218
+ }
3219
+ function InlinePromptComponent(props) {
3220
+ const [prompt, setPrompt] = useState(props.prompt);
3221
+ const promptInputRef = useRef(null);
3222
+ const [inputSubmitted, setInputSubmitted] = useState(false);
3223
+ const onPromptChange = (event) => {
3224
+ const newPrompt = event.target.value;
3225
+ setPrompt(newPrompt);
3226
+ };
3227
+ const handleUserInputSubmit = async () => {
3228
+ var _a;
3229
+ const promptPrefixParts = [];
3230
+ const promptParts = prompt.split(' ');
3231
+ if (promptParts.length > 1) {
3232
+ for (let i = 0; i < Math.min(promptParts.length, 2); i++) {
3233
+ const part = promptParts[i];
3234
+ if (part.startsWith('@') || part.startsWith('/')) {
3235
+ promptPrefixParts.push(part);
3236
+ }
3237
+ }
3238
+ }
3239
+ const messageId = UUID.uuid4();
3240
+ // Hand the id back to the popover so its cancel handler can send a
3241
+ // CancelChatRequest with the matching id.
3242
+ (_a = props.onMessageIdChange) === null || _a === void 0 ? void 0 : _a.call(props, messageId);
3243
+ submitCompletionRequest({
3244
+ messageId,
3245
+ chatId: UUID.uuid4(),
3246
+ type: RunChatCompletionType.GenerateCode,
3247
+ content: prompt,
3248
+ language: props.language || 'python',
3249
+ filename: props.filename || '',
3250
+ prefix: props.prefix,
3251
+ suffix: props.suffix,
3252
+ existingCode: props.existingCode,
3253
+ chatMode: 'ask'
3254
+ }, {
3255
+ emit: async (response) => {
3256
+ props.onResponseEmit(response);
3257
+ }
3258
+ });
3259
+ setInputSubmitted(true);
3260
+ };
3261
+ const onPromptKeyDown = async (event) => {
3262
+ event.stopPropagation();
3263
+ if (event.key === 'Enter' && !event.shiftKey) {
3264
+ event.preventDefault();
3265
+ if (inputSubmitted && (event.metaKey || event.ctrlKey)) {
3266
+ props.onUpdatedCodeAccepted();
3267
+ }
3268
+ else {
3269
+ props.onRequestSubmitted(prompt);
3270
+ handleUserInputSubmit();
3271
+ }
3272
+ }
3273
+ else if (event.key === 'Escape') {
3274
+ event.preventDefault();
3275
+ props.onRequestCancelled();
3276
+ }
3277
+ };
3278
+ const focusPromptInput = () => {
3279
+ const input = promptInputRef.current;
3280
+ if (!input) {
3281
+ return;
3282
+ }
3283
+ input.focus({ preventScroll: true });
3284
+ input.select();
3285
+ };
3286
+ useEffect(() => {
3287
+ focusPromptInput();
3288
+ const animationFrame = requestAnimationFrame(focusPromptInput);
3289
+ const timeout = window.setTimeout(focusPromptInput, 0);
3290
+ return () => {
3291
+ cancelAnimationFrame(animationFrame);
3292
+ window.clearTimeout(timeout);
3293
+ };
3294
+ }, []);
3295
+ return (React.createElement("div", { className: "inline-prompt-container", style: { height: props.limitHeight ? '40px' : '100%' } },
3296
+ React.createElement("textarea", { ref: promptInputRef, rows: 3, onChange: onPromptChange, onClick: event => {
3297
+ event.stopPropagation();
3298
+ focusPromptInput();
3299
+ }, onKeyDown: onPromptKeyDown, onMouseDown: event => event.stopPropagation(), placeholder: "Ask Notebook Intelligence to generate Python code...", spellCheck: false, value: prompt })));
3300
+ }
3301
+ function GitHubCopilotStatusComponent(props) {
3302
+ const [ghLoginStatus, setGHLoginStatus] = useState(GitHubCopilotLoginStatus.NotLoggedIn);
3303
+ const [loginClickCount, _setLoginClickCount] = useState(0);
3304
+ useEffect(() => {
3305
+ const fetchData = () => {
3306
+ setGHLoginStatus(NBIAPI.getLoginStatus());
3307
+ };
3308
+ fetchData();
3309
+ const intervalId = setInterval(fetchData, 1000);
3310
+ return () => clearInterval(intervalId);
3311
+ }, [loginClickCount]);
3312
+ const onStatusClick = () => {
3313
+ props
3314
+ .getApp()
3315
+ .commands.execute('notebook-intelligence:open-github-copilot-login-dialog');
3316
+ };
3317
+ return (React.createElement("div", { title: `GitHub Copilot: ${ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn ? 'Logged in' : 'Not logged in'}`, className: "github-copilot-status-bar", onClick: () => onStatusClick(), dangerouslySetInnerHTML: {
3318
+ __html: ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn
3319
+ ? copilotSvgstr
3320
+ : copilotWarningSvgstr
3321
+ } }));
3322
+ }
3323
+ function GitHubCopilotLoginDialogBodyComponent(props) {
3324
+ const [ghLoginStatus, setGHLoginStatus] = useState(GitHubCopilotLoginStatus.NotLoggedIn);
3325
+ const [loginClickCount, setLoginClickCount] = useState(0);
3326
+ const [loginClicked, setLoginClicked] = useState(false);
3327
+ const [deviceActivationURL, setDeviceActivationURL] = useState('');
3328
+ const [deviceActivationCode, setDeviceActivationCode] = useState('');
3329
+ useEffect(() => {
3330
+ const fetchData = () => {
3331
+ const status = NBIAPI.getLoginStatus();
3332
+ setGHLoginStatus(status);
3333
+ if (status === GitHubCopilotLoginStatus.LoggedIn && loginClicked) {
3334
+ setTimeout(() => {
3335
+ props.onLoggedIn();
3336
+ }, 1000);
3337
+ }
3338
+ };
3339
+ fetchData();
3340
+ const intervalId = setInterval(fetchData, 1000);
3341
+ return () => clearInterval(intervalId);
3342
+ }, [loginClickCount]);
3343
+ const handleLoginClick = async () => {
3344
+ const response = await NBIAPI.loginToGitHub();
3345
+ setDeviceActivationURL(response.verificationURI);
3346
+ setDeviceActivationCode(response.userCode);
3347
+ setLoginClickCount(loginClickCount + 1);
3348
+ setLoginClicked(true);
3349
+ };
3350
+ const handleLogoutClick = async () => {
3351
+ await NBIAPI.logoutFromGitHub();
3352
+ setLoginClickCount(loginClickCount + 1);
3353
+ };
3354
+ const loggedIn = ghLoginStatus === GitHubCopilotLoginStatus.LoggedIn;
3355
+ return (React.createElement("div", { className: "github-copilot-login-dialog" },
3356
+ React.createElement("div", { className: "github-copilot-login-status" },
3357
+ React.createElement("h4", null,
3358
+ "Login status:",
3359
+ ' ',
3360
+ React.createElement("span", { className: `github-copilot-login-status-text ${loggedIn ? 'logged-in' : ''}` }, loggedIn
3361
+ ? 'Logged in'
3362
+ : ghLoginStatus === GitHubCopilotLoginStatus.LoggingIn
3363
+ ? 'Logging in...'
3364
+ : ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice
3365
+ ? 'Activating device...'
3366
+ : ghLoginStatus === GitHubCopilotLoginStatus.NotLoggedIn
3367
+ ? 'Not logged in'
3368
+ : 'Unknown'))),
3369
+ ghLoginStatus === GitHubCopilotLoginStatus.NotLoggedIn && (React.createElement(React.Fragment, null,
3370
+ React.createElement("div", null, "Your code and data are directly transferred to GitHub Copilot as needed without storing any copies other than keeping in the process memory."),
3371
+ React.createElement("div", null,
3372
+ React.createElement("a", { href: "https://github.com/features/copilot", target: "_blank", rel: "noopener noreferrer" },
3373
+ "GitHub Copilot",
3374
+ React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)")),
3375
+ ' ',
3376
+ "requires a subscription and it has a free tier. GitHub Copilot is subject to the",
3377
+ ' ',
3378
+ React.createElement("a", { href: "https://docs.github.com/en/site-policy/github-terms/github-terms-for-additional-products-and-features", target: "_blank", rel: "noopener noreferrer" },
3379
+ "GitHub Terms for Additional Products and Features",
3380
+ React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)")),
3381
+ "."),
3382
+ React.createElement("div", null,
3383
+ React.createElement("h4", null, "Privacy and terms"),
3384
+ "By using Notebook Intelligence with GitHub Copilot subscription you agree to",
3385
+ ' ',
3386
+ React.createElement("a", { href: "https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features/responsible-use-of-github-copilot-chat-in-your-ide", target: "_blank", rel: "noopener noreferrer" },
3387
+ "GitHub Copilot chat terms",
3388
+ React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)")),
3389
+ ". Review the terms to understand about usage, limitations and ways to improve GitHub Copilot. Please review",
3390
+ ' ',
3391
+ React.createElement("a", { href: "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement", target: "_blank", rel: "noopener noreferrer" },
3392
+ "Privacy Statement",
3393
+ React.createElement("span", { className: "nbi-sr-only" }, " (opens in new tab)")),
3394
+ "."),
3395
+ React.createElement("div", null,
3396
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-reject jp-mod-styled", onClick: handleLoginClick },
3397
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Login using your GitHub account"))))),
3398
+ loggedIn && (React.createElement("div", null,
3399
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleLogoutClick },
3400
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Logout")))),
3401
+ ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice &&
3402
+ deviceActivationURL &&
3403
+ deviceActivationCode && (React.createElement("div", null,
3404
+ React.createElement("div", { className: "copilot-activation-message" },
3405
+ "Copy code",
3406
+ ' ',
3407
+ React.createElement("span", { className: "user-code-span", onClick: () => {
3408
+ void writeTextToClipboard(deviceActivationCode);
3409
+ return true;
3410
+ } },
3411
+ React.createElement("b", null,
3412
+ deviceActivationCode,
3413
+ ' ',
3414
+ React.createElement("span", { className: "copy-icon", dangerouslySetInnerHTML: { __html: copySvgstr } }))),
3415
+ ' ',
3416
+ "and enter at",
3417
+ ' ',
3418
+ React.createElement("a", { href: deviceActivationURL, target: "_blank", rel: "noopener noreferrer" }, deviceActivationURL),
3419
+ ' ',
3420
+ "to allow access to GitHub Copilot from this app. Activation could take up to a minute after you enter the code."))),
3421
+ ghLoginStatus === GitHubCopilotLoginStatus.ActivatingDevice && (React.createElement("div", { style: { marginTop: '10px' } },
3422
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-reject jp-mod-styled", onClick: handleLogoutClick },
3423
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Cancel activation"))))));
3424
+ }
3425
+ export class FormInputDialogBody extends ReactWidget {
3426
+ constructor(options) {
3427
+ super();
3428
+ this._fields = options.fields || [];
3429
+ this._onDone = options.onDone || (() => { });
3430
+ }
3431
+ render() {
3432
+ return (React.createElement(FormInputDialogBodyComponent, { fields: this._fields, onDone: this._onDone }));
3433
+ }
3434
+ }
3435
+ function FormInputDialogBodyComponent(props) {
3436
+ const [formData, setFormData] = useState({});
3437
+ const handleInputChange = (event) => {
3438
+ setFormData({ ...formData, [event.target.name]: event.target.value });
3439
+ };
3440
+ return (React.createElement("div", { className: "form-input-dialog-body" },
3441
+ React.createElement("div", { className: "form-input-dialog-body-content" },
3442
+ React.createElement("div", { className: "form-input-dialog-body-content-title" }, props.title),
3443
+ React.createElement("div", { className: "form-input-dialog-body-content-fields" }, props.fields.map((field) => (React.createElement("div", { className: "form-input-dialog-body-content-field", key: field.name },
3444
+ React.createElement("label", { className: "form-input-dialog-body-content-field-label jp-mod-styled", htmlFor: field.name },
3445
+ field.name,
3446
+ field.required ? ' (required)' : ''),
3447
+ React.createElement("input", { className: "form-input-dialog-body-content-field-input jp-mod-styled", type: field.type, id: field.name, name: field.name, onChange: handleInputChange, value: formData[field.name] || '' }))))),
3448
+ React.createElement("div", null,
3449
+ React.createElement("div", { style: { marginTop: '10px' } },
3450
+ React.createElement("button", { className: "jp-Dialog-button jp-mod-accept jp-mod-styled", onClick: () => props.onDone(formData) },
3451
+ React.createElement("div", { className: "jp-Dialog-buttonLabel" }, "Done")))))));
3452
+ }