more-compute 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (55) hide show
  1. frontend/app/globals.css +322 -77
  2. frontend/app/layout.tsx +98 -82
  3. frontend/components/Cell.tsx +234 -95
  4. frontend/components/Notebook.tsx +430 -199
  5. frontend/components/{AddCellButton.tsx → cell/AddCellButton.tsx} +0 -2
  6. frontend/components/cell/MonacoCell.tsx +726 -0
  7. frontend/components/layout/ConnectionBanner.tsx +41 -0
  8. frontend/components/{Sidebar.tsx → layout/Sidebar.tsx} +16 -11
  9. frontend/components/modals/ConfirmModal.tsx +154 -0
  10. frontend/components/modals/SuccessModal.tsx +140 -0
  11. frontend/components/output/MarkdownRenderer.tsx +116 -0
  12. frontend/components/popups/ComputePopup.tsx +674 -365
  13. frontend/components/popups/MetricsPopup.tsx +11 -7
  14. frontend/components/popups/SettingsPopup.tsx +11 -13
  15. frontend/contexts/PodWebSocketContext.tsx +247 -0
  16. frontend/eslint.config.mjs +11 -0
  17. frontend/lib/monaco-themes.ts +160 -0
  18. frontend/lib/settings.ts +128 -26
  19. frontend/lib/themes.json +9973 -0
  20. frontend/lib/websocket-native.ts +19 -8
  21. frontend/lib/websocket.ts +59 -11
  22. frontend/next.config.ts +8 -0
  23. frontend/package-lock.json +1705 -3
  24. frontend/package.json +8 -1
  25. frontend/styling_README.md +18 -0
  26. kernel_run.py +161 -43
  27. more_compute-0.2.0.dist-info/METADATA +126 -0
  28. more_compute-0.2.0.dist-info/RECORD +100 -0
  29. morecompute/__version__.py +1 -0
  30. morecompute/execution/executor.py +31 -20
  31. morecompute/execution/worker.py +68 -7
  32. morecompute/models/__init__.py +31 -0
  33. morecompute/models/api_models.py +197 -0
  34. morecompute/notebook.py +50 -7
  35. morecompute/server.py +574 -94
  36. morecompute/services/data_manager.py +379 -0
  37. morecompute/services/lsp_service.py +335 -0
  38. morecompute/services/pod_manager.py +122 -20
  39. morecompute/services/pod_monitor.py +138 -0
  40. morecompute/services/prime_intellect.py +87 -63
  41. morecompute/utils/config_util.py +59 -0
  42. morecompute/utils/special_commands.py +11 -5
  43. morecompute/utils/zmq_util.py +51 -0
  44. frontend/components/MarkdownRenderer.tsx +0 -84
  45. frontend/components/popups/PythonPopup.tsx +0 -292
  46. more_compute-0.1.3.dist-info/METADATA +0 -173
  47. more_compute-0.1.3.dist-info/RECORD +0 -85
  48. /frontend/components/{CellButton.tsx → cell/CellButton.tsx} +0 -0
  49. /frontend/components/{ErrorModal.tsx → modals/ErrorModal.tsx} +0 -0
  50. /frontend/components/{CellOutput.tsx → output/CellOutput.tsx} +0 -0
  51. /frontend/components/{ErrorDisplay.tsx → output/ErrorDisplay.tsx} +0 -0
  52. {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/WHEEL +0 -0
  53. {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/entry_points.txt +0 -0
  54. {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/licenses/LICENSE +0 -0
  55. {more_compute-0.1.3.dist-info → more_compute-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,726 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useEffect, useState, useCallback } from "react";
4
+ import Editor, { Monaco } from "@monaco-editor/react";
5
+ import * as monaco from "monaco-editor";
6
+ import { Cell as CellType } from "@/types/notebook";
7
+ import CellOutput from "../output/CellOutput";
8
+ import AddCellButton from "./AddCellButton";
9
+ import MarkdownRenderer from "../output/MarkdownRenderer";
10
+ import CellButton from "./CellButton";
11
+ import {
12
+ UpdateIcon,
13
+ LinkBreak2Icon,
14
+ PlayIcon,
15
+ ChevronUpIcon,
16
+ ChevronDownIcon,
17
+ } from "@radix-ui/react-icons";
18
+ import { Check, X } from "lucide-react";
19
+ import { fixIndentation } from "@/lib/api";
20
+ import { loadMonacoThemes } from "@/lib/monaco-themes";
21
+ import { loadSettings } from "@/lib/settings";
22
+
23
+ interface CellProps {
24
+ cell: CellType;
25
+ index: number;
26
+ totalCells: number;
27
+ isActive: boolean;
28
+ isExecuting: boolean;
29
+ onExecute: (index: number) => void;
30
+ onInterrupt: (index: number) => void;
31
+ onDelete: (index: number) => void;
32
+ onUpdate: (index: number, source: string) => void;
33
+ onSetActive: (index: number) => void;
34
+ onAddCell: (type: "code" | "markdown", index: number) => void;
35
+ onMoveUp: (index: number) => void;
36
+ onMoveDown: (index: number) => void;
37
+ }
38
+
39
+ // Global registry of cell editors for LSP
40
+ const cellEditors = new Map<string, monaco.editor.IStandaloneCodeEditor>();
41
+
42
+ // Global flag to ensure providers are registered only once
43
+ let providersRegistered = false;
44
+ // Global flag to ensure themes are loaded only once
45
+ let themesLoaded = false;
46
+
47
+ // Load Monaco themes globally once
48
+ function loadMonacoThemesGlobally(monacoInstance: Monaco) {
49
+ if (themesLoaded) return;
50
+ themesLoaded = true;
51
+
52
+ try {
53
+ loadMonacoThemes(monacoInstance);
54
+ } catch (e) {
55
+ console.warn('Failed to load Monaco themes:', e);
56
+ }
57
+ }
58
+
59
+ // Register global LSP providers once
60
+ function registerGlobalLSPProviders(monacoInstance: Monaco) {
61
+ if (providersRegistered) return;
62
+ providersRegistered = true;
63
+
64
+ // Global completion provider that looks up the editor by model URI
65
+ monacoInstance.languages.registerCompletionItemProvider("python", {
66
+ async provideCompletionItems(model, position) {
67
+ try {
68
+ const cellId = extractCellIdFromUri(model.uri.toString());
69
+ if (!cellId) return { suggestions: [] };
70
+
71
+ const source = model.getValue();
72
+ const response = await fetch("/api/lsp/completions", {
73
+ method: "POST",
74
+ headers: { "Content-Type": "application/json" },
75
+ body: JSON.stringify({
76
+ cell_id: cellId,
77
+ source,
78
+ line: position.lineNumber - 1,
79
+ character: position.column - 1,
80
+ }),
81
+ });
82
+
83
+ if (!response.ok) return { suggestions: [] };
84
+
85
+ const data = await response.json();
86
+ const completions = data.completions || [];
87
+
88
+ const suggestions: monaco.languages.CompletionItem[] = completions.map(
89
+ (item: any) => ({
90
+ label: item.label,
91
+ kind: mapCompletionKind(item.kind, monacoInstance),
92
+ detail: item.detail,
93
+ documentation: item.documentation?.value || item.documentation,
94
+ insertText: item.insertText || item.label,
95
+ range: new monacoInstance.Range(
96
+ position.lineNumber,
97
+ position.column,
98
+ position.lineNumber,
99
+ position.column
100
+ ),
101
+ })
102
+ );
103
+
104
+ return { suggestions };
105
+ } catch (error) {
106
+ console.error("LSP completion error:", error);
107
+ return { suggestions: [] };
108
+ }
109
+ },
110
+ triggerCharacters: [".", "(", "["],
111
+ });
112
+
113
+ // Global hover provider
114
+ monacoInstance.languages.registerHoverProvider("python", {
115
+ async provideHover(model, position) {
116
+ try {
117
+ const cellId = extractCellIdFromUri(model.uri.toString());
118
+ if (!cellId) return null;
119
+
120
+ const source = model.getValue();
121
+ const response = await fetch("/api/lsp/hover", {
122
+ method: "POST",
123
+ headers: { "Content-Type": "application/json" },
124
+ body: JSON.stringify({
125
+ cell_id: cellId,
126
+ source,
127
+ line: position.lineNumber - 1,
128
+ character: position.column - 1,
129
+ }),
130
+ });
131
+
132
+ if (!response.ok) return null;
133
+
134
+ const data = await response.json();
135
+ const hover = data.hover;
136
+
137
+ if (!hover || !hover.contents) return null;
138
+
139
+ const contents = Array.isArray(hover.contents)
140
+ ? hover.contents
141
+ : [hover.contents];
142
+
143
+ return {
144
+ contents: contents.map((content: any) => ({
145
+ value: typeof content === "string" ? content : content.value,
146
+ })),
147
+ };
148
+ } catch (error) {
149
+ console.error("LSP hover error:", error);
150
+ return null;
151
+ }
152
+ },
153
+ });
154
+ }
155
+
156
+ function extractCellIdFromUri(uri: string): string | null {
157
+ // Monaco creates URIs like "inmemory://model/1", "inmemory://model/2", etc.
158
+ // We need to extract the model number and map it to a cell ID
159
+ const match = uri.match(/model\/(\d+)$/);
160
+ if (match) {
161
+ const modelNumber = match[1];
162
+ // Find the editor with this model
163
+ for (const [cellId, editor] of cellEditors.entries()) {
164
+ if (editor.getModel()?.uri.toString().includes(modelNumber)) {
165
+ return cellId;
166
+ }
167
+ }
168
+ }
169
+ return null;
170
+ }
171
+
172
+ function mapCompletionKind(
173
+ lspKind: number,
174
+ monacoInstance: Monaco
175
+ ): monaco.languages.CompletionItemKind {
176
+ const kindMap: { [key: number]: monaco.languages.CompletionItemKind } = {
177
+ 1: monacoInstance.languages.CompletionItemKind.Text,
178
+ 2: monacoInstance.languages.CompletionItemKind.Method,
179
+ 3: monacoInstance.languages.CompletionItemKind.Function,
180
+ 4: monacoInstance.languages.CompletionItemKind.Constructor,
181
+ 5: monacoInstance.languages.CompletionItemKind.Field,
182
+ 6: monacoInstance.languages.CompletionItemKind.Variable,
183
+ 7: monacoInstance.languages.CompletionItemKind.Class,
184
+ 8: monacoInstance.languages.CompletionItemKind.Interface,
185
+ 9: monacoInstance.languages.CompletionItemKind.Module,
186
+ 10: monacoInstance.languages.CompletionItemKind.Property,
187
+ 11: monacoInstance.languages.CompletionItemKind.Unit,
188
+ 12: monacoInstance.languages.CompletionItemKind.Value,
189
+ 13: monacoInstance.languages.CompletionItemKind.Enum,
190
+ 14: monacoInstance.languages.CompletionItemKind.Keyword,
191
+ 15: monacoInstance.languages.CompletionItemKind.Snippet,
192
+ 16: monacoInstance.languages.CompletionItemKind.Color,
193
+ 17: monacoInstance.languages.CompletionItemKind.File,
194
+ 18: monacoInstance.languages.CompletionItemKind.Reference,
195
+ 19: monacoInstance.languages.CompletionItemKind.Folder,
196
+ 20: monacoInstance.languages.CompletionItemKind.EnumMember,
197
+ 21: monacoInstance.languages.CompletionItemKind.Constant,
198
+ 22: monacoInstance.languages.CompletionItemKind.Struct,
199
+ 23: monacoInstance.languages.CompletionItemKind.Event,
200
+ 24: monacoInstance.languages.CompletionItemKind.Operator,
201
+ 25: monacoInstance.languages.CompletionItemKind.TypeParameter,
202
+ };
203
+ return kindMap[lspKind] || monacoInstance.languages.CompletionItemKind.Text;
204
+ }
205
+
206
+
207
+ export const MonacoCell: React.FC<CellProps> = ({
208
+ cell,
209
+ index,
210
+ totalCells,
211
+ isActive,
212
+ isExecuting,
213
+ onExecute,
214
+ onDelete,
215
+ onInterrupt,
216
+ onUpdate,
217
+ onSetActive,
218
+ onAddCell,
219
+ onMoveUp,
220
+ onMoveDown,
221
+ }) => {
222
+ // ============================================================================
223
+ // REFS & STATE
224
+ // ============================================================================
225
+ const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
226
+ const monacoRef = useRef<Monaco | null>(null);
227
+ const wasEditingMarkdown = useRef(false);
228
+ const indexRef = useRef<number>(index);
229
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
230
+ const disposablesRef = useRef<monaco.IDisposable[]>([]);
231
+ const isUnmountingRef = useRef(false);
232
+
233
+ const [isEditing, setIsEditing] = useState(
234
+ () => cell.cell_type === "code" || !cell.source?.trim()
235
+ );
236
+ const [elapsedLabel, setElapsedLabel] = useState<string | null>(
237
+ cell.execution_time ?? null
238
+ );
239
+
240
+ // ============================================================================
241
+ // UTILITIES
242
+ // ============================================================================
243
+ const formatMs = (ms: number): string => {
244
+ if (ms < 1000) return `${ms.toFixed(0)}ms`;
245
+ if (ms < 60_000) return `${(ms / 1000).toFixed(1)}s`;
246
+ const totalSeconds = Math.floor(ms / 1000);
247
+ const minutes = Math.floor(totalSeconds / 60);
248
+ const seconds = totalSeconds % 60;
249
+ return `${minutes}:${seconds.toString().padStart(2, "0")}s`;
250
+ };
251
+
252
+ const parseExecTime = (s?: string | null): number | null => {
253
+ if (!s) return null;
254
+ if (s.endsWith("ms")) return parseFloat(s.replace("ms", ""));
255
+ if (s.endsWith("s")) return parseFloat(s.replace("s", "")) * 1000;
256
+ return null;
257
+ };
258
+
259
+ // ============================================================================
260
+ // COMPUTED VALUES
261
+ // ============================================================================
262
+ const isMarkdownWithContent =
263
+ cell.cell_type === "markdown" && !isEditing && cell.source?.trim();
264
+
265
+ // ============================================================================
266
+ // HANDLERS
267
+ // ============================================================================
268
+ const handleExecute = useCallback(() => {
269
+ if (cell.cell_type === "markdown") {
270
+ onExecute(indexRef.current);
271
+ setIsEditing(false);
272
+ } else {
273
+ if (isExecuting) {
274
+ onInterrupt(indexRef.current);
275
+ } else {
276
+ onExecute(indexRef.current);
277
+ }
278
+ }
279
+ }, [cell.cell_type, isExecuting, onExecute, onInterrupt]);
280
+
281
+ const handleCellClick = () => {
282
+ onSetActive(indexRef.current);
283
+ if (cell.cell_type === "markdown") {
284
+ setIsEditing(true);
285
+ }
286
+ };
287
+
288
+ const handleFixIndentation = async () => {
289
+ try {
290
+ const fixedCode = await fixIndentation(cell.source);
291
+ onUpdate(indexRef.current, fixedCode);
292
+ if (editorRef.current) {
293
+ editorRef.current.setValue(fixedCode);
294
+ }
295
+ } catch (err) {
296
+ console.error("Failed to fix indentation:", err);
297
+ }
298
+ };
299
+
300
+ // ============================================================================
301
+ // MONACO SETUP
302
+ // ============================================================================
303
+ const handleEditorWillMount = (monacoInstance: Monaco) => {
304
+ // Clean up any previous editor instance before mounting new one
305
+ if (editorRef.current) {
306
+ try {
307
+ editorRef.current.dispose();
308
+ } catch (e) {
309
+ // Ignore
310
+ }
311
+ editorRef.current = null;
312
+ }
313
+
314
+ // Load themes globally once
315
+ loadMonacoThemesGlobally(monacoInstance);
316
+
317
+ // Apply theme globally before editor is created
318
+ const settings = loadSettings();
319
+ const userTheme = settings.theme;
320
+
321
+ if (userTheme === 'light') {
322
+ monacoInstance.editor.setTheme('vs');
323
+ } else if (userTheme === 'dark') {
324
+ monacoInstance.editor.setTheme('vs-dark');
325
+ } else {
326
+ try {
327
+ monacoInstance.editor.setTheme(userTheme);
328
+ } catch (e) {
329
+ const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
330
+ monacoInstance.editor.setTheme(isDark ? 'vs-dark' : 'vs');
331
+ }
332
+ }
333
+ };
334
+
335
+ const handleEditorDidMount = (
336
+ editor: monaco.editor.IStandaloneCodeEditor,
337
+ monacoInstance: Monaco
338
+ ) => {
339
+ editorRef.current = editor;
340
+ monacoRef.current = monacoInstance;
341
+
342
+ // Register global LSP providers once
343
+ registerGlobalLSPProviders(monacoInstance);
344
+
345
+ // Apply theme again after editor is mounted to ensure it takes effect
346
+ const settings = loadSettings();
347
+ const userTheme = settings.theme;
348
+
349
+ if (userTheme === 'light') {
350
+ monacoInstance.editor.setTheme('vs');
351
+ } else if (userTheme === 'dark') {
352
+ monacoInstance.editor.setTheme('vs-dark');
353
+ } else {
354
+ try {
355
+ monacoInstance.editor.setTheme(userTheme);
356
+ } catch (e) {
357
+ const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
358
+ monacoInstance.editor.setTheme(isDark ? 'vs-dark' : 'vs');
359
+ }
360
+ }
361
+
362
+ // Register this cell's editor in the global map for LSP
363
+ if (cell.cell_type === "code") {
364
+ cellEditors.set(cell.id, editor);
365
+ }
366
+
367
+ // Focus editor if active
368
+ if (isActive) {
369
+ editor.focus();
370
+ }
371
+
372
+ // Handle content changes
373
+ const changeDisposable = editor.onDidChangeModelContent(() => {
374
+ if (!isUnmountingRef.current) {
375
+ onUpdate(indexRef.current, editor.getValue());
376
+ }
377
+ });
378
+
379
+ // Handle Shift+Enter to execute
380
+ const keyDisposable = editor.addCommand(
381
+ monacoInstance.KeyMod.Shift | monacoInstance.KeyCode.Enter,
382
+ () => {
383
+ handleExecute();
384
+ }
385
+ );
386
+
387
+ // Markdown blur behavior
388
+ if (cell.cell_type === "markdown") {
389
+ const blurDisposable = editor.onDidBlurEditorText(() => {
390
+ if (!isUnmountingRef.current) {
391
+ if (cell.source?.trim()) {
392
+ onExecute(indexRef.current);
393
+ setIsEditing(false);
394
+ }
395
+ wasEditingMarkdown.current = false;
396
+ }
397
+ });
398
+ disposablesRef.current.push(blurDisposable);
399
+ }
400
+
401
+ if (changeDisposable && typeof changeDisposable.dispose === 'function') {
402
+ disposablesRef.current.push(changeDisposable);
403
+ }
404
+ // Note: addCommand returns a command ID (string), not a disposable
405
+ // We don't need to track it for cleanup
406
+ };
407
+
408
+ // ============================================================================
409
+ // EFFECTS
410
+ // ============================================================================
411
+ useEffect(() => {
412
+ indexRef.current = index;
413
+ }, [index]);
414
+
415
+ // Execution timer
416
+ useEffect(() => {
417
+ if (isExecuting) {
418
+ const start = Date.now();
419
+ setElapsedLabel("0ms");
420
+ if (intervalRef.current) clearInterval(intervalRef.current);
421
+ intervalRef.current = setInterval(() => {
422
+ setElapsedLabel(formatMs(Date.now() - start));
423
+ }, 100);
424
+ } else {
425
+ if (intervalRef.current) {
426
+ clearInterval(intervalRef.current);
427
+ intervalRef.current = null;
428
+ }
429
+ const ms = parseExecTime(cell.execution_time as any);
430
+ if (ms != null) setElapsedLabel(formatMs(ms));
431
+ }
432
+
433
+ return () => {
434
+ if (intervalRef.current) {
435
+ clearInterval(intervalRef.current);
436
+ intervalRef.current = null;
437
+ }
438
+ };
439
+ }, [isExecuting, cell.execution_time]);
440
+
441
+ // Track markdown editing
442
+ useEffect(() => {
443
+ if (isActive && cell.cell_type === "markdown" && isEditing) {
444
+ wasEditingMarkdown.current = true;
445
+ }
446
+ }, [isActive, cell.cell_type, isEditing]);
447
+
448
+ // Auto-save markdown on blur
449
+ useEffect(() => {
450
+ if (
451
+ !isActive &&
452
+ wasEditingMarkdown.current &&
453
+ cell.cell_type === "markdown"
454
+ ) {
455
+ if (cell.source?.trim()) {
456
+ onExecute(indexRef.current);
457
+ setIsEditing(false);
458
+ }
459
+ wasEditingMarkdown.current = false;
460
+ }
461
+ }, [isActive, cell.cell_type, cell.source, onExecute]);
462
+
463
+ // Cleanup
464
+ useEffect(() => {
465
+ // Reset unmounting flag on mount
466
+ isUnmountingRef.current = false;
467
+
468
+ return () => {
469
+ // Set unmounting flag to prevent async operations
470
+ isUnmountingRef.current = true;
471
+
472
+ // Dispose the editor instance first
473
+ if (editorRef.current) {
474
+ try {
475
+ const model = editorRef.current.getModel();
476
+
477
+ // Stop any pending operations
478
+ if (editorRef.current) {
479
+ editorRef.current.dispose();
480
+ }
481
+
482
+ if (model && !model.isDisposed()) {
483
+ model.dispose();
484
+ }
485
+ } catch (e) {
486
+ // Silently ignore errors during cleanup
487
+ }
488
+ editorRef.current = null;
489
+ }
490
+
491
+ // Remove editor from global registry
492
+ if (cell.cell_type === "code") {
493
+ cellEditors.delete(cell.id);
494
+ }
495
+
496
+ // Dispose all Monaco disposables
497
+ disposablesRef.current.forEach((d) => {
498
+ if (d && typeof d.dispose === 'function') {
499
+ try {
500
+ d.dispose();
501
+ } catch (e) {
502
+ // Silently ignore errors during cleanup
503
+ }
504
+ }
505
+ });
506
+ disposablesRef.current = [];
507
+ };
508
+ }, [cell.id, cell.cell_type]);
509
+
510
+ // Focus when active
511
+ useEffect(() => {
512
+ if (isActive && editorRef.current) {
513
+ editorRef.current.focus();
514
+ }
515
+ }, [isActive]);
516
+
517
+ // Listen for theme changes and update Monaco editor
518
+ useEffect(() => {
519
+ const handleThemeChange = () => {
520
+ if (!monacoRef.current) return;
521
+
522
+ const settings = loadSettings();
523
+ const userTheme = settings.theme;
524
+
525
+ if (userTheme === 'light') {
526
+ monacoRef.current.editor.setTheme('vs');
527
+ } else if (userTheme === 'dark') {
528
+ monacoRef.current.editor.setTheme('vs-dark');
529
+ } else {
530
+ try {
531
+ monacoRef.current.editor.setTheme(userTheme);
532
+ } catch (e) {
533
+ const isDark = userTheme.includes('dark') || userTheme.includes('night') || userTheme.includes('synthwave');
534
+ monacoRef.current.editor.setTheme(isDark ? 'vs-dark' : 'vs');
535
+ }
536
+ }
537
+ };
538
+
539
+ // Listen for storage events (theme changes from settings)
540
+ window.addEventListener('storage', handleThemeChange);
541
+ // Also listen for custom theme change event
542
+ window.addEventListener('themeChanged', handleThemeChange);
543
+
544
+ return () => {
545
+ window.removeEventListener('storage', handleThemeChange);
546
+ window.removeEventListener('themeChanged', handleThemeChange);
547
+ };
548
+ }, []);
549
+
550
+ // ============================================================================
551
+ // RENDER
552
+ // ============================================================================
553
+ return (
554
+ <div className="cell-wrapper">
555
+ {/* Status Indicator */}
556
+ {!isMarkdownWithContent && (
557
+ <div className="cell-status-indicator">
558
+ <span className="status-indicator">
559
+ <span className="status-bracket">[</span>
560
+ {isExecuting ? (
561
+ <UpdateIcon className="w-1 h-1" />
562
+ ) : cell.error ? (
563
+ <X size={14} color="#dc2626" />
564
+ ) : cell.execution_count != null ? (
565
+ <Check size={14} color="#16a34a" />
566
+ ) : (
567
+ <span
568
+ style={{
569
+ width: "14px",
570
+ height: "14px",
571
+ display: "inline-block",
572
+ }}
573
+ ></span>
574
+ )}
575
+ <span className="status-bracket">]</span>
576
+ </span>
577
+ {elapsedLabel && (
578
+ <span className="status-timer" title="Execution time">
579
+ {elapsedLabel}
580
+ </span>
581
+ )}
582
+ </div>
583
+ )}
584
+
585
+ {/* Add Cell Above Button */}
586
+ <div className="add-cell-line add-line-above">
587
+ <AddCellButton onAddCell={(type) => onAddCell(type, indexRef.current)} />
588
+ </div>
589
+
590
+ {/* Main Cell Container */}
591
+ <div
592
+ className={`cell ${isActive ? "active" : ""} ${isExecuting ? "executing" : ""} ${isMarkdownWithContent ? "markdown-display-mode" : ""}`}
593
+ data-cell-index={index}
594
+ >
595
+ {/* Hover Controls */}
596
+ {!isMarkdownWithContent && (
597
+ <div className="cell-hover-controls">
598
+ <div className="cell-actions-right">
599
+ <CellButton
600
+ icon={<PlayIcon className="w-6 h-6" />}
601
+ onClick={(e) => {
602
+ e.stopPropagation();
603
+ handleExecute();
604
+ }}
605
+ title={isExecuting ? "Stop execution" : "Run cell"}
606
+ isLoading={isExecuting}
607
+ />
608
+ <CellButton
609
+ icon={<ChevronUpIcon className="w-6 h-6" />}
610
+ onClick={(e) => {
611
+ e.stopPropagation();
612
+ onMoveUp(indexRef.current);
613
+ }}
614
+ title="Move cell up"
615
+ disabled={index === 0}
616
+ />
617
+ <CellButton
618
+ icon={<ChevronDownIcon className="w-6 h-6" />}
619
+ onClick={(e) => {
620
+ e.stopPropagation();
621
+ onMoveDown(indexRef.current);
622
+ }}
623
+ title="Move cell down"
624
+ disabled={index === totalCells - 1}
625
+ />
626
+ <CellButton
627
+ icon={<LinkBreak2Icon className="w-5 h-5" />}
628
+ onClick={(e) => {
629
+ e.stopPropagation();
630
+ onDelete(indexRef.current);
631
+ }}
632
+ title="Delete cell"
633
+ />
634
+ </div>
635
+ </div>
636
+ )}
637
+
638
+ {/* Cell Content */}
639
+ <div
640
+ className={`cell-content ${isMarkdownWithContent ? "cursor-pointer" : ""}`}
641
+ onClick={handleCellClick}
642
+ >
643
+ <div className="cell-input">
644
+ {isEditing || cell.cell_type === "code" ? (
645
+ <div
646
+ className={`monaco-editor-container ${cell.cell_type === "markdown" ? "markdown-editor-container" : "code-editor-container"}`}
647
+ >
648
+ <Editor
649
+ key={`${cell.id}-${index}`}
650
+ height={Math.max((cell.source.split('\n').length * 19) + 40, 100)}
651
+ defaultLanguage={
652
+ cell.cell_type === "code" ? "python" : "markdown"
653
+ }
654
+ defaultValue={cell.source}
655
+ beforeMount={handleEditorWillMount}
656
+ onMount={handleEditorDidMount}
657
+ options={{
658
+ minimap: { enabled: false },
659
+ lineNumbers: cell.cell_type === "code" ? "on" : "off",
660
+ scrollBeyondLastLine: false,
661
+ wordWrap: "on",
662
+ wrappingStrategy: "advanced",
663
+ fontSize: 14,
664
+ fontFamily: "'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace",
665
+ lineHeight: 24,
666
+ automaticLayout: true,
667
+ tabSize: 4,
668
+ insertSpaces: true,
669
+ quickSuggestions: cell.cell_type === "code",
670
+ suggestOnTriggerCharacters: cell.cell_type === "code",
671
+ acceptSuggestionOnEnter: "on",
672
+ tabCompletion: "on",
673
+ suggest: {
674
+ showIcons: true,
675
+ showSnippets: true,
676
+ showWords: false,
677
+ insertMode: 'replace',
678
+ filterGraceful: true,
679
+ },
680
+ contextmenu: true,
681
+ folding: cell.cell_type === "code",
682
+ glyphMargin: false,
683
+ lineDecorationsWidth: 0,
684
+ lineNumbersMinChars: 3,
685
+ renderLineHighlight: "none", // Remove grey rectangle
686
+ occurrencesHighlight: "off",
687
+ selectionHighlight: false,
688
+ renderLineHighlightOnlyWhenFocus: false,
689
+ hideCursorInOverviewRuler: true,
690
+ overviewRulerBorder: false,
691
+ overviewRulerLanes: 0,
692
+ // NOTE: fixedOverflowWidgets has known positioning bugs in 2024 - disabled
693
+ padding: { top: 8, bottom: 8 }, // All padding managed by Monaco
694
+ scrollbar: {
695
+ vertical: "auto",
696
+ horizontal: "auto",
697
+ verticalScrollbarSize: 10,
698
+ horizontalScrollbarSize: 10,
699
+ },
700
+ }}
701
+ />
702
+ </div>
703
+ ) : (
704
+ <MarkdownRenderer
705
+ source={cell.source}
706
+ onClick={() => setIsEditing(true)}
707
+ />
708
+ )}
709
+ </div>
710
+ <CellOutput
711
+ outputs={cell.outputs}
712
+ error={cell.error}
713
+ onFixIndentation={handleFixIndentation}
714
+ />
715
+ </div>
716
+ </div>
717
+
718
+ {/* Add Cell Below Button */}
719
+ <div className="add-cell-line add-line-below">
720
+ <AddCellButton
721
+ onAddCell={(type) => onAddCell(type, indexRef.current + 1)}
722
+ />
723
+ </div>
724
+ </div>
725
+ );
726
+ };