jupyterlab-codex-sidebar 0.1.4 → 0.1.6
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/.claude/settings.local.json +9 -0
- package/.github/workflows/unit-tests.yml +27 -0
- package/.jupyterlab-playwright.log +0 -0
- package/README.md +83 -9
- package/docs/images/codex-sidebar-screenshot.png +0 -0
- package/jupyterlab_codex/handlers.py +938 -297
- package/jupyterlab_codex/labextension/package.json +13 -3
- package/jupyterlab_codex/labextension/static/525.224526d045c727069de6.js +2 -0
- package/jupyterlab_codex/labextension/static/737.e7de3ad9dd6ded798340.js +1 -0
- package/jupyterlab_codex/labextension/static/remoteEntry.6ef5e7167763a316c000.js +1 -0
- package/jupyterlab_codex/protocol.py +297 -0
- package/jupyterlab_codex/runner.py +58 -15
- package/jupyterlab_codex/sessions.py +582 -97
- package/lib/codexChat.d.ts +13 -0
- package/lib/codexChat.js +2506 -0
- package/lib/codexChat.js.map +1 -0
- package/lib/codexChatAttachmentDedup.d.ts +10 -0
- package/lib/codexChatAttachmentDedup.js +35 -0
- package/lib/codexChatAttachmentDedup.js.map +1 -0
- package/lib/codexChatAttachmentLimit.d.ts +18 -0
- package/lib/codexChatAttachmentLimit.js +50 -0
- package/lib/codexChatAttachmentLimit.js.map +1 -0
- package/lib/codexChatAttachmentState.d.ts +15 -0
- package/lib/codexChatAttachmentState.js +16 -0
- package/lib/codexChatAttachmentState.js.map +1 -0
- package/lib/codexChatDocumentUtils.d.ts +70 -0
- package/lib/codexChatDocumentUtils.js +506 -0
- package/lib/codexChatDocumentUtils.js.map +1 -0
- package/lib/codexChatFormatting.d.ts +11 -0
- package/lib/codexChatFormatting.js +83 -0
- package/lib/codexChatFormatting.js.map +1 -0
- package/lib/codexChatNotice.d.ts +3 -0
- package/lib/codexChatNotice.js +74 -0
- package/lib/codexChatNotice.js.map +1 -0
- package/lib/codexChatPersistence.d.ts +35 -0
- package/lib/codexChatPersistence.js +158 -0
- package/lib/codexChatPersistence.js.map +1 -0
- package/lib/codexChatPrimitives.d.ts +44 -0
- package/lib/codexChatPrimitives.js +156 -0
- package/lib/codexChatPrimitives.js.map +1 -0
- package/lib/codexChatRender.d.ts +24 -0
- package/lib/codexChatRender.js +293 -0
- package/lib/codexChatRender.js.map +1 -0
- package/lib/codexChatSessionFactory.d.ts +15 -0
- package/lib/codexChatSessionFactory.js +45 -0
- package/lib/codexChatSessionFactory.js.map +1 -0
- package/lib/codexChatSessionKey.d.ts +3 -0
- package/lib/codexChatSessionKey.js +14 -0
- package/lib/codexChatSessionKey.js.map +1 -0
- package/lib/codexChatStorage.d.ts +4 -0
- package/lib/codexChatStorage.js +37 -0
- package/lib/codexChatStorage.js.map +1 -0
- package/lib/codexSessionResolver.d.ts +12 -0
- package/lib/codexSessionResolver.js +38 -0
- package/lib/codexSessionResolver.js.map +1 -0
- package/lib/handlers/activitySummarizer.d.ts +15 -0
- package/lib/handlers/activitySummarizer.js +327 -0
- package/lib/handlers/activitySummarizer.js.map +1 -0
- package/lib/handlers/codexMessageTypes.d.ts +30 -0
- package/lib/handlers/codexMessageTypes.js +2 -0
- package/lib/handlers/codexMessageTypes.js.map +1 -0
- package/lib/handlers/codexMessageUtils.d.ts +46 -0
- package/lib/handlers/codexMessageUtils.js +144 -0
- package/lib/handlers/codexMessageUtils.js.map +1 -0
- package/lib/handlers/handleCodexSocketMessage.d.ts +107 -0
- package/lib/handlers/handleCodexSocketMessage.js +78 -0
- package/lib/handlers/handleCodexSocketMessage.js.map +1 -0
- package/lib/handlers/sessionSyncHandler.d.ts +34 -0
- package/lib/handlers/sessionSyncHandler.js +181 -0
- package/lib/handlers/sessionSyncHandler.js.map +1 -0
- package/lib/hooks/useCodexSocket.d.ts +15 -0
- package/lib/hooks/useCodexSocket.js +84 -0
- package/lib/hooks/useCodexSocket.js.map +1 -0
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/panel.d.ts +1 -11
- package/lib/panel.js +1 -2815
- package/lib/panel.js.map +1 -1
- package/lib/protocol.d.ts +235 -0
- package/lib/protocol.js +278 -0
- package/lib/protocol.js.map +1 -0
- package/package.json +13 -3
- package/playwright.config.cjs +27 -0
- package/playwright.unit.config.cjs +19 -0
- package/pyproject.toml +1 -1
- package/release.sh +52 -14
- package/scripts/run_playwright_e2e.sh +96 -0
- package/scripts/run_playwright_freeze_repro.sh +58 -0
- package/scripts/run_playwright_queue_repro.sh +60 -0
- package/scripts/run_playwright_repro.sh +55 -0
- package/src/codexChat.tsx +3914 -0
- package/src/codexChatAttachmentDedup.ts +47 -0
- package/src/codexChatAttachmentLimit.ts +81 -0
- package/src/codexChatAttachmentState.ts +37 -0
- package/src/codexChatDocumentUtils.ts +644 -0
- package/src/codexChatFormatting.ts +94 -0
- package/src/codexChatNotice.ts +95 -0
- package/src/codexChatPersistence.ts +191 -0
- package/src/codexChatPrimitives.tsx +446 -0
- package/src/codexChatRender.tsx +376 -0
- package/src/codexChatSessionFactory.ts +79 -0
- package/src/codexChatSessionKey.ts +16 -0
- package/src/codexChatStorage.ts +36 -0
- package/src/codexSessionResolver.ts +56 -0
- package/src/handlers/activitySummarizer.ts +369 -0
- package/src/handlers/codexMessageTypes.ts +34 -0
- package/src/handlers/codexMessageUtils.ts +217 -0
- package/src/handlers/handleCodexSocketMessage.ts +204 -0
- package/src/handlers/sessionSyncHandler.ts +308 -0
- package/src/hooks/useCodexSocket.ts +109 -0
- package/src/index.ts +1 -1
- package/src/panel.tsx +1 -4184
- package/src/protocol.ts +582 -0
- package/style/index.css +480 -11
- package/test-results/.last-run.json +4 -0
- package/test.py +0 -0
- package/tests/e2e/cell-output-error-tail.spec.js +156 -0
- package/tests/e2e/codex-ui-test-helpers.js +138 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.ipynb +58 -0
- package/tests/e2e/fixtures/notebooks/error-output-tail.py +19 -0
- package/tests/e2e/fixtures/notebooks/tab1.ipynb +322 -0
- package/tests/e2e/fixtures/notebooks/tab1.py +272 -0
- package/tests/e2e/fixtures/notebooks/tab2.ipynb +252 -0
- package/tests/e2e/fixtures/notebooks/tab2.py +231 -0
- package/tests/e2e/fixtures/notebooks/tab3.ipynb +403 -0
- package/tests/e2e/fixtures/notebooks/tab3.py +331 -0
- package/tests/e2e/fixtures/notebooks/tab4.py +339 -0
- package/tests/e2e/freeze-notebook-tabs-repro.spec.js +295 -0
- package/tests/e2e/mock-codex-cli-flood.py +127 -0
- package/tests/e2e/mock-codex-cli-prompt-echo.py +88 -0
- package/tests/e2e/mock-codex-cli.py +95 -0
- package/tests/e2e/queue-multitab-repro.spec.js +189 -0
- package/tests/test_handlers.py +116 -0
- package/tests/test_protocol.py +169 -0
- package/tests/test_session_store_limits.py +50 -0
- package/tests/unit/codexChatAttachmentDedup.spec.ts +56 -0
- package/tests/unit/codexChatAttachmentLimit.spec.ts +57 -0
- package/tests/unit/codexChatAttachmentState.spec.ts +71 -0
- package/tests/unit/codexChatDocumentUtils.spec.ts +63 -0
- package/tests/unit/codexChatLimit.spec.ts +18 -0
- package/tests/unit/codexChatNotice.spec.ts +45 -0
- package/tests/unit/codexChatPersistence.spec.ts +199 -0
- package/tests/unit/codexChatSessionFactory.spec.ts +94 -0
- package/tests/unit/codexChatSessionKey.spec.ts +18 -0
- package/tests/unit/codexMessageUtils.spec.ts +89 -0
- package/tests/unit/codexSessionResolver.spec.ts +92 -0
- package/tests/unit/handleCodexSocketMessage.spec.ts +476 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/webpack.config.js +6 -0
- package/jupyterlab_codex/labextension/static/504.335f3447c84ba3d74517.js +0 -2
- package/jupyterlab_codex/labextension/static/972.8e856719e40acc1ef4cb.js +0 -1
- package/jupyterlab_codex/labextension/static/remoteEntry.a2982f776a1f0f515640.js +0 -1
- /package/jupyterlab_codex/labextension/static/{504.335f3447c84ba3d74517.js.LICENSE.txt → 525.224526d045c727069de6.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
import type { JupyterFrontEnd } from '@jupyterlab/application';
|
|
2
|
+
import type { MessageContextPreview, SelectionPreview } from './handlers/codexMessageUtils';
|
|
3
|
+
import { truncateEnd as truncateEndShared } from './handlers/codexMessageUtils';
|
|
4
|
+
const truncateEnd = truncateEndShared;
|
|
5
|
+
|
|
6
|
+
export type { MessageContextPreview, SelectionPreview };
|
|
7
|
+
|
|
8
|
+
export type NotebookMode = 'ipynb' | 'jupytext_py' | 'plain_py' | 'unsupported';
|
|
9
|
+
|
|
10
|
+
export const MESSAGE_SELECTION_PREVIEW_DISPLAY_MAX_CHARS = 500;
|
|
11
|
+
export const MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS = 500;
|
|
12
|
+
|
|
13
|
+
export type DocumentWidgetLike = {
|
|
14
|
+
isDisposed?: boolean;
|
|
15
|
+
context?: {
|
|
16
|
+
path?: string;
|
|
17
|
+
model?: { dirty?: boolean };
|
|
18
|
+
save?: () => Promise<void>;
|
|
19
|
+
revert?: () => Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
content?: any;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function getDocumentContext(widget: DocumentWidgetLike | null): any {
|
|
25
|
+
return widget && widget.context ? widget.context : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getSupportedDocumentPath(widget: DocumentWidgetLike | null): string {
|
|
29
|
+
const rawPath = typeof widget?.context?.path === 'string' ? widget.context.path.trim() : '';
|
|
30
|
+
if (!rawPath) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
const lower = rawPath.toLowerCase();
|
|
34
|
+
if (lower.endsWith('.ipynb') || lower.endsWith('.py')) {
|
|
35
|
+
return rawPath;
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getActiveDocumentWidget(
|
|
41
|
+
app: JupyterFrontEnd,
|
|
42
|
+
fallbackWidget: DocumentWidgetLike | null
|
|
43
|
+
): DocumentWidgetLike | null {
|
|
44
|
+
const current = app.shell.currentWidget as any;
|
|
45
|
+
const currentPath = getSupportedDocumentPath(current as DocumentWidgetLike | null);
|
|
46
|
+
if (currentPath) {
|
|
47
|
+
return current as DocumentWidgetLike;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (fallbackWidget && !fallbackWidget.isDisposed && getSupportedDocumentPath(fallbackWidget)) {
|
|
51
|
+
return fallbackWidget;
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function findDocumentWidgetByPath(
|
|
57
|
+
app: JupyterFrontEnd,
|
|
58
|
+
path: string,
|
|
59
|
+
fallbackWidget: DocumentWidgetLike | null = null
|
|
60
|
+
): DocumentWidgetLike | null {
|
|
61
|
+
const normalizedPath = (path || '').trim();
|
|
62
|
+
if (!normalizedPath) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const current = app.shell.currentWidget as any;
|
|
67
|
+
if (getSupportedDocumentPath(current as DocumentWidgetLike | null) === normalizedPath) {
|
|
68
|
+
return current as DocumentWidgetLike;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
fallbackWidget &&
|
|
73
|
+
!fallbackWidget.isDisposed &&
|
|
74
|
+
getSupportedDocumentPath(fallbackWidget) === normalizedPath
|
|
75
|
+
) {
|
|
76
|
+
return fallbackWidget;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const iterator: any = app.shell.widgets('main');
|
|
80
|
+
if (iterator && typeof iterator.next === 'function') {
|
|
81
|
+
while (true) {
|
|
82
|
+
const candidate = iterator.next() as any;
|
|
83
|
+
if (!candidate) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (getSupportedDocumentPath(candidate as DocumentWidgetLike) === normalizedPath) {
|
|
87
|
+
return candidate as DocumentWidgetLike;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function normalizeSelectionPreviewText(text: string): string {
|
|
95
|
+
return (text || '').replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function isNotebookWidget(widget: DocumentWidgetLike | null): boolean {
|
|
99
|
+
return Boolean(widget && widget.content && 'activeCell' in widget.content);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export type DocumentViewState = {
|
|
103
|
+
scrollTop: number;
|
|
104
|
+
scrollLeft: number;
|
|
105
|
+
activeCellIndex: number | null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export function isScrollableElement(element: HTMLElement | null): element is HTMLElement {
|
|
109
|
+
if (!element) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return element.scrollHeight > element.clientHeight + 1 || element.scrollWidth > element.clientWidth + 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function isHTMLElement(value: unknown): value is HTMLElement {
|
|
116
|
+
return Boolean(value && value instanceof HTMLElement);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function querySelectorIncludingSelf(root: HTMLElement, selector: string): HTMLElement | null {
|
|
120
|
+
if (root.matches(selector)) {
|
|
121
|
+
return root;
|
|
122
|
+
}
|
|
123
|
+
return root.querySelector(selector) as HTMLElement | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getPrimaryDocumentScrollContainer(widget: DocumentWidgetLike | null): HTMLElement | null {
|
|
127
|
+
const contentNode = (widget as any)?.content?.node;
|
|
128
|
+
const widgetNode = (widget as any)?.node;
|
|
129
|
+
const roots = [contentNode, widgetNode].filter(isHTMLElement);
|
|
130
|
+
if (roots.length === 0) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const candidates = [
|
|
135
|
+
'.jp-WindowedPanel-outer',
|
|
136
|
+
'.jp-Notebook .jp-WindowedPanel-outer',
|
|
137
|
+
'.jp-NotebookPanel-notebook .jp-WindowedPanel-outer',
|
|
138
|
+
'.jp-FileEditor .cm-scroller',
|
|
139
|
+
'.jp-FileEditor .jp-CodeMirrorEditor',
|
|
140
|
+
'.cm-scroller',
|
|
141
|
+
'.jp-FileEditor'
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
for (const root of roots) {
|
|
145
|
+
for (const selector of candidates) {
|
|
146
|
+
const node = querySelectorIncludingSelf(root, selector);
|
|
147
|
+
if (isScrollableElement(node)) {
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for (const root of roots) {
|
|
154
|
+
if (isScrollableElement(root)) {
|
|
155
|
+
return root;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function captureDocumentViewState(widget: DocumentWidgetLike | null): DocumentViewState {
|
|
163
|
+
const scrollContainer = getPrimaryDocumentScrollContainer(widget);
|
|
164
|
+
const notebookContent: any = isNotebookWidget(widget) ? (widget as any).content : null;
|
|
165
|
+
const rawActiveCellIndex = Number(notebookContent?.activeCellIndex);
|
|
166
|
+
const activeCellIndex = Number.isFinite(rawActiveCellIndex) ? Math.max(0, Math.floor(rawActiveCellIndex)) : null;
|
|
167
|
+
return {
|
|
168
|
+
scrollTop: scrollContainer?.scrollTop ?? 0,
|
|
169
|
+
scrollLeft: scrollContainer?.scrollLeft ?? 0,
|
|
170
|
+
activeCellIndex
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function restoreDocumentViewState(widget: DocumentWidgetLike | null, viewState: DocumentViewState): void {
|
|
175
|
+
if (isNotebookWidget(widget) && viewState.activeCellIndex !== null) {
|
|
176
|
+
try {
|
|
177
|
+
const notebookContent: any = (widget as any).content;
|
|
178
|
+
const cellsLengthRaw = Number(notebookContent?.widgets?.length);
|
|
179
|
+
if (Number.isFinite(cellsLengthRaw) && cellsLengthRaw > 0) {
|
|
180
|
+
const maxIndex = Math.floor(cellsLengthRaw) - 1;
|
|
181
|
+
notebookContent.activeCellIndex = Math.max(0, Math.min(viewState.activeCellIndex, maxIndex));
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore active-cell restore failures.
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const applyScroll = () => {
|
|
189
|
+
const scrollContainer = getPrimaryDocumentScrollContainer(widget);
|
|
190
|
+
if (!scrollContainer) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
scrollContainer.scrollTop = viewState.scrollTop;
|
|
194
|
+
scrollContainer.scrollLeft = viewState.scrollLeft;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
applyScroll();
|
|
198
|
+
window.requestAnimationFrame(() => {
|
|
199
|
+
applyScroll();
|
|
200
|
+
window.requestAnimationFrame(applyScroll);
|
|
201
|
+
});
|
|
202
|
+
window.setTimeout(applyScroll, 120);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getActiveCellText(widget: DocumentWidgetLike | null): string {
|
|
206
|
+
if (!isNotebookWidget(widget)) {
|
|
207
|
+
return '';
|
|
208
|
+
}
|
|
209
|
+
const activeCell = (widget as any).content.activeCell;
|
|
210
|
+
if (!activeCell) {
|
|
211
|
+
return '';
|
|
212
|
+
}
|
|
213
|
+
const source =
|
|
214
|
+
typeof activeCell.model?.sharedModel?.getSource === 'function' ? activeCell.model.sharedModel.getSource() : '';
|
|
215
|
+
return typeof source === 'string' ? source : '';
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type CodeEditorSelection = {
|
|
219
|
+
text: string;
|
|
220
|
+
startLine: number | null;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type SelectedContext =
|
|
224
|
+
| {
|
|
225
|
+
kind: 'cell';
|
|
226
|
+
number: number;
|
|
227
|
+
text: string;
|
|
228
|
+
}
|
|
229
|
+
| {
|
|
230
|
+
kind: 'line';
|
|
231
|
+
number: number;
|
|
232
|
+
text: string;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export function getSelectionFromCodeEditor(editor: any, source: string): CodeEditorSelection | null {
|
|
236
|
+
if (!editor || typeof editor.getSelection !== 'function' || typeof editor.getOffsetAt !== 'function') {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const range = editor.getSelection();
|
|
241
|
+
if (!range || !range.start || !range.end) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const startOffset = Number(editor.getOffsetAt(range.start));
|
|
246
|
+
const endOffset = Number(editor.getOffsetAt(range.end));
|
|
247
|
+
if (!Number.isFinite(startOffset) || !Number.isFinite(endOffset)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const from = Math.max(0, Math.min(startOffset, endOffset));
|
|
252
|
+
const to = Math.max(0, Math.max(startOffset, endOffset));
|
|
253
|
+
if (to <= from || !source) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const text = source.slice(from, to);
|
|
258
|
+
if (!text) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const startLineRaw = Number(range.start.line);
|
|
263
|
+
const endLineRaw = Number(range.end.line);
|
|
264
|
+
const startLine =
|
|
265
|
+
Number.isFinite(startLineRaw) && Number.isFinite(endLineRaw)
|
|
266
|
+
? Math.max(1, Math.min(startLineRaw, endLineRaw) + 1)
|
|
267
|
+
: null;
|
|
268
|
+
|
|
269
|
+
return { text, startLine };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function getSelectedTextFromCodeEditor(editor: any, source: string): string {
|
|
273
|
+
const selection = getSelectionFromCodeEditor(editor, source);
|
|
274
|
+
return selection?.text || '';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function getSelectedTextFromActiveCell(widget: DocumentWidgetLike | null): string {
|
|
278
|
+
if (!isNotebookWidget(widget)) {
|
|
279
|
+
return '';
|
|
280
|
+
}
|
|
281
|
+
const activeCell = (widget as any).content.activeCell;
|
|
282
|
+
if (!activeCell) {
|
|
283
|
+
return '';
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const source =
|
|
288
|
+
typeof activeCell.model?.sharedModel?.getSource === 'function' ? activeCell.model.sharedModel.getSource() : '';
|
|
289
|
+
return getSelectedTextFromCodeEditor((activeCell as any).editor, typeof source === 'string' ? source : '');
|
|
290
|
+
} catch {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function getSelectedContextFromActiveCell(widget: DocumentWidgetLike | null): SelectedContext | null {
|
|
296
|
+
if (!isNotebookWidget(widget)) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const notebookContent: any = (widget as any).content;
|
|
300
|
+
const activeCell = notebookContent?.activeCell;
|
|
301
|
+
if (!activeCell) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const source =
|
|
307
|
+
typeof activeCell.model?.sharedModel?.getSource === 'function' ? activeCell.model.sharedModel.getSource() : '';
|
|
308
|
+
const selection = getSelectionFromCodeEditor(
|
|
309
|
+
(activeCell as any).editor,
|
|
310
|
+
typeof source === 'string' ? source : ''
|
|
311
|
+
);
|
|
312
|
+
if (!selection?.text) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const rawCellIndex = Number(notebookContent?.activeCellIndex);
|
|
317
|
+
const cellNumber = Number.isFinite(rawCellIndex) ? Math.max(1, Math.floor(rawCellIndex) + 1) : 1;
|
|
318
|
+
return {
|
|
319
|
+
kind: 'cell',
|
|
320
|
+
number: cellNumber,
|
|
321
|
+
text: selection.text
|
|
322
|
+
};
|
|
323
|
+
} catch {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function getSelectedTextFromFileEditor(widget: DocumentWidgetLike | null): string {
|
|
329
|
+
if (!widget || isNotebookWidget(widget)) {
|
|
330
|
+
return '';
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try {
|
|
334
|
+
const editor: any = (widget as any).content?.editor;
|
|
335
|
+
if (!editor) {
|
|
336
|
+
return '';
|
|
337
|
+
}
|
|
338
|
+
const source =
|
|
339
|
+
typeof editor.model?.sharedModel?.getSource === 'function' ? editor.model.sharedModel.getSource() : '';
|
|
340
|
+
return getSelectedTextFromCodeEditor(editor, typeof source === 'string' ? source : '');
|
|
341
|
+
} catch {
|
|
342
|
+
return '';
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function getSelectedContextFromFileEditor(widget: DocumentWidgetLike | null): SelectedContext | null {
|
|
347
|
+
if (!widget || isNotebookWidget(widget)) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
const editor: any = (widget as any).content?.editor;
|
|
353
|
+
if (!editor) {
|
|
354
|
+
return null;
|
|
355
|
+
}
|
|
356
|
+
const source =
|
|
357
|
+
typeof editor.model?.sharedModel?.getSource === 'function' ? editor.model.sharedModel.getSource() : '';
|
|
358
|
+
const selection = getSelectionFromCodeEditor(editor, typeof source === 'string' ? source : '');
|
|
359
|
+
if (!selection?.text) {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
362
|
+
const lineNumber = selection.startLine ?? 1;
|
|
363
|
+
return {
|
|
364
|
+
kind: 'line',
|
|
365
|
+
number: lineNumber,
|
|
366
|
+
text: selection.text
|
|
367
|
+
};
|
|
368
|
+
} catch {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function getSelectedContext(
|
|
374
|
+
widget: DocumentWidgetLike | null,
|
|
375
|
+
notebookMode: NotebookMode
|
|
376
|
+
): SelectedContext | null {
|
|
377
|
+
if (notebookMode === 'plain_py') {
|
|
378
|
+
return getSelectedContextFromFileEditor(widget) ?? getSelectedContextFromActiveCell(widget);
|
|
379
|
+
}
|
|
380
|
+
return getSelectedContextFromActiveCell(widget) ?? getSelectedContextFromFileEditor(widget);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function inferLocationLabelFromWidget(
|
|
384
|
+
widget: DocumentWidgetLike | null,
|
|
385
|
+
notebookMode: NotebookMode
|
|
386
|
+
): string {
|
|
387
|
+
if (isNotebookWidget(widget) || notebookMode === 'ipynb' || notebookMode === 'jupytext_py') {
|
|
388
|
+
const rawCellIndex = Number((widget as any)?.content?.activeCellIndex);
|
|
389
|
+
if (Number.isFinite(rawCellIndex)) {
|
|
390
|
+
return `Cell ${Math.max(1, Math.floor(rawCellIndex) + 1)}`;
|
|
391
|
+
}
|
|
392
|
+
return 'Cell';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
const editor: any = widget && !isNotebookWidget(widget) ? (widget as any).content?.editor : null;
|
|
397
|
+
if (editor && typeof editor.getCursorPosition === 'function') {
|
|
398
|
+
const cursor = editor.getCursorPosition();
|
|
399
|
+
const lineRaw = Number(cursor?.line);
|
|
400
|
+
if (Number.isFinite(lineRaw)) {
|
|
401
|
+
return `Line ${Math.max(1, Math.floor(lineRaw) + 1)}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch {
|
|
405
|
+
// Ignore location inference errors and fallback to a generic label.
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return notebookMode === 'plain_py' ? 'Line' : 'Selection';
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function toSelectionPreview(context: SelectedContext | null): SelectionPreview | undefined {
|
|
412
|
+
if (!context) {
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
const normalized = normalizeSelectionPreviewText(context.text);
|
|
416
|
+
if (!normalized) {
|
|
417
|
+
return undefined;
|
|
418
|
+
}
|
|
419
|
+
const locationLabel = context.kind === 'cell' ? `Cell ${context.number}` : `Line ${context.number}`;
|
|
420
|
+
return {
|
|
421
|
+
locationLabel,
|
|
422
|
+
previewText: truncateEnd(normalized, MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS)
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function toMessageSelectionPreview(
|
|
427
|
+
context: SelectedContext | null,
|
|
428
|
+
widget: DocumentWidgetLike | null,
|
|
429
|
+
notebookMode: NotebookMode,
|
|
430
|
+
text: string
|
|
431
|
+
): SelectionPreview | undefined {
|
|
432
|
+
const normalized = normalizeSelectionPreviewText(text);
|
|
433
|
+
if (!normalized) {
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
if (context) {
|
|
437
|
+
const locationLabel = context.kind === 'cell' ? `Cell ${context.number}` : `Line ${context.number}`;
|
|
438
|
+
return {
|
|
439
|
+
locationLabel,
|
|
440
|
+
previewText: truncateEnd(normalized, MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return toFallbackSelectionPreview(widget, notebookMode, normalized);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function formatSelectionPreviewTextForDisplay(previewText: string): string {
|
|
447
|
+
return truncateEnd(normalizeSelectionPreviewText(previewText), MESSAGE_SELECTION_PREVIEW_DISPLAY_MAX_CHARS);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function toFallbackSelectionPreview(
|
|
451
|
+
widget: DocumentWidgetLike | null,
|
|
452
|
+
notebookMode: NotebookMode,
|
|
453
|
+
text: string
|
|
454
|
+
): SelectionPreview | undefined {
|
|
455
|
+
const normalized = normalizeSelectionPreviewText(text);
|
|
456
|
+
if (!normalized) {
|
|
457
|
+
return undefined;
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
locationLabel: inferLocationLabelFromWidget(widget, notebookMode),
|
|
461
|
+
previewText: truncateEnd(normalized, MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS)
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function toCellOutputPreview(
|
|
466
|
+
context: SelectedContext | null,
|
|
467
|
+
widget: DocumentWidgetLike | null,
|
|
468
|
+
notebookMode: NotebookMode,
|
|
469
|
+
outputText: string
|
|
470
|
+
): SelectionPreview | undefined {
|
|
471
|
+
const normalized = normalizeSelectionPreviewText(outputText);
|
|
472
|
+
if (!normalized) {
|
|
473
|
+
return undefined;
|
|
474
|
+
}
|
|
475
|
+
const locationBase =
|
|
476
|
+
context?.kind === 'cell'
|
|
477
|
+
? `Cell ${context.number}`
|
|
478
|
+
: inferLocationLabelFromWidget(widget, notebookMode);
|
|
479
|
+
return {
|
|
480
|
+
locationLabel: `${locationBase} Output`,
|
|
481
|
+
previewText: truncateEnd(normalized, MESSAGE_SELECTION_PREVIEW_STORED_MAX_CHARS)
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export const ACTIVE_CELL_OUTPUT_MAX_CHARS = 20000;
|
|
486
|
+
export const ACTIVE_CELL_OUTPUT_MAX_ITEMS = 24;
|
|
487
|
+
const CELL_OUTPUT_TRUNCATED_MARKER = '... (truncated)';
|
|
488
|
+
|
|
489
|
+
export function stripAnsi(value: string): string {
|
|
490
|
+
// Best-effort removal of ANSI escape codes (tracebacks sometimes include them).
|
|
491
|
+
return value.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function coerceText(value: unknown): string {
|
|
495
|
+
if (typeof value === 'string') {
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
if (Array.isArray(value)) {
|
|
499
|
+
return value.filter(item => typeof item === 'string').join('');
|
|
500
|
+
}
|
|
501
|
+
return '';
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function formatJupyterOutput(output: any): string {
|
|
505
|
+
if (!output || typeof output !== 'object') {
|
|
506
|
+
return '';
|
|
507
|
+
}
|
|
508
|
+
const outputType = typeof output.output_type === 'string' ? output.output_type : '';
|
|
509
|
+
if (!outputType) {
|
|
510
|
+
return '';
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (outputType === 'stream') {
|
|
514
|
+
const text = coerceText(output.text);
|
|
515
|
+
if (!text) {
|
|
516
|
+
return '';
|
|
517
|
+
}
|
|
518
|
+
const name = typeof output.name === 'string' ? output.name : '';
|
|
519
|
+
const cleaned = stripAnsi(text).replace(/\s+$/, '');
|
|
520
|
+
if (!cleaned) {
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
return name === 'stderr' ? `[stderr]\n${cleaned}` : cleaned;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (outputType === 'error') {
|
|
527
|
+
const traceback = Array.isArray(output.traceback)
|
|
528
|
+
? output.traceback.filter((line: unknown) => typeof line === 'string')
|
|
529
|
+
: [];
|
|
530
|
+
const tbText = stripAnsi(traceback.join('\n')).replace(/\s+$/, '');
|
|
531
|
+
if (tbText) {
|
|
532
|
+
return tbText;
|
|
533
|
+
}
|
|
534
|
+
const ename = typeof output.ename === 'string' ? output.ename : '';
|
|
535
|
+
const evalue = typeof output.evalue === 'string' ? output.evalue : '';
|
|
536
|
+
const summary = [ename, evalue].filter(Boolean).join(': ').trim();
|
|
537
|
+
return summary;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (outputType === 'execute_result' || outputType === 'display_data' || outputType === 'update_display_data') {
|
|
541
|
+
const data = output.data && typeof output.data === 'object' ? output.data : null;
|
|
542
|
+
if (!data) {
|
|
543
|
+
return '';
|
|
544
|
+
}
|
|
545
|
+
const textPlain = coerceText((data as any)['text/plain']);
|
|
546
|
+
if (textPlain) {
|
|
547
|
+
const cleaned = stripAnsi(textPlain).replace(/\s+$/, '');
|
|
548
|
+
if (!cleaned) {
|
|
549
|
+
return '';
|
|
550
|
+
}
|
|
551
|
+
if (outputType === 'execute_result' && typeof output.execution_count === 'number') {
|
|
552
|
+
return `Out[${output.execution_count}]:\n${cleaned}`;
|
|
553
|
+
}
|
|
554
|
+
return cleaned;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const mimeTypes = Object.keys(data as any).filter(mime => mime && mime !== 'text/plain');
|
|
558
|
+
if (mimeTypes.length > 0) {
|
|
559
|
+
return `[non-text output omitted: ${mimeTypes.slice(0, 6).join(', ')}${mimeTypes.length > 6 ? ', ...' : ''}]`;
|
|
560
|
+
}
|
|
561
|
+
return '';
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export function isJupyterErrorOutput(output: any): boolean {
|
|
568
|
+
return Boolean(output && typeof output === 'object' && output.output_type === 'error');
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function truncateCellOutput(text: string, maxChars: number, fromEnd: boolean): string {
|
|
572
|
+
if (text.length <= maxChars) {
|
|
573
|
+
return text;
|
|
574
|
+
}
|
|
575
|
+
if (maxChars <= CELL_OUTPUT_TRUNCATED_MARKER.length) {
|
|
576
|
+
return fromEnd ? text.slice(text.length - maxChars) : text.slice(0, maxChars);
|
|
577
|
+
}
|
|
578
|
+
const sliceLength = maxChars - CELL_OUTPUT_TRUNCATED_MARKER.length - 2;
|
|
579
|
+
if (fromEnd) {
|
|
580
|
+
return `${CELL_OUTPUT_TRUNCATED_MARKER}\n\n${text.slice(text.length - sliceLength)}`;
|
|
581
|
+
}
|
|
582
|
+
return `${text.slice(0, sliceLength)}\n\n${CELL_OUTPUT_TRUNCATED_MARKER}`;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function summarizeJupyterOutputs(outputs: any[]): string {
|
|
586
|
+
if (!Array.isArray(outputs) || outputs.length === 0) {
|
|
587
|
+
return '';
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
let appended = 0;
|
|
591
|
+
let exceededMaxItems = false;
|
|
592
|
+
let hasErrorOutput = false;
|
|
593
|
+
const chunks: string[] = [];
|
|
594
|
+
|
|
595
|
+
for (const output of outputs) {
|
|
596
|
+
if (appended >= ACTIVE_CELL_OUTPUT_MAX_ITEMS) {
|
|
597
|
+
exceededMaxItems = true;
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
const chunk = formatJupyterOutput(output);
|
|
601
|
+
if (!chunk) {
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
appended += 1;
|
|
605
|
+
if (isJupyterErrorOutput(output)) {
|
|
606
|
+
hasErrorOutput = true;
|
|
607
|
+
}
|
|
608
|
+
chunks.push(chunk);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
let combined = chunks.join('\n\n').replace(/\s+$/, '');
|
|
612
|
+
if (!combined) {
|
|
613
|
+
return '';
|
|
614
|
+
}
|
|
615
|
+
if (exceededMaxItems) {
|
|
616
|
+
combined = `${combined}\n\n${CELL_OUTPUT_TRUNCATED_MARKER}`;
|
|
617
|
+
}
|
|
618
|
+
combined = truncateCellOutput(combined, ACTIVE_CELL_OUTPUT_MAX_CHARS, hasErrorOutput);
|
|
619
|
+
combined = combined.replace(/\s+$/, '');
|
|
620
|
+
return combined;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function getActiveCellOutput(widget: DocumentWidgetLike | null): string {
|
|
624
|
+
if (!isNotebookWidget(widget)) {
|
|
625
|
+
return '';
|
|
626
|
+
}
|
|
627
|
+
const activeCell = (widget as any).content.activeCell;
|
|
628
|
+
if (!activeCell) {
|
|
629
|
+
return '';
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
try {
|
|
633
|
+
const model: any = activeCell.model as any;
|
|
634
|
+
const cellType = typeof model?.type === 'string' ? model.type : '';
|
|
635
|
+
if (cellType && cellType !== 'code') {
|
|
636
|
+
return '';
|
|
637
|
+
}
|
|
638
|
+
const json = typeof model?.toJSON === 'function' ? model.toJSON() : null;
|
|
639
|
+
const outputs = json && Array.isArray((json as any).outputs) ? (json as any).outputs : [];
|
|
640
|
+
return summarizeJupyterOutputs(outputs);
|
|
641
|
+
} catch {
|
|
642
|
+
return '';
|
|
643
|
+
}
|
|
644
|
+
}
|