@plmbr/notebook-intelligence 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/README.md +412 -0
- package/lib/api.d.ts +288 -0
- package/lib/api.js +927 -0
- package/lib/cell-output-bundle.d.ts +25 -0
- package/lib/cell-output-bundle.js +129 -0
- package/lib/cell-output-toolbar.d.ts +26 -0
- package/lib/cell-output-toolbar.js +188 -0
- package/lib/chat-progress-feedback.d.ts +3 -0
- package/lib/chat-progress-feedback.js +27 -0
- package/lib/chat-sidebar.d.ts +92 -0
- package/lib/chat-sidebar.js +3452 -0
- package/lib/command-ids.d.ts +39 -0
- package/lib/command-ids.js +44 -0
- package/lib/components/ask-user-question.d.ts +2 -0
- package/lib/components/ask-user-question.js +85 -0
- package/lib/components/checkbox.d.ts +2 -0
- package/lib/components/checkbox.js +30 -0
- package/lib/components/claude-mcp-panel.d.ts +2 -0
- package/lib/components/claude-mcp-panel.js +275 -0
- package/lib/components/claude-mcp-paste.d.ts +7 -0
- package/lib/components/claude-mcp-paste.js +104 -0
- package/lib/components/claude-session-picker.d.ts +8 -0
- package/lib/components/claude-session-picker.js +127 -0
- package/lib/components/form-dialog.d.ts +25 -0
- package/lib/components/form-dialog.js +35 -0
- package/lib/components/launcher-picker.d.ts +6 -0
- package/lib/components/launcher-picker.js +135 -0
- package/lib/components/mcp-util.d.ts +2 -0
- package/lib/components/mcp-util.js +37 -0
- package/lib/components/notebook-generation-popover.d.ts +7 -0
- package/lib/components/notebook-generation-popover.js +60 -0
- package/lib/components/pill.d.ts +2 -0
- package/lib/components/pill.js +5 -0
- package/lib/components/plugins-panel.d.ts +3 -0
- package/lib/components/plugins-panel.js +466 -0
- package/lib/components/settings-panel.d.ts +11 -0
- package/lib/components/settings-panel.js +742 -0
- package/lib/components/skills-panel.d.ts +2 -0
- package/lib/components/skills-panel.js +1264 -0
- package/lib/handler.d.ts +8 -0
- package/lib/handler.js +36 -0
- package/lib/icons.d.ts +45 -0
- package/lib/icons.js +54 -0
- package/lib/index.d.ts +8 -0
- package/lib/index.js +2079 -0
- package/lib/markdown-renderer.d.ts +10 -0
- package/lib/markdown-renderer.js +64 -0
- package/lib/notebook-generation-toolbar.d.ts +16 -0
- package/lib/notebook-generation-toolbar.js +197 -0
- package/lib/notebook-generation.d.ts +8 -0
- package/lib/notebook-generation.js +12 -0
- package/lib/open-file-refresh-watcher-env.d.ts +4 -0
- package/lib/open-file-refresh-watcher-env.js +33 -0
- package/lib/open-file-refresh-watcher.d.ts +97 -0
- package/lib/open-file-refresh-watcher.js +190 -0
- package/lib/shell-utils.d.ts +6 -0
- package/lib/shell-utils.js +9 -0
- package/lib/task-target-notebook.d.ts +2 -0
- package/lib/task-target-notebook.js +28 -0
- package/lib/terminal-drag-format.d.ts +9 -0
- package/lib/terminal-drag-format.js +23 -0
- package/lib/terminal-drag.d.ts +12 -0
- package/lib/terminal-drag.js +268 -0
- package/lib/tokens.d.ts +149 -0
- package/lib/tokens.js +88 -0
- package/lib/tour/tour-anchors.d.ts +18 -0
- package/lib/tour/tour-anchors.js +18 -0
- package/lib/tour/tour-config.d.ts +66 -0
- package/lib/tour/tour-config.js +99 -0
- package/lib/tour/tour-defaults.json +58 -0
- package/lib/tour/tour-events.d.ts +19 -0
- package/lib/tour/tour-events.js +30 -0
- package/lib/tour/tour-overlay.d.ts +6 -0
- package/lib/tour/tour-overlay.js +350 -0
- package/lib/tour/tour-state.d.ts +20 -0
- package/lib/tour/tour-state.js +81 -0
- package/lib/tour/tour-steps.d.ts +33 -0
- package/lib/tour/tour-steps.js +216 -0
- package/lib/utils.d.ts +53 -0
- package/lib/utils.js +385 -0
- package/package.json +258 -0
- package/schema/plugin.json +42 -0
- package/src/api.ts +1424 -0
- package/src/cell-output-bundle.ts +176 -0
- package/src/cell-output-toolbar.ts +232 -0
- package/src/chat-progress-feedback.ts +35 -0
- package/src/chat-sidebar.tsx +5147 -0
- package/src/command-ids.ts +67 -0
- package/src/components/ask-user-question.tsx +151 -0
- package/src/components/checkbox.tsx +62 -0
- package/src/components/claude-mcp-panel.tsx +543 -0
- package/src/components/claude-mcp-paste.ts +132 -0
- package/src/components/claude-session-picker.tsx +214 -0
- package/src/components/form-dialog.tsx +75 -0
- package/src/components/launcher-picker.tsx +237 -0
- package/src/components/mcp-util.ts +53 -0
- package/src/components/notebook-generation-popover.tsx +127 -0
- package/src/components/pill.tsx +15 -0
- package/src/components/plugins-panel.tsx +774 -0
- package/src/components/settings-panel.tsx +1631 -0
- package/src/components/skills-panel.tsx +2084 -0
- package/src/handler.ts +51 -0
- package/src/icons.ts +71 -0
- package/src/index.ts +2583 -0
- package/src/markdown-renderer.tsx +153 -0
- package/src/notebook-generation-toolbar.tsx +281 -0
- package/src/notebook-generation.ts +23 -0
- package/src/open-file-refresh-watcher-env.ts +52 -0
- package/src/open-file-refresh-watcher.ts +260 -0
- package/src/shell-utils.ts +10 -0
- package/src/svg.d.ts +4 -0
- package/src/task-target-notebook.ts +37 -0
- package/src/terminal-drag-format.ts +29 -0
- package/src/terminal-drag.ts +382 -0
- package/src/tokens.ts +171 -0
- package/src/tour/tour-anchors.ts +21 -0
- package/src/tour/tour-config.ts +160 -0
- package/src/tour/tour-events.ts +34 -0
- package/src/tour/tour-overlay.tsx +474 -0
- package/src/tour/tour-state.ts +87 -0
- package/src/tour/tour-steps.ts +281 -0
- package/src/utils.ts +455 -0
- package/style/base.css +3238 -0
- package/style/icons/cell-toolbar-bug.svg +5 -0
- package/style/icons/cell-toolbar-chat.svg +5 -0
- package/style/icons/cell-toolbar-sparkle.svg +5 -0
- package/style/icons/claude.svg +1 -0
- package/style/icons/copilot-warning.svg +1 -0
- package/style/icons/copilot.svg +1 -0
- package/style/icons/copy.svg +1 -0
- package/style/icons/openai.svg +1 -0
- package/style/icons/opencode.svg +1 -0
- package/style/icons/sparkles-warning.svg +5 -0
- package/style/icons/sparkles.svg +1 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import Markdown from 'react-markdown';
|
|
5
|
+
import remarkGfm from 'remark-gfm';
|
|
6
|
+
import { Prism as SyntaxHighlighterBase } from 'react-syntax-highlighter';
|
|
7
|
+
const SyntaxHighlighter =
|
|
8
|
+
SyntaxHighlighterBase as unknown as React.ComponentType<any>;
|
|
9
|
+
import {
|
|
10
|
+
oneLight,
|
|
11
|
+
oneDark
|
|
12
|
+
} from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
13
|
+
import { VscNewFile, VscInsert, VscCopy, VscNotebook, VscAdd } from './icons';
|
|
14
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
15
|
+
import { isDarkTheme, writeTextToClipboard } from './utils';
|
|
16
|
+
import { IActiveDocumentInfo } from './tokens';
|
|
17
|
+
|
|
18
|
+
type MarkdownRendererProps = {
|
|
19
|
+
children: string;
|
|
20
|
+
getApp: () => JupyterFrontEnd;
|
|
21
|
+
getActiveDocumentInfo(): IActiveDocumentInfo;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function MarkdownRenderer({
|
|
25
|
+
children: markdown,
|
|
26
|
+
getApp,
|
|
27
|
+
getActiveDocumentInfo
|
|
28
|
+
}: MarkdownRendererProps) {
|
|
29
|
+
const app = getApp();
|
|
30
|
+
const activeDocumentInfo = getActiveDocumentInfo();
|
|
31
|
+
const isNotebook = activeDocumentInfo.filename.endsWith('.ipynb');
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Markdown
|
|
35
|
+
remarkPlugins={[remarkGfm]}
|
|
36
|
+
components={{
|
|
37
|
+
code({ node, inline, className, children, getApp, ...props }: any) {
|
|
38
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
39
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
40
|
+
const language = match ? match[1] : 'text';
|
|
41
|
+
|
|
42
|
+
const handleCopyClick = () => {
|
|
43
|
+
void writeTextToClipboard(codeString);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleInsertAtCursorClick = () => {
|
|
47
|
+
app.commands.execute('notebook-intelligence:insert-at-cursor', {
|
|
48
|
+
language,
|
|
49
|
+
code: codeString
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleAddCodeAsNewCell = () => {
|
|
54
|
+
app.commands.execute('notebook-intelligence:add-code-as-new-cell', {
|
|
55
|
+
language,
|
|
56
|
+
code: codeString
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const handleCreateNewFileClick = () => {
|
|
61
|
+
app.commands.execute('notebook-intelligence:create-new-file', {
|
|
62
|
+
language,
|
|
63
|
+
code: codeString
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleCreateNewNotebookClick = () => {
|
|
68
|
+
app.commands.execute(
|
|
69
|
+
'notebook-intelligence:create-new-notebook-from-py',
|
|
70
|
+
{ language, code: codeString }
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (inline || !match) {
|
|
75
|
+
return (
|
|
76
|
+
<code className={className} {...props}>
|
|
77
|
+
{children}
|
|
78
|
+
</code>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<div>
|
|
83
|
+
<div className="code-block-header">
|
|
84
|
+
<div className="code-block-header-language">
|
|
85
|
+
<span>{language}</span>
|
|
86
|
+
</div>
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
className="code-block-header-button"
|
|
90
|
+
onClick={() => handleCopyClick()}
|
|
91
|
+
aria-label="Copy code to clipboard"
|
|
92
|
+
>
|
|
93
|
+
<VscCopy size={16} aria-hidden="true" />
|
|
94
|
+
<span>Copy</span>
|
|
95
|
+
</button>
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
className="code-block-header-button"
|
|
99
|
+
onClick={() => handleInsertAtCursorClick()}
|
|
100
|
+
aria-label="Insert code at cursor"
|
|
101
|
+
title="Insert at cursor"
|
|
102
|
+
>
|
|
103
|
+
<VscInsert size={16} aria-hidden="true" />
|
|
104
|
+
</button>
|
|
105
|
+
{isNotebook && (
|
|
106
|
+
<button
|
|
107
|
+
type="button"
|
|
108
|
+
className="code-block-header-button"
|
|
109
|
+
onClick={() => handleAddCodeAsNewCell()}
|
|
110
|
+
aria-label="Add code as new cell"
|
|
111
|
+
title="Add as new cell"
|
|
112
|
+
>
|
|
113
|
+
<VscAdd size={16} aria-hidden="true" />
|
|
114
|
+
</button>
|
|
115
|
+
)}
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
className="code-block-header-button"
|
|
119
|
+
onClick={() => handleCreateNewFileClick()}
|
|
120
|
+
aria-label="Create new file from code"
|
|
121
|
+
title="New file"
|
|
122
|
+
>
|
|
123
|
+
<VscNewFile size={16} aria-hidden="true" />
|
|
124
|
+
</button>
|
|
125
|
+
{language === 'python' && (
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
className="code-block-header-button"
|
|
129
|
+
onClick={() => handleCreateNewNotebookClick()}
|
|
130
|
+
aria-label="Create new notebook from code"
|
|
131
|
+
title="New notebook"
|
|
132
|
+
>
|
|
133
|
+
<VscNotebook size={16} aria-hidden="true" />
|
|
134
|
+
</button>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<SyntaxHighlighter
|
|
138
|
+
style={isDarkTheme() ? oneDark : oneLight}
|
|
139
|
+
PreTag="div"
|
|
140
|
+
language={language}
|
|
141
|
+
{...props}
|
|
142
|
+
>
|
|
143
|
+
{codeString}
|
|
144
|
+
</SyntaxHighlighter>
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}}
|
|
149
|
+
>
|
|
150
|
+
{markdown}
|
|
151
|
+
</Markdown>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
4
|
+
import { ReactWidget } from '@jupyterlab/apputils';
|
|
5
|
+
import { DocumentRegistry } from '@jupyterlab/docregistry';
|
|
6
|
+
import { INotebookModel, NotebookPanel } from '@jupyterlab/notebook';
|
|
7
|
+
import { LabIcon, ToolbarButton } from '@jupyterlab/ui-components';
|
|
8
|
+
import { IDisposable, DisposableDelegate } from '@lumino/disposable';
|
|
9
|
+
import { Widget } from '@lumino/widgets';
|
|
10
|
+
import { UUID } from '@lumino/coreutils';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
IRunChatCompletionRequest,
|
|
15
|
+
RunChatCompletionType
|
|
16
|
+
} from './chat-sidebar';
|
|
17
|
+
import {
|
|
18
|
+
buildNotebookGenerationPrompt,
|
|
19
|
+
INotebookGenerationProgressDetail,
|
|
20
|
+
NOTEBOOK_GENERATION_PROGRESS_EVENT
|
|
21
|
+
} from './notebook-generation';
|
|
22
|
+
import { NotebookGenerationPopover } from './components/notebook-generation-popover';
|
|
23
|
+
|
|
24
|
+
const TOOLBAR_BUTTON_NAME = 'nbi-generate-notebook';
|
|
25
|
+
const TOOLBAR_STATUS_NAME = 'nbi-generate-notebook-status';
|
|
26
|
+
|
|
27
|
+
interface INotebookGenerationToolbarOptions {
|
|
28
|
+
app: JupyterFrontEnd;
|
|
29
|
+
icon: LabIcon;
|
|
30
|
+
chatSidebarId: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface INotebookGenerationPopoverWidgetOptions {
|
|
34
|
+
initialShowInChat: boolean;
|
|
35
|
+
onSubmit: (prompt: string, showInChat: boolean) => void;
|
|
36
|
+
onClose: () => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class NotebookGenerationPopoverWidget extends ReactWidget {
|
|
40
|
+
constructor(options: INotebookGenerationPopoverWidgetOptions) {
|
|
41
|
+
super();
|
|
42
|
+
this.addClass('nbi-notebook-generation-popover-host');
|
|
43
|
+
this._options = options;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected onAfterAttach(): void {
|
|
47
|
+
document.addEventListener('mousedown', this._onDocumentMouseDown, true);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
protected onBeforeDetach(): void {
|
|
51
|
+
document.removeEventListener('mousedown', this._onDocumentMouseDown, true);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private _onDocumentMouseDown = (event: MouseEvent): void => {
|
|
55
|
+
const target = event.target as Node | null;
|
|
56
|
+
if (target && this.node.contains(target)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
this._options.onClose();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
positionAt(rect: DOMRect): void {
|
|
63
|
+
const popoverWidth = 360;
|
|
64
|
+
const margin = 8;
|
|
65
|
+
let left = rect.left;
|
|
66
|
+
if (left + popoverWidth + margin > window.innerWidth) {
|
|
67
|
+
left = Math.max(margin, window.innerWidth - popoverWidth - margin);
|
|
68
|
+
}
|
|
69
|
+
const top = rect.bottom + 4;
|
|
70
|
+
this.node.style.position = 'fixed';
|
|
71
|
+
this.node.style.left = `${left}px`;
|
|
72
|
+
this.node.style.top = `${top}px`;
|
|
73
|
+
this.node.style.width = `${popoverWidth}px`;
|
|
74
|
+
this.node.style.zIndex = '10000';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
render(): JSX.Element {
|
|
78
|
+
return (
|
|
79
|
+
<NotebookGenerationPopover
|
|
80
|
+
initialShowInChat={this._options.initialShowInChat}
|
|
81
|
+
onSubmit={this._options.onSubmit}
|
|
82
|
+
onClose={this._options.onClose}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private _options: INotebookGenerationPopoverWidgetOptions;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class NotebookGenerationToolbarController {
|
|
91
|
+
constructor(
|
|
92
|
+
options: INotebookGenerationToolbarOptions,
|
|
93
|
+
panel: NotebookPanel
|
|
94
|
+
) {
|
|
95
|
+
this._app = options.app;
|
|
96
|
+
this._chatSidebarId = options.chatSidebarId;
|
|
97
|
+
this._panel = panel;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
openPopover(button: ToolbarButton): void {
|
|
101
|
+
if (this._popover) {
|
|
102
|
+
this.closePopover();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const buttonRect = button.node.getBoundingClientRect();
|
|
106
|
+
this._popover = new NotebookGenerationPopoverWidget({
|
|
107
|
+
initialShowInChat: NotebookGenerationToolbarController._showInChat,
|
|
108
|
+
onSubmit: (prompt, showInChat) => {
|
|
109
|
+
NotebookGenerationToolbarController._showInChat = showInChat;
|
|
110
|
+
this._submitPrompt(prompt, showInChat);
|
|
111
|
+
this.closePopover();
|
|
112
|
+
},
|
|
113
|
+
onClose: () => this.closePopover()
|
|
114
|
+
});
|
|
115
|
+
Widget.attach(this._popover, document.body);
|
|
116
|
+
// ReactWidget renders on update-request; Widget.attach doesn't queue one.
|
|
117
|
+
this._popover.update();
|
|
118
|
+
this._popover.positionAt(buttonRect);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
closePopover(): void {
|
|
122
|
+
if (!this._popover) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
this._popover.dispose();
|
|
126
|
+
this._popover = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
dispose(): void {
|
|
130
|
+
this.closePopover();
|
|
131
|
+
if (this._activeProgressRequestId) {
|
|
132
|
+
document.removeEventListener(
|
|
133
|
+
NOTEBOOK_GENERATION_PROGRESS_EVENT,
|
|
134
|
+
this._onProgress
|
|
135
|
+
);
|
|
136
|
+
this._activeProgressRequestId = null;
|
|
137
|
+
}
|
|
138
|
+
if (this._statusHideTimer !== null) {
|
|
139
|
+
clearTimeout(this._statusHideTimer);
|
|
140
|
+
this._statusHideTimer = null;
|
|
141
|
+
}
|
|
142
|
+
this._setStatus(null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private _submitPrompt(rawPrompt: string, showInChat: boolean): void {
|
|
146
|
+
const prefixedPrompt = buildNotebookGenerationPrompt(rawPrompt);
|
|
147
|
+
const externalRequestId = UUID.uuid4();
|
|
148
|
+
// chatMode and toolSelections are forced by the chat-sidebar handler
|
|
149
|
+
// (NotebookGeneration always needs agent mode + the notebook-edit
|
|
150
|
+
// toolset). Leaving them off the request keeps the toolbar
|
|
151
|
+
// independent of the sidebar's current configuration.
|
|
152
|
+
const request: Partial<IRunChatCompletionRequest> = {
|
|
153
|
+
type: RunChatCompletionType.NotebookGeneration,
|
|
154
|
+
content: prefixedPrompt,
|
|
155
|
+
externalRequestId,
|
|
156
|
+
hideInChat: !showInChat
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (!showInChat) {
|
|
160
|
+
this._setStatus('Generating notebook…');
|
|
161
|
+
this._activeProgressRequestId = externalRequestId;
|
|
162
|
+
document.addEventListener(
|
|
163
|
+
NOTEBOOK_GENERATION_PROGRESS_EVENT,
|
|
164
|
+
this._onProgress
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
document.dispatchEvent(
|
|
169
|
+
new CustomEvent('copilotSidebar:runPrompt', { detail: request })
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
if (showInChat) {
|
|
173
|
+
this._app.commands.execute('tabsmenu:activate-by-id', {
|
|
174
|
+
id: this._chatSidebarId
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private _onProgress = (event: Event): void => {
|
|
180
|
+
const detail = (event as CustomEvent<INotebookGenerationProgressDetail>)
|
|
181
|
+
.detail;
|
|
182
|
+
if (!detail || detail.requestId !== this._activeProgressRequestId) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (!detail.inProgress) {
|
|
186
|
+
document.removeEventListener(
|
|
187
|
+
NOTEBOOK_GENERATION_PROGRESS_EVENT,
|
|
188
|
+
this._onProgress
|
|
189
|
+
);
|
|
190
|
+
this._activeProgressRequestId = null;
|
|
191
|
+
if (detail.error) {
|
|
192
|
+
this._setStatus(`Generation failed: ${detail.error}`);
|
|
193
|
+
this._scheduleStatusHide(4000);
|
|
194
|
+
} else {
|
|
195
|
+
this._setStatus('Notebook generation complete');
|
|
196
|
+
this._scheduleStatusHide(2500);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
private _scheduleStatusHide(delayMs: number): void {
|
|
202
|
+
if (this._statusHideTimer !== null) {
|
|
203
|
+
clearTimeout(this._statusHideTimer);
|
|
204
|
+
}
|
|
205
|
+
this._statusHideTimer = setTimeout(() => {
|
|
206
|
+
this._statusHideTimer = null;
|
|
207
|
+
this._setStatus(null);
|
|
208
|
+
}, delayMs);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private _setStatus(message: string | null): void {
|
|
212
|
+
if (this._panel.isDisposed) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!message) {
|
|
216
|
+
if (this._statusWidget) {
|
|
217
|
+
this._statusWidget.dispose();
|
|
218
|
+
this._statusWidget = null;
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (!this._statusWidget) {
|
|
223
|
+
const widget = new Widget();
|
|
224
|
+
widget.addClass('nbi-notebook-generation-status');
|
|
225
|
+
this._panel.toolbar.insertAfter(
|
|
226
|
+
TOOLBAR_BUTTON_NAME,
|
|
227
|
+
TOOLBAR_STATUS_NAME,
|
|
228
|
+
widget
|
|
229
|
+
);
|
|
230
|
+
this._statusWidget = widget;
|
|
231
|
+
}
|
|
232
|
+
this._statusWidget.node.textContent = message;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Defaults ON; remembers the user's last choice for the rest of the tab.
|
|
236
|
+
private static _showInChat = true;
|
|
237
|
+
|
|
238
|
+
private _app: JupyterFrontEnd;
|
|
239
|
+
private _chatSidebarId: string;
|
|
240
|
+
private _panel: NotebookPanel;
|
|
241
|
+
private _popover: NotebookGenerationPopoverWidget | null = null;
|
|
242
|
+
private _activeProgressRequestId: string | null = null;
|
|
243
|
+
private _statusWidget: Widget | null = null;
|
|
244
|
+
private _statusHideTimer: ReturnType<typeof setTimeout> | null = null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export class NotebookGenerationToolbarExtension
|
|
248
|
+
implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
|
|
249
|
+
{
|
|
250
|
+
constructor(options: INotebookGenerationToolbarOptions) {
|
|
251
|
+
this._options = options;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
createNew(
|
|
255
|
+
panel: NotebookPanel,
|
|
256
|
+
_context: DocumentRegistry.IContext<INotebookModel>
|
|
257
|
+
): IDisposable {
|
|
258
|
+
const controller = new NotebookGenerationToolbarController(
|
|
259
|
+
this._options,
|
|
260
|
+
panel
|
|
261
|
+
);
|
|
262
|
+
const button: ToolbarButton = new ToolbarButton({
|
|
263
|
+
icon: this._options.icon,
|
|
264
|
+
onClick: () => controller.openPopover(button),
|
|
265
|
+
// Notebook generation works in any chat mode — the chat-sidebar
|
|
266
|
+
// handler forces agent mode and the notebook-edit toolset when
|
|
267
|
+
// routing this request (issue #229). The button used to be gated
|
|
268
|
+
// on isInClaudeCodeMode here, but that gate undermined the
|
|
269
|
+
// sidebar-side fix.
|
|
270
|
+
tooltip: 'Update active notebook with AI'
|
|
271
|
+
});
|
|
272
|
+
button.addClass('nbi-notebook-generation-toolbar-button');
|
|
273
|
+
panel.toolbar.insertAfter('cellType', TOOLBAR_BUTTON_NAME, button);
|
|
274
|
+
return new DisposableDelegate(() => {
|
|
275
|
+
controller.dispose();
|
|
276
|
+
button.dispose();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private _options: INotebookGenerationToolbarOptions;
|
|
281
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
//
|
|
3
|
+
// Pure helpers and event constants used by the notebook-generation toolbar
|
|
4
|
+
// button. Keeping these in their own module (free of JupyterLab/React
|
|
5
|
+
// dependencies) makes them easy to unit-test under jsdom without bringing
|
|
6
|
+
// in the chat sidebar's heavyweight ESM imports.
|
|
7
|
+
|
|
8
|
+
export const NOTEBOOK_GENERATION_PROMPT_PREFIX =
|
|
9
|
+
"Update active notebook based on the user request. Don't create a new notebook, always update the active one. User request: ";
|
|
10
|
+
|
|
11
|
+
export const NOTEBOOK_GENERATION_PROGRESS_EVENT =
|
|
12
|
+
'copilotSidebar:notebookGenerationProgress';
|
|
13
|
+
|
|
14
|
+
export interface INotebookGenerationProgressDetail {
|
|
15
|
+
requestId: string;
|
|
16
|
+
inProgress: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildNotebookGenerationPrompt(rawPrompt: string): string {
|
|
21
|
+
const trimmed = (rawPrompt || '').trim();
|
|
22
|
+
return `${NOTEBOOK_GENERATION_PROMPT_PREFIX}${trimmed}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Copyright (c) Mehmet Bektas <mbektasgh@outlook.com>
|
|
2
|
+
|
|
3
|
+
// Live binding for the open-file refresh watcher. Lives in its own
|
|
4
|
+
// file so unit tests can exercise the pure logic in
|
|
5
|
+
// `open-file-refresh-watcher.ts` without transitively importing
|
|
6
|
+
// `@jupyterlab/docregistry`, which ships ESM that ts-jest's default
|
|
7
|
+
// transform can't parse.
|
|
8
|
+
|
|
9
|
+
import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
10
|
+
import { DocumentWidget } from '@jupyterlab/docregistry';
|
|
11
|
+
import { Contents } from '@jupyterlab/services';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
IRefreshWatcherEnv,
|
|
15
|
+
WATCHED_SHELL_AREAS
|
|
16
|
+
} from './open-file-refresh-watcher';
|
|
17
|
+
|
|
18
|
+
export function buildRefreshWatcherEnv(
|
|
19
|
+
app: JupyterFrontEnd,
|
|
20
|
+
contents: Contents.IManager
|
|
21
|
+
): IRefreshWatcherEnv {
|
|
22
|
+
return {
|
|
23
|
+
iterDocumentWidgets: function* () {
|
|
24
|
+
for (const area of WATCHED_SHELL_AREAS) {
|
|
25
|
+
// Defensive try/catch: LabShell.widgets() throws for areas it
|
|
26
|
+
// doesn't implement. We've audited the current set against
|
|
27
|
+
// the runtime, but a future JL bump could rename or remove
|
|
28
|
+
// an area; surfacing it as a console warning rather than an
|
|
29
|
+
// unhandled rejection keeps the watcher running for the
|
|
30
|
+
// areas that DO work.
|
|
31
|
+
let widgets: Iterable<unknown>;
|
|
32
|
+
try {
|
|
33
|
+
widgets = app.shell.widgets(area);
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.warn(
|
|
36
|
+
`[NBI] open-file-refresh-watcher: skipping shell area "${area}":`,
|
|
37
|
+
err
|
|
38
|
+
);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const widget of widgets) {
|
|
42
|
+
if (widget instanceof DocumentWidget) {
|
|
43
|
+
yield widget;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
fetchDiskModel: path => contents.get(path, { content: false }),
|
|
49
|
+
setInterval: (handler, ms) => window.setInterval(handler, ms),
|
|
50
|
+
clearInterval: handle => window.clearInterval(handle as number)
|
|
51
|
+
};
|
|
52
|
+
}
|