mrmd-editor 0.7.1 → 0.8.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 (58) hide show
  1. package/package.json +3 -1
  2. package/src/commands.js +112 -4
  3. package/src/comment-syntax.js +364 -39
  4. package/src/config/handlers.js +1 -2
  5. package/src/config/schema.js +46 -4
  6. package/src/document-template.js +2236 -0
  7. package/src/frontmatter-updater.js +204 -74
  8. package/src/grammar.js +758 -0
  9. package/src/index.js +1074 -55
  10. package/src/keymap.js +11 -2
  11. package/src/markdown/block-decorations.js +108 -5
  12. package/src/markdown/facets.js +37 -0
  13. package/src/markdown/html-inline.js +9 -5
  14. package/src/markdown/index.js +13 -3
  15. package/src/markdown/inline-commands.js +256 -0
  16. package/src/markdown/inline-model.js +578 -0
  17. package/src/markdown/inline-state.js +103 -0
  18. package/src/markdown/renderer.js +219 -12
  19. package/src/markdown/styles.js +290 -3
  20. package/src/markdown/widgets/alert-title.js +10 -8
  21. package/src/markdown/widgets/frontmatter.js +0 -6
  22. package/src/markdown/widgets/index.js +1 -0
  23. package/src/markdown/widgets/list-marker.js +29 -0
  24. package/src/markdown/wysiwyg.js +1158 -0
  25. package/src/mrp-types.js +2 -0
  26. package/src/output-widget.js +532 -18
  27. package/src/page-view-pagination.js +127 -0
  28. package/src/runtime-lsp.js +1757 -150
  29. package/src/section-controls/commands.js +617 -0
  30. package/src/section-controls/index.js +63 -0
  31. package/src/section-controls/plugin.js +165 -0
  32. package/src/section-controls/widgets.js +936 -0
  33. package/src/shell/ai-menu.js +11 -0
  34. package/src/shell/components/context-panel.js +572 -0
  35. package/src/shell/components/status-bar.js +10 -2
  36. package/src/shell/layouts/studio.js +206 -14
  37. package/src/shell/orchestrator-client.js +69 -0
  38. package/src/spellcheck.js +166 -0
  39. package/src/tables/README.md +97 -0
  40. package/src/tables/commands/insert-linked-table.js +122 -0
  41. package/src/tables/commands/open-table-workspace.js +43 -0
  42. package/src/tables/index.js +24 -0
  43. package/src/tables/jobs/client.js +158 -0
  44. package/src/tables/parsing/anchors.js +82 -0
  45. package/src/tables/parsing/linked-table-blocks.js +61 -0
  46. package/src/tables/state/linked-table-state.js +68 -0
  47. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  48. package/src/tables/widgets/linked-table-widget.js +256 -0
  49. package/src/tables/workspace/controller.js +616 -0
  50. package/src/term-pty-client.js +51 -2
  51. package/src/term-widget.js +43 -3
  52. package/src/widgets/theme-utils.js +24 -16
  53. package/src/widgets/theme.js +1015 -1
  54. package/src/runtime-codelens/detector.js +0 -279
  55. package/src/runtime-codelens/index.js +0 -76
  56. package/src/runtime-codelens/plugin.js +0 -142
  57. package/src/runtime-codelens/styles.js +0 -184
  58. package/src/runtime-codelens/widgets.js +0 -216
package/src/mrp-types.js CHANGED
@@ -299,6 +299,8 @@
299
299
  * @property {string} [type] - Type string
300
300
  * @property {string} [value] - Short value repr
301
301
  * @property {string} [signature] - Function signature
302
+ * @property {string} [docstring] - Documentation string
303
+ * @property {string} [documentation] - Documentation (normalized alias)
302
304
  */
303
305
 
304
306
  // #endregion INTROSPECTION
@@ -33,6 +33,28 @@ import { terminalToHtml, hasAnsi, stripAnsi, ansiStyles, parseAnsiDecorations }
33
33
  // Regex to match ANSI escape sequences (same as in terminal.js)
