nebula-notebook 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +222 -0
  2. package/bin/nebula-notebook.js +33 -0
  3. package/dist/assets/index-C1h_sArD.css +32 -0
  4. package/dist/assets/index-CDSTBon8.js +658 -0
  5. package/dist/favicon.svg +11 -0
  6. package/dist/index.html +73 -0
  7. package/node-server/dist/app.d.ts +5 -0
  8. package/node-server/dist/app.js +38 -0
  9. package/node-server/dist/auth/auth-middleware.d.ts +24 -0
  10. package/node-server/dist/auth/auth-middleware.js +276 -0
  11. package/node-server/dist/auth/auth-service.d.ts +84 -0
  12. package/node-server/dist/auth/auth-service.js +265 -0
  13. package/node-server/dist/auth/index.d.ts +3 -0
  14. package/node-server/dist/auth/index.js +8 -0
  15. package/node-server/dist/cluster/client-registration.d.ts +43 -0
  16. package/node-server/dist/cluster/client-registration.js +217 -0
  17. package/node-server/dist/cluster/cluster-secret.d.ts +11 -0
  18. package/node-server/dist/cluster/cluster-secret.js +90 -0
  19. package/node-server/dist/cluster/kernel-proxy.d.ts +100 -0
  20. package/node-server/dist/cluster/kernel-proxy.js +361 -0
  21. package/node-server/dist/cluster/server-registry.d.ts +109 -0
  22. package/node-server/dist/cluster/server-registry.js +217 -0
  23. package/node-server/dist/config/output-limits.d.ts +7 -0
  24. package/node-server/dist/config/output-limits.js +10 -0
  25. package/node-server/dist/discovery/discovery-service.d.ts +198 -0
  26. package/node-server/dist/discovery/discovery-service.js +811 -0
  27. package/node-server/dist/discovery/index.d.ts +5 -0
  28. package/node-server/dist/discovery/index.js +21 -0
  29. package/node-server/dist/discovery/types.d.ts +48 -0
  30. package/node-server/dist/discovery/types.js +24 -0
  31. package/node-server/dist/fs/fs-service.d.ts +218 -0
  32. package/node-server/dist/fs/fs-service.js +1422 -0
  33. package/node-server/dist/fs/index.d.ts +5 -0
  34. package/node-server/dist/fs/index.js +21 -0
  35. package/node-server/dist/fs/types.d.ts +132 -0
  36. package/node-server/dist/fs/types.js +5 -0
  37. package/node-server/dist/index.d.ts +13 -0
  38. package/node-server/dist/index.js +556 -0
  39. package/node-server/dist/kernel/default-kernel.d.ts +5 -0
  40. package/node-server/dist/kernel/default-kernel.js +138 -0
  41. package/node-server/dist/kernel/index.d.ts +7 -0
  42. package/node-server/dist/kernel/index.js +23 -0
  43. package/node-server/dist/kernel/kernel-service.d.ts +290 -0
  44. package/node-server/dist/kernel/kernel-service.js +1714 -0
  45. package/node-server/dist/kernel/kernelspec.d.ts +29 -0
  46. package/node-server/dist/kernel/kernelspec.js +210 -0
  47. package/node-server/dist/kernel/session-store.d.ts +87 -0
  48. package/node-server/dist/kernel/session-store.js +303 -0
  49. package/node-server/dist/kernel/types.d.ts +143 -0
  50. package/node-server/dist/kernel/types.js +17 -0
  51. package/node-server/dist/llm/index.d.ts +5 -0
  52. package/node-server/dist/llm/index.js +21 -0
  53. package/node-server/dist/llm/llm-service.d.ts +77 -0
  54. package/node-server/dist/llm/llm-service.js +454 -0
  55. package/node-server/dist/llm/types.d.ts +40 -0
  56. package/node-server/dist/llm/types.js +15 -0
  57. package/node-server/dist/notebook/cell-metadata.d.ts +27 -0
  58. package/node-server/dist/notebook/cell-metadata.js +76 -0
  59. package/node-server/dist/notebook/headless-handler.d.ts +127 -0
  60. package/node-server/dist/notebook/headless-handler.js +1530 -0
  61. package/node-server/dist/notebook/notebook-websocket.d.ts +12 -0
  62. package/node-server/dist/notebook/notebook-websocket.js +103 -0
  63. package/node-server/dist/notebook/operation-router.d.ts +115 -0
  64. package/node-server/dist/notebook/operation-router.js +641 -0
  65. package/node-server/dist/notebook/undoRedoManager.d.ts +194 -0
  66. package/node-server/dist/notebook/undoRedoManager.js +558 -0
  67. package/node-server/dist/output/display-data.d.ts +14 -0
  68. package/node-server/dist/output/display-data.js +134 -0
  69. package/node-server/dist/resources/resource-service.d.ts +69 -0
  70. package/node-server/dist/resources/resource-service.js +363 -0
  71. package/node-server/dist/routes/auth.d.ts +5 -0
  72. package/node-server/dist/routes/auth.js +61 -0
  73. package/node-server/dist/routes/cluster.d.ts +7 -0
  74. package/node-server/dist/routes/cluster.js +94 -0
  75. package/node-server/dist/routes/fs.d.ts +7 -0
  76. package/node-server/dist/routes/fs.js +392 -0
  77. package/node-server/dist/routes/kernel.d.ts +13 -0
  78. package/node-server/dist/routes/kernel.js +637 -0
  79. package/node-server/dist/routes/llm.d.ts +8 -0
  80. package/node-server/dist/routes/llm.js +105 -0
  81. package/node-server/dist/routes/notebook.d.ts +10 -0
  82. package/node-server/dist/routes/notebook.js +335 -0
  83. package/node-server/dist/routes/python.d.ts +8 -0
  84. package/node-server/dist/routes/python.js +187 -0
  85. package/node-server/dist/routes/resources.d.ts +7 -0
  86. package/node-server/dist/routes/resources.js +77 -0
  87. package/node-server/dist/scripts/show-auth-qr.d.ts +1 -0
  88. package/node-server/dist/scripts/show-auth-qr.js +81 -0
  89. package/node-server/dist/terminal/pty-manager.d.ts +100 -0
  90. package/node-server/dist/terminal/pty-manager.js +246 -0
  91. package/node-server/dist/terminal/server.d.ts +19 -0
  92. package/node-server/dist/terminal/server.js +254 -0
  93. package/node-server/dist/terminal/types.d.ts +50 -0
  94. package/node-server/dist/terminal/types.js +9 -0
  95. package/node-server/package.json +45 -0
  96. package/package.json +99 -0
  97. package/scripts/postinstall.cjs +26 -0
