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
@@ -132,6 +132,17 @@ export const AI_COMMANDS = {
132
132
  requiresSelection: true,
133
133
  description: 'Clean up markdown formatting',
134
134
  },
135
+
136
+ // Editor actions
137
+ FRONTMATTER_TEMPLATE: {
138
+ id: 'frontmatter-template',
139
+ label: 'Insert Frontmatter Template',
140
+ icon: '¶',
141
+ action: 'insert-frontmatter-template',
142
+ type: 'action',
143
+ requiresSelection: false,
144
+ description: 'Insert scholarly YAML frontmatter at the top of the document',
145
+ },
135
146
  };
136
147
 
137
148
  /**
@@ -0,0 +1,572 @@
1
+ /**
2
+ * @fileoverview Context side panel for markdown-managed AI context.
3
+ */
4
+
5
+ const CONTEXT_PANEL_STYLES = `
6
+ .mrmd-context-panel {
7
+ width: 320px;
8
+ min-width: 240px;
9
+ max-width: 420px;
10
+ display: flex;
11
+ flex-direction: column;
12
+ border-left: 1px solid var(--mrmd-border, #333);
13
+ background: var(--mrmd-panel-bg, #161b22);
14
+ color: var(--mrmd-fg, #c9d1d9);
15
+ font-family: var(--mrmd-ui-font, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
16
+ font-size: 12px;
17
+ overflow: hidden;
18
+ }
19
+
20
+ .mrmd-context-panel--collapsed {
21
+ width: 38px;
22
+ min-width: 38px;
23
+ }
24
+
25
+ .mrmd-context-panel__header {
26
+ display: flex;
27
+ align-items: center;
28
+ gap: 8px;
29
+ padding: 10px 12px;
30
+ border-bottom: 1px solid var(--mrmd-border, #333);
31
+ background: var(--mrmd-panel-header-bg, rgba(255,255,255,0.02));
32
+ }
33
+
34
+ .mrmd-context-panel__title {
35
+ flex: 1;
36
+ font-weight: 600;
37
+ color: var(--mrmd-fg, #c9d1d9);
38
+ }
39
+
40
+ .mrmd-context-panel__actions {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 4px;
44
+ }
45
+
46
+ .mrmd-context-panel__btn {
47
+ display: inline-flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ min-width: 26px;
51
+ height: 26px;
52
+ padding: 0 8px;
53
+ border: 1px solid var(--mrmd-border, #333);
54
+ border-radius: 6px;
55
+ background: var(--mrmd-bg, #0d1117);
56
+ color: var(--mrmd-fg, #c9d1d9);
57
+ cursor: pointer;
58
+ font-size: 12px;
59
+ }
60
+
61
+ .mrmd-context-panel__btn:hover {
62
+ border-color: var(--mrmd-accent, #58a6ff);
63
+ color: var(--mrmd-accent, #58a6ff);
64
+ }
65
+
66
+ .mrmd-context-panel__btn:disabled {
67
+ opacity: 0.45;
68
+ cursor: default;
69
+ }
70
+
71
+ .mrmd-context-panel__body {
72
+ flex: 1;
73
+ overflow: auto;
74
+ padding: 12px;
75
+ }
76
+
77
+ .mrmd-context-panel--collapsed .mrmd-context-panel__body,
78
+ .mrmd-context-panel--collapsed .mrmd-context-panel__title,
79
+ .mrmd-context-panel--collapsed .mrmd-context-panel__actions > :not(.mrmd-context-panel__toggle) {
80
+ display: none;
81
+ }
82
+
83
+ .mrmd-context-panel--collapsed .mrmd-context-panel__header {
84
+ justify-content: center;
85
+ padding: 10px 6px;
86
+ }
87
+
88
+ .mrmd-context-panel__section {
89
+ margin-bottom: 14px;
90
+ }
91
+
92
+ .mrmd-context-panel__section-title {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: 8px;
97
+ font-size: 11px;
98
+ text-transform: uppercase;
99
+ letter-spacing: 0.5px;
100
+ color: var(--mrmd-fg-muted, #8b949e);
101
+ margin-bottom: 6px;
102
+ }
103
+
104
+ .mrmd-context-panel__meta,
105
+ .mrmd-context-panel__message {
106
+ line-height: 1.45;
107
+ color: var(--mrmd-fg-muted, #8b949e);
108
+ }
109
+
110
+ .mrmd-context-panel__message {
111
+ padding: 12px;
112
+ border: 1px dashed var(--mrmd-border, #333);
113
+ border-radius: 8px;
114
+ }
115
+
116
+ .mrmd-context-panel__path {
117
+ font-family: var(--mrmd-code-font, 'SF Mono', Consolas, monospace);
118
+ font-size: 11px;
119
+ color: var(--mrmd-accent, #58a6ff);
120
+ word-break: break-all;
121
+ }
122
+
123
+ .mrmd-context-panel__source-list {
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 8px;
127
+ }
128
+
129
+ .mrmd-context-panel__source {
130
+ border: 1px solid var(--mrmd-border, #333);
131
+ border-radius: 8px;
132
+ padding: 8px 10px;
133
+ background: var(--mrmd-bg-secondary, rgba(255,255,255,0.02));
134
+ }
135
+
136
+ .mrmd-context-panel__source-main {
137
+ display: flex;
138
+ align-items: center;
139
+ justify-content: space-between;
140
+ gap: 8px;
141
+ }
142
+
143
+ .mrmd-context-panel__source-name {
144
+ font-weight: 500;
145
+ color: var(--mrmd-fg, #c9d1d9);
146
+ }
147
+
148
+ .mrmd-context-panel__source-sub {
149
+ margin-top: 4px;
150
+ color: var(--mrmd-fg-muted, #8b949e);
151
+ font-size: 11px;
152
+ line-height: 1.4;
153
+ }
154
+
155
+ .mrmd-context-panel__badge {
156
+ display: inline-flex;
157
+ align-items: center;
158
+ padding: 1px 6px;
159
+ border-radius: 999px;
160
+ border: 1px solid var(--mrmd-border, #333);
161
+ color: var(--mrmd-fg-muted, #8b949e);
162
+ font-size: 10px;
163
+ }
164
+
165
+ .mrmd-context-panel__footer {
166
+ padding-top: 6px;
167
+ border-top: 1px solid var(--mrmd-border, #333);
168
+ }
169
+
170
+ .mrmd-context-panel__tokenbar {
171
+ position: relative;
172
+ height: 8px;
173
+ overflow: hidden;
174
+ border-radius: 999px;
175
+ background: var(--mrmd-hover-bg, rgba(255,255,255,0.06));
176
+ margin: 8px 0 6px;
177
+ }
178
+
179
+ .mrmd-context-panel__tokenfill {
180
+ position: absolute;
181
+ top: 0;
182
+ left: 0;
183
+ bottom: 0;
184
+ background: linear-gradient(90deg, var(--mrmd-accent, #58a6ff), var(--mrmd-success, #2ea043));
185
+ }
186
+
187
+ .mrmd-context-panel__row {
188
+ display: flex;
189
+ align-items: center;
190
+ justify-content: space-between;
191
+ gap: 8px;
192
+ }
193
+
194
+ .mrmd-context-panel__small {
195
+ font-size: 11px;
196
+ color: var(--mrmd-fg-muted, #8b949e);
197
+ }
198
+
199
+ @media (max-width: 960px) {
200
+ .mrmd-context-panel {
201
+ display: none;
202
+ }
203
+ }
204
+ `;
205
+
206
+ let stylesInjected = false;
207
+
208
+ function injectContextPanelStyles() {
209
+ if (stylesInjected) return;
210
+ const style = document.createElement('style');
211
+ style.id = 'mrmd-context-panel-styles';
212
+ style.textContent = CONTEXT_PANEL_STYLES;
213
+ document.head.appendChild(style);
214
+ stylesInjected = true;
215
+ }
216
+
217
+ function formatTokens(count) {
218
+ const value = Number(count || 0);
219
+ if (value >= 1000) return `${(value / 1000).toFixed(1)}k`;
220
+ return String(value);
221
+ }
222
+
223
+ function escapeHtml(value) {
224
+ return String(value ?? '')
225
+ .replace(/&/g, '&')
226
+ .replace(/</g, '&lt;')
227
+ .replace(/>/g, '&gt;')
228
+ .replace(/"/g, '&quot;')
229
+ .replace(/'/g, '&#39;');
230
+ }
231
+
232
+ function titleForSource(source) {
233
+ const type = source?.type || 'source';
234
+ switch (type) {
235
+ case 'document': return `📄 Document`;
236
+ case 'linked-page': return `🔗 ${source.name || 'Linked Page'}`;
237
+ case 'images': return `🖼️ Images`;
238
+ case 'runtime': return `🐍 Runtime`;
239
+ case 'runtime-variables': return `🐍 Variables`;
240
+ case 'runtime-docstrings': return `📚 Docstrings`;
241
+ case 'runtime-paths': return `📍 Source Paths`;
242
+ case 'runtime-source': return `💾 Source Code`;
243
+ case 'file': return `📎 ${source.path || 'File'}`;
244
+ case 'url': return `🌐 ${source.url || 'URL'}`;
245
+ case 'notes': return `📝 Notes`;
246
+ default: return type;
247
+ }
248
+ }
249
+
250
+ function describeSource(source) {
251
+ if (!source) return '';
252
+ if (source.type === 'document') {
253
+ return `mode=${source.mode || 'full'}`;
254
+ }
255
+ if (source.type === 'linked-page') {
256
+ return [source.path, source.depth != null ? `depth ${source.depth}` : null].filter(Boolean).join(' · ');
257
+ }
258
+ if (source.type === 'runtime') {
259
+ return source.available ? [source.runtimeUrl, source.runtimePort ? `port ${source.runtimePort}` : null].filter(Boolean).join(' · ') : 'No runtime attached';
260
+ }
261
+ if (source.type === 'images') {
262
+ return source.count != null ? `${source.count} image${source.count === 1 ? '' : 's'}` : '';
263
+ }
264
+ if (source.type === 'file') {
265
+ return source.path || '';
266
+ }
267
+ if (source.type === 'url') {
268
+ return source.url || '';
269
+ }
270
+ if (source.count != null) {
271
+ return `${source.count} item${source.count === 1 ? '' : 's'}`;
272
+ }
273
+ return '';
274
+ }
275
+
276
+ export function createContextPanel(options) {
277
+ const {
278
+ container,
279
+ orchestratorClient,
280
+ shellState,
281
+ getCurrentDocument,
282
+ getEditor,
283
+ getAiContext,
284
+ onOpenRaw,
285
+ } = options;
286
+
287
+ injectContextPanelStyles();
288
+
289
+ const state = {
290
+ collapsed: false,
291
+ loading: false,
292
+ doc: null,
293
+ data: null,
294
+ };
295
+
296
+ const panel = document.createElement('aside');
297
+ panel.className = 'mrmd-context-panel';
298
+
299
+ const header = document.createElement('div');
300
+ header.className = 'mrmd-context-panel__header';
301
+
302
+ const title = document.createElement('div');
303
+ title.className = 'mrmd-context-panel__title';
304
+ title.textContent = 'Context';
305
+
306
+ const actions = document.createElement('div');
307
+ actions.className = 'mrmd-context-panel__actions';
308
+
309
+ const refreshBtn = document.createElement('button');
310
+ refreshBtn.className = 'mrmd-context-panel__btn';
311
+ refreshBtn.title = 'Refresh resolved context';
312
+ refreshBtn.textContent = '⟳';
313
+
314
+ const rawBtn = document.createElement('button');
315
+ rawBtn.className = 'mrmd-context-panel__btn';
316
+ rawBtn.title = 'Open raw context markdown';
317
+ rawBtn.textContent = '✏';
318
+
319
+ const materializeBtn = document.createElement('button');
320
+ materializeBtn.className = 'mrmd-context-panel__btn';
321
+ materializeBtn.title = 'Create a document-specific context from the current effective context';
322
+ materializeBtn.textContent = '⎘';
323
+
324
+ const toggleBtn = document.createElement('button');
325
+ toggleBtn.className = 'mrmd-context-panel__btn mrmd-context-panel__toggle';
326
+ toggleBtn.title = 'Collapse context panel';
327
+ toggleBtn.textContent = '›';
328
+
329
+ actions.append(refreshBtn, rawBtn, materializeBtn, toggleBtn);
330
+ header.append(title, actions);
331
+
332
+ const body = document.createElement('div');
333
+ body.className = 'mrmd-context-panel__body';
334
+
335
+ panel.append(header, body);
336
+ container.appendChild(panel);
337
+
338
+ function setCollapsed(collapsed) {
339
+ state.collapsed = collapsed;
340
+ panel.classList.toggle('mrmd-context-panel--collapsed', collapsed);
341
+ toggleBtn.textContent = collapsed ? '‹' : '›';
342
+ toggleBtn.title = collapsed ? 'Expand context panel' : 'Collapse context panel';
343
+ }
344
+
345
+ function currentContextRequest() {
346
+ const doc = getCurrentDocument?.();
347
+ const editor = getEditor?.();
348
+ let content = null;
349
+ let cursorPos = null;
350
+ let selection = null;
351
+
352
+ try {
353
+ if (editor?.view) {
354
+ const view = editor.view;
355
+ content = view.state.doc.toString();
356
+ if (getAiContext) {
357
+ const aiContext = getAiContext(view);
358
+ cursorPos = aiContext.cursorPos;
359
+ selection = { from: aiContext.selectionFrom, to: aiContext.selectionTo };
360
+ }
361
+ }
362
+ } catch {
363
+ // Ignore and fall back to document-only resolve.
364
+ }
365
+
366
+ return { doc, content, cursorPos, selection, ensureExists: true };
367
+ }
368
+
369
+ function render() {
370
+ const doc = state.doc;
371
+ const data = state.data;
372
+ const hiddenDoc = !doc || doc.startsWith('_');
373
+ body.innerHTML = '';
374
+
375
+ refreshBtn.disabled = !doc || state.loading || hiddenDoc;
376
+ rawBtn.disabled = !doc;
377
+ materializeBtn.disabled = !doc || state.loading || hiddenDoc;
378
+
379
+ if (!doc) {
380
+ const message = document.createElement('div');
381
+ message.className = 'mrmd-context-panel__message';
382
+ message.textContent = 'No document open.';
383
+ body.appendChild(message);
384
+ return;
385
+ }
386
+
387
+ if (hiddenDoc) {
388
+ const message = document.createElement('div');
389
+ message.className = 'mrmd-context-panel__message';
390
+ message.innerHTML = 'Context panel is disabled for infrastructure documents like <code>_assets/context/*.md</code>. Use the raw editor to edit this file directly.';
391
+ body.appendChild(message);
392
+ return;
393
+ }
394
+
395
+ if (state.loading && !data) {
396
+ const message = document.createElement('div');
397
+ message.className = 'mrmd-context-panel__message';
398
+ message.textContent = 'Resolving context…';
399
+ body.appendChild(message);
400
+ return;
401
+ }
402
+
403
+ if (!data) {
404
+ const message = document.createElement('div');
405
+ message.className = 'mrmd-context-panel__message';
406
+ message.textContent = 'No resolved context yet.';
407
+ body.appendChild(message);
408
+ return;
409
+ }
410
+
411
+ const overview = document.createElement('section');
412
+ overview.className = 'mrmd-context-panel__section';
413
+ overview.innerHTML = `
414
+ <div class="mrmd-context-panel__section-title">
415
+ <span>Active Context</span>
416
+ <span class="mrmd-context-panel__badge">${data.contextFileSource || 'document'}</span>
417
+ </div>
418
+ <div class="mrmd-context-panel__meta">
419
+ <div class="mrmd-context-panel__path">${escapeHtml(data.contextFilePath || '')}</div>
420
+ <div class="mrmd-context-panel__small">${data.usingDefault ? 'Using project default context' : 'Using document-specific context'}</div>
421
+ </div>
422
+ `;
423
+ body.appendChild(overview);
424
+
425
+ const sourcesSection = document.createElement('section');
426
+ sourcesSection.className = 'mrmd-context-panel__section';
427
+
428
+ const titleRow = document.createElement('div');
429
+ titleRow.className = 'mrmd-context-panel__section-title';
430
+ titleRow.innerHTML = `<span>Resolved Sources</span><span>${(data.sources || []).length}</span>`;
431
+ sourcesSection.appendChild(titleRow);
432
+
433
+ const list = document.createElement('div');
434
+ list.className = 'mrmd-context-panel__source-list';
435
+
436
+ for (const source of data.sources || []) {
437
+ const item = document.createElement('div');
438
+ item.className = 'mrmd-context-panel__source';
439
+ const sub = describeSource(source);
440
+ item.innerHTML = `
441
+ <div class="mrmd-context-panel__source-main">
442
+ <div class="mrmd-context-panel__source-name">${escapeHtml(titleForSource(source))}</div>
443
+ <div class="mrmd-context-panel__badge">${formatTokens(source.tokens || 0)} tok</div>
444
+ </div>
445
+ ${sub ? `<div class="mrmd-context-panel__source-sub">${escapeHtml(sub)}</div>` : ''}
446
+ `;
447
+ list.appendChild(item);
448
+ }
449
+
450
+ if (!list.childElementCount) {
451
+ const empty = document.createElement('div');
452
+ empty.className = 'mrmd-context-panel__message';
453
+ empty.textContent = 'No context sources resolved. Edit the context markdown to add sources.';
454
+ sourcesSection.appendChild(empty);
455
+ } else {
456
+ sourcesSection.appendChild(list);
457
+ }
458
+
459
+ body.appendChild(sourcesSection);
460
+
461
+ const footer = document.createElement('section');
462
+ footer.className = 'mrmd-context-panel__section mrmd-context-panel__footer';
463
+ const percent = Math.max(0, Math.min(100, ((data.tokenEstimate || 0) / 8000) * 100));
464
+ footer.innerHTML = `
465
+ <div class="mrmd-context-panel__row">
466
+ <span>Total</span>
467
+ <strong>${formatTokens(data.tokenEstimate || 0)} tokens</strong>
468
+ </div>
469
+ <div class="mrmd-context-panel__tokenbar">
470
+ <div class="mrmd-context-panel__tokenfill" style="width: ${percent}%;"></div>
471
+ </div>
472
+ <div class="mrmd-context-panel__row mrmd-context-panel__small">
473
+ <span>Budget reference</span>
474
+ <span>8k tokens</span>
475
+ </div>
476
+ ${(data.images || []).length ? `<div class="mrmd-context-panel__small" style="margin-top:8px;">${data.images.length} image reference${data.images.length === 1 ? '' : 's'} resolved</div>` : ''}
477
+ `;
478
+ body.appendChild(footer);
479
+ }
480
+
481
+ async function refresh() {
482
+ const request = currentContextRequest();
483
+ state.doc = request.doc;
484
+ render();
485
+ if (!request.doc || request.doc.startsWith('_')) return;
486
+
487
+ state.loading = true;
488
+ render();
489
+ try {
490
+ state.data = await orchestratorClient.resolveContext(request);
491
+ } catch (error) {
492
+ state.data = {
493
+ contextFileSource: 'error',
494
+ contextFilePath: '_assets/context',
495
+ usingDefault: false,
496
+ tokenEstimate: 0,
497
+ sources: [],
498
+ images: [],
499
+ };
500
+ const message = document.createElement('div');
501
+ message.className = 'mrmd-context-panel__message';
502
+ message.textContent = `Failed to resolve context: ${error.message}`;
503
+ body.innerHTML = '';
504
+ body.appendChild(message);
505
+ state.loading = false;
506
+ return;
507
+ }
508
+ state.loading = false;
509
+ render();
510
+ }
511
+
512
+ async function openRaw() {
513
+ const doc = getCurrentDocument?.();
514
+ if (!doc) return;
515
+ if (doc.startsWith('_')) return;
516
+
517
+ try {
518
+ const current = await orchestratorClient.getContext(doc);
519
+ if (current.source === 'default' || current.source === 'builtin') {
520
+ await orchestratorClient.saveContext(doc, current.content);
521
+ } else if (!current.exists) {
522
+ await orchestratorClient.initContext(doc);
523
+ }
524
+ const rawPath = `_assets/context/${doc.endsWith('.md') ? doc : `${doc}.md`}`;
525
+ await onOpenRaw?.(rawPath);
526
+ } catch (error) {
527
+ console.error('[ContextPanel] Failed to open raw context:', error);
528
+ }
529
+ }
530
+
531
+ async function materialize() {
532
+ const doc = getCurrentDocument?.();
533
+ if (!doc) return;
534
+ try {
535
+ const current = await orchestratorClient.getContext(doc);
536
+ await orchestratorClient.saveContext(doc, current.content);
537
+ await refresh();
538
+ } catch (error) {
539
+ console.error('[ContextPanel] Failed to materialize context:', error);
540
+ }
541
+ }
542
+
543
+ refreshBtn.addEventListener('click', () => refresh());
544
+ rawBtn.addEventListener('click', () => openRaw());
545
+ materializeBtn.addEventListener('click', () => materialize());
546
+ toggleBtn.addEventListener('click', () => setCollapsed(!state.collapsed));
547
+
548
+ const unsubFile = shellState.onPath('file.path', () => {
549
+ state.doc = getCurrentDocument?.();
550
+ refresh();
551
+ });
552
+
553
+ render();
554
+
555
+ return {
556
+ element: panel,
557
+ async refresh() {
558
+ await refresh();
559
+ },
560
+ setDocument(doc) {
561
+ state.doc = doc;
562
+ render();
563
+ },
564
+ setEditor() {
565
+ render();
566
+ },
567
+ destroy() {
568
+ unsubFile?.();
569
+ panel.remove();
570
+ },
571
+ };
572
+ }
@@ -272,6 +272,14 @@ function createFilesSegment({ shellState, orchestratorClient, handlers, onCleanu
272
272
  onClick: () => handlers.onOpenFilePicker?.(),
273
273
  });
274
274
 
275
+ if (handlers.onImportLinkedTable && handlers.supportsLinkedTableImport?.() !== false) {
276
+ items.push({
277
+ icon: '▦',
278
+ label: 'Import Linked Table...',
279
+ onClick: () => handlers.onImportLinkedTable?.(),
280
+ });
281
+ }
282
+
275
283
  items.push({
276
284
  icon: '➕',
277
285
  label: 'New File...',
@@ -1124,7 +1132,7 @@ function createAiSegment({ shellState, handlers, onCleanup }) {
1124
1132
  // Known dark themes for proper icon display
1125
1133
  const DARK_THEMES = new Set([
1126
1134
  'midnight', 'moonlight', 'github', 'nord', 'nord-outputs',
1127
- 'grayscale-dark',
1135
+ 'grayscale-dark', 'newsprint-dark', 'plain-dark',
1128
1136
  ]);
1129
1137
 
1130
1138
  // Custom themes storage key
@@ -1277,7 +1285,7 @@ function createThemeSegment({ editorRef, shellState, handlers, onCleanup }) {
1277
1285
  }
1278
1286
 
1279
1287
  // Check for name conflicts with built-in themes
1280
- const builtInThemes = ['midnight', 'daylight', 'moonlight', 'github', 'nord', 'nord-outputs', 'grayscale-dark', 'grayscale-light', 'openresponses'];
1288
+ const builtInThemes = ['midnight', 'daylight', 'moonlight', 'github', 'nord', 'nord-outputs', 'grayscale-dark', 'grayscale-light', 'openresponses', 'newsprint-dark', 'newsprint-light', 'plain-dark', 'plain-light'];
1281
1289
  if (builtInThemes.includes(theme.name)) {
1282
1290
  alert(`Cannot use reserved theme name "${theme.name}". Please rename your theme.`);
1283
1291
  return;