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 +1 -1
- package/src/cell-controls/widgets.js +30 -0
- package/src/cells.js +9 -9
- package/src/ctrl-k-modal.js +190 -14
- package/src/document-languages.js +105 -0
- package/src/execution.js +73 -25
- package/src/frontmatter-updater.js +224 -0
- package/src/index.js +173 -86
- package/src/markdown/renderer.js +52 -3
- package/src/markdown/styles.js +126 -0
- package/src/monitor-coordination.js +1 -3
- package/src/mrp-client.js +36 -169
- package/src/mrp-types.js +1 -37
- package/src/output-widget.js +54 -0
- package/src/runtime-codelens/index.js +3 -3
- package/src/shell/ai-menu.js +70 -0
- package/src/shell/components/menu.js +39 -1
- package/src/shell/components/status-bar.js +213 -11
- package/src/shell/dialogs/file-picker.js +378 -6
- package/src/shell/layouts/studio.js +31 -9
- package/src/shell/orchestrator-client.js +105 -18
- package/src/shell/state.js +63 -42
- package/src/shell/styles.js +328 -0
- package/src/term-pty-client.js +62 -7
- package/src/widgets/theme-utils.js +12 -12
- package/src/widgets/theme.js +520 -0
package/package.json
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
155
|
+
// Parse terminal language for named terminal runtime support
|
|
156
156
|
const terminalInfo = parseTerminalLanguage(blockLanguage);
|
|
157
|
-
// Terminal
|
|
158
|
-
const terminalSession = terminalInfo.sessionName || (terminalInfo.isTerminal ?
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
blockContext = null;
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
|
package/src/ctrl-k-modal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|