@plmbr/notebook-intelligence 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +412 -0
  3. package/lib/api.d.ts +288 -0
  4. package/lib/api.js +927 -0
  5. package/lib/cell-output-bundle.d.ts +25 -0
  6. package/lib/cell-output-bundle.js +129 -0
  7. package/lib/cell-output-toolbar.d.ts +26 -0
  8. package/lib/cell-output-toolbar.js +188 -0
  9. package/lib/chat-progress-feedback.d.ts +3 -0
  10. package/lib/chat-progress-feedback.js +27 -0
  11. package/lib/chat-sidebar.d.ts +92 -0
  12. package/lib/chat-sidebar.js +3452 -0
  13. package/lib/command-ids.d.ts +39 -0
  14. package/lib/command-ids.js +44 -0
  15. package/lib/components/ask-user-question.d.ts +2 -0
  16. package/lib/components/ask-user-question.js +85 -0
  17. package/lib/components/checkbox.d.ts +2 -0
  18. package/lib/components/checkbox.js +30 -0
  19. package/lib/components/claude-mcp-panel.d.ts +2 -0
  20. package/lib/components/claude-mcp-panel.js +275 -0
  21. package/lib/components/claude-mcp-paste.d.ts +7 -0
  22. package/lib/components/claude-mcp-paste.js +104 -0
  23. package/lib/components/claude-session-picker.d.ts +8 -0
  24. package/lib/components/claude-session-picker.js +127 -0
  25. package/lib/components/form-dialog.d.ts +25 -0
  26. package/lib/components/form-dialog.js +35 -0
  27. package/lib/components/launcher-picker.d.ts +6 -0
  28. package/lib/components/launcher-picker.js +135 -0
  29. package/lib/components/mcp-util.d.ts +2 -0
  30. package/lib/components/mcp-util.js +37 -0
  31. package/lib/components/notebook-generation-popover.d.ts +7 -0
  32. package/lib/components/notebook-generation-popover.js +60 -0
  33. package/lib/components/pill.d.ts +2 -0
  34. package/lib/components/pill.js +5 -0
  35. package/lib/components/plugins-panel.d.ts +3 -0
  36. package/lib/components/plugins-panel.js +466 -0
  37. package/lib/components/settings-panel.d.ts +11 -0
  38. package/lib/components/settings-panel.js +742 -0
  39. package/lib/components/skills-panel.d.ts +2 -0
  40. package/lib/components/skills-panel.js +1264 -0
  41. package/lib/handler.d.ts +8 -0
  42. package/lib/handler.js +36 -0
  43. package/lib/icons.d.ts +45 -0
  44. package/lib/icons.js +54 -0
  45. package/lib/index.d.ts +8 -0
  46. package/lib/index.js +2079 -0
  47. package/lib/markdown-renderer.d.ts +10 -0
  48. package/lib/markdown-renderer.js +64 -0
  49. package/lib/notebook-generation-toolbar.d.ts +16 -0
  50. package/lib/notebook-generation-toolbar.js +197 -0
  51. package/lib/notebook-generation.d.ts +8 -0
  52. package/lib/notebook-generation.js +12 -0
  53. package/lib/open-file-refresh-watcher-env.d.ts +4 -0
  54. package/lib/open-file-refresh-watcher-env.js +33 -0
  55. package/lib/open-file-refresh-watcher.d.ts +97 -0
  56. package/lib/open-file-refresh-watcher.js +190 -0
  57. package/lib/shell-utils.d.ts +6 -0
  58. package/lib/shell-utils.js +9 -0
  59. package/lib/task-target-notebook.d.ts +2 -0
  60. package/lib/task-target-notebook.js +28 -0
  61. package/lib/terminal-drag-format.d.ts +9 -0
  62. package/lib/terminal-drag-format.js +23 -0
  63. package/lib/terminal-drag.d.ts +12 -0
  64. package/lib/terminal-drag.js +268 -0
  65. package/lib/tokens.d.ts +149 -0
  66. package/lib/tokens.js +88 -0
  67. package/lib/tour/tour-anchors.d.ts +18 -0
  68. package/lib/tour/tour-anchors.js +18 -0
  69. package/lib/tour/tour-config.d.ts +66 -0
  70. package/lib/tour/tour-config.js +99 -0
  71. package/lib/tour/tour-defaults.json +58 -0
  72. package/lib/tour/tour-events.d.ts +19 -0
  73. package/lib/tour/tour-events.js +30 -0
  74. package/lib/tour/tour-overlay.d.ts +6 -0
  75. package/lib/tour/tour-overlay.js +350 -0
  76. package/lib/tour/tour-state.d.ts +20 -0
  77. package/lib/tour/tour-state.js +81 -0
  78. package/lib/tour/tour-steps.d.ts +33 -0
  79. package/lib/tour/tour-steps.js +216 -0
  80. package/lib/utils.d.ts +53 -0
  81. package/lib/utils.js +385 -0
  82. package/package.json +258 -0
  83. package/schema/plugin.json +42 -0
  84. package/src/api.ts +1424 -0
  85. package/src/cell-output-bundle.ts +176 -0
  86. package/src/cell-output-toolbar.ts +232 -0
  87. package/src/chat-progress-feedback.ts +35 -0
  88. package/src/chat-sidebar.tsx +5147 -0
  89. package/src/command-ids.ts +67 -0
  90. package/src/components/ask-user-question.tsx +151 -0
  91. package/src/components/checkbox.tsx +62 -0
  92. package/src/components/claude-mcp-panel.tsx +543 -0
  93. package/src/components/claude-mcp-paste.ts +132 -0
  94. package/src/components/claude-session-picker.tsx +214 -0
  95. package/src/components/form-dialog.tsx +75 -0
  96. package/src/components/launcher-picker.tsx +237 -0
  97. package/src/components/mcp-util.ts +53 -0
  98. package/src/components/notebook-generation-popover.tsx +127 -0
  99. package/src/components/pill.tsx +15 -0
  100. package/src/components/plugins-panel.tsx +774 -0
  101. package/src/components/settings-panel.tsx +1631 -0
  102. package/src/components/skills-panel.tsx +2084 -0
  103. package/src/handler.ts +51 -0
  104. package/src/icons.ts +71 -0
  105. package/src/index.ts +2583 -0
  106. package/src/markdown-renderer.tsx +153 -0
  107. package/src/notebook-generation-toolbar.tsx +281 -0
  108. package/src/notebook-generation.ts +23 -0
  109. package/src/open-file-refresh-watcher-env.ts +52 -0
  110. package/src/open-file-refresh-watcher.ts +260 -0
  111. package/src/shell-utils.ts +10 -0
  112. package/src/svg.d.ts +4 -0
  113. package/src/task-target-notebook.ts +37 -0
  114. package/src/terminal-drag-format.ts +29 -0
  115. package/src/terminal-drag.ts +382 -0
  116. package/src/tokens.ts +171 -0
  117. package/src/tour/tour-anchors.ts +21 -0
  118. package/src/tour/tour-config.ts +160 -0
  119. package/src/tour/tour-events.ts +34 -0
  120. package/src/tour/tour-overlay.tsx +474 -0
  121. package/src/tour/tour-state.ts +87 -0
  122. package/src/tour/tour-steps.ts +281 -0
  123. package/src/utils.ts +455 -0
  124. package/style/base.css +3238 -0
  125. package/style/icons/cell-toolbar-bug.svg +5 -0
  126. package/style/icons/cell-toolbar-chat.svg +5 -0
  127. package/style/icons/cell-toolbar-sparkle.svg +5 -0
  128. package/style/icons/claude.svg +1 -0
  129. package/style/icons/copilot-warning.svg +1 -0
  130. package/style/icons/copilot.svg +1 -0
  131. package/style/icons/copy.svg +1 -0
  132. package/style/icons/openai.svg +1 -0
  133. package/style/icons/opencode.svg +1 -0
  134. package/style/icons/sparkles-warning.svg +5 -0
  135. package/style/icons/sparkles.svg +1 -0
  136. package/style/index.css +1 -0
  137. package/style/index.js +1 -0
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;