@@ -0,0 +1,558 @@
1
+ "use strict";
2
+ /**
3
+ * Headless Undo/Redo Manager
4
+ *
5
+ * Manages undo/redo state for notebooks when operated via the headless backend (MCP).
6
+ * This mirrors the frontend UndoRedoManager but uses NebulaCell types.
7
+ *
8
+ * Note: This is a simplified version of lib/undoRedoCore.ts adapted for the backend.
9
+ * The core logic is intentionally similar to maintain feature parity.
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.HeadlessUndoRedoManager = void 0;
13
+ exports.getUndoRedoManager = getUndoRedoManager;
14
+ const uuid_1 = require("uuid");
15
+ // ============================================================================
16
+ // PURE FUNCTIONS
17
+ // ============================================================================
18
+ function cloneCell(cell) {
19
+ return {
20
+ ...cell,
21
+ outputs: (cell.outputs || []).map(output => ({ ...output }))
22
+ };
23
+ }
24
+ function stripCellOutputs(cell) {
25
+ return {
26
+ ...cell,
27
+ outputs: [],
28
+ isExecuting: false
29
+ };
30
+ }
31
+ function applyOperation(cells, op) {
32
+ switch (op.type) {
33
+ case 'insertCell': {
34
+ const newCells = [...cells];
35
+ newCells.splice(op.index, 0, cloneCell(op.cell));
36
+ return newCells;
37
+ }
38
+ case 'deleteCell': {
39
+ return cells.filter((_, i) => i !== op.index);
40
+ }
41
+ case 'moveCell': {
42
+ const newCells = [...cells];
43
+ const [moved] = newCells.splice(op.fromIndex, 1);
44
+ newCells.splice(op.toIndex, 0, moved);
45
+ return newCells;
46
+ }
47
+ case 'updateContent': {
48
+ return cells.map(c => c.id === op.cellId ? { ...c, content: op.newContent } : c);
49
+ }
50
+ case 'updateMetadata': {
51
+ return cells.map(c => {
52
+ if (c.id !== op.cellId)
53
+ return c;
54
+ const updated = { ...c };
55
+ for (const [key, change] of Object.entries(op.changes)) {
56
+ updated[key] = change.new;
57
+ }
58
+ return updated;
59
+ });
60
+ }
61
+ case 'batch': {
62
+ return op.operations.reduce((acc, subOp) => applyOperation(acc, subOp), cells);
63
+ }
64
+ default:
65
+ return cells;
66
+ }
67
+ }
68
+ function reverseOperation(op) {
69
+ switch (op.type) {
70
+ case 'insertCell':
71
+ return { type: 'deleteCell', index: op.index, cell: op.cell };
72
+ case 'deleteCell':
73
+ return { type: 'insertCell', index: op.index, cell: op.cell };
74
+ case 'moveCell':
75
+ return { type: 'moveCell', fromIndex: op.toIndex, toIndex: op.fromIndex };
76
+ case 'updateContent':
77
+ return {
78
+ type: 'updateContent',
79
+ cellId: op.cellId,
80
+ oldContent: op.newContent,
81
+ newContent: op.oldContent
82
+ };
83
+ case 'updateMetadata': {
84
+ const reversed = {};
85
+ for (const [key, change] of Object.entries(op.changes)) {
86
+ reversed[key] = { old: change.new, new: change.old };
87
+ }
88
+ return { type: 'updateMetadata', cellId: op.cellId, changes: reversed };
89
+ }
90
+ case 'batch':
91
+ return {
92
+ type: 'batch',
93
+ operations: op.operations.map(reverseOperation).reverse()
94
+ };
95
+ default:
96
+ return op;
97
+ }
98
+ }
99
+ function getAffectedCellIds(op, cells) {
100
+ switch (op.type) {
101
+ case 'insertCell':
102
+ case 'deleteCell':
103
+ return [op.cell.id];
104
+ case 'moveCell':
105
+ if (cells && op.toIndex >= 0 && op.toIndex < cells.length) {
106
+ return [cells[op.toIndex].id];
107
+ }
108
+ return [];
109
+ case 'updateContent':
110
+ case 'updateMetadata':
111
+ return [op.cellId];
112
+ case 'batch': {
113
+ const ids = new Set();
114
+ for (const subOp of op.operations) {
115
+ for (const id of getAffectedCellIds(subOp, cells)) {
116
+ ids.add(id);
117
+ }
118
+ }
119
+ return Array.from(ids);
120
+ }
121
+ default:
122
+ return [];
123
+ }
124
+ }
125
+ // ============================================================================
126
+ // HEADLESS UNDO/REDO MANAGER
127
+ // ============================================================================
128
+ /**
129
+ * Manages undo/redo state for multiple notebooks in the headless backend.
130
+ */
131
+ class HeadlessUndoRedoManager {
132
+ states = new Map();
133
+ fsService;
134
+ constructor(fsService) {
135
+ this.fsService = fsService;
136
+ }
137
+ /**
138
+ * Get or initialize undo state for a notebook.
139
+ */
140
+ getState(notebookPath, cells) {
141
+ if (!this.states.has(notebookPath)) {
142
+ // Try to load existing history
143
+ let loadedHistory = [];
144
+ try {
145
+ loadedHistory = this.fsService.loadHistory(notebookPath);
146
+ }
147
+ catch (err) {
148
+ console.warn(`[UndoRedoManager] Failed to load history for ${notebookPath}:`, err);
149
+ }
150
+ // Initialize state
151
+ const state = {
152
+ undoStack: [],
153
+ redoStack: [],
154
+ fullHistory: loadedHistory.length > 0 ? loadedHistory : [{
155
+ type: 'snapshot',
156
+ cells: cells.map(stripCellOutputs),
157
+ timestamp: Date.now()
158
+ }],
159
+ lastContent: new Map(cells.map(c => [c.id, c.content]))
160
+ };
161
+ // Rebuild undo stack from history
162
+ if (loadedHistory.length > 0) {
163
+ state.undoStack = this.rebuildUndoStack(loadedHistory);
164
+ }
165
+ this.states.set(notebookPath, state);
166
+ }
167
+ return this.states.get(notebookPath);
168
+ }
169
+ /**
170
+ * Rebuild undo stack from history.
171
+ */
172
+ rebuildUndoStack(history) {
173
+ const undoneIds = new Set();
174
+ for (const op of history) {
175
+ if (op.isUndo && op.undoesOperationId) {
176
+ undoneIds.add(op.undoesOperationId);
177
+ }
178
+ }
179
+ const undoableOps = [];
180
+ for (const op of history) {
181
+ if (op.isUndo)
182
+ continue;
183
+ if (op.operationId && undoneIds.has(op.operationId))
184
+ continue;
185
+ if (op.type === 'insertCell' || op.type === 'deleteCell' ||
186
+ op.type === 'moveCell' || op.type === 'updateContent' ||
187
+ op.type === 'updateMetadata' || op.type === 'batch') {
188
+ const { timestamp, operationId, isUndo, undoesOperationId, ...operation } = op;
189
+ undoableOps.push(operation);
190
+ }
191
+ }
192
+ return undoableOps;
193
+ }
194
+ /**
195
+ * Record an operation (called by headless handler after executing an operation).
196
+ */
197
+ recordOperation(notebookPath, cells, op) {
198
+ const state = this.getState(notebookPath, cells);
199
+ // Clear redo stack (can't redo after new operation)
200
+ if (state.redoStack.length > 0) {
201
+ // Record the undos that would have been in redo stack
202
+ for (const redoOp of state.redoStack) {
203
+ const reversedOp = reverseOperation(redoOp);
204
+ const timestampedOp = {
205
+ ...reversedOp,
206
+ timestamp: Date.now(),
207
+ operationId: (0, uuid_1.v4)(),
208
+ isUndo: true,
209
+ undoesOperationId: redoOp.operationId,
210
+ };
211
+ state.fullHistory.push(timestampedOp);
212
+ }
213
+ state.redoStack = [];
214
+ }
215
+ // Add operation to history
216
+ const operationId = (0, uuid_1.v4)();
217
+ const timestampedOp = {
218
+ ...op,
219
+ timestamp: Date.now(),
220
+ operationId,
221
+ };
222
+ state.fullHistory.push(timestampedOp);
223
+ // Add to undo stack
224
+ const opWithId = { ...op, operationId };
225
+ state.undoStack.push(opWithId);
226
+ // Update content tracking
227
+ if (op.type === 'updateContent') {
228
+ state.lastContent.set(op.cellId, op.newContent);
229
+ }
230
+ else if (op.type === 'insertCell') {
231
+ state.lastContent.set(op.cell.id, op.cell.content);
232
+ }
233
+ else if (op.type === 'deleteCell') {
234
+ state.lastContent.delete(op.cell.id);
235
+ }
236
+ }
237
+ /**
238
+ * Record a non-undoable log operation (history only).
239
+ */
240
+ recordLogOperation(notebookPath, cells, op) {
241
+ const state = this.getState(notebookPath, cells);
242
+ const timestampedOp = {
243
+ ...op,
244
+ timestamp: Date.now(),
245
+ operationId: (0, uuid_1.v4)(),
246
+ };
247
+ state.fullHistory.push(timestampedOp);
248
+ }
249
+ /**
250
+ * Undo the last operation.
251
+ */
252
+ undo(notebookPath, cells) {
253
+ const state = this.getState(notebookPath, cells);
254
+ if (state.undoStack.length === 0) {
255
+ return {
256
+ cells,
257
+ result: { success: false, affectedCellIds: [], operationType: '', error: 'Nothing to undo' }
258
+ };
259
+ }
260
+ const op = state.undoStack.pop();
261
+ const reversedOp = reverseOperation(op);
262
+ // Apply the reversed operation
263
+ const newCells = applyOperation(cells, reversedOp);
264
+ // Push to redo stack
265
+ state.redoStack.push(op);
266
+ // Update content tracking
267
+ this.updateContentTrackingAfterUndo(state, op, newCells);
268
+ return {
269
+ cells: newCells,
270
+ result: {
271
+ success: true,
272
+ affectedCellIds: getAffectedCellIds(reversedOp, newCells),
273
+ operationType: op.type
274
+ }
275
+ };
276
+ }
277
+ /**
278
+ * Redo the last undone operation.
279
+ */
280
+ redo(notebookPath, cells) {
281
+ const state = this.getState(notebookPath, cells);
282
+ if (state.redoStack.length === 0) {
283
+ return {
284
+ cells,
285
+ result: { success: false, affectedCellIds: [], operationType: '', error: 'Nothing to redo' }
286
+ };
287
+ }
288
+ const op = state.redoStack.pop();
289
+ // Apply the operation
290
+ const newCells = applyOperation(cells, op);
291
+ // Push to undo stack
292
+ state.undoStack.push(op);
293
+ // Update content tracking
294
+ this.updateContentTrackingAfterRedo(state, op, newCells);
295
+ return {
296
+ cells: newCells,
297
+ result: {
298
+ success: true,
299
+ affectedCellIds: getAffectedCellIds(op, newCells),
300
+ operationType: op.type
301
+ }
302
+ };
303
+ }
304
+ updateContentTrackingAfterUndo(state, op, cells) {
305
+ if (op.type === 'updateContent') {
306
+ state.lastContent.set(op.cellId, op.oldContent);
307
+ }
308
+ else if (op.type === 'insertCell') {
309
+ state.lastContent.delete(op.cell.id);
310
+ }
311
+ else if (op.type === 'deleteCell') {
312
+ state.lastContent.set(op.cell.id, op.cell.content);
313
+ }
314
+ else if (op.type === 'batch') {
315
+ for (let i = op.operations.length - 1; i >= 0; i--) {
316
+ this.updateContentTrackingAfterUndo(state, op.operations[i], cells);
317
+ }
318
+ }
319
+ }
320
+ updateContentTrackingAfterRedo(state, op, cells) {
321
+ if (op.type === 'updateContent') {
322
+ state.lastContent.set(op.cellId, op.newContent);
323
+ }
324
+ else if (op.type === 'insertCell') {
325
+ state.lastContent.set(op.cell.id, op.cell.content);
326
+ }
327
+ else if (op.type === 'deleteCell') {
328
+ state.lastContent.delete(op.cell.id);
329
+ }
330
+ else if (op.type === 'batch') {
331
+ for (const subOp of op.operations) {
332
+ this.updateContentTrackingAfterRedo(state, subOp, cells);
333
+ }
334
+ }
335
+ }
336
+ /**
337
+ * Check if undo is available.
338
+ */
339
+ canUndo(notebookPath, cells) {
340
+ const state = this.getState(notebookPath, cells);
341
+ return state.undoStack.length > 0;
342
+ }
343
+ /**
344
+ * Check if redo is available.
345
+ */
346
+ canRedo(notebookPath, cells) {
347
+ const state = this.getState(notebookPath, cells);
348
+ return state.redoStack.length > 0;
349
+ }
350
+ /**
351
+ * Get history for a notebook.
352
+ */
353
+ getHistory(notebookPath, cells) {
354
+ const state = this.getState(notebookPath, cells);
355
+ return state.fullHistory;
356
+ }
357
+ /**
358
+ * Clear state for a notebook (e.g., when closing).
359
+ */
360
+ clearState(notebookPath) {
361
+ this.states.delete(notebookPath);
362
+ }
363
+ /**
364
+ * Get updates since a timestamp (for agent awareness).
365
+ * Returns human-readable summaries of operations made between agent sessions.
366
+ * Includes both edits and non-undoable events (no source filtering).
367
+ */
368
+ getUpdatesSince(notebookPath, cells, sinceTimestamp) {
369
+ const state = this.getState(notebookPath, cells);
370
+ const summaries = [];
371
+ const describeEvent = (event, timestamp) => {
372
+ const cellId = event.target?.cellId;
373
+ const cellIndex = event.target?.cellIndex;
374
+ const data = event.data ? { ...event.data } : undefined;
375
+ if (data && event.name === 'runCellComplete' && 'output' in data) {
376
+ delete data.output;
377
+ }
378
+ let description = '';
379
+ switch (event.name) {
380
+ case 'runCell': {
381
+ description = `Ran cell${cellIndex !== undefined ? ` #${cellIndex + 1}` : ''}`;
382
+ break;
383
+ }
384
+ case 'runAllCells': {
385
+ const count = typeof data?.cellCount === 'number'
386
+ ? data.cellCount
387
+ : (Array.isArray(data?.cellIds) ? data.cellIds.length : undefined);
388
+ description = `Ran all cells${count !== undefined ? ` (${count})` : ''}`;
389
+ break;
390
+ }
391
+ case 'runCellComplete': {
392
+ const durationMs = typeof data?.durationMs === 'number' ? data.durationMs : undefined;
393
+ const success = typeof data?.success === 'boolean' ? data.success : undefined;
394
+ const status = success === undefined ? 'complete' : (success ? 'success' : 'failed');
395
+ description = `Run complete${cellIndex !== undefined ? ` for cell #${cellIndex + 1}` : ''}${durationMs !== undefined ? ` (${durationMs}ms)` : ''} • ${status}`;
396
+ break;
397
+ }
398
+ case 'interruptKernel':
399
+ description = 'Kernel interrupted';
400
+ break;
401
+ case 'restartKernel':
402
+ description = 'Kernel restarted';
403
+ break;
404
+ case 'startKernel': {
405
+ const kernelName = typeof data?.kernelName === 'string' ? data.kernelName : undefined;
406
+ description = `Kernel started${kernelName ? ` (${kernelName})` : ''}`;
407
+ break;
408
+ }
409
+ case 'shutdownKernel':
410
+ description = 'Kernel shutdown';
411
+ break;
412
+ default:
413
+ description = `Event: ${event.name}`;
414
+ }
415
+ summaries.push({
416
+ kind: 'event',
417
+ type: event.name,
418
+ category: event.category,
419
+ name: event.name,
420
+ cellId,
421
+ cellIndex,
422
+ timestamp,
423
+ description,
424
+ source: event.source,
425
+ runId: event.runId,
426
+ data,
427
+ });
428
+ };
429
+ const toEvent = (op) => {
430
+ if (op.type === 'event') {
431
+ return {
432
+ category: op.category,
433
+ name: op.name,
434
+ target: op.target,
435
+ data: op.data,
436
+ runId: op.runId,
437
+ source: op.source,
438
+ };
439
+ }
440
+ switch (op.type) {
441
+ case 'runCell':
442
+ return {
443
+ category: 'execution',
444
+ name: 'runCell',
445
+ target: { cellId: op.cellId, cellIndex: op.cellIndex },
446
+ runId: op.runId,
447
+ };
448
+ case 'runAllCells':
449
+ return {
450
+ category: 'execution',
451
+ name: 'runAllCells',
452
+ data: {
453
+ cellCount: op.cellCount,
454
+ cellIds: op.cellIds,
455
+ },
456
+ };
457
+ case 'runCellComplete':
458
+ return {
459
+ category: 'execution',
460
+ name: 'runCellComplete',
461
+ target: { cellId: op.cellId, cellIndex: op.cellIndex },
462
+ data: {
463
+ durationMs: op.durationMs,
464
+ success: op.success,
465
+ output: op.output,
466
+ },
467
+ runId: op.runId,
468
+ };
469
+ case 'interruptKernel':
470
+ case 'restartKernel':
471
+ return {
472
+ category: 'kernel',
473
+ name: op.type,
474
+ };
475
+ default:
476
+ return null;
477
+ }
478
+ };
479
+ for (const op of state.fullHistory) {
480
+ if (op.timestamp <= sinceTimestamp)
481
+ continue;
482
+ if (op.isUndo)
483
+ continue;
484
+ if (op.type === 'snapshot')
485
+ continue;
486
+ const event = toEvent(op);
487
+ if (event) {
488
+ describeEvent(event, op.timestamp);
489
+ continue;
490
+ }
491
+ let description = '';
492
+ let cellId;
493
+ let cellIndex;
494
+ switch (op.type) {
495
+ case 'insertCell':
496
+ cellId = op.cell.id;
497
+ cellIndex = op.index;
498
+ description = `Inserted ${op.cell.type} cell at #${op.index + 1}`;
499
+ break;
500
+ case 'deleteCell':
501
+ cellId = op.cell.id;
502
+ cellIndex = op.index;
503
+ description = `Deleted cell #${op.index + 1}`;
504
+ break;
505
+ case 'moveCell':
506
+ if (op.toIndex >= 0 && op.toIndex < cells.length) {
507
+ cellId = cells[op.toIndex].id;
508
+ }
509
+ description = `Moved cell from #${op.fromIndex + 1} to #${op.toIndex + 1}`;
510
+ break;
511
+ case 'updateContent': {
512
+ cellId = op.cellId;
513
+ cellIndex = cells.findIndex(c => c.id === op.cellId);
514
+ if (cellIndex === -1)
515
+ cellIndex = undefined;
516
+ const preview = op.newContent.slice(0, 50).replace(/\n/g, ' ');
517
+ description = `Edited cell${cellIndex !== undefined ? ` #${cellIndex + 1}` : ''}: \"${preview}${preview.length >= 50 ? '...' : ''}\"`;
518
+ break;
519
+ }
520
+ case 'updateMetadata': {
521
+ cellId = op.cellId;
522
+ cellIndex = cells.findIndex(c => c.id === op.cellId);
523
+ if (cellIndex === -1)
524
+ cellIndex = undefined;
525
+ const changes = Object.keys(op.changes).join(', ');
526
+ description = `Changed ${changes} on cell${cellIndex !== undefined ? ` #${cellIndex + 1}` : ''}`;
527
+ break;
528
+ }
529
+ case 'batch':
530
+ description = `Batch operation (${op.operations.length} changes)`;
531
+ break;
532
+ default:
533
+ description = `Operation: ${op.type}`;
534
+ }
535
+ summaries.push({
536
+ kind: 'edit',
537
+ type: op.type,
538
+ cellId,
539
+ cellIndex,
540
+ timestamp: op.timestamp,
541
+ description,
542
+ source: op.source,
543
+ });
544
+ }
545
+ return summaries;
546
+ }
547
+ }
548
+ exports.HeadlessUndoRedoManager = HeadlessUndoRedoManager;
549
+ // Manager instances per FilesystemService to avoid cross-root leakage in tests/multi-root setups
550
+ const managers = new WeakMap();
551
+ function getUndoRedoManager(fsService) {
552
+ let manager = managers.get(fsService);
553
+ if (!manager) {
554
+ manager = new HeadlessUndoRedoManager(fsService);
555
+ managers.set(fsService, manager);
556
+ }
557
+ return manager;
558
+ }
@@ -0,0 +1,14 @@
1
+ import { JsonValue, MimeBundle, OutputType } from '../fs/types';
2
+ export interface NormalizedDisplayOutput {
3
+ type: OutputType;
4
+ content: string;
5
+ mimeBundle: MimeBundle;
6
+ metadata?: Record<string, JsonValue>;
7
+ preferredMimeType: string;
8
+ }
9
+ export declare function stripAutoplayFromHtml(html: string): string;
10
+ export declare function normalizeMimeValue(mimeType: string, value: unknown): JsonValue | null;
11
+ export declare function normalizeMimeBundle(data: Record<string, unknown>): MimeBundle;
12
+ export declare function pickPreferredMimeType(bundle: MimeBundle): string | null;
13
+ export declare function buildDisplayOutput(data: Record<string, unknown>, metadata?: Record<string, unknown>): NormalizedDisplayOutput | null;
14
+ export declare function convertMimeBundleToJupyter(bundle: MimeBundle): Record<string, JsonValue>;