@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
package/lib/index.js
ADDED
|
@@ -0,0 +1,2079 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
import { IDocumentManager } from '@jupyterlab/docmanager';
|
|
3
|
+
import { Dialog, ICommandPalette, MainAreaWidget, Notification } from '@jupyterlab/apputils';
|
|
4
|
+
import { dispatchShowTour } from './tour/tour-events';
|
|
5
|
+
import { resetTour } from './tour/tour-state';
|
|
6
|
+
import { TOUR_DEFAULTS } from './tour/tour-steps';
|
|
7
|
+
import { commandLabel } from './tour/tour-config';
|
|
8
|
+
import { IMainMenu } from '@jupyterlab/mainmenu';
|
|
9
|
+
import { IEditorLanguageRegistry } from '@jupyterlab/codemirror';
|
|
10
|
+
import { StateEffect, StateField } from '@codemirror/state';
|
|
11
|
+
import { Decoration, EditorView, WidgetType } from '@codemirror/view';
|
|
12
|
+
import { CodeCell } from '@jupyterlab/cells';
|
|
13
|
+
import { ICompletionProviderManager } from '@jupyterlab/completer';
|
|
14
|
+
import { NotebookActions, NotebookPanel } from '@jupyterlab/notebook';
|
|
15
|
+
import { FileEditorWidget } from '@jupyterlab/fileeditor';
|
|
16
|
+
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
17
|
+
import { ContentsManager, KernelSpecManager } from '@jupyterlab/services';
|
|
18
|
+
import { LabIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
19
|
+
import { Menu, Panel } from '@lumino/widgets';
|
|
20
|
+
import { CommandRegistry } from '@lumino/commands';
|
|
21
|
+
import { IStatusBar } from '@jupyterlab/statusbar';
|
|
22
|
+
import { ILauncher } from '@jupyterlab/launcher';
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { ReactWidget } from '@jupyterlab/apputils';
|
|
25
|
+
import { LauncherPicker } from './components/launcher-picker';
|
|
26
|
+
import stripAnsi from 'strip-ansi';
|
|
27
|
+
import { ChatSidebar, FormInputDialogBody, GitHubCopilotLoginDialogBody, InlinePopoverComponent, GitHubCopilotStatusBarItem, RunChatCompletionType } from './chat-sidebar';
|
|
28
|
+
import { NBIAPI, GitHubCopilotLoginStatus } from './api';
|
|
29
|
+
import { CellOutputHoverToolbar } from './cell-output-toolbar';
|
|
30
|
+
import { attachOpenFileRefreshWatcher } from './open-file-refresh-watcher';
|
|
31
|
+
import { buildRefreshWatcherEnv } from './open-file-refresh-watcher-env';
|
|
32
|
+
import { BackendMessageType, GITHUB_COPILOT_PROVIDER_ID, INotebookIntelligence, RequestDataType, TelemetryEventType } from './tokens';
|
|
33
|
+
import sparklesSvgstr from '../style/icons/sparkles.svg';
|
|
34
|
+
import copilotSvgstr from '../style/icons/copilot.svg';
|
|
35
|
+
import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg';
|
|
36
|
+
import claudeSvgstr from '../style/icons/claude.svg';
|
|
37
|
+
import openaiSvgstr from '../style/icons/openai.svg';
|
|
38
|
+
import opencodeSvgstr from '../style/icons/opencode.svg';
|
|
39
|
+
import { applyCodeToSelectionInEditor, cellOutputAsText, cellOutputHasError, chooseWorkspaceDirectory, compareSelections, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
|
|
40
|
+
import { cellOutputAsContextBundle } from './cell-output-bundle';
|
|
41
|
+
import { UUID } from '@lumino/coreutils';
|
|
42
|
+
import * as path from 'path';
|
|
43
|
+
import { createRoot } from 'react-dom/client';
|
|
44
|
+
import { SettingsPanel } from './components/settings-panel';
|
|
45
|
+
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
46
|
+
import { NotebookGenerationToolbarExtension } from './notebook-generation-toolbar';
|
|
47
|
+
import { attachTerminalDragDrop } from './terminal-drag';
|
|
48
|
+
import { CommandIDs } from './command-ids';
|
|
49
|
+
const addInlinePromptEffect = StateEffect.define();
|
|
50
|
+
const removeInlinePromptEffect = StateEffect.define();
|
|
51
|
+
const inlinePromptField = StateField.define({
|
|
52
|
+
create() {
|
|
53
|
+
return Decoration.none;
|
|
54
|
+
},
|
|
55
|
+
update(decorations, transaction) {
|
|
56
|
+
decorations = decorations.map(transaction.changes);
|
|
57
|
+
for (const effect of transaction.effects) {
|
|
58
|
+
if (effect.is(removeInlinePromptEffect)) {
|
|
59
|
+
decorations = Decoration.none;
|
|
60
|
+
}
|
|
61
|
+
else if (effect.is(addInlinePromptEffect)) {
|
|
62
|
+
decorations = Decoration.set([
|
|
63
|
+
Decoration.widget({
|
|
64
|
+
block: true,
|
|
65
|
+
side: 1,
|
|
66
|
+
widget: effect.value.widget
|
|
67
|
+
}).range(effect.value.pos)
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return decorations;
|
|
72
|
+
},
|
|
73
|
+
provide: field => EditorView.decorations.from(field)
|
|
74
|
+
});
|
|
75
|
+
class InlinePromptBlockWidget extends WidgetType {
|
|
76
|
+
constructor(_content, _onNodeCreated, _onDismissRequested) {
|
|
77
|
+
super();
|
|
78
|
+
this._content = _content;
|
|
79
|
+
this._onNodeCreated = _onNodeCreated;
|
|
80
|
+
this._onDismissRequested = _onDismissRequested;
|
|
81
|
+
this._root = null;
|
|
82
|
+
this._focusinGuard = null;
|
|
83
|
+
}
|
|
84
|
+
eq(other) {
|
|
85
|
+
return other === this;
|
|
86
|
+
}
|
|
87
|
+
toDOM() {
|
|
88
|
+
const host = document.createElement('div');
|
|
89
|
+
host.className = 'nbi-inline-prompt-block';
|
|
90
|
+
host.contentEditable = 'false';
|
|
91
|
+
const node = document.createElement('div');
|
|
92
|
+
node.className = 'inline-prompt-widget inline-prompt-widget-inline';
|
|
93
|
+
node.style.height = '48px';
|
|
94
|
+
host.appendChild(node);
|
|
95
|
+
// Bubble-phase stopPropagation so JupyterLab's document-level
|
|
96
|
+
// keybindings (Ctrl+S, etc.) don't intercept keys typed in the
|
|
97
|
+
// textarea. CM's own handling is already opted out via ignoreEvent;
|
|
98
|
+
// this is purely about JL keymap.
|
|
99
|
+
const stopEditorEvent = (event) => event.stopPropagation();
|
|
100
|
+
for (const eventName of [
|
|
101
|
+
'keydown',
|
|
102
|
+
'keypress',
|
|
103
|
+
'keyup',
|
|
104
|
+
'mousedown',
|
|
105
|
+
'mouseup',
|
|
106
|
+
'click',
|
|
107
|
+
'pointerdown',
|
|
108
|
+
'pointerup',
|
|
109
|
+
'input',
|
|
110
|
+
'beforeinput'
|
|
111
|
+
]) {
|
|
112
|
+
host.addEventListener(eventName, stopEditorEvent);
|
|
113
|
+
}
|
|
114
|
+
// While the popover is mounted, suppress JupyterLab's notebook-panel
|
|
115
|
+
// _evtFocusIn handler for focus events inside the owning cell. Without
|
|
116
|
+
// this, JL's _ensureFocus reacts to every focusin on the textarea by
|
|
117
|
+
// refocusing .cm-content, which then triggers our own refocus, which
|
|
118
|
+
// re-fires JL — an infinite loop until the stack overflows.
|
|
119
|
+
//
|
|
120
|
+
// Capture-phase listener on document fires before JL's bubble-phase
|
|
121
|
+
// handler on the notebook panel; stopPropagation here is sufficient.
|
|
122
|
+
// The owning .cm-content / .jp-Cell are resolved inside the handler
|
|
123
|
+
// because at toDOM() time the host is created but not yet inserted.
|
|
124
|
+
this._focusinGuard = (event) => {
|
|
125
|
+
if (!host.isConnected) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ownCmContent = host.closest('.cm-content');
|
|
129
|
+
if (!ownCmContent) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const ownCell = ownCmContent.closest('.jp-Cell');
|
|
133
|
+
const target = event.target;
|
|
134
|
+
if (!ownCell || !target || !ownCell.contains(target)) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
event.stopPropagation();
|
|
138
|
+
if (target === ownCmContent) {
|
|
139
|
+
const ta = node.querySelector('textarea');
|
|
140
|
+
if (ta && document.activeElement !== ta) {
|
|
141
|
+
ta.focus({ preventScroll: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
document.addEventListener('focusin', this._focusinGuard, true);
|
|
146
|
+
// Outside-click / focus-leave dismissal. The previous floating-widget
|
|
147
|
+
// path relied on InlinePromptWidget's own focusout listener; the
|
|
148
|
+
// block-widget path mounts the React tree directly so we replicate
|
|
149
|
+
// it here. Focus moving within the popover (textarea -> Accept /
|
|
150
|
+
// Cancel) is ignored.
|
|
151
|
+
host.addEventListener('focusout', (event) => {
|
|
152
|
+
const next = event.relatedTarget;
|
|
153
|
+
if (next && host.contains(next)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
this._onDismissRequested();
|
|
157
|
+
});
|
|
158
|
+
this._root = createRoot(node);
|
|
159
|
+
this._root.render(this._content);
|
|
160
|
+
this._onNodeCreated(node);
|
|
161
|
+
return host;
|
|
162
|
+
}
|
|
163
|
+
ignoreEvent() {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
destroy() {
|
|
167
|
+
var _a;
|
|
168
|
+
if (this._focusinGuard) {
|
|
169
|
+
document.removeEventListener('focusin', this._focusinGuard, true);
|
|
170
|
+
this._focusinGuard = null;
|
|
171
|
+
}
|
|
172
|
+
(_a = this._root) === null || _a === void 0 ? void 0 : _a.unmount();
|
|
173
|
+
this._root = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function getCodeMirrorView(editor) {
|
|
177
|
+
var _a;
|
|
178
|
+
const codeMirrorEditor = editor;
|
|
179
|
+
return (_a = codeMirrorEditor.editor) !== null && _a !== void 0 ? _a : null;
|
|
180
|
+
}
|
|
181
|
+
function ensureInlinePromptExtension(view) {
|
|
182
|
+
if (view.state.field(inlinePromptField, false)) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
view.dispatch({
|
|
186
|
+
effects: StateEffect.appendConfig.of([inlinePromptField])
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function getLineEndOffset(editor, offset) {
|
|
190
|
+
const position = editor.getPositionAt(offset);
|
|
191
|
+
return editor.getOffsetAt({
|
|
192
|
+
line: position.line,
|
|
193
|
+
column: editor.getLine(position.line).length
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
const DOCUMENT_WATCH_INTERVAL = 1000;
|
|
197
|
+
const MAX_TOKENS = 4096;
|
|
198
|
+
const githubCopilotIcon = new LabIcon({
|
|
199
|
+
name: 'notebook-intelligence:github-copilot-icon',
|
|
200
|
+
svgstr: copilotSvgstr
|
|
201
|
+
});
|
|
202
|
+
const sparkleIcon = new LabIcon({
|
|
203
|
+
name: 'notebook-intelligence:sparkles-icon',
|
|
204
|
+
svgstr: sparklesSvgstr
|
|
205
|
+
});
|
|
206
|
+
const claudeIcon = new LabIcon({
|
|
207
|
+
name: 'notebook-intelligence:claude-icon',
|
|
208
|
+
svgstr: claudeSvgstr
|
|
209
|
+
});
|
|
210
|
+
const openaiIcon = new LabIcon({
|
|
211
|
+
name: 'notebook-intelligence:openai-icon',
|
|
212
|
+
svgstr: openaiSvgstr
|
|
213
|
+
});
|
|
214
|
+
const opencodeIcon = new LabIcon({
|
|
215
|
+
name: 'notebook-intelligence:opencode-icon',
|
|
216
|
+
svgstr: opencodeSvgstr
|
|
217
|
+
});
|
|
218
|
+
const sparkleWarningIcon = new LabIcon({
|
|
219
|
+
name: 'notebook-intelligence:sparkles-warning-icon',
|
|
220
|
+
svgstr: sparklesWarningSvgstr
|
|
221
|
+
});
|
|
222
|
+
const emptyNotebookContent = {
|
|
223
|
+
cells: [],
|
|
224
|
+
metadata: {},
|
|
225
|
+
nbformat: 4,
|
|
226
|
+
nbformat_minor: 5
|
|
227
|
+
};
|
|
228
|
+
const BACKEND_TELEMETRY_LISTENER_NAME = 'backend-telemetry-listener';
|
|
229
|
+
class ActiveDocumentWatcher {
|
|
230
|
+
static initialize(app, languageRegistry, fileBrowser) {
|
|
231
|
+
var _a;
|
|
232
|
+
ActiveDocumentWatcher._languageRegistry = languageRegistry;
|
|
233
|
+
(_a = app.shell.currentChanged) === null || _a === void 0 ? void 0 : _a.connect((_sender, args) => {
|
|
234
|
+
ActiveDocumentWatcher.watchDocument(args.newValue);
|
|
235
|
+
});
|
|
236
|
+
ActiveDocumentWatcher.activeDocumentInfo.activeWidget =
|
|
237
|
+
app.shell.currentWidget;
|
|
238
|
+
ActiveDocumentWatcher.handleWatchDocument();
|
|
239
|
+
if (fileBrowser) {
|
|
240
|
+
const onPathChanged = (model) => {
|
|
241
|
+
ActiveDocumentWatcher.currentDirectory = model.path;
|
|
242
|
+
};
|
|
243
|
+
fileBrowser.model.pathChanged.connect(onPathChanged);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
static watchDocument(widget) {
|
|
247
|
+
if (ActiveDocumentWatcher.activeDocumentInfo.activeWidget === widget) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
clearInterval(ActiveDocumentWatcher._watchTimer);
|
|
251
|
+
ActiveDocumentWatcher.activeDocumentInfo.activeWidget = widget;
|
|
252
|
+
ActiveDocumentWatcher._watchTimer = setInterval(() => {
|
|
253
|
+
ActiveDocumentWatcher.handleWatchDocument();
|
|
254
|
+
}, DOCUMENT_WATCH_INTERVAL);
|
|
255
|
+
ActiveDocumentWatcher.handleWatchDocument();
|
|
256
|
+
}
|
|
257
|
+
static handleWatchDocument() {
|
|
258
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
259
|
+
const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo;
|
|
260
|
+
const previousDocumentInfo = {
|
|
261
|
+
...activeDocumentInfo,
|
|
262
|
+
...{ activeWidget: null }
|
|
263
|
+
};
|
|
264
|
+
const activeWidget = activeDocumentInfo.activeWidget;
|
|
265
|
+
if (activeWidget instanceof NotebookPanel) {
|
|
266
|
+
const np = activeWidget;
|
|
267
|
+
activeDocumentInfo.filename = np.sessionContext.name;
|
|
268
|
+
activeDocumentInfo.filePath = np.sessionContext.path;
|
|
269
|
+
activeDocumentInfo.language =
|
|
270
|
+
((_d = (_c = (_b = (_a = np.model) === null || _a === void 0 ? void 0 : _a.sharedModel) === null || _b === void 0 ? void 0 : _b.metadata) === null || _c === void 0 ? void 0 : _c.kernelspec) === null || _d === void 0 ? void 0 : _d.language) ||
|
|
271
|
+
'python';
|
|
272
|
+
const { activeCellIndex, activeCell } = np.content;
|
|
273
|
+
activeDocumentInfo.activeCellIndex = activeCellIndex;
|
|
274
|
+
activeDocumentInfo.selection = (_e = activeCell === null || activeCell === void 0 ? void 0 : activeCell.editor) === null || _e === void 0 ? void 0 : _e.getSelection();
|
|
275
|
+
}
|
|
276
|
+
else if (activeWidget) {
|
|
277
|
+
const dw = activeWidget;
|
|
278
|
+
const contentsModel = (_f = dw.context) === null || _f === void 0 ? void 0 : _f.contentsModel;
|
|
279
|
+
if ((contentsModel === null || contentsModel === void 0 ? void 0 : contentsModel.format) === 'text') {
|
|
280
|
+
const fileName = contentsModel.name;
|
|
281
|
+
const filePath = contentsModel.path;
|
|
282
|
+
const language = ActiveDocumentWatcher._languageRegistry.findByMIME(contentsModel.mimetype) || ActiveDocumentWatcher._languageRegistry.findByFileName(fileName);
|
|
283
|
+
activeDocumentInfo.language = (language === null || language === void 0 ? void 0 : language.name) || 'unknown';
|
|
284
|
+
activeDocumentInfo.filename = fileName;
|
|
285
|
+
activeDocumentInfo.filePath = filePath;
|
|
286
|
+
if (activeWidget instanceof FileEditorWidget) {
|
|
287
|
+
const fe = activeWidget;
|
|
288
|
+
activeDocumentInfo.selection = (_g = fe.content.editor) === null || _g === void 0 ? void 0 : _g.getSelection();
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
activeDocumentInfo.selection = undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
activeDocumentInfo.filename = '';
|
|
296
|
+
activeDocumentInfo.filePath = '';
|
|
297
|
+
activeDocumentInfo.language = '';
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (ActiveDocumentWatcher.documentInfoChanged(previousDocumentInfo, activeDocumentInfo)) {
|
|
301
|
+
ActiveDocumentWatcher.fireActiveDocumentChangedEvent();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
static documentInfoChanged(lhs, rhs) {
|
|
305
|
+
if (!lhs || !rhs) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
return (lhs.filename !== rhs.filename ||
|
|
309
|
+
lhs.filePath !== rhs.filePath ||
|
|
310
|
+
lhs.language !== rhs.language ||
|
|
311
|
+
lhs.activeCellIndex !== rhs.activeCellIndex ||
|
|
312
|
+
!compareSelections(lhs.selection, rhs.selection));
|
|
313
|
+
}
|
|
314
|
+
static getActiveSelectionContent() {
|
|
315
|
+
var _a, _b;
|
|
316
|
+
const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo;
|
|
317
|
+
const activeWidget = activeDocumentInfo.activeWidget;
|
|
318
|
+
if (activeWidget instanceof NotebookPanel) {
|
|
319
|
+
const np = activeWidget;
|
|
320
|
+
const editor = np.content.activeCell.editor;
|
|
321
|
+
if (isSelectionEmpty(editor.getSelection())) {
|
|
322
|
+
return getWholeNotebookContent(np);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
return getSelectionInEditor(editor);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (activeWidget instanceof FileEditorWidget) {
|
|
329
|
+
const fe = activeWidget;
|
|
330
|
+
const editor = fe.content.editor;
|
|
331
|
+
if (isSelectionEmpty(editor.getSelection())) {
|
|
332
|
+
return editor.model.sharedModel.getSource();
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
return getSelectionInEditor(editor);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
const dw = activeWidget;
|
|
340
|
+
const content = (_b = (_a = dw === null || dw === void 0 ? void 0 : dw.context) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.toString();
|
|
341
|
+
const maxContext = 0.5 * MAX_TOKENS;
|
|
342
|
+
return content.substring(0, maxContext);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
static getCurrentCellContents() {
|
|
346
|
+
const activeDocumentInfo = ActiveDocumentWatcher.activeDocumentInfo;
|
|
347
|
+
const activeWidget = activeDocumentInfo.activeWidget;
|
|
348
|
+
if (activeWidget instanceof NotebookPanel) {
|
|
349
|
+
const np = activeWidget;
|
|
350
|
+
const activeCell = np.content.activeCell;
|
|
351
|
+
const input = activeCell.model.sharedModel.source.trim();
|
|
352
|
+
let output = '';
|
|
353
|
+
if (activeCell instanceof CodeCell) {
|
|
354
|
+
output = cellOutputAsText(np.content.activeCell);
|
|
355
|
+
}
|
|
356
|
+
return { input, output };
|
|
357
|
+
}
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
static fireActiveDocumentChangedEvent() {
|
|
361
|
+
document.dispatchEvent(new CustomEvent('copilotSidebar:activeDocumentChanged', {
|
|
362
|
+
detail: {
|
|
363
|
+
activeDocumentInfo: ActiveDocumentWatcher.activeDocumentInfo
|
|
364
|
+
}
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
ActiveDocumentWatcher.currentDirectory = '';
|
|
369
|
+
ActiveDocumentWatcher.activeDocumentInfo = {
|
|
370
|
+
language: 'python',
|
|
371
|
+
filename: 'nb-doesnt-exist.ipynb',
|
|
372
|
+
filePath: 'nb-doesnt-exist.ipynb',
|
|
373
|
+
activeWidget: null,
|
|
374
|
+
activeCellIndex: -1,
|
|
375
|
+
selection: null
|
|
376
|
+
};
|
|
377
|
+
class NBIInlineCompletionProvider {
|
|
378
|
+
constructor(telemetryEmitter) {
|
|
379
|
+
this._lastRequestInfo = null;
|
|
380
|
+
this._telemetryEmitter = telemetryEmitter;
|
|
381
|
+
}
|
|
382
|
+
get schema() {
|
|
383
|
+
return {
|
|
384
|
+
default: {
|
|
385
|
+
debouncerDelay: NBIAPI.config.inlineCompletionDebouncerDelay,
|
|
386
|
+
timeout: 15000
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
fetch(request, context) {
|
|
391
|
+
let preContent = '';
|
|
392
|
+
let postContent = '';
|
|
393
|
+
const preCursor = request.text.substring(0, request.offset);
|
|
394
|
+
const postCursor = request.text.substring(request.offset);
|
|
395
|
+
let language = ActiveDocumentWatcher.activeDocumentInfo.language;
|
|
396
|
+
let editorType = 'file-editor';
|
|
397
|
+
if (context.widget instanceof NotebookPanel) {
|
|
398
|
+
editorType = 'notebook';
|
|
399
|
+
const activeCell = context.widget.content.activeCell;
|
|
400
|
+
if (activeCell.model.sharedModel.cell_type === 'markdown') {
|
|
401
|
+
language = 'markdown';
|
|
402
|
+
}
|
|
403
|
+
let activeCellReached = false;
|
|
404
|
+
for (const cell of context.widget.content.widgets) {
|
|
405
|
+
const cellModel = cell.model.sharedModel;
|
|
406
|
+
if (cell === activeCell) {
|
|
407
|
+
activeCellReached = true;
|
|
408
|
+
}
|
|
409
|
+
else if (!activeCellReached) {
|
|
410
|
+
if (cellModel.cell_type === 'code') {
|
|
411
|
+
preContent += cellModel.source + '\n';
|
|
412
|
+
}
|
|
413
|
+
else if (cellModel.cell_type === 'markdown') {
|
|
414
|
+
preContent += markdownToComment(cellModel.source) + '\n';
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
if (cellModel.cell_type === 'code') {
|
|
419
|
+
postContent += cellModel.source + '\n';
|
|
420
|
+
}
|
|
421
|
+
else if (cellModel.cell_type === 'markdown') {
|
|
422
|
+
postContent += markdownToComment(cellModel.source) + '\n';
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const nbiConfig = NBIAPI.config;
|
|
428
|
+
const inlineCompletionsEnabled = nbiConfig.isInClaudeCodeMode ||
|
|
429
|
+
(nbiConfig.inlineCompletionModel.provider === GITHUB_COPILOT_PROVIDER_ID
|
|
430
|
+
? NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.LoggedIn
|
|
431
|
+
: nbiConfig.inlineCompletionModel.provider !== 'none');
|
|
432
|
+
this._telemetryEmitter.emitTelemetryEvent({
|
|
433
|
+
type: TelemetryEventType.InlineCompletionRequest,
|
|
434
|
+
data: {
|
|
435
|
+
inlineCompletionModel: {
|
|
436
|
+
provider: NBIAPI.config.inlineCompletionModel.provider,
|
|
437
|
+
model: NBIAPI.config.inlineCompletionModel.model
|
|
438
|
+
},
|
|
439
|
+
editorType
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
return new Promise((resolve, reject) => {
|
|
443
|
+
const items = [];
|
|
444
|
+
if (!inlineCompletionsEnabled) {
|
|
445
|
+
resolve({ items });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (this._lastRequestInfo) {
|
|
449
|
+
NBIAPI.sendWebSocketMessage(this._lastRequestInfo.messageId, RequestDataType.CancelInlineCompletionRequest, { chatId: this._lastRequestInfo.chatId });
|
|
450
|
+
}
|
|
451
|
+
const messageId = UUID.uuid4();
|
|
452
|
+
const chatId = UUID.uuid4();
|
|
453
|
+
this._lastRequestInfo = { chatId, messageId, requestTime: new Date() };
|
|
454
|
+
NBIAPI.inlineCompletionsRequest(chatId, messageId, preContent + preCursor, postCursor + postContent, language, ActiveDocumentWatcher.activeDocumentInfo.filename, {
|
|
455
|
+
emit: (response) => {
|
|
456
|
+
if (response.type === BackendMessageType.StreamMessage &&
|
|
457
|
+
response.id === this._lastRequestInfo.messageId) {
|
|
458
|
+
items.push({
|
|
459
|
+
insertText: response.data.completions
|
|
460
|
+
});
|
|
461
|
+
const timeElapsed = (new Date().getTime() -
|
|
462
|
+
this._lastRequestInfo.requestTime.getTime()) /
|
|
463
|
+
1000;
|
|
464
|
+
this._telemetryEmitter.emitTelemetryEvent({
|
|
465
|
+
type: TelemetryEventType.InlineCompletionResponse,
|
|
466
|
+
data: {
|
|
467
|
+
inlineCompletionModel: {
|
|
468
|
+
provider: NBIAPI.config.inlineCompletionModel.provider,
|
|
469
|
+
model: NBIAPI.config.inlineCompletionModel.model
|
|
470
|
+
},
|
|
471
|
+
timeElapsed
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
resolve({ items });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
reject();
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
get name() {
|
|
484
|
+
return 'Notebook Intelligence';
|
|
485
|
+
}
|
|
486
|
+
get identifier() {
|
|
487
|
+
return '@plmbr/notebook-intelligence';
|
|
488
|
+
}
|
|
489
|
+
get icon() {
|
|
490
|
+
const isClaudeModel = NBIAPI.config.isInClaudeCodeMode &&
|
|
491
|
+
NBIAPI.config.claudeSettings.inline_completion_model !== 'none' &&
|
|
492
|
+
NBIAPI.config.claudeSettings.inline_completion_model !== 'inherit';
|
|
493
|
+
return isClaudeModel
|
|
494
|
+
? claudeIcon
|
|
495
|
+
: NBIAPI.config.usingGitHubCopilotModel
|
|
496
|
+
? githubCopilotIcon
|
|
497
|
+
: sparkleIcon;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
class TelemetryEmitter {
|
|
501
|
+
constructor() {
|
|
502
|
+
this._listeners = new Set();
|
|
503
|
+
}
|
|
504
|
+
registerTelemetryListener(listener) {
|
|
505
|
+
const listenerName = listener.name;
|
|
506
|
+
if (listenerName !== BACKEND_TELEMETRY_LISTENER_NAME) {
|
|
507
|
+
console.warn(`Notebook Intelligence telemetry listener '${listenerName}' registered. Make sure it is from a trusted source.`);
|
|
508
|
+
}
|
|
509
|
+
let listenerAlreadyExists = false;
|
|
510
|
+
this._listeners.forEach(existingListener => {
|
|
511
|
+
if (existingListener.name === listenerName) {
|
|
512
|
+
listenerAlreadyExists = true;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
if (listenerAlreadyExists) {
|
|
516
|
+
console.error(`Notebook Intelligence telemetry listener '${listenerName}' already exists!`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
this._listeners.add(listener);
|
|
520
|
+
}
|
|
521
|
+
unregisterTelemetryListener(listener) {
|
|
522
|
+
this._listeners.delete(listener);
|
|
523
|
+
}
|
|
524
|
+
emitTelemetryEvent(event) {
|
|
525
|
+
this._listeners.forEach(listener => {
|
|
526
|
+
listener.onTelemetryEvent(event);
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
class MCPConfigEditor {
|
|
531
|
+
constructor(docManager) {
|
|
532
|
+
this._docWidget = null;
|
|
533
|
+
this._tmpMCPConfigFilename = 'nbi.mcp.temp.json';
|
|
534
|
+
this._isOpen = false;
|
|
535
|
+
this._docManager = docManager;
|
|
536
|
+
}
|
|
537
|
+
async open() {
|
|
538
|
+
const contents = new ContentsManager();
|
|
539
|
+
const newJSONFile = await contents.newUntitled({
|
|
540
|
+
ext: '.json'
|
|
541
|
+
});
|
|
542
|
+
const mcpConfig = await NBIAPI.getMCPConfigFile();
|
|
543
|
+
try {
|
|
544
|
+
await contents.delete(this._tmpMCPConfigFilename);
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
// ignore
|
|
548
|
+
}
|
|
549
|
+
await contents.save(newJSONFile.path, {
|
|
550
|
+
content: JSON.stringify(mcpConfig, null, 2),
|
|
551
|
+
format: 'text',
|
|
552
|
+
type: 'file'
|
|
553
|
+
});
|
|
554
|
+
await contents.rename(newJSONFile.path, this._tmpMCPConfigFilename);
|
|
555
|
+
this._docWidget = this._docManager.openOrReveal(this._tmpMCPConfigFilename, 'Editor');
|
|
556
|
+
this._addListeners();
|
|
557
|
+
// tab closed
|
|
558
|
+
this._docWidget.disposed.connect((_, args) => {
|
|
559
|
+
this._removeListeners();
|
|
560
|
+
contents.delete(this._tmpMCPConfigFilename);
|
|
561
|
+
});
|
|
562
|
+
this._isOpen = true;
|
|
563
|
+
}
|
|
564
|
+
close() {
|
|
565
|
+
if (!this._isOpen) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
this._isOpen = false;
|
|
569
|
+
this._docWidget.dispose();
|
|
570
|
+
this._docWidget = null;
|
|
571
|
+
}
|
|
572
|
+
get isOpen() {
|
|
573
|
+
return this._isOpen;
|
|
574
|
+
}
|
|
575
|
+
_addListeners() {
|
|
576
|
+
this._docWidget.context.model.stateChanged.connect(this._onStateChanged, this);
|
|
577
|
+
}
|
|
578
|
+
_removeListeners() {
|
|
579
|
+
this._docWidget.context.model.stateChanged.disconnect(this._onStateChanged, this);
|
|
580
|
+
}
|
|
581
|
+
_onStateChanged(model, args) {
|
|
582
|
+
if (args.name === 'dirty' && args.newValue === false) {
|
|
583
|
+
this._onSave();
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async _onSave() {
|
|
587
|
+
var _a;
|
|
588
|
+
const mcpConfig = this._docWidget.context.model.toJSON();
|
|
589
|
+
try {
|
|
590
|
+
await NBIAPI.setMCPConfigFile(mcpConfig);
|
|
591
|
+
}
|
|
592
|
+
catch (reason) {
|
|
593
|
+
// Surface server-side validation rejections (400 from the
|
|
594
|
+
// shape validator, 500 from a downstream save / reconcile
|
|
595
|
+
// failure) to the user. Without this, the document model
|
|
596
|
+
// goes clean on save and the user has no signal that their
|
|
597
|
+
// edit did not actually persist. ServerConnection.ResponseError
|
|
598
|
+
// carries the handler's JSON `message` field on reason.message.
|
|
599
|
+
Notification.error(`Failed to save MCP config: ${(_a = reason === null || reason === void 0 ? void 0 : reason.message) !== null && _a !== void 0 ? _a : reason}`, { autoClose: 5000 });
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await NBIAPI.fetchCapabilities();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Initialization data for the @plmbr/notebook-intelligence extension.
|
|
607
|
+
*/
|
|
608
|
+
const plugin = {
|
|
609
|
+
id: '@plmbr/notebook-intelligence:plugin',
|
|
610
|
+
description: 'Notebook Intelligence',
|
|
611
|
+
autoStart: true,
|
|
612
|
+
requires: [
|
|
613
|
+
ICompletionProviderManager,
|
|
614
|
+
IDocumentManager,
|
|
615
|
+
IDefaultFileBrowser,
|
|
616
|
+
IEditorLanguageRegistry,
|
|
617
|
+
ICommandPalette,
|
|
618
|
+
IMainMenu
|
|
619
|
+
],
|
|
620
|
+
// @jupyterlab/terminal nests its own @lumino/coreutils copy, so its
|
|
621
|
+
// Token class is structurally identical but nominally distinct from
|
|
622
|
+
// ours. Cast through the top-level Token type to keep the plugin
|
|
623
|
+
// declaration well-typed for the other optionals.
|
|
624
|
+
optional: [
|
|
625
|
+
IStatusBar,
|
|
626
|
+
ILauncher,
|
|
627
|
+
ITerminalTracker
|
|
628
|
+
],
|
|
629
|
+
provides: INotebookIntelligence,
|
|
630
|
+
activate: async (app, completionManager, docManager, defaultBrowser, languageRegistry, palette, mainMenu, statusBar, launcher, terminalTracker) => {
|
|
631
|
+
console.log('JupyterLab extension @plmbr/notebook-intelligence is activated!');
|
|
632
|
+
const telemetryEmitter = new TelemetryEmitter();
|
|
633
|
+
telemetryEmitter.registerTelemetryListener({
|
|
634
|
+
name: BACKEND_TELEMETRY_LISTENER_NAME,
|
|
635
|
+
onTelemetryEvent: event => {
|
|
636
|
+
NBIAPI.emitTelemetryEvent(event);
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
const extensionService = {
|
|
640
|
+
registerTelemetryListener: (listener) => {
|
|
641
|
+
telemetryEmitter.registerTelemetryListener(listener);
|
|
642
|
+
},
|
|
643
|
+
unregisterTelemetryListener: (listener) => {
|
|
644
|
+
telemetryEmitter.unregisterTelemetryListener(listener);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
await NBIAPI.initialize();
|
|
648
|
+
if (terminalTracker) {
|
|
649
|
+
attachTerminalDragDrop({
|
|
650
|
+
tracker: terminalTracker,
|
|
651
|
+
// dragover fires at ~60Hz, so we read straight off the
|
|
652
|
+
// capabilities object (cheap property lookup) instead of going
|
|
653
|
+
// through the `featurePolicies` getter (which rebuilds an
|
|
654
|
+
// 11-key object every call). Re-evaluated per event so a future
|
|
655
|
+
// capabilities reload (policy flipped server-side) takes effect
|
|
656
|
+
// without tearing down listeners.
|
|
657
|
+
isEnabled: () => {
|
|
658
|
+
var _a, _b;
|
|
659
|
+
const policy = (_b = (_a = NBIAPI.config.capabilities) === null || _a === void 0 ? void 0 : _a.feature_policies) === null || _b === void 0 ? void 0 : _b.terminal_drag_drop;
|
|
660
|
+
// Default-enabled when the key is absent: an older backend or
|
|
661
|
+
// a future capability schema bump shouldn't silently disable
|
|
662
|
+
// the feature.
|
|
663
|
+
if (!policy) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return policy.enabled !== false;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
let closeOpenPopover = null;
|
|
671
|
+
let mcpConfigEditor = null;
|
|
672
|
+
completionManager.registerInlineProvider(new NBIInlineCompletionProvider(telemetryEmitter));
|
|
673
|
+
// JL plugins have no deactivate hook, so the watcher runs for the
|
|
674
|
+
// lifetime of the Lab session and the returned teardown is intentionally
|
|
675
|
+
// discarded (it exists for test ergonomics).
|
|
676
|
+
attachOpenFileRefreshWatcher({
|
|
677
|
+
env: buildRefreshWatcherEnv(app, app.serviceManager.contents),
|
|
678
|
+
isEnabled: () => NBIAPI.config.featurePolicies.refresh_open_files_on_disk_change.enabled
|
|
679
|
+
});
|
|
680
|
+
const waitForFileToBeActive = async (filePath) => {
|
|
681
|
+
const isNotebook = filePath.endsWith('.ipynb');
|
|
682
|
+
return new Promise((resolve, reject) => {
|
|
683
|
+
const checkIfActive = () => {
|
|
684
|
+
const activeFilePath = ActiveDocumentWatcher.activeDocumentInfo.filePath;
|
|
685
|
+
const filePathToCheck = filePath;
|
|
686
|
+
const currentWidget = app.shell.currentWidget;
|
|
687
|
+
if (activeFilePath === filePathToCheck &&
|
|
688
|
+
((isNotebook &&
|
|
689
|
+
currentWidget instanceof NotebookPanel &&
|
|
690
|
+
currentWidget.content.activeCell &&
|
|
691
|
+
currentWidget.content.activeCell.node.contains(document.activeElement)) ||
|
|
692
|
+
(!isNotebook &&
|
|
693
|
+
currentWidget instanceof FileEditorWidget &&
|
|
694
|
+
currentWidget.content.editor.hasFocus()))) {
|
|
695
|
+
resolve(true);
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
setTimeout(checkIfActive, 200);
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
checkIfActive();
|
|
702
|
+
waitForDuration(10000).then(() => {
|
|
703
|
+
resolve(false);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
};
|
|
707
|
+
const panel = new Panel();
|
|
708
|
+
panel.id = 'notebook-intelligence-tab';
|
|
709
|
+
panel.title.caption = 'Notebook Intelligence';
|
|
710
|
+
const sidebarIcon = new LabIcon({
|
|
711
|
+
name: 'notebook-intelligence:sidebar-icon',
|
|
712
|
+
svgstr: sparklesSvgstr
|
|
713
|
+
});
|
|
714
|
+
panel.title.icon = sidebarIcon;
|
|
715
|
+
const sidebar = new ChatSidebar({
|
|
716
|
+
getCurrentDirectory: () => {
|
|
717
|
+
return ActiveDocumentWatcher.currentDirectory;
|
|
718
|
+
},
|
|
719
|
+
getActiveDocumentInfo: () => {
|
|
720
|
+
return ActiveDocumentWatcher.activeDocumentInfo;
|
|
721
|
+
},
|
|
722
|
+
getActiveSelectionContent: () => {
|
|
723
|
+
return ActiveDocumentWatcher.getActiveSelectionContent();
|
|
724
|
+
},
|
|
725
|
+
getCurrentCellContents: () => {
|
|
726
|
+
return ActiveDocumentWatcher.getCurrentCellContents();
|
|
727
|
+
},
|
|
728
|
+
openFile: (path) => {
|
|
729
|
+
docManager.openOrReveal(path);
|
|
730
|
+
},
|
|
731
|
+
getApp() {
|
|
732
|
+
return app;
|
|
733
|
+
},
|
|
734
|
+
getTelemetryEmitter() {
|
|
735
|
+
return telemetryEmitter;
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
panel.addWidget(sidebar);
|
|
739
|
+
app.shell.add(panel, 'left', { rank: 1000 });
|
|
740
|
+
app.shell.activateById(panel.id);
|
|
741
|
+
// Global focus shortcut. Activates the NBI sidebar (revealing it if
|
|
742
|
+
// collapsed) and lands focus on the prompt textarea, so keyboard-
|
|
743
|
+
// first users can start typing without mousing through panel tabs.
|
|
744
|
+
// Ctrl/Cmd+Shift+L mirrors common "focus search / focus input"
|
|
745
|
+
// bindings used by other editors and doesn't collide with any
|
|
746
|
+
// built-in JupyterLab shortcut.
|
|
747
|
+
//
|
|
748
|
+
// Implementation: dispatch a CustomEvent the React sidebar listens
|
|
749
|
+
// for. The sidebar owns `promptInputRef` and can focus the
|
|
750
|
+
// textarea reliably regardless of whether the panel was collapsed
|
|
751
|
+
// (the event fires after the React tree has been mounted by the
|
|
752
|
+
// activate path), so we don't have to race the Lumino layout with
|
|
753
|
+
// a DOM-id `querySelector`. Matches the existing
|
|
754
|
+
// `copilotSidebar:*` event pattern used elsewhere in this file.
|
|
755
|
+
app.commands.addCommand(CommandIDs.focusChatInput, {
|
|
756
|
+
label: 'Focus Notebook Intelligence chat input',
|
|
757
|
+
caption: 'Open the NBI sidebar and move focus to the prompt textarea',
|
|
758
|
+
execute: () => {
|
|
759
|
+
app.shell.activateById(panel.id);
|
|
760
|
+
document.dispatchEvent(new CustomEvent('copilotSidebar:focusPrompt'));
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
app.commands.addKeyBinding({
|
|
764
|
+
command: CommandIDs.focusChatInput,
|
|
765
|
+
keys: ['Accel Shift L'],
|
|
766
|
+
selector: 'body'
|
|
767
|
+
});
|
|
768
|
+
app.docRegistry.addWidgetExtension('Notebook', new NotebookGenerationToolbarExtension({
|
|
769
|
+
app,
|
|
770
|
+
icon: sparkleIcon,
|
|
771
|
+
chatSidebarId: panel.id
|
|
772
|
+
}));
|
|
773
|
+
const updateSidebarIcon = () => {
|
|
774
|
+
if (NBIAPI.getChatEnabled()) {
|
|
775
|
+
panel.title.icon = sidebarIcon;
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
panel.title.icon = sparkleWarningIcon;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
NBIAPI.githubLoginStatusChanged.connect((_, args) => {
|
|
782
|
+
updateSidebarIcon();
|
|
783
|
+
});
|
|
784
|
+
NBIAPI.configChanged.connect((_, args) => {
|
|
785
|
+
updateSidebarIcon();
|
|
786
|
+
});
|
|
787
|
+
setTimeout(() => {
|
|
788
|
+
updateSidebarIcon();
|
|
789
|
+
}, 2000);
|
|
790
|
+
app.commands.addCommand(CommandIDs.chatuserInput, {
|
|
791
|
+
execute: args => {
|
|
792
|
+
NBIAPI.sendChatUserInput(args.id, args.data);
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
app.commands.addCommand(CommandIDs.insertAtCursor, {
|
|
796
|
+
execute: args => {
|
|
797
|
+
const currentWidget = app.shell.currentWidget;
|
|
798
|
+
if (currentWidget instanceof NotebookPanel) {
|
|
799
|
+
const activeCell = currentWidget.content.activeCell;
|
|
800
|
+
if (activeCell) {
|
|
801
|
+
applyCodeToSelectionInEditor(activeCell.editor, args.code);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
else if (currentWidget instanceof FileEditorWidget) {
|
|
806
|
+
applyCodeToSelectionInEditor(currentWidget.content.editor, args.code);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
app.commands.execute('apputils:notify', {
|
|
810
|
+
message: 'Failed to insert at cursor. Open a notebook or file to insert the code.',
|
|
811
|
+
type: 'error',
|
|
812
|
+
options: { autoClose: true }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
app.commands.addCommand(CommandIDs.addCodeAsNewCell, {
|
|
817
|
+
execute: args => {
|
|
818
|
+
var _a;
|
|
819
|
+
const currentWidget = app.shell.currentWidget;
|
|
820
|
+
if (currentWidget instanceof NotebookPanel) {
|
|
821
|
+
let activeCellIndex = currentWidget.content.activeCellIndex;
|
|
822
|
+
activeCellIndex =
|
|
823
|
+
activeCellIndex === -1
|
|
824
|
+
? currentWidget.content.widgets.length
|
|
825
|
+
: activeCellIndex + 1;
|
|
826
|
+
(_a = currentWidget.model) === null || _a === void 0 ? void 0 : _a.sharedModel.insertCell(activeCellIndex, {
|
|
827
|
+
cell_type: 'code',
|
|
828
|
+
metadata: { trusted: true },
|
|
829
|
+
source: args.code
|
|
830
|
+
});
|
|
831
|
+
currentWidget.content.activeCellIndex = activeCellIndex;
|
|
832
|
+
}
|
|
833
|
+
else {
|
|
834
|
+
app.commands.execute('apputils:notify', {
|
|
835
|
+
message: 'Open a notebook to insert the code as new cell',
|
|
836
|
+
type: 'error',
|
|
837
|
+
options: { autoClose: true }
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
});
|
|
842
|
+
app.commands.addCommand(CommandIDs.createNewFile, {
|
|
843
|
+
execute: async (args) => {
|
|
844
|
+
const contents = new ContentsManager();
|
|
845
|
+
const newPyFile = await contents.newUntitled({
|
|
846
|
+
ext: '.py',
|
|
847
|
+
path: defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path
|
|
848
|
+
});
|
|
849
|
+
contents.save(newPyFile.path, {
|
|
850
|
+
content: extractLLMGeneratedCode(args.code),
|
|
851
|
+
format: 'text',
|
|
852
|
+
type: 'file'
|
|
853
|
+
});
|
|
854
|
+
docManager.openOrReveal(newPyFile.path);
|
|
855
|
+
await waitForFileToBeActive(newPyFile.path);
|
|
856
|
+
return newPyFile;
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
app.commands.addCommand(CommandIDs.showTour, {
|
|
860
|
+
label: () => {
|
|
861
|
+
var _a, _b;
|
|
862
|
+
return commandLabel(NBIAPI.config.tourOverrides, (_b = (_a = TOUR_DEFAULTS.command) === null || _a === void 0 ? void 0 : _a.label) !== null && _b !== void 0 ? _b : 'Show NBI tour');
|
|
863
|
+
},
|
|
864
|
+
caption: 'Replay the first-run tour highlighting the chat sidebar affordances',
|
|
865
|
+
execute: async () => {
|
|
866
|
+
// Make sure the sidebar is open before the tour fires; the
|
|
867
|
+
// overlay anchors to elements that only exist when the
|
|
868
|
+
// sidebar widget is mounted. Activate the left-rail panel
|
|
869
|
+
// directly (the same id used in app.shell.add above).
|
|
870
|
+
app.shell.activateById(panel.id);
|
|
871
|
+
// Reset persistence so re-runs always fire.
|
|
872
|
+
resetTour();
|
|
873
|
+
// activateById is fire-and-forget: the panel can still be
|
|
874
|
+
// mid-layout when this returns, and dispatching synchronously
|
|
875
|
+
// would race the anchor measurement. Wait for the panel to
|
|
876
|
+
// actually be visible before firing, then dispatch on a frame
|
|
877
|
+
// boundary so the sidebar's React tree has committed.
|
|
878
|
+
const fire = () => {
|
|
879
|
+
requestAnimationFrame(() => dispatchShowTour());
|
|
880
|
+
};
|
|
881
|
+
if (panel.isVisible) {
|
|
882
|
+
fire();
|
|
883
|
+
}
|
|
884
|
+
else {
|
|
885
|
+
// Use a bounded rAF poll waiting for the panel to become
|
|
886
|
+
// visible. activateById is asynchronous and we can't
|
|
887
|
+
// synchronously observe completion from outside the widget.
|
|
888
|
+
let attempts = 0;
|
|
889
|
+
const tick = () => {
|
|
890
|
+
attempts += 1;
|
|
891
|
+
if (panel.isVisible || attempts >= 30) {
|
|
892
|
+
fire();
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
requestAnimationFrame(tick);
|
|
896
|
+
};
|
|
897
|
+
requestAnimationFrame(tick);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
});
|
|
901
|
+
palette.addItem({
|
|
902
|
+
command: CommandIDs.showTour,
|
|
903
|
+
category: 'Notebook Intelligence'
|
|
904
|
+
});
|
|
905
|
+
// The label thunk above reads from NBIAPI.config.tourOverrides,
|
|
906
|
+
// which arrives asynchronously over the capabilities call. The
|
|
907
|
+
// command palette caches labels until told otherwise, so tell it.
|
|
908
|
+
NBIAPI.configChanged.connect(() => {
|
|
909
|
+
app.commands.notifyCommandChanged(CommandIDs.showTour);
|
|
910
|
+
});
|
|
911
|
+
app.commands.addCommand(CommandIDs.showFormInputDialog, {
|
|
912
|
+
execute: async (args) => {
|
|
913
|
+
const title = args.title;
|
|
914
|
+
const fields = args.fields;
|
|
915
|
+
return new Promise((resolve, reject) => {
|
|
916
|
+
let dialog = null;
|
|
917
|
+
const dialogBody = new FormInputDialogBody({
|
|
918
|
+
fields: fields,
|
|
919
|
+
onDone: (formData) => {
|
|
920
|
+
dialog.dispose();
|
|
921
|
+
resolve(formData);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
dialog = new Dialog({
|
|
925
|
+
title: title,
|
|
926
|
+
hasClose: true,
|
|
927
|
+
body: dialogBody,
|
|
928
|
+
buttons: []
|
|
929
|
+
});
|
|
930
|
+
dialog
|
|
931
|
+
.launch()
|
|
932
|
+
.then((result) => {
|
|
933
|
+
reject();
|
|
934
|
+
})
|
|
935
|
+
.catch(() => {
|
|
936
|
+
reject(new Error('Failed to show form input dialog'));
|
|
937
|
+
});
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
app.commands.addCommand(CommandIDs.createNewNotebookFromPython, {
|
|
942
|
+
execute: async (args) => {
|
|
943
|
+
var _a;
|
|
944
|
+
let pythonKernelSpec = null;
|
|
945
|
+
const contents = new ContentsManager();
|
|
946
|
+
const kernels = new KernelSpecManager();
|
|
947
|
+
await kernels.ready;
|
|
948
|
+
const kernelspecs = (_a = kernels.specs) === null || _a === void 0 ? void 0 : _a.kernelspecs;
|
|
949
|
+
if (kernelspecs) {
|
|
950
|
+
for (const key in kernelspecs) {
|
|
951
|
+
const kernelspec = kernelspecs[key];
|
|
952
|
+
if ((kernelspec === null || kernelspec === void 0 ? void 0 : kernelspec.language) === 'python') {
|
|
953
|
+
pythonKernelSpec = kernelspec;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
const newNBFile = await contents.newUntitled({
|
|
959
|
+
ext: '.ipynb',
|
|
960
|
+
path: defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path
|
|
961
|
+
});
|
|
962
|
+
const nbFileContent = structuredClone(emptyNotebookContent);
|
|
963
|
+
if (pythonKernelSpec) {
|
|
964
|
+
nbFileContent.metadata = {
|
|
965
|
+
kernelspec: {
|
|
966
|
+
language: 'python',
|
|
967
|
+
name: pythonKernelSpec.name,
|
|
968
|
+
display_name: pythonKernelSpec.display_name
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
if (args.code) {
|
|
973
|
+
nbFileContent.cells.push({
|
|
974
|
+
cell_type: 'code',
|
|
975
|
+
metadata: { trusted: true },
|
|
976
|
+
source: [args.code],
|
|
977
|
+
outputs: []
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
contents.save(newNBFile.path, {
|
|
981
|
+
content: nbFileContent,
|
|
982
|
+
format: 'json',
|
|
983
|
+
type: 'notebook'
|
|
984
|
+
});
|
|
985
|
+
docManager.openOrReveal(newNBFile.path);
|
|
986
|
+
await waitForFileToBeActive(newNBFile.path);
|
|
987
|
+
return newNBFile;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
app.commands.addCommand(CommandIDs.renameNotebook, {
|
|
991
|
+
execute: async (args) => {
|
|
992
|
+
const activeWidget = app.shell.currentWidget;
|
|
993
|
+
if (activeWidget instanceof NotebookPanel) {
|
|
994
|
+
const oldPath = activeWidget.context.path;
|
|
995
|
+
const oldParentPath = path.dirname(oldPath);
|
|
996
|
+
let newPath = path.join(oldParentPath, args.newName);
|
|
997
|
+
if (path.extname(newPath) !== '.ipynb') {
|
|
998
|
+
newPath += '.ipynb';
|
|
999
|
+
}
|
|
1000
|
+
if (path.dirname(newPath) !== oldParentPath) {
|
|
1001
|
+
return 'Failed to rename notebook. New path is outside the old parent directory';
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
await app.serviceManager.contents.rename(oldPath, newPath);
|
|
1005
|
+
return 'Successfully renamed notebook';
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
return `Failed to rename notebook: ${error}`;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
else {
|
|
1012
|
+
return 'Cannot rename non notebook files';
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
app.commands.addCommand(CommandIDs.runCommandInTerminal, {
|
|
1017
|
+
execute: async (args) => {
|
|
1018
|
+
var _a;
|
|
1019
|
+
const command = args.command;
|
|
1020
|
+
const terminal = await app.commands.execute('terminal:create-new', {
|
|
1021
|
+
cwd: args.cwd || ActiveDocumentWatcher.currentDirectory
|
|
1022
|
+
});
|
|
1023
|
+
const session = (_a = terminal === null || terminal === void 0 ? void 0 : terminal.content) === null || _a === void 0 ? void 0 : _a.session;
|
|
1024
|
+
if (!session) {
|
|
1025
|
+
return 'Failed to execute command in Jupyter terminal';
|
|
1026
|
+
}
|
|
1027
|
+
return new Promise((resolve, reject) => {
|
|
1028
|
+
let lastMessageReceivedTime = Date.now();
|
|
1029
|
+
let lastMessageCheckInterval = null;
|
|
1030
|
+
const messageCheckTimeout = 5000;
|
|
1031
|
+
const messageCheckInterval = 1000;
|
|
1032
|
+
let output = '';
|
|
1033
|
+
const messageReceivedHandler = (sender, message) => {
|
|
1034
|
+
const content = stripAnsi(message.content.join(''));
|
|
1035
|
+
output += content;
|
|
1036
|
+
lastMessageReceivedTime = Date.now();
|
|
1037
|
+
};
|
|
1038
|
+
session.messageReceived.connect(messageReceivedHandler);
|
|
1039
|
+
session.send({
|
|
1040
|
+
type: 'stdin',
|
|
1041
|
+
content: [command + '\n'] // Add newline to execute the command
|
|
1042
|
+
});
|
|
1043
|
+
// wait for the messageCheckInterval and if no message received, return the output.
|
|
1044
|
+
// otherwise wait for the next message.
|
|
1045
|
+
lastMessageCheckInterval = setInterval(() => {
|
|
1046
|
+
if (Date.now() - lastMessageReceivedTime > messageCheckTimeout) {
|
|
1047
|
+
clearInterval(lastMessageCheckInterval);
|
|
1048
|
+
session.messageReceived.disconnect(messageReceivedHandler);
|
|
1049
|
+
resolve(`Command executed in Jupyter terminal, output: ${output}`);
|
|
1050
|
+
}
|
|
1051
|
+
}, messageCheckInterval);
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
// Claude Code launcher tile: shows a session picker backed by history.jsonl
|
|
1056
|
+
// (all projects), then opens a terminal at the session's project directory.
|
|
1057
|
+
// Waits for bash's first prompt before sending, avoiding the race condition
|
|
1058
|
+
// where the command is sent before the shell has started.
|
|
1059
|
+
const launchCliInTerminal = async (command, cwd) => {
|
|
1060
|
+
const mgr = app.serviceManager.terminals;
|
|
1061
|
+
const before = new Set([...mgr.running()].map((s) => s.name));
|
|
1062
|
+
try {
|
|
1063
|
+
await app.commands.execute('terminal:create-new', cwd ? { cwd } : {});
|
|
1064
|
+
}
|
|
1065
|
+
catch (_a) {
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
const newModel = [...mgr.running()].find((s) => !before.has(s.name));
|
|
1069
|
+
if (!newModel) {
|
|
1070
|
+
return;
|
|
1071
|
+
}
|
|
1072
|
+
const conn = mgr.connectTo({ model: newModel });
|
|
1073
|
+
let sent = false;
|
|
1074
|
+
const sendCommand = () => {
|
|
1075
|
+
if (sent) {
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
sent = true;
|
|
1079
|
+
conn.messageReceived.disconnect(handler);
|
|
1080
|
+
conn.send({ type: 'stdin', content: [command + '\r'] });
|
|
1081
|
+
};
|
|
1082
|
+
const handler = (_, msg) => {
|
|
1083
|
+
if (msg.type === 'stdout') {
|
|
1084
|
+
sendCommand();
|
|
1085
|
+
}
|
|
1086
|
+
};
|
|
1087
|
+
conn.messageReceived.connect(handler);
|
|
1088
|
+
setTimeout(sendCommand, 3000);
|
|
1089
|
+
};
|
|
1090
|
+
app.commands.addCommand(CommandIDs.openClaudeCodeLauncher, {
|
|
1091
|
+
label: 'Claude Code',
|
|
1092
|
+
caption: 'Resume or start a Claude Code session',
|
|
1093
|
+
icon: claudeIcon,
|
|
1094
|
+
// The launcher tile opens a Jupyter terminal that runs the
|
|
1095
|
+
// `claude` CLI directly — it doesn't depend on NBI being in
|
|
1096
|
+
// Claude Code chat mode (which gates the chat-sidebar SDK
|
|
1097
|
+
// backend). CLI presence on PATH is the only real prerequisite
|
|
1098
|
+
// (issue #230). Honor the admin policy on the same gate as the
|
|
1099
|
+
// four CLI-launcher tiles so an admin force-off blocks the
|
|
1100
|
+
// command palette too, not just the launcher tile.
|
|
1101
|
+
isVisible: () => NBIAPI.config.isClaudeCliAvailable &&
|
|
1102
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('claude-code'),
|
|
1103
|
+
execute: async () => {
|
|
1104
|
+
class PickerWidget extends ReactWidget {
|
|
1105
|
+
getValue() {
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
render() {
|
|
1109
|
+
return React.createElement(LauncherPicker, {
|
|
1110
|
+
onSessionSelected: (session) => {
|
|
1111
|
+
dialog.close();
|
|
1112
|
+
const cmd = session.cwd
|
|
1113
|
+
? `cd ${session.cwd} && claude --resume ${session.session_id}`
|
|
1114
|
+
: `claude --resume ${session.session_id}`;
|
|
1115
|
+
launchCliInTerminal(cmd);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const picker = new PickerWidget();
|
|
1121
|
+
picker.addClass('nbi-claude-code-picker');
|
|
1122
|
+
const dialog = new Dialog({
|
|
1123
|
+
title: 'Claude Code terminal session',
|
|
1124
|
+
body: picker,
|
|
1125
|
+
buttons: [
|
|
1126
|
+
Dialog.cancelButton({ label: 'Cancel' }),
|
|
1127
|
+
Dialog.okButton({ label: '+ New Session' })
|
|
1128
|
+
],
|
|
1129
|
+
hasClose: true
|
|
1130
|
+
});
|
|
1131
|
+
const result = await dialog.launch();
|
|
1132
|
+
if (result.button.accept) {
|
|
1133
|
+
// New Session: let the user choose a start directory (defaulting
|
|
1134
|
+
// to the file browser's current path). If they cancel the folder
|
|
1135
|
+
// picker, abort rather than silently opening the terminal in an
|
|
1136
|
+
// unexpected location.
|
|
1137
|
+
const cwd = await chooseWorkspaceDirectory(docManager, 'Choose start directory for Claude Code', defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path);
|
|
1138
|
+
if (cwd === undefined) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
launchCliInTerminal('claude', cwd);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
// Add or dispose a launcher entry based on a live availability check.
|
|
1146
|
+
// The launcher renders every item in its model regardless of the
|
|
1147
|
+
// backing command's `isVisible`, so gating tile visibility requires
|
|
1148
|
+
// adding only when available and disposing when not. Re-evaluates on
|
|
1149
|
+
// every NBIAPI.configChanged so a late capabilities load (or a
|
|
1150
|
+
// future hot-reload of the CLI on PATH) takes effect without a
|
|
1151
|
+
// browser refresh.
|
|
1152
|
+
const syncLauncherEntry = (commandId, itemOptions, isAvailable) => {
|
|
1153
|
+
if (!launcher) {
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
let entry = null;
|
|
1157
|
+
const sync = () => {
|
|
1158
|
+
const available = isAvailable();
|
|
1159
|
+
if (available && !entry) {
|
|
1160
|
+
entry = launcher.add({ command: commandId, ...itemOptions });
|
|
1161
|
+
}
|
|
1162
|
+
else if (!available && entry) {
|
|
1163
|
+
entry.dispose();
|
|
1164
|
+
entry = null;
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
sync();
|
|
1168
|
+
NBIAPI.configChanged.connect(sync);
|
|
1169
|
+
};
|
|
1170
|
+
syncLauncherEntry(CommandIDs.openClaudeCodeLauncher, { category: 'Coding Agent', rank: -1 }, () => NBIAPI.config.isClaudeCliAvailable &&
|
|
1171
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('claude-code'));
|
|
1172
|
+
// Additional coding-agent CLIs (issue #260). First-phase scope: detect
|
|
1173
|
+
// the binary on PATH (backend exposes `<agent>_cli_available`), show a
|
|
1174
|
+
// tile when present, click opens a terminal in the file-browser's
|
|
1175
|
+
// current directory and types the CLI command. No session picker.
|
|
1176
|
+
const registerAgentCliLauncher = (config) => {
|
|
1177
|
+
app.commands.addCommand(config.commandId, {
|
|
1178
|
+
label: config.label,
|
|
1179
|
+
caption: config.caption,
|
|
1180
|
+
icon: config.icon,
|
|
1181
|
+
isVisible: () => config.isAvailable(),
|
|
1182
|
+
execute: async () => {
|
|
1183
|
+
const cwd = await chooseWorkspaceDirectory(docManager, `Choose start directory for ${config.label}`, defaultBrowser === null || defaultBrowser === void 0 ? void 0 : defaultBrowser.model.path);
|
|
1184
|
+
if (cwd === undefined) {
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1187
|
+
launchCliInTerminal(config.cliCommand, cwd);
|
|
1188
|
+
}
|
|
1189
|
+
});
|
|
1190
|
+
syncLauncherEntry(config.commandId, { category: 'Coding Agent' }, config.isAvailable);
|
|
1191
|
+
NBIAPI.configChanged.connect(() => {
|
|
1192
|
+
app.commands.notifyCommandChanged(config.commandId);
|
|
1193
|
+
});
|
|
1194
|
+
};
|
|
1195
|
+
registerAgentCliLauncher({
|
|
1196
|
+
commandId: CommandIDs.openOpenCodeLauncher,
|
|
1197
|
+
label: 'opencode',
|
|
1198
|
+
caption: 'Start an opencode session in a Jupyter terminal',
|
|
1199
|
+
icon: opencodeIcon,
|
|
1200
|
+
cliCommand: 'opencode',
|
|
1201
|
+
isAvailable: () => NBIAPI.config.isOpenCodeCliAvailable &&
|
|
1202
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('opencode')
|
|
1203
|
+
});
|
|
1204
|
+
registerAgentCliLauncher({
|
|
1205
|
+
commandId: CommandIDs.openPiLauncher,
|
|
1206
|
+
label: 'Pi',
|
|
1207
|
+
caption: 'Start a Pi session in a Jupyter terminal',
|
|
1208
|
+
icon: terminalIcon,
|
|
1209
|
+
cliCommand: 'pi',
|
|
1210
|
+
isAvailable: () => NBIAPI.config.isPiCliAvailable &&
|
|
1211
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('pi')
|
|
1212
|
+
});
|
|
1213
|
+
registerAgentCliLauncher({
|
|
1214
|
+
commandId: CommandIDs.openGitHubCopilotCliLauncher,
|
|
1215
|
+
label: 'GitHub Copilot CLI',
|
|
1216
|
+
caption: 'Start a GitHub Copilot CLI session in a Jupyter terminal',
|
|
1217
|
+
icon: githubCopilotIcon,
|
|
1218
|
+
cliCommand: 'copilot',
|
|
1219
|
+
isAvailable: () => NBIAPI.config.isGitHubCopilotCliAvailable &&
|
|
1220
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('github-copilot-cli')
|
|
1221
|
+
});
|
|
1222
|
+
registerAgentCliLauncher({
|
|
1223
|
+
commandId: CommandIDs.openCodexLauncher,
|
|
1224
|
+
label: 'Codex',
|
|
1225
|
+
caption: 'Start an OpenAI Codex CLI session in a Jupyter terminal',
|
|
1226
|
+
icon: openaiIcon,
|
|
1227
|
+
cliCommand: 'codex',
|
|
1228
|
+
isAvailable: () => NBIAPI.config.isCodexCliAvailable &&
|
|
1229
|
+
!NBIAPI.config.isCodingAgentLauncherDisabledByPolicy('codex')
|
|
1230
|
+
});
|
|
1231
|
+
// Refresh the Claude Code command's palette-visibility state when the
|
|
1232
|
+
// user installs/uninstalls the CLI. The launcher tile is already gated
|
|
1233
|
+
// via syncLauncherEntry; this is for the command palette only.
|
|
1234
|
+
NBIAPI.configChanged.connect(() => {
|
|
1235
|
+
app.commands.notifyCommandChanged(CommandIDs.openClaudeCodeLauncher);
|
|
1236
|
+
});
|
|
1237
|
+
const isNewEmptyNotebook = (model) => {
|
|
1238
|
+
return (model.cells.length === 1 &&
|
|
1239
|
+
model.cells[0].cell_type === 'code' &&
|
|
1240
|
+
model.cells[0].source === '');
|
|
1241
|
+
};
|
|
1242
|
+
const githubLoginRequired = () => {
|
|
1243
|
+
return (NBIAPI.config.usingGitHubCopilotModel &&
|
|
1244
|
+
NBIAPI.getLoginStatus() === GitHubCopilotLoginStatus.NotLoggedIn);
|
|
1245
|
+
};
|
|
1246
|
+
const isChatEnabled = () => {
|
|
1247
|
+
return (NBIAPI.config.isInClaudeCodeMode ||
|
|
1248
|
+
(NBIAPI.config.chatModel.provider === GITHUB_COPILOT_PROVIDER_ID
|
|
1249
|
+
? !githubLoginRequired()
|
|
1250
|
+
: NBIAPI.config.chatModel.provider !== 'none'));
|
|
1251
|
+
};
|
|
1252
|
+
const isActiveCellCodeCell = () => {
|
|
1253
|
+
if (!(app.shell.currentWidget instanceof NotebookPanel)) {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
const np = app.shell.currentWidget;
|
|
1257
|
+
const activeCell = np.content.activeCell;
|
|
1258
|
+
return activeCell instanceof CodeCell;
|
|
1259
|
+
};
|
|
1260
|
+
const isCurrentWidgetFileEditor = () => {
|
|
1261
|
+
return app.shell.currentWidget instanceof FileEditorWidget;
|
|
1262
|
+
};
|
|
1263
|
+
const addCellToNotebook = (filePath, cellType, source) => {
|
|
1264
|
+
const widget = docManager.findWidget(filePath);
|
|
1265
|
+
const notebook = widget instanceof NotebookPanel && widget.model ? widget : null;
|
|
1266
|
+
if (!notebook) {
|
|
1267
|
+
app.commands.execute('apputils:notify', {
|
|
1268
|
+
message: `Failed to access the notebook: ${filePath}`,
|
|
1269
|
+
type: 'error',
|
|
1270
|
+
options: { autoClose: true }
|
|
1271
|
+
});
|
|
1272
|
+
return false;
|
|
1273
|
+
}
|
|
1274
|
+
const model = notebook.model.sharedModel;
|
|
1275
|
+
const newCellIndex = isNewEmptyNotebook(model)
|
|
1276
|
+
? 0
|
|
1277
|
+
: model.cells.length - 1;
|
|
1278
|
+
model.insertCell(newCellIndex, {
|
|
1279
|
+
cell_type: cellType,
|
|
1280
|
+
metadata: { trusted: true },
|
|
1281
|
+
source
|
|
1282
|
+
});
|
|
1283
|
+
return true;
|
|
1284
|
+
};
|
|
1285
|
+
app.commands.addCommand(CommandIDs.addCodeCellToNotebook, {
|
|
1286
|
+
execute: args => {
|
|
1287
|
+
return addCellToNotebook(args.path, 'code', args.code);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
app.commands.addCommand(CommandIDs.addMarkdownCellToNotebook, {
|
|
1291
|
+
execute: args => {
|
|
1292
|
+
return addCellToNotebook(args.path, 'markdown', args.markdown);
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
// Resolve the notebook a cell-targeting command should operate on.
|
|
1296
|
+
//
|
|
1297
|
+
// When the chat sidebar dispatches a RunUICommand on behalf of the
|
|
1298
|
+
// agent it injects `notebookPath` — the notebook that was active at
|
|
1299
|
+
// chat-request time. We look that one up via the doc manager so the
|
|
1300
|
+
// command keeps targeting the right notebook even after the user
|
|
1301
|
+
// switches tabs mid-task. If a `notebookPath` was supplied but no
|
|
1302
|
+
// longer resolves (target tab was closed, or the notebook was
|
|
1303
|
+
// renamed), we *do not* silently fall through to `currentWidget` —
|
|
1304
|
+
// that would let the agent mutate a different notebook than it
|
|
1305
|
+
// believed it was operating on. Surface the error and bail.
|
|
1306
|
+
//
|
|
1307
|
+
// For manually-invoked commands (palette, menu, toolbar) there's no
|
|
1308
|
+
// `notebookPath`; the current widget is the intended target and the
|
|
1309
|
+
// fallback applies.
|
|
1310
|
+
const resolveTargetNotebook = (args) => {
|
|
1311
|
+
const requestedPath = args === null || args === void 0 ? void 0 : args.notebookPath;
|
|
1312
|
+
if (requestedPath) {
|
|
1313
|
+
const widget = docManager.findWidget(requestedPath);
|
|
1314
|
+
if (widget instanceof NotebookPanel && widget.model) {
|
|
1315
|
+
return widget;
|
|
1316
|
+
}
|
|
1317
|
+
app.commands.execute('apputils:notify', {
|
|
1318
|
+
message: `Failed to find notebook: ${requestedPath}`,
|
|
1319
|
+
type: 'error',
|
|
1320
|
+
options: { autoClose: true }
|
|
1321
|
+
});
|
|
1322
|
+
return null;
|
|
1323
|
+
}
|
|
1324
|
+
const currentWidget = app.shell.currentWidget;
|
|
1325
|
+
if (currentWidget instanceof NotebookPanel && currentWidget.model) {
|
|
1326
|
+
return currentWidget;
|
|
1327
|
+
}
|
|
1328
|
+
app.commands.execute('apputils:notify', {
|
|
1329
|
+
message: 'Failed to find active notebook',
|
|
1330
|
+
type: 'error',
|
|
1331
|
+
options: { autoClose: true }
|
|
1332
|
+
});
|
|
1333
|
+
return null;
|
|
1334
|
+
};
|
|
1335
|
+
const ensureAFileEditorIsActive = () => {
|
|
1336
|
+
const currentWidget = app.shell.currentWidget;
|
|
1337
|
+
const textFileOpen = currentWidget instanceof FileEditorWidget;
|
|
1338
|
+
if (!textFileOpen) {
|
|
1339
|
+
app.commands.execute('apputils:notify', {
|
|
1340
|
+
message: 'Failed to find active file',
|
|
1341
|
+
type: 'error',
|
|
1342
|
+
options: { autoClose: true }
|
|
1343
|
+
});
|
|
1344
|
+
return false;
|
|
1345
|
+
}
|
|
1346
|
+
return true;
|
|
1347
|
+
};
|
|
1348
|
+
app.commands.addCommand(CommandIDs.addMarkdownCellToActiveNotebook, {
|
|
1349
|
+
execute: args => {
|
|
1350
|
+
const np = resolveTargetNotebook(args);
|
|
1351
|
+
if (!np) {
|
|
1352
|
+
return false;
|
|
1353
|
+
}
|
|
1354
|
+
const model = np.model.sharedModel;
|
|
1355
|
+
const newCellIndex = isNewEmptyNotebook(model)
|
|
1356
|
+
? 0
|
|
1357
|
+
: model.cells.length - 1;
|
|
1358
|
+
model.insertCell(newCellIndex, {
|
|
1359
|
+
cell_type: 'markdown',
|
|
1360
|
+
metadata: { trusted: true },
|
|
1361
|
+
source: args.source
|
|
1362
|
+
});
|
|
1363
|
+
return true;
|
|
1364
|
+
}
|
|
1365
|
+
});
|
|
1366
|
+
app.commands.addCommand(CommandIDs.addCodeCellToActiveNotebook, {
|
|
1367
|
+
execute: args => {
|
|
1368
|
+
const np = resolveTargetNotebook(args);
|
|
1369
|
+
if (!np) {
|
|
1370
|
+
return false;
|
|
1371
|
+
}
|
|
1372
|
+
const model = np.model.sharedModel;
|
|
1373
|
+
const newCellIndex = isNewEmptyNotebook(model)
|
|
1374
|
+
? 0
|
|
1375
|
+
: model.cells.length - 1;
|
|
1376
|
+
model.insertCell(newCellIndex, {
|
|
1377
|
+
cell_type: 'code',
|
|
1378
|
+
metadata: { trusted: true },
|
|
1379
|
+
source: args.source
|
|
1380
|
+
});
|
|
1381
|
+
return true;
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
app.commands.addCommand(CommandIDs.getCellTypeAndSource, {
|
|
1385
|
+
execute: args => {
|
|
1386
|
+
const np = resolveTargetNotebook(args);
|
|
1387
|
+
if (!np) {
|
|
1388
|
+
return false;
|
|
1389
|
+
}
|
|
1390
|
+
const model = np.model.sharedModel;
|
|
1391
|
+
return {
|
|
1392
|
+
type: model.cells[args.cellIndex].cell_type,
|
|
1393
|
+
source: model.cells[args.cellIndex].source
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
app.commands.addCommand(CommandIDs.setCellTypeAndSource, {
|
|
1398
|
+
execute: args => {
|
|
1399
|
+
const np = resolveTargetNotebook(args);
|
|
1400
|
+
if (!np) {
|
|
1401
|
+
return false;
|
|
1402
|
+
}
|
|
1403
|
+
const model = np.model.sharedModel;
|
|
1404
|
+
const cellIndex = args.cellIndex;
|
|
1405
|
+
const cellType = args.cellType;
|
|
1406
|
+
const cell = model.getCell(cellIndex);
|
|
1407
|
+
model.deleteCell(cellIndex);
|
|
1408
|
+
model.insertCell(cellIndex, {
|
|
1409
|
+
cell_type: cellType,
|
|
1410
|
+
metadata: cell.metadata,
|
|
1411
|
+
source: args.source
|
|
1412
|
+
});
|
|
1413
|
+
return true;
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
app.commands.addCommand(CommandIDs.getNumberOfCells, {
|
|
1417
|
+
execute: args => {
|
|
1418
|
+
const np = resolveTargetNotebook(args);
|
|
1419
|
+
if (!np) {
|
|
1420
|
+
return false;
|
|
1421
|
+
}
|
|
1422
|
+
const model = np.model.sharedModel;
|
|
1423
|
+
return model.cells.length;
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
app.commands.addCommand(CommandIDs.getCellOutput, {
|
|
1427
|
+
execute: args => {
|
|
1428
|
+
const np = resolveTargetNotebook(args);
|
|
1429
|
+
if (!np) {
|
|
1430
|
+
return false;
|
|
1431
|
+
}
|
|
1432
|
+
const cellIndex = args.cellIndex;
|
|
1433
|
+
const cell = np.content.widgets[cellIndex];
|
|
1434
|
+
if (!(cell instanceof CodeCell)) {
|
|
1435
|
+
return '';
|
|
1436
|
+
}
|
|
1437
|
+
const content = cellOutputAsText(cell);
|
|
1438
|
+
return content;
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
app.commands.addCommand(CommandIDs.insertCellAtIndex, {
|
|
1442
|
+
execute: args => {
|
|
1443
|
+
const np = resolveTargetNotebook(args);
|
|
1444
|
+
if (!np) {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
const model = np.model.sharedModel;
|
|
1448
|
+
const cellIndex = args.cellIndex;
|
|
1449
|
+
const cellType = args.cellType;
|
|
1450
|
+
model.insertCell(cellIndex, {
|
|
1451
|
+
cell_type: cellType,
|
|
1452
|
+
metadata: { trusted: true },
|
|
1453
|
+
source: args.source
|
|
1454
|
+
});
|
|
1455
|
+
return true;
|
|
1456
|
+
}
|
|
1457
|
+
});
|
|
1458
|
+
app.commands.addCommand(CommandIDs.deleteCellAtIndex, {
|
|
1459
|
+
execute: args => {
|
|
1460
|
+
const np = resolveTargetNotebook(args);
|
|
1461
|
+
if (!np) {
|
|
1462
|
+
return false;
|
|
1463
|
+
}
|
|
1464
|
+
const model = np.model.sharedModel;
|
|
1465
|
+
const cellIndex = args.cellIndex;
|
|
1466
|
+
model.deleteCell(cellIndex);
|
|
1467
|
+
return true;
|
|
1468
|
+
}
|
|
1469
|
+
});
|
|
1470
|
+
app.commands.addCommand(CommandIDs.runCellAtIndex, {
|
|
1471
|
+
execute: async (args) => {
|
|
1472
|
+
const np = resolveTargetNotebook(args);
|
|
1473
|
+
if (!np) {
|
|
1474
|
+
return false;
|
|
1475
|
+
}
|
|
1476
|
+
np.content.activeCellIndex = args.cellIndex;
|
|
1477
|
+
// Drive the cell run via NotebookActions directly rather than the
|
|
1478
|
+
// app-level `notebook:run-cell` command. The command operates on the
|
|
1479
|
+
// currently-focused notebook; calling NotebookActions.run with our
|
|
1480
|
+
// resolved target avoids stealing focus from whichever tab the user
|
|
1481
|
+
// is on while the agent is running.
|
|
1482
|
+
await NotebookActions.run(np.content, np.sessionContext);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
app.commands.addCommand(CommandIDs.getCurrentFileContent, {
|
|
1486
|
+
execute: async (args) => {
|
|
1487
|
+
if (!ensureAFileEditorIsActive()) {
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
const currentWidget = app.shell.currentWidget;
|
|
1491
|
+
const editor = currentWidget.content.editor;
|
|
1492
|
+
return editor.model.sharedModel.getSource();
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
app.commands.addCommand(CommandIDs.setCurrentFileContent, {
|
|
1496
|
+
execute: async (args) => {
|
|
1497
|
+
if (!ensureAFileEditorIsActive()) {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
const currentWidget = app.shell.currentWidget;
|
|
1501
|
+
const editor = currentWidget.content.editor;
|
|
1502
|
+
editor.model.sharedModel.setSource(args.content);
|
|
1503
|
+
return editor.model.sharedModel.getSource();
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
app.commands.addCommand(CommandIDs.openGitHubCopilotLoginDialog, {
|
|
1507
|
+
execute: args => {
|
|
1508
|
+
let dialog = null;
|
|
1509
|
+
const dialogBody = new GitHubCopilotLoginDialogBody({
|
|
1510
|
+
onLoggedIn: () => dialog === null || dialog === void 0 ? void 0 : dialog.dispose()
|
|
1511
|
+
});
|
|
1512
|
+
dialog = new Dialog({
|
|
1513
|
+
title: 'GitHub Copilot Status',
|
|
1514
|
+
hasClose: true,
|
|
1515
|
+
body: dialogBody,
|
|
1516
|
+
buttons: []
|
|
1517
|
+
});
|
|
1518
|
+
dialog.launch();
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
const createNewSettingsWidget = () => {
|
|
1522
|
+
const settingsPanel = new SettingsPanel({
|
|
1523
|
+
onSave: () => {
|
|
1524
|
+
NBIAPI.fetchCapabilities();
|
|
1525
|
+
},
|
|
1526
|
+
onEditMCPConfigClicked: () => {
|
|
1527
|
+
app.commands.execute('notebook-intelligence:open-mcp-config-editor');
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
const widget = new MainAreaWidget({ content: settingsPanel });
|
|
1531
|
+
widget.id = 'nbi-settings';
|
|
1532
|
+
widget.title.label = 'NBI Settings';
|
|
1533
|
+
widget.title.closable = true;
|
|
1534
|
+
return widget;
|
|
1535
|
+
};
|
|
1536
|
+
let settingsWidget = createNewSettingsWidget();
|
|
1537
|
+
app.commands.addCommand(CommandIDs.openConfigurationDialog, {
|
|
1538
|
+
label: 'Notebook Intelligence Settings',
|
|
1539
|
+
execute: args => {
|
|
1540
|
+
if (settingsWidget.isDisposed) {
|
|
1541
|
+
settingsWidget = createNewSettingsWidget();
|
|
1542
|
+
}
|
|
1543
|
+
if (!settingsWidget.isAttached) {
|
|
1544
|
+
app.shell.add(settingsWidget, 'main');
|
|
1545
|
+
}
|
|
1546
|
+
app.shell.activateById(settingsWidget.id);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
app.commands.addCommand(CommandIDs.openMCPConfigEditor, {
|
|
1550
|
+
label: 'Open MCP Config Editor',
|
|
1551
|
+
execute: args => {
|
|
1552
|
+
if (mcpConfigEditor && mcpConfigEditor.isOpen) {
|
|
1553
|
+
mcpConfigEditor.close();
|
|
1554
|
+
}
|
|
1555
|
+
mcpConfigEditor = new MCPConfigEditor(docManager);
|
|
1556
|
+
mcpConfigEditor.open();
|
|
1557
|
+
}
|
|
1558
|
+
});
|
|
1559
|
+
palette.addItem({
|
|
1560
|
+
command: CommandIDs.openConfigurationDialog,
|
|
1561
|
+
category: 'Notebook Intelligence'
|
|
1562
|
+
});
|
|
1563
|
+
palette.addItem({
|
|
1564
|
+
command: CommandIDs.focusChatInput,
|
|
1565
|
+
category: 'Notebook Intelligence'
|
|
1566
|
+
});
|
|
1567
|
+
mainMenu.settingsMenu.addGroup([
|
|
1568
|
+
{
|
|
1569
|
+
command: CommandIDs.openConfigurationDialog
|
|
1570
|
+
}
|
|
1571
|
+
]);
|
|
1572
|
+
const getPrefixAndSuffixForActiveCell = () => {
|
|
1573
|
+
let prefix = '';
|
|
1574
|
+
let suffix = '';
|
|
1575
|
+
const currentWidget = app.shell.currentWidget;
|
|
1576
|
+
if (!(currentWidget instanceof NotebookPanel &&
|
|
1577
|
+
currentWidget.content.activeCell)) {
|
|
1578
|
+
return { prefix, suffix };
|
|
1579
|
+
}
|
|
1580
|
+
const activeCellIndex = currentWidget.content.activeCellIndex;
|
|
1581
|
+
const numCells = currentWidget.content.widgets.length;
|
|
1582
|
+
const maxContext = 0.7 * MAX_TOKENS;
|
|
1583
|
+
for (let d = 1; d < numCells; ++d) {
|
|
1584
|
+
const above = activeCellIndex - d;
|
|
1585
|
+
const below = activeCellIndex + d;
|
|
1586
|
+
if ((above < 0 && below >= numCells) ||
|
|
1587
|
+
getTokenCount(`${prefix} ${suffix}`) >= maxContext) {
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1590
|
+
if (above >= 0) {
|
|
1591
|
+
const aboveCell = currentWidget.content.widgets[above];
|
|
1592
|
+
const cellModel = aboveCell.model.sharedModel;
|
|
1593
|
+
if (cellModel.cell_type === 'code') {
|
|
1594
|
+
prefix = cellModel.source + '\n' + prefix;
|
|
1595
|
+
}
|
|
1596
|
+
else if (cellModel.cell_type === 'markdown') {
|
|
1597
|
+
prefix = markdownToComment(cellModel.source) + '\n' + prefix;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (below < numCells) {
|
|
1601
|
+
const belowCell = currentWidget.content.widgets[below];
|
|
1602
|
+
const cellModel = belowCell.model.sharedModel;
|
|
1603
|
+
if (cellModel.cell_type === 'code') {
|
|
1604
|
+
suffix += cellModel.source + '\n';
|
|
1605
|
+
}
|
|
1606
|
+
else if (cellModel.cell_type === 'markdown') {
|
|
1607
|
+
suffix += markdownToComment(cellModel.source) + '\n';
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return { prefix, suffix };
|
|
1612
|
+
};
|
|
1613
|
+
const getPrefixAndSuffixForFileEditor = () => {
|
|
1614
|
+
let prefix = '';
|
|
1615
|
+
let suffix = '';
|
|
1616
|
+
const currentWidget = app.shell.currentWidget;
|
|
1617
|
+
if (!(currentWidget instanceof FileEditorWidget)) {
|
|
1618
|
+
return { prefix, suffix };
|
|
1619
|
+
}
|
|
1620
|
+
const fe = currentWidget;
|
|
1621
|
+
const cursor = fe.content.editor.getCursorPosition();
|
|
1622
|
+
const offset = fe.content.editor.getOffsetAt(cursor);
|
|
1623
|
+
const source = fe.content.editor.model.sharedModel.getSource();
|
|
1624
|
+
prefix = source.substring(0, offset);
|
|
1625
|
+
suffix = source.substring(offset);
|
|
1626
|
+
return { prefix, suffix };
|
|
1627
|
+
};
|
|
1628
|
+
const generateCodeForCellOrFileEditor = () => {
|
|
1629
|
+
const isCodeCell = isActiveCellCodeCell();
|
|
1630
|
+
const currentWidget = app.shell.currentWidget;
|
|
1631
|
+
let editor;
|
|
1632
|
+
let codeInput = null;
|
|
1633
|
+
if (isCodeCell) {
|
|
1634
|
+
const np = currentWidget;
|
|
1635
|
+
const activeCell = np.content.activeCell;
|
|
1636
|
+
codeInput = activeCell.node.querySelector('.jp-InputArea-editor');
|
|
1637
|
+
if (!codeInput) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
editor = activeCell.editor;
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
const fe = currentWidget;
|
|
1644
|
+
editor = fe.content.editor;
|
|
1645
|
+
}
|
|
1646
|
+
const editorView = getCodeMirrorView(editor);
|
|
1647
|
+
if (!editorView) {
|
|
1648
|
+
return;
|
|
1649
|
+
}
|
|
1650
|
+
let blockPromptView = null;
|
|
1651
|
+
let removed = false;
|
|
1652
|
+
const removePopover = () => {
|
|
1653
|
+
// Cleared outside the `removed` guard so the auto-insert path's
|
|
1654
|
+
// second call still removes the class added between calls.
|
|
1655
|
+
if (isCodeCell) {
|
|
1656
|
+
codeInput === null || codeInput === void 0 ? void 0 : codeInput.classList.remove('generating');
|
|
1657
|
+
}
|
|
1658
|
+
if (removed) {
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
removed = true;
|
|
1662
|
+
closeOpenPopover = null;
|
|
1663
|
+
if (blockPromptView) {
|
|
1664
|
+
blockPromptView.dispatch({
|
|
1665
|
+
effects: removeInlinePromptEffect.of()
|
|
1666
|
+
});
|
|
1667
|
+
blockPromptView = null;
|
|
1668
|
+
}
|
|
1669
|
+
};
|
|
1670
|
+
let userPrompt = '';
|
|
1671
|
+
let existingCode = '';
|
|
1672
|
+
let generatedContent = '';
|
|
1673
|
+
let prefix = '', suffix = '';
|
|
1674
|
+
if (isCodeCell) {
|
|
1675
|
+
const ps = getPrefixAndSuffixForActiveCell();
|
|
1676
|
+
prefix = ps.prefix;
|
|
1677
|
+
suffix = ps.suffix;
|
|
1678
|
+
}
|
|
1679
|
+
else {
|
|
1680
|
+
const ps = getPrefixAndSuffixForFileEditor();
|
|
1681
|
+
prefix = ps.prefix;
|
|
1682
|
+
suffix = ps.suffix;
|
|
1683
|
+
}
|
|
1684
|
+
const selection = editor.getSelection();
|
|
1685
|
+
const startOffset = editor.getOffsetAt(selection.start);
|
|
1686
|
+
const endOffset = editor.getOffsetAt(selection.end);
|
|
1687
|
+
const source = editor.model.sharedModel.getSource();
|
|
1688
|
+
if (isCodeCell) {
|
|
1689
|
+
prefix += '\n' + source.substring(0, startOffset);
|
|
1690
|
+
existingCode = source.substring(startOffset, endOffset);
|
|
1691
|
+
suffix = source.substring(endOffset) + '\n' + suffix;
|
|
1692
|
+
}
|
|
1693
|
+
else {
|
|
1694
|
+
existingCode = source.substring(startOffset, endOffset);
|
|
1695
|
+
}
|
|
1696
|
+
const applyGeneratedCode = () => {
|
|
1697
|
+
generatedContent = extractLLMGeneratedCode(generatedContent);
|
|
1698
|
+
// extractLLMGeneratedCode preserves the newline that sits before
|
|
1699
|
+
// the closing ``` in fenced LLM output. If the user's selection
|
|
1700
|
+
// didn't already end with a newline (or there's no selection at
|
|
1701
|
+
// all in the auto-insert path), inserting that trailing \n leaves
|
|
1702
|
+
// an extra blank line below the generated code. Strip one
|
|
1703
|
+
// trailing newline in those cases so the result matches the
|
|
1704
|
+
// selection's original line-break state.
|
|
1705
|
+
if (!existingCode.endsWith('\n') && generatedContent.endsWith('\n')) {
|
|
1706
|
+
generatedContent = generatedContent.slice(0, -1);
|
|
1707
|
+
}
|
|
1708
|
+
applyCodeToSelectionInEditor(editor, generatedContent);
|
|
1709
|
+
generatedContent = '';
|
|
1710
|
+
removePopover();
|
|
1711
|
+
};
|
|
1712
|
+
closeOpenPopover === null || closeOpenPopover === void 0 ? void 0 : closeOpenPopover();
|
|
1713
|
+
const promptOptions = {
|
|
1714
|
+
prompt: userPrompt,
|
|
1715
|
+
existingCode,
|
|
1716
|
+
prefix: prefix,
|
|
1717
|
+
suffix: suffix,
|
|
1718
|
+
language: ActiveDocumentWatcher.activeDocumentInfo.language,
|
|
1719
|
+
filename: ActiveDocumentWatcher.activeDocumentInfo.filePath,
|
|
1720
|
+
onRequestSubmitted: (prompt) => {
|
|
1721
|
+
userPrompt = prompt;
|
|
1722
|
+
generatedContent = '';
|
|
1723
|
+
if (existingCode !== '') {
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
removePopover();
|
|
1727
|
+
if (isCodeCell) {
|
|
1728
|
+
codeInput === null || codeInput === void 0 ? void 0 : codeInput.classList.add('generating');
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
onRequestCancelled: () => {
|
|
1732
|
+
removePopover();
|
|
1733
|
+
editor.focus();
|
|
1734
|
+
},
|
|
1735
|
+
onContentStream: (content) => {
|
|
1736
|
+
if (existingCode !== '') {
|
|
1737
|
+
return;
|
|
1738
|
+
}
|
|
1739
|
+
generatedContent += content;
|
|
1740
|
+
},
|
|
1741
|
+
onContentStreamEnd: (streamError) => {
|
|
1742
|
+
if (existingCode !== '') {
|
|
1743
|
+
return;
|
|
1744
|
+
}
|
|
1745
|
+
if (streamError) {
|
|
1746
|
+
// The backend tagged this stream as interrupted. Discard the
|
|
1747
|
+
// partial buffer rather than auto-inserting truncated code (or
|
|
1748
|
+
// worse, the [Stream interrupted] marker text itself) into the
|
|
1749
|
+
// user's cell, and surface the failure as a toast.
|
|
1750
|
+
generatedContent = '';
|
|
1751
|
+
removePopover();
|
|
1752
|
+
app.commands.execute('apputils:notify', {
|
|
1753
|
+
message: `Inline chat failed: ${streamError}`,
|
|
1754
|
+
type: 'error',
|
|
1755
|
+
options: { autoClose: true }
|
|
1756
|
+
});
|
|
1757
|
+
editor.focus();
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
applyGeneratedCode();
|
|
1761
|
+
editor.focus();
|
|
1762
|
+
},
|
|
1763
|
+
onUpdatedCodeChange: (content) => {
|
|
1764
|
+
generatedContent = content;
|
|
1765
|
+
},
|
|
1766
|
+
onUpdatedCodeAccepted: () => {
|
|
1767
|
+
applyGeneratedCode();
|
|
1768
|
+
editor.focus();
|
|
1769
|
+
},
|
|
1770
|
+
telemetryEmitter: telemetryEmitter
|
|
1771
|
+
};
|
|
1772
|
+
let requestTime = null;
|
|
1773
|
+
let streamError = null;
|
|
1774
|
+
let blockPromptNode = null;
|
|
1775
|
+
const onRequestSubmitted = (prompt) => {
|
|
1776
|
+
if (blockPromptNode && existingCode !== '') {
|
|
1777
|
+
blockPromptNode.style.height = '300px';
|
|
1778
|
+
}
|
|
1779
|
+
promptOptions.prompt = prompt;
|
|
1780
|
+
promptOptions.onRequestSubmitted(prompt);
|
|
1781
|
+
requestTime = new Date();
|
|
1782
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1783
|
+
type: TelemetryEventType.InlineChatRequest,
|
|
1784
|
+
data: {
|
|
1785
|
+
chatModel: {
|
|
1786
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1787
|
+
model: NBIAPI.config.chatModel.model
|
|
1788
|
+
},
|
|
1789
|
+
prompt: prompt
|
|
1790
|
+
}
|
|
1791
|
+
});
|
|
1792
|
+
};
|
|
1793
|
+
const onResponseEmit = (response) => {
|
|
1794
|
+
var _a, _b, _c, _d;
|
|
1795
|
+
if (response.type === BackendMessageType.StreamMessage) {
|
|
1796
|
+
if (typeof ((_a = response.data) === null || _a === void 0 ? void 0 : _a.nbi_stream_error) === 'string') {
|
|
1797
|
+
streamError = response.data.nbi_stream_error;
|
|
1798
|
+
}
|
|
1799
|
+
const responseMessage = (_d = (_c = (_b = response.data['choices']) === null || _b === void 0 ? void 0 : _b[0]) === null || _c === void 0 ? void 0 : _c['delta']) === null || _d === void 0 ? void 0 : _d['content'];
|
|
1800
|
+
if (responseMessage) {
|
|
1801
|
+
promptOptions.onContentStream(responseMessage);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
else if (response.type === BackendMessageType.StreamEnd) {
|
|
1805
|
+
promptOptions.onContentStreamEnd(streamError);
|
|
1806
|
+
streamError = null;
|
|
1807
|
+
const timeElapsed = requestTime === null
|
|
1808
|
+
? 0
|
|
1809
|
+
: (new Date().getTime() - requestTime.getTime()) / 1000;
|
|
1810
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1811
|
+
type: TelemetryEventType.InlineChatResponse,
|
|
1812
|
+
data: {
|
|
1813
|
+
chatModel: {
|
|
1814
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1815
|
+
model: NBIAPI.config.chatModel.model
|
|
1816
|
+
},
|
|
1817
|
+
timeElapsed
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
// Handed up by InlinePopoverComponent on mount so this scope can
|
|
1823
|
+
// cancel the in-flight WS request from non-React dismissal paths
|
|
1824
|
+
// (focus-leave) without going through onRequestCancelled (which
|
|
1825
|
+
// would also yank focus back to the editor).
|
|
1826
|
+
let cancelInflightRequest = null;
|
|
1827
|
+
const widget = new InlinePromptBlockWidget(React.createElement(InlinePopoverComponent, {
|
|
1828
|
+
prompt: promptOptions.prompt,
|
|
1829
|
+
existingCode: promptOptions.existingCode,
|
|
1830
|
+
onRequestSubmitted,
|
|
1831
|
+
onRequestCancelled: promptOptions.onRequestCancelled,
|
|
1832
|
+
onResponseEmit,
|
|
1833
|
+
prefix: promptOptions.prefix,
|
|
1834
|
+
suffix: promptOptions.suffix,
|
|
1835
|
+
language: promptOptions.language,
|
|
1836
|
+
filename: promptOptions.filename,
|
|
1837
|
+
onUpdatedCodeChange: promptOptions.onUpdatedCodeChange,
|
|
1838
|
+
onUpdatedCodeAccepted: promptOptions.onUpdatedCodeAccepted,
|
|
1839
|
+
registerCancel: (fn) => {
|
|
1840
|
+
cancelInflightRequest = fn;
|
|
1841
|
+
}
|
|
1842
|
+
}), node => {
|
|
1843
|
+
blockPromptNode = node;
|
|
1844
|
+
},
|
|
1845
|
+
// Focus-leave dismissal: cancel the request so the backend stops
|
|
1846
|
+
// streaming, but don't call onRequestCancelled — that would steal
|
|
1847
|
+
// focus back from whatever the user clicked.
|
|
1848
|
+
() => {
|
|
1849
|
+
cancelInflightRequest === null || cancelInflightRequest === void 0 ? void 0 : cancelInflightRequest();
|
|
1850
|
+
removePopover();
|
|
1851
|
+
});
|
|
1852
|
+
const anchorOffset = getLineEndOffset(editor, Math.max(startOffset, endOffset));
|
|
1853
|
+
ensureInlinePromptExtension(editorView);
|
|
1854
|
+
blockPromptView = editorView;
|
|
1855
|
+
// Replace-on-reopen path: when a second Ctrl+G fires while this
|
|
1856
|
+
// popover is still here, cancel our in-flight request before the
|
|
1857
|
+
// widget is torn down so the backend stops streaming for the
|
|
1858
|
+
// discarded prompt.
|
|
1859
|
+
closeOpenPopover = () => {
|
|
1860
|
+
cancelInflightRequest === null || cancelInflightRequest === void 0 ? void 0 : cancelInflightRequest();
|
|
1861
|
+
removePopover();
|
|
1862
|
+
};
|
|
1863
|
+
editorView.dispatch({
|
|
1864
|
+
effects: [
|
|
1865
|
+
addInlinePromptEffect.of({
|
|
1866
|
+
pos: anchorOffset,
|
|
1867
|
+
widget
|
|
1868
|
+
}),
|
|
1869
|
+
EditorView.scrollIntoView(anchorOffset, { y: 'center' })
|
|
1870
|
+
]
|
|
1871
|
+
});
|
|
1872
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1873
|
+
type: TelemetryEventType.GenerateCodeRequest,
|
|
1874
|
+
data: {
|
|
1875
|
+
chatModel: {
|
|
1876
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1877
|
+
model: NBIAPI.config.chatModel.model
|
|
1878
|
+
},
|
|
1879
|
+
editorType: isCodeCell ? 'notebook' : 'file-editor'
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
};
|
|
1883
|
+
const generateCellCodeCommand = {
|
|
1884
|
+
execute: args => {
|
|
1885
|
+
generateCodeForCellOrFileEditor();
|
|
1886
|
+
},
|
|
1887
|
+
label: 'Generate code',
|
|
1888
|
+
isEnabled: () => isChatEnabled() &&
|
|
1889
|
+
(isActiveCellCodeCell() || isCurrentWidgetFileEditor())
|
|
1890
|
+
};
|
|
1891
|
+
app.commands.addCommand(CommandIDs.editorGenerateCode, generateCellCodeCommand);
|
|
1892
|
+
const copilotMenuCommands = new CommandRegistry();
|
|
1893
|
+
copilotMenuCommands.addCommand(CommandIDs.editorGenerateCode, generateCellCodeCommand);
|
|
1894
|
+
copilotMenuCommands.addCommand(CommandIDs.editorExplainThisCode, {
|
|
1895
|
+
execute: () => {
|
|
1896
|
+
const np = app.shell.currentWidget;
|
|
1897
|
+
const activeCell = np.content.activeCell;
|
|
1898
|
+
const content = (activeCell === null || activeCell === void 0 ? void 0 : activeCell.model.sharedModel.source) || '';
|
|
1899
|
+
document.dispatchEvent(new CustomEvent('copilotSidebar:runPrompt', {
|
|
1900
|
+
detail: {
|
|
1901
|
+
type: RunChatCompletionType.ExplainThis,
|
|
1902
|
+
content,
|
|
1903
|
+
language: ActiveDocumentWatcher.activeDocumentInfo.language,
|
|
1904
|
+
filename: ActiveDocumentWatcher.activeDocumentInfo.filename
|
|
1905
|
+
}
|
|
1906
|
+
}));
|
|
1907
|
+
app.commands.execute('tabsmenu:activate-by-id', { id: panel.id });
|
|
1908
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1909
|
+
type: TelemetryEventType.ExplainThisRequest,
|
|
1910
|
+
data: {
|
|
1911
|
+
chatModel: {
|
|
1912
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1913
|
+
model: NBIAPI.config.chatModel.model
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
},
|
|
1918
|
+
label: 'Explain code',
|
|
1919
|
+
isEnabled: () => isChatEnabled() && isActiveCellCodeCell()
|
|
1920
|
+
});
|
|
1921
|
+
copilotMenuCommands.addCommand(CommandIDs.editorFixThisCode, {
|
|
1922
|
+
execute: () => {
|
|
1923
|
+
const np = app.shell.currentWidget;
|
|
1924
|
+
const activeCell = np.content.activeCell;
|
|
1925
|
+
const content = (activeCell === null || activeCell === void 0 ? void 0 : activeCell.model.sharedModel.source) || '';
|
|
1926
|
+
document.dispatchEvent(new CustomEvent('copilotSidebar:runPrompt', {
|
|
1927
|
+
detail: {
|
|
1928
|
+
type: RunChatCompletionType.FixThis,
|
|
1929
|
+
content,
|
|
1930
|
+
language: ActiveDocumentWatcher.activeDocumentInfo.language,
|
|
1931
|
+
filename: ActiveDocumentWatcher.activeDocumentInfo.filename
|
|
1932
|
+
}
|
|
1933
|
+
}));
|
|
1934
|
+
app.commands.execute('tabsmenu:activate-by-id', { id: panel.id });
|
|
1935
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1936
|
+
type: TelemetryEventType.FixThisCodeRequest,
|
|
1937
|
+
data: {
|
|
1938
|
+
chatModel: {
|
|
1939
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1940
|
+
model: NBIAPI.config.chatModel.model
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
},
|
|
1945
|
+
label: 'Fix code',
|
|
1946
|
+
isEnabled: () => isChatEnabled() && isActiveCellCodeCell()
|
|
1947
|
+
});
|
|
1948
|
+
const registerOutputContextCommand = (opts) => {
|
|
1949
|
+
const isFlagOn = () => !opts.featureFlag ||
|
|
1950
|
+
NBIAPI.config.cellOutputFeatures[opts.featureFlag].enabled;
|
|
1951
|
+
copilotMenuCommands.addCommand(opts.commandId, {
|
|
1952
|
+
execute: () => {
|
|
1953
|
+
const np = app.shell.currentWidget;
|
|
1954
|
+
const activeCell = np.content.activeCell;
|
|
1955
|
+
if (!(activeCell instanceof CodeCell)) {
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
const outputContext = cellOutputAsContextBundle(activeCell, { supportsVision: NBIAPI.config.chatModelSupportsVision });
|
|
1959
|
+
document.dispatchEvent(new CustomEvent('copilotSidebar:addOutputContext', {
|
|
1960
|
+
detail: {
|
|
1961
|
+
outputContext,
|
|
1962
|
+
cellIndex: np.content.activeCellIndex,
|
|
1963
|
+
notebookFilename: np.sessionContext.name,
|
|
1964
|
+
cellId: activeCell.model.id,
|
|
1965
|
+
autoSubmitPrompt: opts.autoSubmitPrompt
|
|
1966
|
+
}
|
|
1967
|
+
}));
|
|
1968
|
+
app.commands.execute('tabsmenu:activate-by-id', { id: panel.id });
|
|
1969
|
+
telemetryEmitter.emitTelemetryEvent({
|
|
1970
|
+
type: opts.telemetryType,
|
|
1971
|
+
data: {
|
|
1972
|
+
chatModel: {
|
|
1973
|
+
provider: NBIAPI.config.chatModel.provider,
|
|
1974
|
+
model: NBIAPI.config.chatModel.model
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
});
|
|
1978
|
+
},
|
|
1979
|
+
label: opts.label,
|
|
1980
|
+
isEnabled: () => {
|
|
1981
|
+
if (!(isChatEnabled() &&
|
|
1982
|
+
app.shell.currentWidget instanceof NotebookPanel)) {
|
|
1983
|
+
return false;
|
|
1984
|
+
}
|
|
1985
|
+
if (!isFlagOn()) {
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
const np = app.shell.currentWidget;
|
|
1989
|
+
const activeCell = np.content.activeCell;
|
|
1990
|
+
if (!(activeCell instanceof CodeCell)) {
|
|
1991
|
+
return false;
|
|
1992
|
+
}
|
|
1993
|
+
if (activeCell.outputArea.model.length === 0) {
|
|
1994
|
+
return false;
|
|
1995
|
+
}
|
|
1996
|
+
if (opts.requireError) {
|
|
1997
|
+
return cellOutputHasError(activeCell);
|
|
1998
|
+
}
|
|
1999
|
+
return true;
|
|
2000
|
+
},
|
|
2001
|
+
isVisible: opts.featureFlag ? isFlagOn : undefined
|
|
2002
|
+
});
|
|
2003
|
+
};
|
|
2004
|
+
registerOutputContextCommand({
|
|
2005
|
+
commandId: CommandIDs.editorExplainThisOutput,
|
|
2006
|
+
label: 'Explain output',
|
|
2007
|
+
telemetryType: TelemetryEventType.ExplainThisOutputRequest,
|
|
2008
|
+
autoSubmitPrompt: "Explain this cell's output.",
|
|
2009
|
+
featureFlag: 'output_followup'
|
|
2010
|
+
});
|
|
2011
|
+
registerOutputContextCommand({
|
|
2012
|
+
commandId: CommandIDs.editorAskAboutThisOutput,
|
|
2013
|
+
label: 'Ask about this output',
|
|
2014
|
+
telemetryType: TelemetryEventType.OutputFollowUpRequest,
|
|
2015
|
+
featureFlag: 'output_followup'
|
|
2016
|
+
});
|
|
2017
|
+
registerOutputContextCommand({
|
|
2018
|
+
commandId: CommandIDs.editorTroubleshootThisOutput,
|
|
2019
|
+
label: 'Troubleshoot errors in output',
|
|
2020
|
+
telemetryType: TelemetryEventType.TroubleshootThisOutputRequest,
|
|
2021
|
+
autoSubmitPrompt: "Troubleshoot the error in this cell's output.",
|
|
2022
|
+
featureFlag: 'explain_error',
|
|
2023
|
+
requireError: true
|
|
2024
|
+
});
|
|
2025
|
+
const copilotContextMenu = new Menu({ commands: copilotMenuCommands });
|
|
2026
|
+
copilotContextMenu.id = 'notebook-intelligence:editor-context-menu';
|
|
2027
|
+
copilotContextMenu.title.label = 'Notebook Intelligence';
|
|
2028
|
+
copilotContextMenu.title.icon = sidebarIcon;
|
|
2029
|
+
copilotContextMenu.addItem({ command: CommandIDs.editorGenerateCode });
|
|
2030
|
+
copilotContextMenu.addItem({ command: CommandIDs.editorExplainThisCode });
|
|
2031
|
+
copilotContextMenu.addItem({ command: CommandIDs.editorFixThisCode });
|
|
2032
|
+
copilotContextMenu.addItem({ command: CommandIDs.editorExplainThisOutput });
|
|
2033
|
+
copilotContextMenu.addItem({
|
|
2034
|
+
command: CommandIDs.editorAskAboutThisOutput
|
|
2035
|
+
});
|
|
2036
|
+
copilotContextMenu.addItem({
|
|
2037
|
+
command: CommandIDs.editorTroubleshootThisOutput
|
|
2038
|
+
});
|
|
2039
|
+
app.contextMenu.addItem({
|
|
2040
|
+
type: 'submenu',
|
|
2041
|
+
submenu: copilotContextMenu,
|
|
2042
|
+
selector: '.jp-Editor',
|
|
2043
|
+
rank: 1
|
|
2044
|
+
});
|
|
2045
|
+
app.contextMenu.addItem({
|
|
2046
|
+
type: 'submenu',
|
|
2047
|
+
submenu: copilotContextMenu,
|
|
2048
|
+
selector: '.jp-OutputArea-child',
|
|
2049
|
+
rank: 1
|
|
2050
|
+
});
|
|
2051
|
+
new CellOutputHoverToolbar(app, copilotMenuCommands);
|
|
2052
|
+
if (statusBar) {
|
|
2053
|
+
const githubCopilotStatusBarItem = new GitHubCopilotStatusBarItem({
|
|
2054
|
+
getApp: () => app
|
|
2055
|
+
});
|
|
2056
|
+
statusBar.registerStatusItem('notebook-intelligence:github-copilot-status', {
|
|
2057
|
+
item: githubCopilotStatusBarItem,
|
|
2058
|
+
align: 'right',
|
|
2059
|
+
rank: 100,
|
|
2060
|
+
isActive: () => !NBIAPI.config.isInClaudeCodeMode &&
|
|
2061
|
+
NBIAPI.config.usingGitHubCopilotModel
|
|
2062
|
+
});
|
|
2063
|
+
NBIAPI.configChanged.connect(() => {
|
|
2064
|
+
if (!NBIAPI.config.isInClaudeCodeMode &&
|
|
2065
|
+
NBIAPI.config.usingGitHubCopilotModel) {
|
|
2066
|
+
githubCopilotStatusBarItem.show();
|
|
2067
|
+
}
|
|
2068
|
+
else {
|
|
2069
|
+
githubCopilotStatusBarItem.hide();
|
|
2070
|
+
}
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
const jlabApp = app;
|
|
2074
|
+
ActiveDocumentWatcher.initialize(jlabApp, languageRegistry, defaultBrowser);
|
|
2075
|
+
return extensionService;
|
|
2076
|
+
}
|
|
2077
|
+
};
|
|
2078
|
+
export * from './tokens';
|
|
2079
|
+
export default plugin;
|