@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.
- package/LICENSE +674 -0
- package/README.md +412 -0
- package/lib/api.d.ts +288 -0
- package/lib/api.js +927 -0
- package/lib/cell-output-bundle.d.ts +25 -0
- package/lib/cell-output-bundle.js +129 -0
- package/lib/cell-output-toolbar.d.ts +26 -0
- package/lib/cell-output-toolbar.js +188 -0
- package/lib/chat-progress-feedback.d.ts +3 -0
- package/lib/chat-progress-feedback.js +27 -0
- package/lib/chat-sidebar.d.ts +92 -0
- package/lib/chat-sidebar.js +3452 -0
- package/lib/command-ids.d.ts +39 -0
- package/lib/command-ids.js +44 -0
- package/lib/components/ask-user-question.d.ts +2 -0
- package/lib/components/ask-user-question.js +85 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +30 -0
- package/lib/components/claude-mcp-panel.d.ts +2 -0
- package/lib/components/claude-mcp-panel.js +275 -0
- package/lib/components/claude-mcp-paste.d.ts +7 -0
- package/lib/components/claude-mcp-paste.js +104 -0
- package/lib/components/claude-session-picker.d.ts +8 -0
- package/lib/components/claude-session-picker.js +127 -0
- package/lib/components/form-dialog.d.ts +25 -0
- package/lib/components/form-dialog.js +35 -0
- package/lib/components/launcher-picker.d.ts +6 -0
- package/lib/components/launcher-picker.js +135 -0
- package/lib/components/mcp-util.d.ts +2 -0
- package/lib/components/mcp-util.js +37 -0
- package/lib/components/notebook-generation-popover.d.ts +7 -0
- package/lib/components/notebook-generation-popover.js +60 -0
- package/lib/components/pill.d.ts +2 -0
- package/lib/components/pill.js +5 -0
- package/lib/components/plugins-panel.d.ts +3 -0
- package/lib/components/plugins-panel.js +466 -0
- package/lib/components/settings-panel.d.ts +11 -0
- package/lib/components/settings-panel.js +742 -0
- package/lib/components/skills-panel.d.ts +2 -0
- package/lib/components/skills-panel.js +1264 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/icons.d.ts +45 -0
- package/lib/icons.js +54 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +2079 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +64 -0
- package/lib/notebook-generation-toolbar.d.ts +16 -0
- package/lib/notebook-generation-toolbar.js +197 -0
- package/lib/notebook-generation.d.ts +8 -0
- package/lib/notebook-generation.js +12 -0
- package/lib/open-file-refresh-watcher-env.d.ts +4 -0
- package/lib/open-file-refresh-watcher-env.js +33 -0
- package/lib/open-file-refresh-watcher.d.ts +97 -0
- package/lib/open-file-refresh-watcher.js +190 -0
- package/lib/shell-utils.d.ts +6 -0
- package/lib/shell-utils.js +9 -0
- package/lib/task-target-notebook.d.ts +2 -0
- package/lib/task-target-notebook.js +28 -0
- package/lib/terminal-drag-format.d.ts +9 -0
- package/lib/terminal-drag-format.js +23 -0
- package/lib/terminal-drag.d.ts +12 -0
- package/lib/terminal-drag.js +268 -0
- package/lib/tokens.d.ts +149 -0
- package/lib/tokens.js +88 -0
- package/lib/tour/tour-anchors.d.ts +18 -0
- package/lib/tour/tour-anchors.js +18 -0
- package/lib/tour/tour-config.d.ts +66 -0
- package/lib/tour/tour-config.js +99 -0
- package/lib/tour/tour-defaults.json +58 -0
- package/lib/tour/tour-events.d.ts +19 -0
- package/lib/tour/tour-events.js +30 -0
- package/lib/tour/tour-overlay.d.ts +6 -0
- package/lib/tour/tour-overlay.js +350 -0
- package/lib/tour/tour-state.d.ts +20 -0
- package/lib/tour/tour-state.js +81 -0
- package/lib/tour/tour-steps.d.ts +33 -0
- package/lib/tour/tour-steps.js +216 -0
- package/lib/utils.d.ts +53 -0
- package/lib/utils.js +385 -0
- package/package.json +258 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +1424 -0
- package/src/cell-output-bundle.ts +176 -0
- package/src/cell-output-toolbar.ts +232 -0
- package/src/chat-progress-feedback.ts +35 -0
- package/src/chat-sidebar.tsx +5147 -0
- package/src/command-ids.ts +67 -0
- package/src/components/ask-user-question.tsx +151 -0
- package/src/components/checkbox.tsx +62 -0
- package/src/components/claude-mcp-panel.tsx +543 -0
- package/src/components/claude-mcp-paste.ts +132 -0
- package/src/components/claude-session-picker.tsx +214 -0
- package/src/components/form-dialog.tsx +75 -0
- package/src/components/launcher-picker.tsx +237 -0
- package/src/components/mcp-util.ts +53 -0
- package/src/components/notebook-generation-popover.tsx +127 -0
- package/src/components/pill.tsx +15 -0
- package/src/components/plugins-panel.tsx +774 -0
- package/src/components/settings-panel.tsx +1631 -0
- package/src/components/skills-panel.tsx +2084 -0
- package/src/handler.ts +51 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +2583 -0
- package/src/markdown-renderer.tsx +153 -0
- package/src/notebook-generation-toolbar.tsx +281 -0
- package/src/notebook-generation.ts +23 -0
- package/src/open-file-refresh-watcher-env.ts +52 -0
- package/src/open-file-refresh-watcher.ts +260 -0
- package/src/shell-utils.ts +10 -0
- package/src/svg.d.ts +4 -0
- package/src/task-target-notebook.ts +37 -0
- package/src/terminal-drag-format.ts +29 -0
- package/src/terminal-drag.ts +382 -0
- package/src/tokens.ts +171 -0
- package/src/tour/tour-anchors.ts +21 -0
- package/src/tour/tour-config.ts +160 -0
- package/src/tour/tour-events.ts +34 -0
- package/src/tour/tour-overlay.tsx +474 -0
- package/src/tour/tour-state.ts +87 -0
- package/src/tour/tour-steps.ts +281 -0
- package/src/utils.ts +455 -0
- package/style/base.css +3238 -0
- package/style/icons/cell-toolbar-bug.svg +5 -0
- package/style/icons/cell-toolbar-chat.svg +5 -0
- package/style/icons/cell-toolbar-sparkle.svg +5 -0
- package/style/icons/claude.svg +1 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/openai.svg +1 -0
- package/style/icons/opencode.svg +1 -0
- package/style/icons/sparkles-warning.svg +5 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- 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
|
+
}
|