34
34
  const ANSI_ESCAPE_REGEX = /\x1b\[[0-9;]*[a-zA-Z]/g;
35
35
 
36
+ const LONG_OUTPUT_WIDGET_LINE_THRESHOLD = 15;
37
+ const JSON_OUTPUT_WIDGET_SETTING_KEY = 'mrmd-json-output-widget-enabled';
38
+ const LONG_OUTPUT_WIDGET_SETTING_KEY = 'mrmd-long-output-widget-enabled';
39
+
40
+ function readBooleanSetting(key, defaultValue = true) {
41
+ try {
42
+ if (typeof window === 'undefined' || !window.localStorage) return defaultValue;
43
+ const raw = window.localStorage.getItem(key);
44
+ if (raw == null) return defaultValue;
45
+ return !['0', 'false', 'off', 'no'].includes(String(raw).trim().toLowerCase());
46
+ } catch {
47
+ return defaultValue;
48
+ }
49
+ }
50
+
51
+ function getOutputWidgetSettings() {
52
+ return {
53
+ jsonEnabled: readBooleanSetting(JSON_OUTPUT_WIDGET_SETTING_KEY, true),
54
+ longOutputEnabled: readBooleanSetting(LONG_OUTPUT_WIDGET_SETTING_KEY, true),
55
+ };
56
+ }
57
+
36
58
  /**
37
59
  * Zero-width widget used to completely hide ANSI escape sequences.
38
60
  * Using Decoration.replace with this widget removes the escape codes
@@ -64,22 +86,303 @@ function escapeHtml(text) {
64
86
  .replace(/'/g, ''');
65
87
  }
66
88
 
89
+ function removeTrailingCommasOutsideStrings(input) {
90
+ let output = '';
91
+ let inDouble = false;
92
+
93
+ for (let i = 0; i < input.length; i++) {
94
+ const ch = input[i];
95
+
96
+ if (inDouble) {
97
+ output += ch;
98
+ if (ch === '\\' && i + 1 < input.length) {
99
+ output += input[i + 1];
100
+ i++;
101
+ } else if (ch === '"') {
102
+ inDouble = false;
103
+ }
104
+ continue;
105
+ }
106
+
107
+ if (ch === '"') {
108
+ inDouble = true;
109
+ output += ch;
110
+ continue;
111
+ }
112
+
113
+ if (ch === ',') {
114
+ let lookahead = i + 1;
115
+ while (lookahead < input.length && /\s/.test(input[lookahead])) lookahead++;
116
+ if (lookahead < input.length && (input[lookahead] === '}' || input[lookahead] === ']')) {
117
+ continue;
118
+ }
119
+ }
120
+
121
+ output += ch;
122
+ }
123
+
124
+ return output;
125
+ }
126
+
127
+ function replacePythonLiteralsOutsideStrings(input) {
128
+ const replacements = {
129
+ True: 'true',
130
+ False: 'false',
131
+ None: 'null',
132
+ };
133
+
134
+ let output = '';
135
+ let token = '';
136
+ let inDouble = false;
137
+
138
+ const flushToken = () => {
139
+ if (!token) return;
140
+ output += replacements[token] ?? token;
141
+ token = '';
142
+ };
143
+
144
+ for (let i = 0; i < input.length; i++) {
145
+ const ch = input[i];
146
+
147
+ if (inDouble) {
148
+ flushToken();
149
+ output += ch;
150
+ if (ch === '\\' && i + 1 < input.length) {
151
+ output += input[i + 1];
152
+ i++;
153
+ } else if (ch === '"') {
154
+ inDouble = false;
155
+ }
156
+ continue;
157
+ }
158
+
159
+ if (ch === '"') {
160
+ flushToken();
161
+ inDouble = true;
162
+ output += ch;
163
+ continue;
164
+ }
165
+
166
+ if (/[A-Za-z_]/.test(ch)) {
167
+ token += ch;
168
+ continue;
169
+ }
170
+
171
+ flushToken();
172
+ output += ch;
173
+ }
174
+
175
+ flushToken();
176
+ return output;
177
+ }
178
+
179
+ function normalizeJsonLikeOutput(input) {
180
+ let output = '';
181
+ let inSingle = false;
182
+ let inDouble = false;
183
+
184
+ for (let i = 0; i < input.length; i++) {
185
+ const ch = input[i];
186
+
187
+ if (inSingle) {
188
+ if (ch === '\\') {
189
+ const next = input[i + 1];
190
+ if (next === undefined) {
191
+ output += '\\\\';
192
+ continue;
193
+ }
194
+
195
+ if (next === "'") {
196
+ output += "'";
197
+ i++;
198
+ continue;
199
+ }
200
+ if (next === '"') {
201
+ output += '\\"';
202
+ i++;
203
+ continue;
204
+ }
205
+ if (next === '\\') {
206
+ output += '\\\\';
207
+ i++;
208
+ continue;
209
+ }
210
+ if (next === 'x' && /^[0-9A-Fa-f]{2}$/.test(input.slice(i + 2, i + 4))) {
211
+ output += `\\u00${input.slice(i + 2, i + 4)}`;
212
+ i += 3;
213
+ continue;
214
+ }
215
+ if (next === 'u' && /^[0-9A-Fa-f]{4}$/.test(input.slice(i + 2, i + 6))) {
216
+ output += `\\u${input.slice(i + 2, i + 6)}`;
217
+ i += 5;
218
+ continue;
219
+ }
220
+ if ('bfnrt/'.includes(next)) {
221
+ output += `\\${next}`;
222
+ i++;
223
+ continue;
224
+ }
225
+
226
+ output += `\\${next}`;
227
+ i++;
228
+ continue;
229
+ }
230
+
231
+ if (ch === "'") {
232
+ inSingle = false;
233
+ output += '"';
234
+ continue;
235
+ }
236
+
237
+ if (ch === '"') {
238
+ output += '\\"';
239
+ } else if (ch === '\n') {
240
+ output += '\\n';
241
+ } else if (ch === '\r') {
242
+ output += '\\r';
243
+ } else {
244
+ output += ch;
245
+ }
246
+ continue;
247
+ }
248
+
249
+ if (inDouble) {
250
+ output += ch;
251
+ if (ch === '\\' && i + 1 < input.length) {
252
+ output += input[i + 1];
253
+ i++;
254
+ } else if (ch === '"') {
255
+ inDouble = false;
256
+ }
257
+ continue;
258
+ }
259
+
260
+ if (ch === "'") {
261
+ inSingle = true;
262
+ output += '"';
263
+ continue;
264
+ }
265
+
266
+ if (ch === '"') {
267
+ inDouble = true;
268
+ output += ch;
269
+ continue;
270
+ }
271
+
272
+ output += ch;
273
+ }
274
+
275
+ if (inSingle || inDouble) return null;
276
+
277
+ const withoutTrailingCommas = removeTrailingCommasOutsideStrings(output);
278
+ return replacePythonLiteralsOutsideStrings(withoutTrailingCommas);
279
+ }
280
+
281
+ function isJsonContainerString(value) {
282
+ return (
283
+ (value.startsWith('{') && value.endsWith('}')) ||
284
+ (value.startsWith('[') && value.endsWith(']'))
285
+ );
286
+ }
287
+
288
+ function isJsonContainerValue(value) {
289
+ return value !== null && (Array.isArray(value) || typeof value === 'object');
290
+ }
291
+
292
+ function tryParseJsonLikeContainer(value) {
293
+ const trimmed = String(value ?? '').trim();
294
+ if (!trimmed || !isJsonContainerString(trimmed)) return null;
295
+
296
+ try {
297
+ const parsed = JSON.parse(trimmed);
298
+ return isJsonContainerValue(parsed) ? parsed : null;
299
+ } catch {
300
+ const normalized = normalizeJsonLikeOutput(trimmed);
301
+ if (!normalized) return null;
302
+
303
+ try {
304
+ const parsed = JSON.parse(normalized);
305
+ return isJsonContainerValue(parsed) ? parsed : null;
306
+ } catch {
307
+ return null;
308
+ }
309
+ }
310
+ }
311
+
312
+ function parseOutLabelLine(line) {
313
+ const match = /^\s*out\[(\d+)\]:\s*(.*)$/i.exec(line);
314
+ if (!match) return null;
315
+ return {
316
+ label: `Out[${match[1]}]`,
317
+ remainder: match[2] ?? '',
318
+ };
319
+ }
320
+
321
+ function tryParseOutLabeledJsonOutput(input) {
322
+ const lines = String(input ?? '').split(/\r?\n/);
323
+ const labels = [];
324
+ const values = [];
325
+
326
+ let i = 0;
327
+ while (i < lines.length) {
328
+ while (i < lines.length && !lines[i].trim()) i++;
329
+ if (i >= lines.length) break;
330
+
331
+ const parsedLabel = parseOutLabelLine(lines[i]);
332
+ if (!parsedLabel) return null;
333
+
334
+ labels.push(parsedLabel.label);
335
+ i++;
336
+
337
+ const sectionLines = [];
338
+ if (parsedLabel.remainder.trim()) {
339
+ sectionLines.push(parsedLabel.remainder);
340
+ }
341
+
342
+ while (i < lines.length) {
343
+ if (parseOutLabelLine(lines[i])) break;
344
+ sectionLines.push(lines[i]);
345
+ i++;
346
+ }
347
+
348
+ const sectionText = sectionLines.join('\n').trim();
349
+ const parsedValue = tryParseJsonLikeContainer(sectionText);
350
+ if (parsedValue === null) return null;
351
+ values.push(parsedValue);
352
+ }
353
+
354
+ if (values.length === 0) return null;
355
+ return {
356
+ value: values.length === 1 ? values[0] : values,
357
+ labels,
358
+ };
359
+ }
360
+
67
361
  function tryParseJsonOutput(content) {
68
362
  if (!content || hasAnsi(content)) return null;
69
363
  if (content.length > 250_000) return null; // Guard large payloads
70
364
 
71
365
  const trimmed = content.trim();
72
366
  if (!trimmed) return null;
73
- const looksLikeObjectOrArray =
74
- (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
75
- (trimmed.startsWith('[') && trimmed.endsWith(']'));
76
- if (!looksLikeObjectOrArray) return null;
77
367
 
78
- try {
79
- return JSON.parse(trimmed);
80
- } catch {
81
- return null;
368
+ const direct = tryParseJsonLikeContainer(trimmed);
369
+ if (direct !== null) {
370
+ return {
371
+ value: direct,
372
+ labels: [],
373
+ };
374
+ }
375
+
376
+ return tryParseOutLabeledJsonOutput(trimmed);
377
+ }
378
+
379
+ function countOutputLines(content) {
380
+ const normalized = String(content ?? '').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
381
+ const lines = normalized.split('\n');
382
+ while (lines.length > 0 && lines[lines.length - 1] === '') {
383
+ lines.pop();
82
384
  }
385
+ return lines.length;
83
386
  }
84
387
 
85
388
  function jsonType(value) {
@@ -738,6 +1041,97 @@ class CssOutputWidget extends WidgetType {
738
1041
  }
739
1042
  }
740
1043
 
1044
+ /**
1045
+ * Widget for long plain output blocks (line-threshold based).
1046
+ * Keeps output scrollable so very long results don't expand notebook height.
1047
+ */
1048
+ class ScrollableOutputWidget extends WidgetType {
1049
+ /**
1050
+ * @param {string} content - Output content
1051
+ * @param {boolean} hidden - Whether widget should be hidden
1052
+ * @param {number} blockStart - Document position where this output block starts
1053
+ * @param {string|null} execId - Execution ID for this output block
1054
+ * @param {number} lineCount - Number of output lines
1055
+ */
1056
+ constructor(content, hidden = false, blockStart = 0, execId = null, lineCount = 0) {
1057
+ super();
1058
+ this.content = content;
1059
+ this.hidden = hidden;
1060
+ this.blockStart = blockStart;
1061
+ this.execId = execId;
1062
+ this.lineCount = lineCount;
1063
+ }
1064
+
1065
+ eq(other) {
1066
+ return other.content === this.content &&
1067
+ other.hidden === this.hidden &&
1068
+ other.blockStart === this.blockStart &&
1069
+ other.execId === this.execId &&
1070
+ other.lineCount === this.lineCount;
1071
+ }
1072
+
1073
+ toDOM() {
1074
+ const container = document.createElement('div');
1075
+ container.className = 'cm-scroll-output-widget' + (this.hidden ? ' cm-output-widget-hidden' : '');
1076
+ container.dataset.outputBlockStart = String(this.blockStart);
1077
+ if (this.execId) {
1078
+ container.dataset.execId = this.execId;
1079
+ }
1080
+
1081
+ const header = document.createElement('div');
1082
+ header.className = 'cm-scroll-output-header';
1083
+ header.innerHTML = `
1084
+ <span class="cm-scroll-output-badge">Output</span>
1085
+ <span class="cm-scroll-output-lines">${escapeHtml(String(this.lineCount))} lines</span>
1086
+ <div class="cm-scroll-output-actions">
1087
+ <button type="button" class="cm-scroll-output-action" data-action="expand">Expand</button>
1088
+ <button type="button" class="cm-scroll-output-action" data-action="collapse">Collapse</button>
1089
+ <button type="button" class="cm-scroll-output-action" data-action="copy">Copy</button>
1090
+ </div>
1091
+ `;
1092
+ container.appendChild(header);
1093
+
1094
+ const contentWrap = document.createElement('div');
1095
+ contentWrap.className = 'cm-scroll-output-body';
1096
+
1097
+ const pre = document.createElement('pre');
1098
+ pre.className = 'cm-scroll-output-content';
1099
+ pre.innerHTML = terminalToHtml(this.content);
1100
+
1101
+ contentWrap.appendChild(pre);
1102
+ container.appendChild(contentWrap);
1103
+
1104
+ const actionButtons = header.querySelectorAll('.cm-scroll-output-action');
1105
+ actionButtons.forEach((btn) => {
1106
+ btn.addEventListener('click', (e) => {
1107
+ e.preventDefault();
1108
+ e.stopPropagation();
1109
+
1110
+ const action = btn.getAttribute('data-action');
1111
+ if (action === 'expand') {
1112
+ contentWrap.classList.add('cm-scroll-output-body-expanded');
1113
+ } else if (action === 'collapse') {
1114
+ contentWrap.classList.remove('cm-scroll-output-body-expanded');
1115
+ } else if (action === 'copy') {
1116
+ navigator.clipboard.writeText(stripAnsi(this.content)).then(() => {
1117
+ const previous = btn.textContent;
1118
+ btn.textContent = 'Copied';
1119
+ setTimeout(() => {
1120
+ btn.textContent = previous;
1121
+ }, 1200);
1122
+ });
1123
+ }
1124
+ });
1125
+ });
1126
+
1127
+ return container;
1128
+ }
1129
+
1130
+ ignoreEvent() {
1131
+ return false;
1132
+ }
1133
+ }
1134
+
741
1135
  /**
742
1136
  * Widget for rendering JSON output with an expandable tree.
743
1137
  */
@@ -771,17 +1165,23 @@ class JsonOutputWidget extends WidgetType {
771
1165
  container.dataset.execId = this.execId;
772
1166
  }
773
1167
 
774
- const parsed = tryParseJsonOutput(this.content);
775
- if (parsed === null) {
1168
+ const parsedResult = tryParseJsonOutput(this.content);
1169
+ if (parsedResult === null) {
776
1170
  container.innerHTML = `<pre class="cm-json-fallback">${escapeHtml(this.content)}</pre>`;
777
1171
  return container;
778
1172
  }
779
1173
 
1174
+ const parsedValue = parsedResult.value;
1175
+ const originLabel = parsedResult.labels.length > 0
1176
+ ? (parsedResult.labels.length === 1 ? parsedResult.labels[0] : parsedResult.labels.join(', '))
1177
+ : null;
1178
+
780
1179
  const header = document.createElement('div');
781
1180
  header.className = 'cm-json-header';
782
1181
  header.innerHTML = `
783
1182
  <span class="cm-json-badge">JSON</span>
784
- <span class="cm-json-summary">${escapeHtml(summarizeJson(parsed))}</span>
1183
+ ${originLabel ? `<span class="cm-json-origin">${escapeHtml(originLabel)}</span>` : ''}
1184
+ <span class="cm-json-summary">${escapeHtml(summarizeJson(parsedValue))}</span>
785
1185
  <div class="cm-json-actions">
786
1186
  <button type="button" class="cm-json-action" data-action="expand">Expand</button>
787
1187
  <button type="button" class="cm-json-action" data-action="collapse">Collapse</button>
@@ -792,7 +1192,7 @@ class JsonOutputWidget extends WidgetType {
792
1192
 
793
1193
  const tree = document.createElement('div');
794
1194
  tree.className = 'cm-json-tree';
795
- tree.appendChild(buildJsonTreeNode(null, parsed));
1195
+ tree.appendChild(buildJsonTreeNode(null, parsedValue));
796
1196
  container.appendChild(tree);
797
1197
 
798
1198
  const actionButtons = header.querySelectorAll('.cm-json-action');
@@ -814,7 +1214,7 @@ class JsonOutputWidget extends WidgetType {
814
1214
  }
815
1215
  });
816
1216
  } else if (action === 'copy') {
817
- navigator.clipboard.writeText(JSON.stringify(parsed, null, 2)).then(() => {
1217
+ navigator.clipboard.writeText(JSON.stringify(parsedValue, null, 2)).then(() => {
818
1218
  const previous = btn.textContent;
819
1219
  btn.textContent = 'Copied';
820
1220
  setTimeout(() => {
@@ -978,6 +1378,7 @@ function buildDecorations(view, awarenessSystem) {
978
1378
  const cursorPos = view.state.selection.main.head;
979
1379
  const cursorLine = doc.lineAt(cursorPos).number;
980
1380
  const text = doc.toString();
1381
+ const outputWidgetSettings = getOutputWidgetSettings();
981
1382
 
982
1383
  // Find ```output or ```output:execId blocks (supports 3+ backticks for nesting)
983
1384
  // Group 1: backticks, Group 2: optional execId, Group 3: content
@@ -1046,10 +1447,17 @@ function buildDecorations(view, awarenessSystem) {
1046
1447
  // Check if output is empty (just whitespace)
1047
1448
  const trimmedContent = content.trim();
1048
1449
  const isEmpty = trimmedContent.length === 0;
1049
- const parsedJson = !isEmpty && (outputType === null || outputType === 'json')
1450
+ const parsedJson = !isEmpty && outputWidgetSettings.jsonEnabled && (outputType === null || outputType === 'json')
1050
1451
  ? tryParseJsonOutput(trimmedContent)
1051
1452
  : null;
1052
1453
  const shouldRenderJson = parsedJson !== null;
1454
+ const outputLineCount = isEmpty ? 0 : countOutputLines(content);
1455
+ const shouldRenderScrollableOutput =
1456
+ outputWidgetSettings.longOutputEnabled &&
1457
+ !isEmpty &&
1458
+ outputType === null &&
1459
+ !shouldRenderJson &&
1460
+ outputLineCount > LONG_OUTPUT_WIDGET_LINE_THRESHOLD;
1053
1461
 
1054
1462
  if (anyCollaboratorFocused) {
1055
1463
  // EDITING MODE: Keep ANSI colors rendered, but make escape sequences
@@ -1116,7 +1524,7 @@ function buildDecorations(view, awarenessSystem) {
1116
1524
  // Style the fence lines (opening and closing fences).
1117
1525
  // Rich output widgets (HTML/CSS, including Mermaid rendered as HTML) are
1118
1526
  // attached to the opening fence line, so that line must remain unclipped.
1119
- const richOutput = outputType === 'html' || outputType === 'css' || shouldRenderJson;
1527
+ const richOutput = outputType === 'html' || outputType === 'css' || shouldRenderJson || shouldRenderScrollableOutput;
1120
1528
  const startFenceClass = richOutput
1121
1529
  ? 'cm-output-fence-line cm-output-fence-start cm-output-fence-rich-start'
1122
1530
  : 'cm-output-fence-line cm-output-fence-start';
@@ -1189,6 +1597,23 @@ function buildDecorations(view, awarenessSystem) {
1189
1597
  side: 1,
1190
1598
  }).range(startLine.to)
1191
1599
  );
1600
+ } else if (shouldRenderScrollableOutput) {
1601
+ // LONG OUTPUT: Hide raw lines and show a scrollable output widget
1602
+ for (let i = startLine.number + 1; i < endLine.number; i++) {
1603
+ const line = doc.line(i);
1604
+ decorations.push(
1605
+ Decoration.line({
1606
+ class: 'cm-output-content-line cm-rich-output-hidden',
1607
+ }).range(line.from)
1608
+ );
1609
+ }
1610
+
1611
+ decorations.push(
1612
+ Decoration.widget({
1613
+ widget: new ScrollableOutputWidget(content, false, blockStart, execId, outputLineCount),
1614
+ side: 1,
1615
+ }).range(startLine.to)
1616
+ );
1192
1617
  } else {
1193
1618
  // REGULAR OUTPUT: Show with ANSI styling
1194
1619
  // Style content lines with output block background
@@ -1433,7 +1858,7 @@ export const outputWidgetStyles = `
1433
1858
  /* Widget is absolutely positioned - overlays on transparent text lines, doesn't add to flow */
1434
1859
  .cm-output-widget {
1435
1860
  position: absolute;
1436
- left: var(--widget-inset-left, 0);
1861
+ left: var(--widget-inset-left, 24px);
1437
1862
  right: 0;
1438
1863
  top: var(--widget-offset-top, 0); /* Can be negative to pull widget up closer to code block */
1439
1864
  z-index: 1;
@@ -1544,7 +1969,7 @@ export const outputWidgetStyles = `
1544
1969
  */
1545
1970
  .cm-output-content-line {
1546
1971
  background: color-mix(in srgb, var(--widget-surface, rgba(0, 0, 0, 0.35)) 85%, transparent);
1547
- margin-left: var(--widget-inset-left, 0);
1972
+ margin-left: var(--widget-inset-left, 24px);
1548
1973
  padding-left: var(--widget-padding-x, 16px);
1549
1974
  padding-right: var(--widget-padding-x, 16px);
1550
1975
  font-family: var(--widget-font-mono, 'Roboto Mono', 'SF Mono', Monaco, Consolas, monospace);
@@ -1599,7 +2024,7 @@ export const outputWidgetStyles = `
1599
2024
  font-size: 0.65em;
1600
2025
  color: var(--widget-text-muted, rgba(255, 255, 255, 0.3));
1601
2026
  padding-left: var(--widget-padding-x, 16px);
1602
- margin-left: var(--widget-inset-left, 0);
2027
+ margin-left: var(--widget-inset-left, 24px);
1603
2028
  opacity: 0.7;
1604
2029
  }
1605
2030
 
@@ -1978,6 +2403,88 @@ export const outputWidgetStyles = `
1978
2403
  font-family: var(--widget-font-mono, monospace);
1979
2404
  }
1980
2405
 
2406
+ /* Scrollable plain output widget (for long outputs) */
2407
+ .cm-scroll-output-widget {
2408
+ position: relative;
2409
+ margin: 8px 0;
2410
+ background: var(--widget-surface, rgba(0, 0, 0, 0.35));
2411
+ border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.1));
2412
+ border-left: 3px solid var(--widget-border-accent, rgba(100, 149, 237, 0.6));
2413
+ border-radius: var(--widget-border-radius, 6px);
2414
+ overflow: hidden;
2415
+ }
2416
+
2417
+ .cm-scroll-output-header {
2418
+ display: flex;
2419
+ align-items: center;
2420
+ gap: 8px;
2421
+ padding: 8px 10px;
2422
+ border-bottom: 1px solid var(--widget-border, rgba(255, 255, 255, 0.08));
2423
+ background: var(--widget-surface-elevated, rgba(255, 255, 255, 0.02));
2424
+ }
2425
+
2426
+ .cm-scroll-output-badge {
2427
+ font-size: 10px;
2428
+ color: var(--widget-text-accent, #8cc0ff);
2429
+ background: color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 16%, transparent);
2430
+ border: 1px solid color-mix(in srgb, var(--widget-text-accent, #8cc0ff) 35%, transparent);
2431
+ border-radius: 3px;
2432
+ padding: 2px 6px;
2433
+ letter-spacing: 0.4px;
2434
+ text-transform: uppercase;
2435
+ font-family: var(--widget-font-mono, monospace);
2436
+ }
2437
+
2438
+ .cm-scroll-output-lines {
2439
+ color: var(--widget-text-muted, rgba(255, 255, 255, 0.65));
2440
+ font-size: 11px;
2441
+ }
2442
+
2443
+ .cm-scroll-output-actions {
2444
+ margin-left: auto;
2445
+ display: flex;
2446
+ gap: 6px;
2447
+ }
2448
+
2449
+ .cm-scroll-output-action {
2450
+ background: var(--widget-surface-inset, rgba(255, 255, 255, 0.04));
2451
+ border: 1px solid var(--widget-border, rgba(255, 255, 255, 0.14));
2452
+ color: var(--widget-text-muted, rgba(255, 255, 255, 0.75));
2453
+ border-radius: 4px;
2454
+ padding: 2px 7px;
2455
+ font-size: 11px;
2456
+ cursor: pointer;
2457
+ font-family: var(--widget-font-mono, monospace);
2458
+ }
2459
+
2460
+ .cm-scroll-output-action:hover {
2461
+ background: var(--widget-surface-hover, rgba(255, 255, 255, 0.08));
2462
+ color: var(--widget-text, #e0e0e0);
2463
+ }
2464
+
2465
+ .cm-scroll-output-body {
2466
+ max-height: 320px;
2467
+ overflow: auto;
2468
+ padding: 8px 10px 10px 10px;
2469
+ user-select: text;
2470
+ cursor: text;
2471
+ }
2472
+
2473
+ .cm-scroll-output-body.cm-scroll-output-body-expanded {
2474
+ max-height: none;
2475
+ overflow: visible;
2476
+ }
2477
+
2478
+ .cm-scroll-output-content {
2479
+ margin: 0;
2480
+ white-space: var(--widget-white-space, pre-wrap);
2481
+ word-break: var(--widget-word-break, break-word);
2482
+ color: var(--widget-text, #e0e0e0);
2483
+ font-size: 12px;
2484
+ line-height: 1.45;
2485
+ user-select: text;
2486
+ }
2487
+
1981
2488
  /* JSON Output Widget - expandable tree view */
1982
2489
  .cm-json-output-widget {
1983
2490
  position: relative;
@@ -2015,6 +2522,13 @@ export const outputWidgetStyles = `
2015
2522
  font-size: 11px;
2016
2523
  }
2017
2524
 
2525
+ .cm-json-origin {
2526
+ color: var(--widget-text, #e0e0e0);
2527
+ font-size: 11px;
2528
+ font-family: var(--widget-font-mono, monospace);
2529
+ opacity: 0.9;
2530
+ }
2531
+
2018
2532
  .cm-json-actions {
2019
2533
  margin-left: auto;
2020
2534
  display: flex;