more-compute 0.1.4__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 +159 -42
  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 -1
  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.4.dist-info/METADATA +0 -173
  47. more_compute-0.1.4.dist-info/RECORD +0 -86
  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.4.dist-info → more_compute-0.2.0.dist-info}/WHEEL +0 -0
  53. {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/entry_points.txt +0 -0
  54. {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/licenses/LICENSE +0 -0
  55. {more_compute-0.1.4.dist-info → more_compute-0.2.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,44 @@
1
- 'use client';
2
-
3
- import React, { useState, useEffect, useRef, useCallback, useReducer } from 'react';
4
- import { Cell as CellComponent } from './Cell';
5
- import { Cell, Output, StreamOutput, ExecuteResultOutput, ErrorOutput } from '@/types/notebook';
6
- import { WebSocketService } from '@/lib/websocket-native';
7
- import AddCellButton from './AddCellButton';
8
- import { loadSettings, applyTheme } from '@/lib/settings';
1
+ "use client";
2
+
3
+ import React, {
4
+ useState,
5
+ useEffect,
6
+ useRef,
7
+ useCallback,
8
+ useReducer,
9
+ } from "react";
10
+ import dynamic from "next/dynamic";
11
+ import {
12
+ Cell,
13
+ Output,
14
+ StreamOutput,
15
+ ExecuteResultOutput,
16
+ ErrorOutput,
17
+ } from "@/types/notebook";
18
+ import { WebSocketService } from "@/lib/websocket-native";
19
+ import AddCellButton from "./cell/AddCellButton";
20
+ import { loadSettings, applyTheme } from "@/lib/settings";
21
+
22
+ // Dynamically import MonacoCell to avoid SSR issues with Monaco Editor
23
+ const CellComponent = dynamic(() => import("./cell/MonacoCell").then(mod => ({ default: mod.MonacoCell })), {
24
+ ssr: false,
25
+ loading: () => (
26
+ <div style={{
27
+ height: "150px",
28
+ background: "var(--mc-cell-background)",
29
+ border: "2px solid #DBC7A8",
30
+ borderRadius: "0.5px",
31
+ display: "flex",
32
+ alignItems: "center",
33
+ justifyContent: "center",
34
+ color: "#6b7280",
35
+ fontSize: "14px",
36
+ margin: "20px 0"
37
+ }}>
38
+ Loading Monaco editor...
39
+ </div>
40
+ )
41
+ });
9
42
 
10
43
  // --- State Management with useReducer ---
11
44
 
@@ -15,15 +48,35 @@ interface NotebookState {
15
48
  }
16
49
 
17
50
  type NotebookAction =
18
- | { type: 'NOTEBOOK_LOADED'; payload: { cells: Cell[] } }
19
- | { type: 'EXECUTION_START'; payload: { cell_index: number; execution_count: number } }
20
- | { type: 'STREAM_OUTPUT'; payload: { cell_index: number; stream: 'stdout' | 'stderr'; text: string } }
21
- | { type: 'EXECUTE_RESULT'; payload: { cell_index: number; execution_count: number; data: ExecuteResultOutput['data'] } }
22
- | { type: 'EXECUTION_COMPLETE'; payload: { cell_index: number; result: any } }
23
- | { type: 'EXECUTION_ERROR'; payload: { cell_index: number; error: Output } }
24
- | { type: 'NOTEBOOK_UPDATED'; payload: { cells: Cell[] } }
25
- | { type: 'UPDATE_CELL_SOURCE'; payload: { cell_index: number; source: string } }
26
- | { type: 'RESET_KERNEL' };
51
+ | { type: "NOTEBOOK_LOADED"; payload: { cells: Cell[] } }
52
+ | {
53
+ type: "EXECUTION_START";
54
+ payload: { cell_index: number; execution_count: number };
55
+ }
56
+ | {
57
+ type: "STREAM_OUTPUT";
58
+ payload: {
59
+ cell_index: number;
60
+ stream: "stdout" | "stderr";
61
+ text: string;
62
+ };
63
+ }
64
+ | {
65
+ type: "EXECUTE_RESULT";
66
+ payload: {
67
+ cell_index: number;
68
+ execution_count: number;
69
+ data: ExecuteResultOutput["data"];
70
+ };
71
+ }
72
+ | { type: "EXECUTION_COMPLETE"; payload: { cell_index: number; result: any } }
73
+ | { type: "EXECUTION_ERROR"; payload: { cell_index: number; error: Output } }
74
+ | { type: "NOTEBOOK_UPDATED"; payload: { cells: Cell[] } }
75
+ | {
76
+ type: "UPDATE_CELL_SOURCE";
77
+ payload: { cell_index: number; source: string };
78
+ }
79
+ | { type: "RESET_KERNEL" };
27
80
 
28
81
  const initialState: NotebookState = {
29
82
  cells: [],
@@ -35,16 +88,16 @@ applyTheme(initialSettings.theme);
35
88
 
36
89
  const coerceToString = (value: unknown): string => {
37
90
  if (Array.isArray(value)) {
38
- return value.join('');
91
+ return value.join("");
39
92
  }
40
- if (typeof value === 'string') {
93
+ if (typeof value === "string") {
41
94
  return value;
42
95
  }
43
- return value != null ? String(value) : '';
96
+ return value != null ? String(value) : "";
44
97
  };
45
98
 
46
99
  const normalizeError = (error: any): ErrorOutput | undefined => {
47
- if (!error || typeof error !== 'object') {
100
+ if (!error || typeof error !== "object") {
48
101
  return undefined;
49
102
  }
50
103
 
@@ -53,16 +106,16 @@ const normalizeError = (error: any): ErrorOutput | undefined => {
53
106
 
54
107
  const traceback = Array.isArray(tracebackRaw)
55
108
  ? tracebackRaw.map(coerceToString)
56
- : coerceToString(tracebackRaw).split('\n');
109
+ : coerceToString(tracebackRaw).split("\n");
57
110
 
58
111
  const suggestions = Array.isArray(suggestionsRaw)
59
112
  ? suggestionsRaw.map(coerceToString)
60
113
  : undefined;
61
114
 
62
115
  return {
63
- output_type: 'error',
64
- ename: coerceToString((error as any).ename ?? 'Error'),
65
- evalue: coerceToString((error as any).evalue ?? ''),
116
+ output_type: "error",
117
+ ename: coerceToString((error as any).ename ?? "Error"),
118
+ evalue: coerceToString((error as any).evalue ?? ""),
66
119
  traceback,
67
120
  error_type: (error as any).error_type,
68
121
  suggestions,
@@ -75,20 +128,20 @@ const normalizeOutputs = (outputs: any): Output[] => {
75
128
  }
76
129
 
77
130
  return outputs.map((output) => {
78
- if (!output || typeof output !== 'object') {
131
+ if (!output || typeof output !== "object") {
79
132
  return output as Output;
80
133
  }
81
134
 
82
- if (output.output_type === 'stream') {
83
- const text = coerceToString((output as any).text ?? '');
135
+ if (output.output_type === "stream") {
136
+ const text = coerceToString((output as any).text ?? "");
84
137
  return { ...output, text } as StreamOutput;
85
138
  }
86
139
 
87
- if (output.output_type === 'execute_result') {
140
+ if (output.output_type === "execute_result") {
88
141
  const data = { ...(output as any).data };
89
- if (data && typeof data === 'object') {
90
- const plain = coerceToString((data as any)['text/plain'] ?? '');
91
- data['text/plain'] = plain;
142
+ if (data && typeof data === "object") {
143
+ const plain = coerceToString((data as any)["text/plain"] ?? "");
144
+ data["text/plain"] = plain;
92
145
  }
93
146
  return {
94
147
  ...output,
@@ -96,7 +149,7 @@ const normalizeOutputs = (outputs: any): Output[] => {
96
149
  } as ExecuteResultOutput;
97
150
  }
98
151
 
99
- if (output.output_type === 'error') {
152
+ if (output.output_type === "error") {
100
153
  return normalizeError(output) as ErrorOutput;
101
154
  }
102
155
 
@@ -105,12 +158,12 @@ const normalizeOutputs = (outputs: any): Output[] => {
105
158
  };
106
159
 
107
160
  const normalizeCell = (cell: any, index: number): Cell => {
108
- const source = coerceToString(cell?.source ?? '');
161
+ const source = coerceToString(cell?.source ?? "");
109
162
  const outputs = normalizeOutputs(cell?.outputs);
110
163
  const error = normalizeError(cell?.error);
111
164
  return {
112
- id: typeof cell?.id === 'string' && cell.id ? cell.id : `cell-${index}`,
113
- cell_type: cell?.cell_type || 'code',
165
+ id: typeof cell?.id === "string" && cell.id ? cell.id : `cell-${index}`,
166
+ cell_type: cell?.cell_type || "code",
114
167
  source,
115
168
  outputs,
116
169
  metadata: cell?.metadata || {},
@@ -120,16 +173,21 @@ const normalizeCell = (cell: any, index: number): Cell => {
120
173
  } as Cell;
121
174
  };
122
175
 
123
- function notebookReducer(state: NotebookState, action: NotebookAction): NotebookState {
176
+ function notebookReducer(
177
+ state: NotebookState,
178
+ action: NotebookAction,
179
+ ): NotebookState {
124
180
  switch (action.type) {
125
- case 'NOTEBOOK_LOADED':
126
- case 'NOTEBOOK_UPDATED':
181
+ case "NOTEBOOK_LOADED":
182
+ case "NOTEBOOK_UPDATED":
127
183
  return {
128
184
  ...state,
129
- cells: action.payload.cells.map((cell, index) => normalizeCell(cell, index)),
185
+ cells: action.payload.cells.map((cell, index) =>
186
+ normalizeCell(cell, index),
187
+ ),
130
188
  };
131
189
 
132
- case 'EXECUTION_START': {
190
+ case "EXECUTION_START": {
133
191
  const newExecuting = new Set(state.executingCells);
134
192
  newExecuting.add(action.payload.cell_index);
135
193
  return {
@@ -137,13 +195,18 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
137
195
  executingCells: newExecuting,
138
196
  cells: state.cells.map((cell, i) =>
139
197
  i === action.payload.cell_index
140
- ? { ...cell, outputs: [], error: null, execution_count: action.payload.execution_count }
141
- : cell
198
+ ? {
199
+ ...cell,
200
+ outputs: [],
201
+ error: null,
202
+ execution_count: action.payload.execution_count,
203
+ }
204
+ : cell,
142
205
  ),
143
206
  };
144
207
  }
145
208
 
146
- case 'STREAM_OUTPUT': {
209
+ case "STREAM_OUTPUT": {
147
210
  const { cell_index, stream, text } = action.payload;
148
211
  return {
149
212
  ...state,
@@ -152,7 +215,7 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
152
215
 
153
216
  const outputs = [...(cell.outputs || [])];
154
217
  const streamIndex = outputs.findIndex(
155
- (o) => o.output_type === 'stream' && o.name === stream
218
+ (o) => o.output_type === "stream" && o.name === stream,
156
219
  );
157
220
 
158
221
  if (streamIndex > -1) {
@@ -162,54 +225,59 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
162
225
  text: streamOutput.text + text,
163
226
  };
164
227
  } else {
165
- outputs.push({ output_type: 'stream', name: stream, text } as StreamOutput);
228
+ outputs.push({
229
+ output_type: "stream",
230
+ name: stream,
231
+ text,
232
+ } as StreamOutput);
166
233
  }
167
234
  return { ...cell, outputs };
168
235
  }),
169
236
  };
170
237
  }
171
238
 
172
- case 'EXECUTE_RESULT': {
239
+ case "EXECUTE_RESULT": {
173
240
  const { cell_index, execution_count, data } = action.payload;
174
241
  return {
175
242
  ...state,
176
243
  cells: state.cells.map((cell, i) => {
177
244
  if (i !== cell_index) return cell;
178
245
  const outputs = [...(cell.outputs || [])];
179
-
246
+
180
247
  // Check if this is display data (e.g., matplotlib image)
181
- const hasImageData = (data as any)?.['image/png'] || (data as any)?.['image/jpeg'] || (data as any)?.['image/svg+xml'];
182
-
248
+ const hasImageData =
249
+ (data as any)?.["image/png"] ||
250
+ (data as any)?.["image/jpeg"] ||
251
+ (data as any)?.["image/svg+xml"];
252
+
183
253
  if (hasImageData) {
184
254
  // Create display_data output for images (matplotlib plots)
185
255
  outputs.push({
186
- output_type: 'display_data',
256
+ output_type: "display_data",
187
257
  data: data || {},
188
258
  } as any);
189
259
  } else {
190
260
  // Create execute_result output for text results
191
261
  outputs.push({
192
- output_type: 'execute_result',
262
+ output_type: "execute_result",
193
263
  execution_count,
194
264
  data: {
195
265
  ...(data || {}),
196
- 'text/plain': coerceToString(data?.['text/plain'] ?? ''),
266
+ "text/plain": coerceToString(data?.["text/plain"] ?? ""),
197
267
  },
198
268
  } as ExecuteResultOutput);
199
269
  }
200
-
270
+
201
271
  return { ...cell, outputs, execution_count };
202
272
  }),
203
273
  };
204
274
  }
205
-
206
- case 'EXECUTION_COMPLETE': {
275
+
276
+ case "EXECUTION_COMPLETE": {
207
277
  const payload = action.payload || {};
208
278
  const cell_index = payload.cell_index;
209
279
  // Support both shapes: { result: {...} } and flat payload {...}
210
- const result = (payload && payload.result)
211
- ? payload.result
212
- : payload || {};
280
+ const result = payload && payload.result ? payload.result : payload || {};
213
281
  const newExecuting = new Set(state.executingCells);
214
282
  newExecuting.delete(cell_index);
215
283
  return {
@@ -217,11 +285,17 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
217
285
  executingCells: newExecuting,
218
286
  cells: state.cells.map((cell, i) => {
219
287
  if (i !== cell_index) return cell;
220
- const normalizedOutputs = normalizeOutputs(Array.isArray((result as any).outputs) ? (result as any).outputs : []);
288
+ const normalizedOutputs = normalizeOutputs(
289
+ Array.isArray((result as any).outputs)
290
+ ? (result as any).outputs
291
+ : [],
292
+ );
221
293
  const normalizedError = normalizeError(result?.error);
222
294
 
223
295
  const existingOutputs = cell.outputs || [];
224
- const finalNonStream = normalizedOutputs.filter(o => (o as any).output_type !== 'stream');
296
+ const finalNonStream = normalizedOutputs.filter(
297
+ (o) => (o as any).output_type !== "stream",
298
+ );
225
299
  return {
226
300
  ...cell,
227
301
  ...result,
@@ -234,44 +308,48 @@ function notebookReducer(state: NotebookState, action: NotebookAction): Notebook
234
308
  };
235
309
  }
236
310
 
237
- case 'EXECUTION_ERROR': {
238
- const { cell_index, error } = action.payload;
239
- const newExecuting = new Set(state.executingCells);
240
- newExecuting.delete(cell_index);
241
- const normalizedError = normalizeError(error);
242
- return {
243
- ...state,
244
- executingCells: newExecuting,
245
- cells: state.cells.map((cell, i) =>
246
- i === cell_index
247
- ? {
248
- ...cell,
249
- error: normalizedError,
250
- outputs: normalizedError
251
- ? [...(cell.outputs || []), normalizedError]
252
- : cell.outputs || [],
253
- }
254
- : cell
255
- ),
256
- };
311
+ case "EXECUTION_ERROR": {
312
+ const { cell_index, error } = action.payload;
313
+ const newExecuting = new Set(state.executingCells);
314
+ newExecuting.delete(cell_index);
315
+ const normalizedError = normalizeError(error);
316
+ return {
317
+ ...state,
318
+ executingCells: newExecuting,
319
+ cells: state.cells.map((cell, i) =>
320
+ i === cell_index
321
+ ? {
322
+ ...cell,
323
+ error: normalizedError,
324
+ outputs: normalizedError
325
+ ? [...(cell.outputs || []), normalizedError]
326
+ : cell.outputs || [],
327
+ }
328
+ : cell,
329
+ ),
330
+ };
257
331
  }
258
332
 
259
- case 'UPDATE_CELL_SOURCE': {
333
+ case "UPDATE_CELL_SOURCE": {
260
334
  const { cell_index, source } = action.payload;
261
335
  return {
262
336
  ...state,
263
337
  cells: state.cells.map((cell, i) =>
264
- i === cell_index ? { ...cell, source } : cell
338
+ i === cell_index ? { ...cell, source } : cell,
265
339
  ),
266
340
  };
267
341
  }
268
342
 
269
- case 'RESET_KERNEL':
270
- return {
271
- ...state,
272
- executingCells: new Set(),
273
- cells: state.cells.map(cell => ({ ...cell, outputs: [], execution_count: null })),
274
- };
343
+ case "RESET_KERNEL":
344
+ return {
345
+ ...state,
346
+ executingCells: new Set(),
347
+ cells: state.cells.map((cell) => ({
348
+ ...cell,
349
+ outputs: [],
350
+ execution_count: null,
351
+ })),
352
+ };
275
353
 
276
354
  default:
277
355
  return state;
@@ -284,17 +362,37 @@ interface NotebookProps {
284
362
  notebookName?: string;
285
363
  }
286
364
 
287
- export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' }) => {
365
+ // Deletion queue for undo functionality
366
+ interface DeletedCell {
367
+ cell: Cell;
368
+ index: number;
369
+ timestamp: number;
370
+ }
371
+
372
+ export const Notebook: React.FC<NotebookProps> = ({
373
+ notebookName = "default",
374
+ }) => {
288
375
  const [state, dispatch] = useReducer(notebookReducer, initialState);
289
376
  const { cells, executingCells } = state;
290
-
377
+
291
378
  const [currentCellIndex, setCurrentCellIndex] = useState<number | null>(null);
292
- const [kernelStatus, setKernelStatus] = useState<'connecting' | 'connected' | 'disconnected'>('connecting');
379
+ const [kernelStatus, setKernelStatus] = useState<
380
+ "connecting" | "connected" | "disconnected"
381
+ >("connecting");
293
382
  const wsRef = useRef<WebSocketService | null>(null);
294
383
 
384
+ // Deletion queue for undo functionality (lasts entire session, cleared on close)
385
+ const deletionQueueRef = useRef<DeletedCell[]>([]);
386
+ const [showUndoHint, setShowUndoHint] = useState(false);
387
+
388
+ // DEBUG: Verify new code is loaded
389
+ useEffect(() => {
390
+ console.log("✅ NEW NOTEBOOK CODE LOADED - Undo functionality available");
391
+ }, []);
392
+
295
393
  useEffect(() => {
296
394
  const body = document.body;
297
- const pathAttr = body.getAttribute('data-notebook-path');
395
+ const pathAttr = body.getAttribute("data-notebook-path");
298
396
  if (pathAttr) {
299
397
  document.title = `MoreCompute – ${pathAttr}`;
300
398
  }
@@ -303,24 +401,24 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
303
401
  // --- Event Handlers ---
304
402
 
305
403
  const handleNotebookLoaded = useCallback((data: any) => {
306
- dispatch({ type: 'NOTEBOOK_LOADED', payload: data });
404
+ dispatch({ type: "NOTEBOOK_LOADED", payload: data });
307
405
  }, []);
308
406
 
309
407
  const handleExecutionStart = useCallback((data: any) => {
310
- dispatch({ type: 'EXECUTION_START', payload: data });
408
+ dispatch({ type: "EXECUTION_START", payload: data });
311
409
  }, []);
312
-
410
+
313
411
  const handleStreamOutput = useCallback((data: any) => {
314
- dispatch({ type: 'STREAM_OUTPUT', payload: data });
412
+ dispatch({ type: "STREAM_OUTPUT", payload: data });
315
413
  }, []);
316
414
 
317
415
  const handleExecutionComplete = useCallback((data: any) => {
318
- dispatch({ type: 'EXECUTION_COMPLETE', payload: data });
416
+ dispatch({ type: "EXECUTION_COMPLETE", payload: data });
319
417
  }, []);
320
418
 
321
419
  const handleExecuteResult = useCallback((data: any) => {
322
420
  dispatch({
323
- type: 'EXECUTE_RESULT',
421
+ type: "EXECUTE_RESULT",
324
422
  payload: {
325
423
  cell_index: data.cell_index,
326
424
  execution_count: data.execution_count,
@@ -330,134 +428,236 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
330
428
  }, []);
331
429
 
332
430
  const handleExecutionError = useCallback((data: any) => {
333
- dispatch({ type: 'EXECUTION_ERROR', payload: data });
431
+ dispatch({ type: "EXECUTION_ERROR", payload: data });
334
432
  }, []);
335
433
 
336
434
  const handleNotebookUpdate = useCallback((data: any) => {
337
- dispatch({ type: 'NOTEBOOK_UPDATED', payload: data });
435
+ dispatch({ type: "NOTEBOOK_UPDATED", payload: data });
338
436
  }, []);
339
-
340
- const handleKernelStatusUpdate = useCallback((status: 'connecting' | 'connected' | 'disconnected') => {
341
- setKernelStatus(status);
342
- const dot = document.getElementById("kernel-status-dot");
343
- const text = document.getElementById("kernel-status-text");
344
-
345
- if (dot && text) {
346
- dot.className = 'status-dot'; // Reset classes
347
- switch (status) {
348
- case "connecting":
349
- dot.classList.add("connecting");
350
- text.textContent = "Connecting...";
351
- break;
352
- case "connected":
353
- dot.classList.add("connected");
354
- text.textContent = "Kernel Ready";
355
- break;
356
- case "disconnected":
357
- dot.classList.add("disconnected");
358
- text.textContent = "Kernel Disconnected";
359
- break;
437
+
438
+ const handleKernelStatusUpdate = useCallback(
439
+ (status: "connecting" | "connected" | "disconnected") => {
440
+ setKernelStatus(status);
441
+ const dot = document.getElementById("kernel-status-dot");
442
+ const text = document.getElementById("kernel-status-text");
443
+
444
+ if (dot && text) {
445
+ dot.className = "status-dot"; // Reset classes
446
+ switch (status) {
447
+ case "connecting":
448
+ dot.classList.add("connecting");
449
+ text.textContent = "Connecting...";
450
+ break;
451
+ case "connected":
452
+ dot.classList.add("connected");
453
+ text.textContent = "Kernel Ready";
454
+ break;
455
+ case "disconnected":
456
+ dot.classList.add("disconnected");
457
+ text.textContent = "Kernel Disconnected";
458
+ break;
459
+ }
360
460
  }
361
- }
362
- }, []);
461
+ },
462
+ [],
463
+ );
363
464
 
364
465
  useEffect(() => {
365
466
  const ws = new WebSocketService();
366
467
  wsRef.current = ws;
367
- handleKernelStatusUpdate('connecting');
468
+ handleKernelStatusUpdate("connecting");
368
469
 
369
- ws.connect('ws://127.0.0.1:8000/ws')
470
+ ws.connect("ws://127.0.0.1:8000/ws")
370
471
  .then(() => {
371
- ws.loadNotebook(notebookName || 'default');
472
+ ws.loadNotebook(notebookName || "default");
372
473
  })
373
- .catch(error => {
374
- console.error('Failed to connect:', error);
375
- handleKernelStatusUpdate('disconnected');
474
+ .catch((error) => {
475
+ console.error("Failed to connect:", error);
476
+ handleKernelStatusUpdate("disconnected");
376
477
  });
377
478
 
378
- ws.on('connect', () => handleKernelStatusUpdate('connected'));
379
- ws.on('disconnect', () => handleKernelStatusUpdate('disconnected'));
380
- ws.on('notebook_loaded', handleNotebookLoaded);
381
- ws.on('notebook_updated', handleNotebookUpdate);
382
- ws.on('execution_start', handleExecutionStart);
383
- ws.on('stream_output', handleStreamOutput);
384
- ws.on('execution_complete', handleExecutionComplete);
385
- ws.on('execution_result', handleExecuteResult);
386
- ws.on('execution_error', handleExecutionError);
387
-
479
+ ws.on("connect", () => handleKernelStatusUpdate("connected"));
480
+ ws.on("disconnect", () => handleKernelStatusUpdate("disconnected"));
481
+ ws.on("notebook_loaded", handleNotebookLoaded);
482
+ ws.on("notebook_updated", handleNotebookUpdate);
483
+ ws.on("execution_start", handleExecutionStart);
484
+ ws.on("stream_output", handleStreamOutput);
485
+ ws.on("execution_complete", handleExecutionComplete);
486
+ ws.on("execution_result", handleExecuteResult);
487
+ ws.on("execution_error", handleExecutionError);
488
+
388
489
  return () => ws.disconnect();
389
- }, [notebookName, handleKernelStatusUpdate, handleNotebookLoaded, handleNotebookUpdate, handleExecutionStart, handleStreamOutput, handleExecuteResult, handleExecutionComplete, handleExecutionError]);
490
+ }, [
491
+ notebookName,
492
+ handleKernelStatusUpdate,
493
+ handleNotebookLoaded,
494
+ handleNotebookUpdate,
495
+ handleExecutionStart,
496
+ handleStreamOutput,
497
+ handleExecuteResult,
498
+ handleExecutionComplete,
499
+ handleExecutionError,
500
+ ]);
390
501
 
391
502
  // Simplified save management - only save on Ctrl+S or Run
392
- const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle');
503
+ const [saveState, setSaveState] = useState<"idle" | "saving" | "saved">(
504
+ "idle",
505
+ );
393
506
 
394
507
  // Simple save function (defined BEFORE cell actions that use it)
395
508
  const saveNotebook = useCallback(() => {
396
- setSaveState('saving');
509
+ setSaveState("saving");
397
510
  wsRef.current?.saveNotebook();
398
511
 
399
512
  // Show "Saving..." for 500ms, then "Saved" for 2 seconds
400
513
  setTimeout(() => {
401
- setSaveState('saved');
514
+ setSaveState("saved");
402
515
  setTimeout(() => {
403
- setSaveState('idle');
516
+ setSaveState("idle");
404
517
  }, 2000);
405
518
  }, 500);
406
519
  }, []);
407
520
 
408
- // Keyboard shortcut (Cmd+S / Ctrl+S)
409
- useEffect(() => {
410
- const handleKeyDown = (e: KeyboardEvent) => {
411
- if ((e.metaKey || e.ctrlKey) && e.key === 's') {
412
- e.preventDefault();
521
+ // --- Cell Actions ---
522
+
523
+ const executeCell = useCallback(
524
+ (index: number) => {
525
+ const cell = cells[index];
526
+ if (!cell) return;
527
+
528
+ if (cell.cell_type === "markdown") {
529
+ // Save before rendering markdown
413
530
  saveNotebook();
531
+ // Markdown rendering is handled locally in Cell.tsx now
532
+ return;
414
533
  }
415
- };
416
534
 
417
- window.addEventListener('keydown', handleKeyDown);
418
- return () => window.removeEventListener('keydown', handleKeyDown);
419
- }, [saveNotebook]);
535
+ // Save before executing code
536
+ saveNotebook();
537
+ wsRef.current?.executeCell(index, cell.source);
538
+ },
539
+ [cells, saveNotebook],
540
+ );
420
541
 
421
- // --- Cell Actions ---
542
+ const interruptCell = useCallback((index: number) => {
543
+ wsRef.current?.interruptKernel(index);
544
+ }, []);
422
545
 
423
- const executeCell = useCallback((index: number) => {
424
- const cell = cells[index];
425
- if (!cell) return;
546
+ const deleteCell = useCallback(
547
+ (index: number) => {
548
+ const cell = cells[index];
549
+ if (!cell) return;
426
550
 
427
- if (cell.cell_type === 'markdown') {
428
- // Save before rendering markdown
429
- saveNotebook();
430
- // Markdown rendering is handled locally in Cell.tsx now
551
+ // Add to deletion queue before deleting
552
+ const deletedCell: DeletedCell = {
553
+ cell: { ...cell },
554
+ index,
555
+ timestamp: Date.now(),
556
+ };
557
+
558
+ // Add to front of queue (most recent first)
559
+ deletionQueueRef.current.unshift(deletedCell);
560
+
561
+ // Show undo hint
562
+ setShowUndoHint(true);
563
+ setTimeout(() => setShowUndoHint(false), 5000); // Hide after 5 seconds
564
+
565
+ wsRef.current?.deleteCell(index);
566
+ },
567
+ [cells],
568
+ );
569
+
570
+ const undoDelete = useCallback(() => {
571
+ const deletedCell = deletionQueueRef.current.shift(); // Get most recent deletion
572
+ if (!deletedCell) {
573
+ console.log("No deletions to undo");
431
574
  return;
432
575
  }
433
576
 
434
- // Save before executing code
435
- saveNotebook();
436
- wsRef.current?.executeCell(index, cell.source);
437
- }, [cells, saveNotebook]);
577
+ // Hide undo hint when user actually undoes
578
+ setShowUndoHint(false);
438
579
 
439
- const interruptCell = useCallback((index: number) => {
440
- wsRef.current?.interruptKernel(index);
580
+ // Add the cell back at its original index with full cell data (outputs, metadata, etc.)
581
+ wsRef.current?.addCell(
582
+ deletedCell.index,
583
+ deletedCell.cell.cell_type,
584
+ deletedCell.cell.source,
585
+ deletedCell.cell, // Pass full cell to restore outputs, execution_count, metadata
586
+ );
441
587
  }, []);
442
588
 
443
- const deleteCell = useCallback((index: number) => {
444
- wsRef.current?.deleteCell(index);
445
- }, []);
589
+ // Keyboard shortcuts (Cmd+S / Ctrl+S for save, Ctrl+Z for undo)
590
+ useEffect(() => {
591
+ const handleKeyDown = (e: KeyboardEvent) => {
592
+ if ((e.metaKey || e.ctrlKey) && e.key === "s") {
593
+ e.preventDefault();
594
+ e.stopPropagation();
595
+ saveNotebook();
596
+ }
597
+ // Only intercept Ctrl+Z for cell deletion undo (not editor undo)
598
+ // Check if we have deletions in queue and we're not focused in an editor
599
+ if ((e.metaKey || e.ctrlKey) && e.key === "z" && !e.shiftKey) {
600
+ const hasDeletions = deletionQueueRef.current.length > 0;
601
+ const target = e.target as HTMLElement;
602
+ const isInEditor =
603
+ target.closest(".cm-editor") ||
604
+ target.tagName === "TEXTAREA" ||
605
+ target.tagName === "INPUT";
606
+
607
+ // If we have deletions and NOT in an editor, intercept for cell undo
608
+ if (hasDeletions && !isInEditor) {
609
+ e.preventDefault();
610
+ e.stopPropagation();
611
+ undoDelete();
612
+ }
613
+ }
614
+ };
615
+
616
+ // Use capture phase to intercept before CodeMirror
617
+ window.addEventListener("keydown", handleKeyDown, true);
618
+ return () => window.removeEventListener("keydown", handleKeyDown, true);
619
+ }, [saveNotebook, undoDelete]);
446
620
 
447
621
  const updateCell = useCallback((index: number, source: string) => {
448
- dispatch({ type: 'UPDATE_CELL_SOURCE', payload: { cell_index: index, source } });
622
+ dispatch({
623
+ type: "UPDATE_CELL_SOURCE",
624
+ payload: { cell_index: index, source },
625
+ });
449
626
  wsRef.current?.updateCell(index, source);
450
627
  }, []);
451
628
 
452
- const addCell = useCallback((type: 'code' | 'markdown' = 'code', index: number) => {
453
- wsRef.current?.addCell(index, type);
454
- setCurrentCellIndex(index);
455
- }, []);
629
+ const addCell = useCallback(
630
+ (type: "code" | "markdown" = "code", index: number) => {
631
+ wsRef.current?.addCell(index, type);
632
+ setCurrentCellIndex(index);
633
+ },
634
+ [],
635
+ );
636
+
637
+ const moveUp = useCallback((index: number) => {
638
+ if (index > 0) {
639
+ wsRef.current?.moveCell(index, index - 1);
640
+ // Save after moving
641
+ saveNotebook();
642
+ }
643
+ }, [saveNotebook]);
644
+
645
+ const moveDown = useCallback((index: number) => {
646
+ if (index < cells.length - 1) {
647
+ wsRef.current?.moveCell(index, index + 1);
648
+ // Save after moving
649
+ saveNotebook();
650
+ }
651
+ }, [cells.length, saveNotebook]);
456
652
 
457
653
  const resetKernel = () => {
458
- if (confirm('Are you sure you want to restart the kernel? All variables will be lost.')) {
654
+ if (
655
+ confirm(
656
+ "Are you sure you want to restart the kernel? All variables will be lost.",
657
+ )
658
+ ) {
459
659
  wsRef.current?.resetKernel();
460
- dispatch({ type: 'RESET_KERNEL' });
660
+ dispatch({ type: "RESET_KERNEL" });
461
661
  }
462
662
  };
463
663
 
@@ -466,37 +666,66 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
466
666
  return (
467
667
  <>
468
668
  {/* Save Status Indicator - show saving and saved states */}
469
- {saveState !== 'idle' && (
669
+ {saveState !== "idle" && (
470
670
  <div
471
671
  style={{
472
- position: 'fixed',
473
- bottom: '20px',
474
- right: '20px',
475
- padding: '8px 14px',
476
- borderRadius: '8px',
477
- fontSize: '12px',
672
+ position: "fixed",
673
+ bottom: "20px",
674
+ right: "20px",
675
+ padding: "8px 14px",
676
+ borderRadius: "8px",
677
+ fontSize: "12px",
478
678
  fontWeight: 500,
479
- backgroundColor: saveState === 'saved' ? '#10b98114' : '#3b82f614',
480
- color: saveState === 'saved' ? '#10b981' : '#3b82f6',
481
- border: `1px solid ${saveState === 'saved' ? '#10b981' : '#3b82f6'}`,
482
- display: 'flex',
483
- alignItems: 'center',
484
- gap: '8px',
679
+ backgroundColor: saveState === "saved" ? "#10b98114" : "#3b82f614",
680
+ color: saveState === "saved" ? "#10b981" : "#3b82f6",
681
+ border: `1px solid ${saveState === "saved" ? "#10b981" : "#3b82f6"}`,
682
+ display: "flex",
683
+ alignItems: "center",
684
+ gap: "8px",
485
685
  zIndex: 1000,
486
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.1)',
487
- animation: 'fadeIn 0.2s ease',
686
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
687
+ animation: "fadeIn 0.2s ease",
488
688
  }}
489
689
  >
490
- {saveState === 'saving' && '⟳ Saving...'}
491
- {saveState === 'saved' && '✓ Saved'}
690
+ {saveState === "saving" && "⟳ Saving..."}
691
+ {saveState === "saved" && "✓ Saved"}
492
692
  </div>
493
693
  )}
494
694
 
695
+ {/* Undo Hint - show when cells are deleted */}
696
+ {showUndoHint && (
697
+ <div
698
+ style={{
699
+ position: "fixed",
700
+ bottom: "20px",
701
+ left: "20px",
702
+ padding: "8px 14px",
703
+ borderRadius: "8px",
704
+ fontSize: "12px",
705
+ fontWeight: 500,
706
+ backgroundColor: "#f59e0b14",
707
+ color: "#f59e0b",
708
+ border: "1px solid #f59e0b",
709
+ display: "flex",
710
+ alignItems: "center",
711
+ gap: "8px",
712
+ zIndex: 1000,
713
+ boxShadow: "0 2px 8px rgba(0, 0, 0, 0.1)",
714
+ animation: "fadeIn 0.2s ease",
715
+ }}
716
+ >
717
+ Cell deleted. Press{" "}
718
+ {navigator.platform.includes("Mac") ? "⌘Z" : "Ctrl+Z"} to undo
719
+ </div>
720
+ )}
721
+
722
+ {/* Cell List */}
495
723
  {cells.map((cell, index) => (
496
724
  <CellComponent
497
725
  key={cell.id}
498
726
  cell={cell}
499
727
  index={index}
728
+ totalCells={cells.length}
500
729
  isActive={currentCellIndex === index}
501
730
  isExecuting={executingCells.has(index)}
502
731
  onExecute={executeCell}
@@ -505,16 +734,18 @@ export const Notebook: React.FC<NotebookProps> = ({ notebookName = 'default' })
505
734
  onUpdate={updateCell}
506
735
  onSetActive={setCurrentCellIndex}
507
736
  onAddCell={addCell}
737
+ onMoveUp={moveUp}
738
+ onMoveDown={moveDown}
508
739
  />
509
740
  ))}
510
741
 
511
742
  {cells.length === 0 && (
512
743
  <div id="empty-state" className="empty-state">
513
- <div className="add-cell-line" data-position="0">
514
- <AddCellButton onAddCell={(type) => addCell(type, 0)} />
515
- </div>
744
+ <div className="add-cell-line" data-position="0">
745
+ <AddCellButton onAddCell={(type) => addCell(type, 0)} />
746
+ </div>
516
747
  </div>
517
748
  )}
518
749
  </>
519
750
  );
520
- };
751
+ };