mrmd-editor 0.6.0 → 0.7.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mrmd-editor",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Markdown editor with realtime collaboration - the core editor package",
5
5
  "type": "module",
6
6
  "main": "dist/mrmd.cjs",
@@ -492,6 +492,36 @@ export function injectCellControlsStyles() {
492
492
  .${PREFIX}-btn-terminal:hover {
493
493
  color: var(--widget-info, #60a5fa);
494
494
  }
495
+
496
+ /* ---- Mobile: larger touch targets for cell controls ---- */
497
+ @media (pointer: coarse) {
498
+ .${PREFIX}-btn {
499
+ width: 32px;
500
+ height: 32px;
501
+ opacity: 0.8;
502
+ }
503
+
504
+ .${PREFIX}-btn svg {
505
+ width: 16px;
506
+ height: 16px;
507
+ }
508
+
509
+ .${PREFIX} {
510
+ gap: 8px;
511
+ margin-left: 12px;
512
+ }
513
+
514
+ .${PREFIX}-status {
515
+ padding: 4px 10px;
516
+ font-size: 12px;
517
+ }
518
+
519
+ .${PREFIX}-gutter {
520
+ width: 32px;
521
+ height: 32px;
522
+ font-size: 14px;
523
+ }
524
+ }
495
525
  `;
496
526
 
497
527
  document.head.appendChild(style);
package/src/cells.js CHANGED
@@ -105,7 +105,7 @@ function parseArtifactLanguage(lang) {
105
105
  * @param {string} content - Document content
106
106
  * @returns {Array<{
107
107
  * language: string,
108
- * session: string|null, // session name if specified (e.g., ```js mysession)
108
+ * context: string|null, // execution context if specified (e.g., ```js sandbox)
109
109
  * code: string,
110
110
  * start: number, // start of opening fence
111
111
  * end: number, // end of closing fence
@@ -124,7 +124,7 @@ export function findCodeBlocks(content) {
124
124
  let inBlock = false;
125
125
  let blockStart = 0;
126
126
  let blockLanguage = '';
127
- let blockSession = null;
127
+ let blockContext = null;
128
128
  let codeStart = 0;
129
129
  let blockLine = 0;
130
130
  let charOffset = 0;
@@ -134,7 +134,7 @@ export function findCodeBlocks(content) {
134
134
  const lineStart = charOffset;
135
135
 
136
136
  if (!inBlock) {
137
- // Look for opening fence: ```language [session]
137
+ // Look for opening fence: ```language [context]
138
138
  // Examples: ```js, ```js sandbox, ```python myenv, ```html:artifact, ```css:myapp
139
139
  // Language can include colon for targets (html:name, css:name, js:name, term:session)
140
140
  const match = line.match(/^(`{3,})([\w:.-]*)(?:\s+(\S+))?/);
@@ -142,7 +142,7 @@ export function findCodeBlocks(content) {
142
142
  inBlock = true;
143
143
  blockStart = lineStart;
144
144
  blockLanguage = match[2].toLowerCase();
145
- blockSession = match[3] || null; // session name after language
145
+ blockContext = match[3] || null; // optional context name after language
146
146
  codeStart = lineStart + line.length + 1; // +1 for newline
147
147
  blockLine = i;
148
148
  }
@@ -152,10 +152,10 @@ export function findCodeBlocks(content) {
152
152
  const codeEnd = lineStart;
153
153
  const blockEnd = lineStart + line.length;
154
154
 
155
- // Parse terminal language for session support
155
+ // Parse terminal language for named terminal runtime support
156
156
  const terminalInfo = parseTerminalLanguage(blockLanguage);
157
- // Terminal session can come from term:session or from space-separated session
158
- const terminalSession = terminalInfo.sessionName || (terminalInfo.isTerminal ? blockSession : null);
157
+ // Terminal runtime name can come from term:session or from space-separated context
158
+ const terminalSession = terminalInfo.sessionName || (terminalInfo.isTerminal ? blockContext : null);
159
159
 
160
160
  // Parse artifact language for artifact panel support
161
161
  const artifactInfo = parseArtifactLanguage(blockLanguage);
@@ -166,7 +166,7 @@ export function findCodeBlocks(content) {
166
166
  blocks.push({
167
167
  language: blockLanguage,
168
168
  baseLanguage: effectiveLanguage, // The language without :target suffix
169
- session: blockSession,
169
+ context: blockContext,
170
170
  code: content.slice(codeStart, codeEnd),
171
171
  start: blockStart,
172
172
  end: blockEnd,
@@ -184,7 +184,7 @@ export function findCodeBlocks(content) {
184
184
  });
185
185
 
186
186
  inBlock = false;
187
- blockSession = null;
187
+ blockContext = null;
188
188
  }
189
189
  }
190
190
 
@@ -71,8 +71,10 @@ function showCtrlKModal(view) {
71
71
  // Get cursor screen coordinates
72
72
  const cursorPos = view.state.selection.main.head;
73
73
  const coords = view.coordsAtPos(cursorPos);
74
+ const isMobile = window.matchMedia('(max-width: 768px)').matches;
74
75
 
75
- if (!coords) {
76
+ // On desktop, coords are required for positioning. On mobile, CSS handles it.
77
+ if (!coords && !isMobile) {
76
78
  console.warn('[Ctrl-K] Could not get cursor coordinates');
77
79
  return;
78
80
  }
@@ -83,8 +85,11 @@ function showCtrlKModal(view) {
83
85
  // Create modal container
84
86
  const modal = document.createElement('div');
85
87
  modal.className = 'cm-ctrl-k-modal';
86
- modal.style.left = `${coords.left}px`;
87
- modal.style.top = `${coords.bottom + 8}px`;
88
+ // On mobile, CSS positions it as a bottom sheet — don't set inline styles
89
+ if (!isMobile && coords) {
90
+ modal.style.left = `${coords.left}px`;
91
+ modal.style.top = `${coords.bottom + 8}px`;
92
+ }
88
93
 
89
94
  // Track expanded/collapsed state
90
95
  let isExpanded = false;
@@ -208,10 +213,16 @@ function showCtrlKModal(view) {
208
213
 
209
214
  modal.appendChild(controls);
210
215
 
216
+ // On mobile: start with controls visible (expanded), skip dragging
217
+ if (isMobile) {
218
+ isExpanded = true;
219
+ modal.classList.add('expanded');
220
+ }
221
+
211
222
  document.body.appendChild(modal);
212
223
  activeModal = modal;
213
224
 
214
- // Make draggable
225
+ // Make draggable (desktop only)
215
226
  let isDragging = false;
216
227
  let dragStartX, dragStartY, modalStartX, modalStartY;
217
228
 
@@ -242,16 +253,18 @@ function showCtrlKModal(view) {
242
253
  }
243
254
  });
244
255
 
245
- // Adjust position if off-screen
246
- requestAnimationFrame(() => {
247
- const rect = modal.getBoundingClientRect();
248
- if (rect.right > window.innerWidth - 10) {
249
- modal.style.left = `${Math.max(10, window.innerWidth - rect.width - 10)}px`;
250
- }
251
- if (rect.bottom > window.innerHeight - 10) {
252
- modal.style.top = `${Math.max(10, coords.top - rect.height - 4)}px`;
253
- }
254
- });
256
+ // Adjust position if off-screen (desktop only — mobile uses CSS bottom sheet)
257
+ if (!isMobile) {
258
+ requestAnimationFrame(() => {
259
+ const rect = modal.getBoundingClientRect();
260
+ if (rect.right > window.innerWidth - 10) {
261
+ modal.style.left = `${Math.max(10, window.innerWidth - rect.width - 10)}px`;
262
+ }
263
+ if (coords && rect.bottom > window.innerHeight - 10) {
264
+ modal.style.top = `${Math.max(10, coords.top - rect.height - 4)}px`;
265
+ }
266
+ });
267
+ }
255
268
 
256
269
  // Focus input
257
270
  input.focus();
@@ -494,6 +507,169 @@ function injectCtrlKStyles() {
494
507
  .cm-ctrl-k-modal.loading .cm-ctrl-k-toggle {
495
508
  display: none;
496
509
  }
510
+
511
+ /* ==========================================================================
512
+ MOBILE: Ctrl-K as a focused, keyboard-aware command bar.
513
+
514
+ On mobile, the modal becomes a clean input bar that sits right above
515
+ the virtual keyboard — like iMessage's text field or Spotlight search.
516
+ It uses the visualViewport API (via --mobile-vh custom property) to
517
+ stay visible when the keyboard is open.
518
+
519
+ Design: minimal, one clear action. The input is the hero.
520
+ Settings (quality/thinking) start expanded since there's room.
521
+ ========================================================================== */
522
+ @media (max-width: 768px) {
523
+ .cm-ctrl-k-modal {
524
+ position: fixed !important;
525
+ left: 0 !important;
526
+ right: 0 !important;
527
+ /* Anchor to bottom of visual viewport, not layout viewport */
528
+ bottom: 0 !important;
529
+ bottom: calc(100vh - var(--mobile-vh, 100vh)) !important;
530
+ top: auto !important;
531
+ min-width: 100%;
532
+ max-width: 100%;
533
+ border-radius: 14px 14px 0 0;
534
+ border: none;
535
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
536
+ box-shadow: none;
537
+ background: var(--bg-secondary, #1c1c1c);
538
+ animation: cm-ctrl-k-slide-up 0.2s cubic-bezier(0.2, 0, 0, 1);
539
+ /* Let content size naturally */
540
+ max-height: none;
541
+ }
542
+
543
+ @keyframes cm-ctrl-k-slide-up {
544
+ from {
545
+ transform: translateY(100%);
546
+ opacity: 0.8;
547
+ }
548
+ to {
549
+ transform: translateY(0);
550
+ opacity: 1;
551
+ }
552
+ }
553
+
554
+ /* Header: slim, just close button on the right */
555
+ .cm-ctrl-k-header {
556
+ cursor: default;
557
+ justify-content: flex-end;
558
+ padding: 6px 8px 0;
559
+ background: transparent;
560
+ border-bottom: none;
561
+ min-height: 0;
562
+ }
563
+
564
+ /* Hide the toggle — controls are always visible on mobile */
565
+ .cm-ctrl-k-toggle {
566
+ display: none !important;
567
+ }
568
+
569
+ .cm-ctrl-k-close {
570
+ width: 36px;
571
+ height: 36px;
572
+ font-size: 20px;
573
+ border-radius: 18px;
574
+ color: var(--text-muted, #888);
575
+ }
576
+
577
+ .cm-ctrl-k-close:hover {
578
+ background: rgba(255, 255, 255, 0.06);
579
+ }
580
+
581
+ /* Input: the hero element. Big, clear, inviting. */
582
+ .cm-ctrl-k-input-wrap {
583
+ padding: 4px 16px 12px;
584
+ gap: 10px;
585
+ }
586
+
587
+ .cm-ctrl-k-input {
588
+ font-size: 17px; /* Comfortable reading size, prevents iOS zoom */
589
+ padding: 10px 0;
590
+ line-height: 1.4;
591
+ color: var(--text, #e0e0e0);
592
+ }
593
+
594
+ .cm-ctrl-k-input::placeholder {
595
+ color: var(--text-dim, #555);
596
+ font-weight: 400;
597
+ }
598
+
599
+ /* Loader: bigger, more visible */
600
+ .cm-ctrl-k-loader {
601
+ width: 18px;
602
+ height: 18px;
603
+ border-width: 2px;
604
+ }
605
+
606
+ /* Controls: always visible on mobile, no expand/collapse */
607
+ .cm-ctrl-k-controls {
608
+ max-height: none !important;
609
+ overflow: visible !important;
610
+ padding: 0 16px 12px !important;
611
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
612
+ padding-top: 12px !important;
613
+ padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
614
+ }
615
+
616
+ /* Keep controls visible even when not "expanded" */
617
+ .cm-ctrl-k-modal:not(.expanded) .cm-ctrl-k-controls {
618
+ max-height: none !important;
619
+ padding: 12px 16px !important;
620
+ padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px)) !important;
621
+ border-top-color: rgba(255, 255, 255, 0.06);
622
+ }
623
+
624
+ .cm-ctrl-k-row {
625
+ margin-bottom: 10px;
626
+ gap: 14px;
627
+ }
628
+
629
+ .cm-ctrl-k-label {
630
+ font-size: 12px;
631
+ font-weight: 500;
632
+ text-transform: uppercase;
633
+ letter-spacing: 0.3px;
634
+ color: var(--text-dim, #666);
635
+ width: 64px;
636
+ }
637
+
638
+ .cm-ctrl-k-btns {
639
+ gap: 6px;
640
+ flex: 1;
641
+ justify-content: flex-start;
642
+ }
643
+
644
+ .cm-ctrl-k-btn {
645
+ width: 38px;
646
+ height: 38px;
647
+ font-size: 14px;
648
+ font-weight: 500;
649
+ border-radius: 10px;
650
+ border-color: rgba(255, 255, 255, 0.1);
651
+ transition: all 0.15s ease;
652
+ }
653
+
654
+ .cm-ctrl-k-btn.active {
655
+ border-color: var(--accent, #58a6ff);
656
+ box-shadow: 0 0 0 1px var(--accent, #58a6ff);
657
+ }
658
+
659
+ /* Loading state */
660
+ .cm-ctrl-k-modal.loading .cm-ctrl-k-input-wrap {
661
+ opacity: 0.7;
662
+ }
663
+
664
+ .cm-ctrl-k-modal.loading .cm-ctrl-k-controls {
665
+ display: none !important;
666
+ }
667
+
668
+ /* Error state */
669
+ .cm-ctrl-k-modal.error {
670
+ border-top-color: rgba(255, 80, 80, 0.3);
671
+ }
672
+ }
497
673
  `;
498
674
 
499
675
  const style = document.createElement('style');
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Document Language Detection
3
+ *
4
+ * Scans a document's content for fenced code blocks and extracts
5
+ * the set of executable programming languages used.
6
+ *
7
+ * Used by the notebook-scoped runtimes panel to show only relevant runtimes.
8
+ *
9
+ * @module document-languages
10
+ */
11
+
12
+ // Languages that have runtimes (executable code blocks)
13
+ const RUNTIME_LANGUAGES = new Set([
14
+ 'python', 'bash', 'javascript', 'julia', 'r', 'shell', 'node', 'typescript',
15
+ ]);
16
+
17
+ // Non-executable languages (display-only, config, or markup)
18
+ const NON_EXECUTABLE_LANGUAGES = new Set([
19
+ 'yaml', 'json', 'toml', 'xml', 'html', 'css', 'mermaid',
20
+ 'output', 'markdown', 'md', 'text', 'txt', 'diff', 'csv',
21
+ 'sql', 'graphql', 'latex', 'tex', 'bibtex',
22
+ ]);
23
+
24
+ // Language alias normalization map
25
+ const LANGUAGE_ALIASES = {
26
+ py: 'python',
27
+ python3: 'python',
28
+ js: 'javascript',
29
+ node: 'javascript',
30
+ ts: 'typescript',
31
+ typescript: 'javascript', // Same runtime as JS
32
+ jl: 'julia',
33
+ sh: 'bash',
34
+ shell: 'bash',
35
+ zsh: 'bash',
36
+ rlang: 'r',
37
+ };
38
+
39
+ /**
40
+ * Extract the set of executable programming languages from a document.
41
+ *
42
+ * Scans for fenced code blocks (``` or ~~~) and collects their language tags,
43
+ * filtering out non-executable languages (yaml, json, html, css, mermaid, output, etc.)
44
+ * and normalizing aliases (py → python, js → javascript, etc.)
45
+ *
46
+ * @param {string} content - Document content (full markdown text)
47
+ * @returns {Set<string>} Set of normalized language names (e.g. Set(['python', 'bash']))
48
+ */
49
+ export function getDocumentLanguages(content) {
50
+ const languages = new Set();
51
+ // Match opening fences: ```python or ~~~julia
52
+ // Also handle ```python config (skip config blocks)
53
+ const fenceRegex = /^(`{3,}|~{3,})(\w+)(?:\s+(.*))?$/gm;
54
+ let match;
55
+
56
+ while ((match = fenceRegex.exec(content))) {
57
+ const rawLang = match[2].toLowerCase();
58
+ const extra = (match[3] || '').trim().toLowerCase();
59
+
60
+ // Skip config blocks (```yaml config)
61
+ if (extra === 'config') continue;
62
+
63
+ // Skip non-executable languages
64
+ if (NON_EXECUTABLE_LANGUAGES.has(rawLang)) continue;
65
+
66
+ // Normalize aliases
67
+ const normalized = LANGUAGE_ALIASES[rawLang] || rawLang;
68
+
69
+ // Only include if it's a known runtime language
70
+ // (to avoid random annotation languages like "diagram" etc.)
71
+ if (RUNTIME_LANGUAGES.has(rawLang) || RUNTIME_LANGUAGES.has(normalized)) {
72
+ languages.add(normalized);
73
+ }
74
+ }
75
+
76
+ return languages;
77
+ }
78
+
79
+ /**
80
+ * Map a normalized language name to its display label and badge color.
81
+ *
82
+ * @param {string} language - Normalized language name
83
+ * @returns {{ label: string, badgeClass: string }}
84
+ */
85
+ export function getLanguageDisplay(language) {
86
+ const displays = {
87
+ python: { label: 'Python', badgeClass: 'python' },
88
+ javascript: { label: 'JavaScript', badgeClass: 'javascript' },
89
+ bash: { label: 'Bash', badgeClass: 'bash' },
90
+ julia: { label: 'Julia', badgeClass: 'julia' },
91
+ r: { label: 'R', badgeClass: 'r' },
92
+ };
93
+ return displays[language] || { label: language, badgeClass: language };
94
+ }
95
+
96
+ /**
97
+ * Check if a language is executable (has a runtime).
98
+ *
99
+ * @param {string} language - Language name (raw or normalized)
100
+ * @returns {boolean}
101
+ */
102
+ export function isExecutableLanguage(language) {
103
+ const normalized = LANGUAGE_ALIASES[language.toLowerCase()] || language.toLowerCase();
104
+ return RUNTIME_LANGUAGES.has(normalized);
105
+ }
package/src/execution.js CHANGED
@@ -105,9 +105,6 @@ export class ExecutionManager {
105
105
  /** @type {string|null} Default runtime URL for monitor mode */
106
106
  this._defaultRuntimeUrl = null;
107
107
 
108
- /** @type {string} Session name for execution isolation */
109
- this._monitorSession = 'default';
110
-
111
108
  /** @type {Map<string, Function>} Unsubscribe functions for monitor status watchers */
112
109
  this._monitorUnsubscribes = new Map();
113
110
 
@@ -130,9 +127,8 @@ export class ExecutionManager {
130
127
  * @param {Y.Doc} options.ydoc - Yjs document
131
128
  * @param {string} options.runtimeUrl - Default runtime URL for MRP
132
129
  * @param {import('y-protocols/awareness').Awareness} [options.awareness] - For monitor detection
133
- * @param {string} [options.session] - Session name for execution isolation (defaults to 'default')
134
130
  */
135
- enableMonitorMode({ ydoc, runtimeUrl, awareness, session }) {
131
+ enableMonitorMode({ ydoc, runtimeUrl, awareness }) {
136
132
  if (this.coordination) {
137
133
  this.coordination.destroy();
138
134
  }
@@ -141,9 +137,8 @@ export class ExecutionManager {
141
137
  this._defaultRuntimeUrl = runtimeUrl;
142
138
  this._monitorMode = true;
143
139
  this._awareness = awareness;
144
- this._monitorSession = session || 'default';
145
140
 
146
- console.log('[ExecutionManager] Monitor mode enabled, runtimeUrl:', runtimeUrl, 'session:', this._monitorSession);
141
+ console.log('[ExecutionManager] Monitor mode enabled, runtimeUrl:', runtimeUrl);
147
142
  }
148
143
 
149
144
  /**
@@ -742,6 +737,9 @@ export class ExecutionManager {
742
737
  // Emit start event
743
738
  this._emit('cellRun', index, cell, execId);
744
739
 
740
+ // Direct execution output write throttling (reduces CRDT churn for progress bars)
741
+ let clearChunkFlushTimer = null;
742
+
745
743
  try {
746
744
  // Prepare output position
747
745
  // Re-read content as it may have changed
@@ -820,22 +818,11 @@ export class ExecutionManager {
820
818
 
821
819
  // Track current output length in document for replacement
822
820
  let currentDocOutputLen = 0;
821
+ const chunkFlushMs = 100;
822
+ let chunkFlushTimer = null;
823
+ let latestProcessedOutput = '';
823
824
 
824
- const onChunk = (chunk, accumulatedRaw, done) => {
825
- if (controller.signal.aborted) return;
826
-
827
- // Process through terminal buffer (handles \r, cursor movement, ANSI)
828
- buffer.write(chunk);
829
-
830
- // Get processed output with ANSI codes preserved
831
- let processedOutput = buffer.toAnsi();
832
-
833
- // Ensure output ends with newline so closing ``` stays on its own line
834
- // This is critical for maintaining valid markdown structure
835
- if (processedOutput && !processedOutput.endsWith('\n')) {
836
- processedOutput += '\n';
837
- }
838
-
825
+ const applyProcessedOutput = (processedOutput) => {
839
826
  // Get current position - prefer finding by execId for robustness
840
827
  let currentOutputStart = outputContentStart;
841
828
 
@@ -870,6 +857,61 @@ export class ExecutionManager {
870
857
  });
871
858
 
872
859
  currentDocOutputLen = processedOutput.length;
860
+ };
861
+
862
+ const flushChunkOutputNow = () => {
863
+ if (chunkFlushTimer) {
864
+ clearTimeout(chunkFlushTimer);
865
+ chunkFlushTimer = null;
866
+ }
867
+ applyProcessedOutput(latestProcessedOutput);
868
+ };
869
+
870
+ clearChunkFlushTimer = () => {
871
+ if (chunkFlushTimer) {
872
+ clearTimeout(chunkFlushTimer);
873
+ chunkFlushTimer = null;
874
+ }
875
+ };
876
+
877
+ const scheduleChunkOutputFlush = () => {
878
+ if (chunkFlushMs === 0) {
879
+ flushChunkOutputNow();
880
+ return;
881
+ }
882
+ if (chunkFlushTimer) return;
883
+ chunkFlushTimer = setTimeout(() => {
884
+ chunkFlushTimer = null;
885
+ applyProcessedOutput(latestProcessedOutput);
886
+ }, chunkFlushMs);
887
+ };
888
+
889
+ controller.signal.addEventListener('abort', () => {
890
+ clearChunkFlushTimer?.();
891
+ }, { once: true });
892
+
893
+ const onChunk = (chunk, accumulatedRaw, done) => {
894
+ if (controller.signal.aborted) return;
895
+
896
+ // Process through terminal buffer (handles \r, cursor movement, ANSI)
897
+ buffer.write(chunk);
898
+
899
+ // Get processed output with ANSI codes preserved
900
+ let processedOutput = buffer.toAnsi();
901
+
902
+ // Ensure output ends with newline so closing ``` stays on its own line
903
+ // This is critical for maintaining valid markdown structure
904
+ if (processedOutput && !processedOutput.endsWith('\n')) {
905
+ processedOutput += '\n';
906
+ }
907
+
908
+ latestProcessedOutput = processedOutput;
909
+
910
+ if (done) {
911
+ flushChunkOutputNow();
912
+ } else {
913
+ scheduleChunkOutputFlush();
914
+ }
873
915
 
874
916
  this._emit('cellOutput', index, chunk, processedOutput, execId);
875
917
  };
@@ -888,6 +930,9 @@ export class ExecutionManager {
888
930
  return;
889
931
  }
890
932
 
933
+ // Flush pending output so prompt context is visible immediately
934
+ flushChunkOutputNow();
935
+
891
936
  const stdinExecId = execId;
892
937
 
893
938
  // Wait for any pending output to be written to the document
@@ -1006,14 +1051,17 @@ export class ExecutionManager {
1006
1051
 
1007
1052
  // Execute with streaming (pass onStdinRequest for input() support)
1008
1053
  // Pass execId so hub runtimes can find the output block
1009
- // Pass session name for named session support (e.g., ```js sandbox)
1054
+ // Pass context hint for runtime-specific isolation (e.g., ```js sandbox)
1010
1055
  const result = await this.registry.executeStreaming(code, runtimeLanguage, onChunk, onStdinRequest, {
1011
1056
  execId,
1012
1057
  cellId: cell.id || `cell-${index}`,
1013
- session: cell.session,
1058
+ context: cell.context,
1014
1059
  onAsset,
1015
1060
  });
1016
1061
 
1062
+ // Flush any pending throttled output before final normalization
1063
+ flushChunkOutputNow();
1064
+
1017
1065
  // Final update - find output block by execId for robustness
1018
1066
  content = this.editor.getContent();
1019
1067
  const finalOutputBlock = findOutputBlockByExecId(content, execId);
@@ -1132,6 +1180,7 @@ export class ExecutionManager {
1132
1180
  }
1133
1181
  return execId;
1134
1182
  } finally {
1183
+ clearChunkFlushTimer?.();
1135
1184
  this.running.delete(execId);
1136
1185
  this.buffers.delete(execId);
1137
1186
 
@@ -1176,7 +1225,6 @@ export class ExecutionManager {
1176
1225
  code,
1177
1226
  language,
1178
1227
  runtimeUrl,
1179
- session: this._monitorSession,
1180
1228
  cellId: cell.id || `cell-${index}`,
1181
1229
  });
1182
1230