mrmd-editor 0.7.0 → 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 (61) 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/execution.js +69 -15
  8. package/src/frontmatter-updater.js +204 -74
  9. package/src/grammar.js +758 -0
  10. package/src/index.js +1120 -55
  11. package/src/keymap.js +11 -2
  12. package/src/markdown/block-decorations.js +108 -5
  13. package/src/markdown/facets.js +37 -0
  14. package/src/markdown/html-inline.js +9 -5
  15. package/src/markdown/index.js +13 -3
  16. package/src/markdown/inline-commands.js +256 -0
  17. package/src/markdown/inline-model.js +578 -0
  18. package/src/markdown/inline-state.js +103 -0
  19. package/src/markdown/renderer.js +219 -12
  20. package/src/markdown/styles.js +290 -3
  21. package/src/markdown/widgets/alert-title.js +10 -8
  22. package/src/markdown/widgets/frontmatter.js +0 -6
  23. package/src/markdown/widgets/index.js +1 -0
  24. package/src/markdown/widgets/list-marker.js +29 -0
  25. package/src/markdown/wysiwyg.js +1158 -0
  26. package/src/mrp-types.js +2 -0
  27. package/src/output-widget.js +532 -18
  28. package/src/page-view-pagination.js +127 -0
  29. package/src/runtime-lsp.js +1757 -150
  30. package/src/section-controls/commands.js +617 -0
  31. package/src/section-controls/index.js +63 -0
  32. package/src/section-controls/plugin.js +165 -0
  33. package/src/section-controls/widgets.js +936 -0
  34. package/src/shell/ai-menu.js +11 -0
  35. package/src/shell/components/context-panel.js +572 -0
  36. package/src/shell/components/status-bar.js +218 -8
  37. package/src/shell/dialogs/file-picker.js +211 -0
  38. package/src/shell/layouts/studio.js +229 -14
  39. package/src/shell/orchestrator-client.js +114 -0
  40. package/src/shell/styles.js +62 -0
  41. package/src/spellcheck.js +166 -0
  42. package/src/tables/README.md +97 -0
  43. package/src/tables/commands/insert-linked-table.js +122 -0
  44. package/src/tables/commands/open-table-workspace.js +43 -0
  45. package/src/tables/index.js +24 -0
  46. package/src/tables/jobs/client.js +158 -0
  47. package/src/tables/parsing/anchors.js +82 -0
  48. package/src/tables/parsing/linked-table-blocks.js +61 -0
  49. package/src/tables/state/linked-table-state.js +68 -0
  50. package/src/tables/widgets/linked-table-source-banner.js +77 -0
  51. package/src/tables/widgets/linked-table-widget.js +256 -0
  52. package/src/tables/workspace/controller.js +616 -0
  53. package/src/term-pty-client.js +111 -7
  54. package/src/term-widget.js +43 -3
  55. package/src/widgets/theme-utils.js +24 -16
  56. package/src/widgets/theme.js +1535 -1
  57. package/src/runtime-codelens/detector.js +0 -279
  58. package/src/runtime-codelens/index.js +0 -76
  59. package/src/runtime-codelens/plugin.js +0 -142
  60. package/src/runtime-codelens/styles.js +0 -184
  61. package/src/runtime-codelens/widgets.js +0 -216
@@ -14,12 +14,14 @@
14
14
  import { OrchestratorClient } from '../orchestrator-client.js';
15
15
  import { ShellStateManager } from '../state.js';
16
16
  import { createStatusBar } from '../components/status-bar.js';
17
+ import { createContextPanel } from '../components/context-panel.js';
17
18
  import { injectShellStyles } from '../styles.js';
18
19
  import { showFilePicker, showFolderPicker } from '../dialogs/file-picker.js';
19
20
  import { prompt, confirm } from '../dialogs/base-dialog.js';
20
21
  import { Drive } from '../drive.js';
21
22
  import { AiClient } from '../ai-client.js';
22
23
  import { showAiMenu, AI_COMMANDS, injectAiMenuStyles } from '../ai-menu.js';
24
+ import { canImportLinkedTableFromHost } from '../../tables/index.js';
23
25
 
24
26
  // =============================================================================
25
27
  // STUDIO
@@ -92,6 +94,16 @@ export async function createStudio(target, options = {}) {
92
94
  overflow: hidden;
93
95
  `;
94
96
 
97
+ // Create main row container (editor + context rail)
98
+ const mainContainer = document.createElement('div');
99
+ mainContainer.className = 'mrmd-studio__main';
100
+ mainContainer.style.cssText = `
101
+ display: flex;
102
+ flex: 1;
103
+ min-height: 0;
104
+ overflow: hidden;
105
+ `;
106
+
95
107
  // Create editor container
96
108
  const editorContainer = document.createElement('div');
97
109
  editorContainer.className = 'mrmd-studio__editor';
@@ -99,8 +111,19 @@ export async function createStudio(target, options = {}) {
99
111
  flex: 1;
100
112
  overflow: hidden;
101
113
  position: relative;
114
+ min-width: 0;
102
115
  `;
103
116
 
117
+ const contextPanelContainer = document.createElement('div');
118
+ contextPanelContainer.className = 'mrmd-studio__context';
119
+ contextPanelContainer.style.cssText = `
120
+ display: flex;
121
+ min-height: 0;
122
+ `;
123
+
124
+ mainContainer.appendChild(editorContainer);
125
+ mainContainer.appendChild(contextPanelContainer);
126
+
104
127
  // Create status bar container
105
128
  const statusBarContainer = document.createElement('div');
106
129
  statusBarContainer.className = 'mrmd-studio__statusbar';
@@ -108,9 +131,9 @@ export async function createStudio(target, options = {}) {
108
131
  // Assemble layout
109
132
  if (statusBarConfig.position === 'top') {
110
133
  studioEl.appendChild(statusBarContainer);
111
- studioEl.appendChild(editorContainer);
134
+ studioEl.appendChild(mainContainer);
112
135
  } else {
113
- studioEl.appendChild(editorContainer);
136
+ studioEl.appendChild(mainContainer);
114
137
  studioEl.appendChild(statusBarContainer);
115
138
  }
116
139
 
@@ -142,6 +165,16 @@ export async function createStudio(target, options = {}) {
142
165
  return () => eventHandlers.get(event).delete(handler);
143
166
  }
144
167
 
168
+ function getCurrentDocumentMarkdownPath(docName = currentDocName) {
169
+ if (!docName) return null;
170
+ if (docName.startsWith('/')) return null;
171
+ return docName.endsWith('.md') ? docName : `${docName}.md`;
172
+ }
173
+
174
+ function supportsLinkedTableImport() {
175
+ return canImportLinkedTableFromHost();
176
+ }
177
+
145
178
  /**
146
179
  * Detect if cursor is inside a code block and return block info
147
180
  * @param {EditorView} view
@@ -266,6 +299,7 @@ export async function createStudio(target, options = {}) {
266
299
  // Track current editor instance and preserved state
267
300
  let editor = null;
268
301
  let currentDocName = null;
302
+ let contextPanelComponent = null;
269
303
  let preservedEditorState = {
270
304
  theme: editorOptions.theme || null,
271
305
  dark: editorOptions.dark ?? null,
@@ -288,6 +322,9 @@ export async function createStudio(target, options = {}) {
288
322
  // Preserve theme across switches
289
323
  theme: preservedEditorState.theme,
290
324
  dark: preservedEditorState.dark,
325
+ // Linked-table host context
326
+ projectRoot: shellState.get('projectRoot') || editorOptions.projectRoot || null,
327
+ documentPath: getCurrentDocumentMarkdownPath(docName) || editorOptions.documentPath || null,
291
328
  };
292
329
 
293
330
  // Remove any sync options since we're providing ydoc directly
@@ -376,14 +413,17 @@ export async function createStudio(target, options = {}) {
376
413
  });
377
414
  }
378
415
 
379
- // Add Comment Syntax extension
380
- if (mrmd.default.commentSyntax?.createCommentSyntaxExtension) {
381
- const commentExtensions = mrmd.default.commentSyntax.createCommentSyntaxExtension({
382
- aiClient,
383
- juiceLevel: shellState.get('ai')?.juiceLevel || 0,
384
- });
416
+ // Configure built-in comment syntax with the AI client.
417
+ // The base editor already installs the extension, so only append
418
+ // updated facet config here.
419
+ if (mrmd.default.commentSyntax?.commentConfigFacet) {
385
420
  newEditor.view.dispatch({
386
- effects: mrmd.codemirror.StateEffect.appendConfig.of(commentExtensions),
421
+ effects: mrmd.codemirror.StateEffect.appendConfig.of(
422
+ mrmd.default.commentSyntax.commentConfigFacet.of({
423
+ aiClient,
424
+ juiceLevel: shellState.get('ai')?.juiceLevel || 0,
425
+ })
426
+ ),
387
427
  });
388
428
  }
389
429
  }
@@ -398,15 +438,47 @@ export async function createStudio(target, options = {}) {
398
438
  * @param {Object|null} codeBlock - Code block info if cursor is in a code block
399
439
  */
400
440
  async function executeAiCommand(cmd, targetEditor, codeBlock = null) {
401
- if (!aiClient || !mrmd.default.ai) return;
441
+ if (!mrmd.default.ai) return;
402
442
 
403
443
  const view = targetEditor.view;
444
+
445
+ if (cmd.action === 'insert-frontmatter-template') {
446
+ const handled = targetEditor.commands?.insertFrontmatterTemplate?.() || false;
447
+ if (handled) {
448
+ emit('aiCommandExecuted', { command: cmd.id, action: cmd.action });
449
+ } else {
450
+ emit('aiCommandError', { command: cmd.id, error: 'Could not insert frontmatter template' });
451
+ }
452
+ return;
453
+ }
454
+
455
+ if (!aiClient) return;
456
+
404
457
  const context = mrmd.default.ai.getAiContext(view);
405
458
  const juiceLevel = shellState.get('ai')?.juiceLevel || 0;
406
459
 
407
460
  // Detect language from code block, or fall back to python
408
461
  const detectedLanguage = codeBlock?.language || 'python';
409
462
 
463
+ // Resolve richer context from _assets/context/*.md when available
464
+ let resolvedContextText = context.documentContext;
465
+ try {
466
+ if (currentDocName) {
467
+ const resolved = await orchestratorClient.resolveContext({
468
+ doc: currentDocName,
469
+ content: context.documentContext,
470
+ cursorPos: context.cursorPos,
471
+ selection: { from: context.selectionFrom, to: context.selectionTo },
472
+ ensureExists: true,
473
+ });
474
+ if (resolved?.contextText) {
475
+ resolvedContextText = resolved.contextText;
476
+ }
477
+ }
478
+ } catch (error) {
479
+ console.warn('[Studio] Failed to resolve AI context, falling back to document only:', error);
480
+ }
481
+
410
482
  // Mark AI as active
411
483
  shellState._set('ai.active', true);
412
484
 
@@ -419,20 +491,20 @@ export async function createStudio(target, options = {}) {
419
491
  params = {
420
492
  text_before_cursor: context.textBeforeCursor,
421
493
  local_context: context.localContext,
422
- document_context: context.documentContext,
494
+ document_context: resolvedContextText,
423
495
  };
424
496
  } else if (cmd.program.includes('Fix') || cmd.program.includes('Correct')) {
425
497
  params = {
426
498
  text_to_fix: context.selectedText,
427
499
  local_context: context.localContext,
428
- document_context: context.documentContext,
500
+ document_context: resolvedContextText,
429
501
  };
430
502
  } else if (cmd.program.includes('Code')) {
431
503
  params = {
432
504
  code: context.selectedText,
433
505
  language: detectedLanguage,
434
506
  local_context: context.localContext,
435
- document_context: context.documentContext,
507
+ document_context: resolvedContextText,
436
508
  };
437
509
  } else if (cmd.program.includes('Synonym')) {
438
510
  params = {
@@ -440,7 +512,7 @@ export async function createStudio(target, options = {}) {
440
512
  local_context: context.localContext,
441
513
  };
442
514
  } else if (cmd.program.includes('Document')) {
443
- params = { document: context.documentContext };
515
+ params = { document: resolvedContextText };
444
516
  }
445
517
 
446
518
  await mrmd.default.ai.executeAiOperation(view, aiClient, {
@@ -518,6 +590,18 @@ export async function createStudio(target, options = {}) {
518
590
  editor = createEditorForDocument(handle, normalizedName);
519
591
  currentDocName = normalizedName;
520
592
 
593
+ // Place cursor on first empty line (after frontmatter) for a clean first impression.
594
+ // Without this, cursor lands at position 0, showing raw YAML frontmatter.
595
+ if (mrmd.findInitialCursorPosition) {
596
+ const pos = mrmd.findInitialCursorPosition(editor.view.state.doc.toString());
597
+ if (pos > 0) {
598
+ editor.view.dispatch({
599
+ selection: { anchor: pos },
600
+ scrollIntoView: true,
601
+ });
602
+ }
603
+ }
604
+
521
605
  // Ensure runtime attachment exists (starts monitor if needed)
522
606
  try {
523
607
  await orchestratorClient.createRuntimeAttachment(normalizedName, 'shared');
@@ -532,12 +616,24 @@ export async function createStudio(target, options = {}) {
532
616
  path: normalizedName.endsWith('.md') ? normalizedName : `${normalizedName}.md`,
533
617
  root: filesResult.root,
534
618
  });
619
+ editor?.setLinkedTableHostContext?.({
620
+ projectRoot: shellState.get('projectRoot') || null,
621
+ documentPath: normalizedName.endsWith('.md') ? normalizedName : `${normalizedName}.md`,
622
+ });
535
623
 
536
624
  // Update status bar with new editor
537
625
  if (statusBarComponent) {
538
626
  statusBarComponent.setEditor(editor);
539
627
  }
540
628
 
629
+ if (contextPanelComponent) {
630
+ contextPanelComponent.setEditor(editor);
631
+ contextPanelComponent.setDocument(normalizedName);
632
+ contextPanelComponent.refresh().catch((e) => {
633
+ console.warn('[Studio] Failed to refresh context panel:', e);
634
+ });
635
+ }
636
+
541
637
  emit('fileOpened', { doc: normalizedName });
542
638
 
543
639
  } catch (e) {
@@ -557,6 +653,17 @@ export async function createStudio(target, options = {}) {
557
653
  editor = createEditorForDocument(handle, docToOpen);
558
654
  currentDocName = docToOpen;
559
655
 
656
+ // Place cursor on first empty line (after frontmatter) for a clean first impression
657
+ if (mrmd.findInitialCursorPosition) {
658
+ const pos = mrmd.findInitialCursorPosition(editor.view.state.doc.toString());
659
+ if (pos > 0) {
660
+ editor.view.dispatch({
661
+ selection: { anchor: pos },
662
+ scrollIntoView: true,
663
+ });
664
+ }
665
+ }
666
+
560
667
  // Ensure runtime attachment exists (starts monitor if needed)
561
668
  try {
562
669
  await orchestratorClient.createRuntimeAttachment(docToOpen, 'shared');
@@ -570,6 +677,8 @@ export async function createStudio(target, options = {}) {
570
677
 
571
678
  // Shell action handlers
572
679
  const handlers = {
680
+ supportsLinkedTableImport,
681
+
573
682
  async onRename() {
574
683
  const file = shellState.get('file');
575
684
  if (!file) return;
@@ -647,6 +756,65 @@ export async function createStudio(target, options = {}) {
647
756
  });
648
757
  },
649
758
 
759
+ async onImportLinkedTable() {
760
+ const file = shellState.get('file');
761
+ const projectRoot = shellState.get('projectRoot');
762
+
763
+ if (!supportsLinkedTableImport()) {
764
+ await confirm({
765
+ title: 'Linked table import unavailable',
766
+ message: 'This build does not expose the Electron linked-table host API yet.',
767
+ confirmLabel: 'OK',
768
+ cancelLabel: '',
769
+ });
770
+ return;
771
+ }
772
+
773
+ if (!file || !editor) {
774
+ return;
775
+ }
776
+
777
+ if (!projectRoot || file.isOutsideProject || !file.path || file.path.startsWith('/')) {
778
+ await confirm({
779
+ title: 'Linked table import requires a project file',
780
+ message: 'Open a markdown document inside a project before importing a linked table.',
781
+ confirmLabel: 'OK',
782
+ cancelLabel: '',
783
+ });
784
+ return;
785
+ }
786
+
787
+ showFilePicker({
788
+ mode: 'open',
789
+ title: 'Import Linked Table',
790
+ orchestratorClient,
791
+ initialPath: projectRoot || '~',
792
+ allowOutsideProject: true,
793
+ onSelect: async (sourceFilePath) => {
794
+ try {
795
+ const result = await editor.importLinkedTableFromHost(sourceFilePath, {
796
+ projectRoot,
797
+ documentPath: file.path,
798
+ cacheFormat: 'csv',
799
+ });
800
+ emit('linkedTableImported', {
801
+ sourceFilePath,
802
+ tableId: result.tableId,
803
+ spec: result.spec,
804
+ });
805
+ } catch (error) {
806
+ console.error('[Studio] Linked table import failed:', error);
807
+ await confirm({
808
+ title: 'Linked table import failed',
809
+ message: error.message || String(error),
810
+ confirmLabel: 'OK',
811
+ cancelLabel: '',
812
+ });
813
+ }
814
+ },
815
+ });
816
+ },
817
+
650
818
  // NOTE: onChangeVenv, onChangeCwd, and onRestartRuntime are no longer used.
651
819
  // The new runtime model doesn't support changing venv/cwd on running runtimes.
652
820
  // Instead, users start new runtimes via the status bar menu and attach docs to them.
@@ -873,10 +1041,33 @@ export async function createStudio(target, options = {}) {
873
1041
  path: currentDocName.endsWith('.md') ? currentDocName : `${currentDocName}.md`,
874
1042
  root: result.root,
875
1043
  });
1044
+ editor?.setLinkedTableHostContext?.({
1045
+ projectRoot: shellState.get('projectRoot') || null,
1046
+ documentPath: currentDocName.endsWith('.md') ? currentDocName : `${currentDocName}.md`,
1047
+ });
876
1048
  } catch (e) {
877
1049
  console.warn('Could not set initial file context:', e);
878
1050
  }
879
1051
 
1052
+ // Create context panel
1053
+ contextPanelComponent = createContextPanel({
1054
+ container: contextPanelContainer,
1055
+ orchestratorClient,
1056
+ shellState,
1057
+ getCurrentDocument: () => currentDocName,
1058
+ getEditor: () => editor,
1059
+ getAiContext: mrmd.default.ai?.getAiContext,
1060
+ onOpenRaw: async (contextPath) => {
1061
+ const rawDoc = contextPath.replace(/\.md$/, '');
1062
+ await switchDocument(rawDoc);
1063
+ },
1064
+ });
1065
+ contextPanelComponent.setDocument(currentDocName);
1066
+ contextPanelComponent.setEditor(editor);
1067
+ contextPanelComponent.refresh().catch((e) => {
1068
+ console.warn('[Studio] Failed to initialize context panel:', e);
1069
+ });
1070
+
880
1071
  // Create studio object
881
1072
  const studio = {
882
1073
  /** Current editor instance (may change on file switch) */
@@ -915,6 +1106,29 @@ export async function createStudio(target, options = {}) {
915
1106
  await switchDocument(docName);
916
1107
  },
917
1108
 
1109
+ /**
1110
+ * Import a linked table into the current document.
1111
+ * If no source path is provided, opens the file picker flow.
1112
+ *
1113
+ * @param {string} [sourceFilePath]
1114
+ */
1115
+ async importLinkedTable(sourceFilePath) {
1116
+ if (sourceFilePath) {
1117
+ const file = shellState.get('file');
1118
+ const projectRoot = shellState.get('projectRoot');
1119
+ if (!editor || !file?.path || !projectRoot) {
1120
+ throw new Error('Linked table import requires an open project document');
1121
+ }
1122
+ return editor.importLinkedTableFromHost(sourceFilePath, {
1123
+ projectRoot,
1124
+ documentPath: file.path,
1125
+ cacheFormat: 'csv',
1126
+ });
1127
+ }
1128
+
1129
+ return handlers.onImportLinkedTable();
1130
+ },
1131
+
918
1132
  /**
919
1133
  * Save current file to a new location
920
1134
  * @param {string} targetPath - Target path
@@ -995,6 +1209,7 @@ export async function createStudio(target, options = {}) {
995
1209
 
996
1210
  // Destroy status bar
997
1211
  statusBarComponent?.destroy();
1212
+ contextPanelComponent?.destroy();
998
1213
 
999
1214
  // Destroy current editor
1000
1215
  if (editor) {
@@ -218,6 +218,51 @@ export class OrchestratorClient {
218
218
  return this._fetch(`/api/browse${query ? '?' + query : ''}`);
219
219
  }
220
220
 
221
+ // ===========================================================================
222
+ // Machine Catalog (multi-machine sync)
223
+ // ===========================================================================
224
+
225
+ /**
226
+ * Get catalog of files across all connected machines.
227
+ * @param {Object} [options]
228
+ * @param {string} [options.project] - Filter to a specific project
229
+ * @returns {Promise<{userId: string, machines: Array, cloudOnlyProjects?: string[]}>}
230
+ */
231
+ async getCatalog(options = {}) {
232
+ const params = new URLSearchParams();
233
+ if (options.project) params.set('project', options.project);
234
+ const query = params.toString();
235
+ return this._fetch(`/api/catalog${query ? '?' + query : ''}`);
236
+ }
237
+
238
+ /**
239
+ * Get list of connected machines.
240
+ * @returns {Promise<{userId: string, machines: Array}>}
241
+ */
242
+ async getMachines() {
243
+ return this._fetch('/api/machines');
244
+ }
245
+
246
+ /**
247
+ * Get currently active runtime machine.
248
+ * @returns {Promise<{activeMachineId: string|null, provider: Object|null}>}
249
+ */
250
+ async getActiveMachine() {
251
+ return this._fetch('/api/machines/active');
252
+ }
253
+
254
+ /**
255
+ * Set active runtime machine.
256
+ * @param {string|null} machineId
257
+ * @returns {Promise<{ok: boolean, activeMachineId: string|null, provider: Object|null}>}
258
+ */
259
+ async setActiveMachine(machineId) {
260
+ return this._fetch('/api/machines/active', {
261
+ method: 'POST',
262
+ body: JSON.stringify({ machineId: machineId ?? null }),
263
+ });
264
+ }
265
+
221
266
  // ===========================================================================
222
267
  // Environment Management
223
268
  // ===========================================================================
@@ -371,6 +416,75 @@ export class OrchestratorClient {
371
416
  return this.listRuntimeAttachments();
372
417
  }
373
418
 
419
+ // ===========================================================================
420
+ // Context Management
421
+ // ===========================================================================
422
+
423
+ /**
424
+ * Resolve markdown-managed AI context for a document.
425
+ * @param {Object} request
426
+ * @param {string} request.doc
427
+ * @param {string} [request.content]
428
+ * @param {number} [request.cursorPos]
429
+ * @param {{from: number, to: number}} [request.selection]
430
+ * @param {string[]} [request.codeSymbols]
431
+ * @param {boolean} [request.ensureExists=false]
432
+ */
433
+ async resolveContext(request) {
434
+ return this._fetch('/api/context/resolve', {
435
+ method: 'POST',
436
+ body: JSON.stringify(request),
437
+ });
438
+ }
439
+
440
+ /**
441
+ * Get context markdown for a document.
442
+ * @param {string} doc
443
+ */
444
+ async getContext(doc) {
445
+ return this._fetch(`/api/context/${encodeURIComponent(doc)}`);
446
+ }
447
+
448
+ /**
449
+ * Save context markdown for a document.
450
+ * @param {string} doc
451
+ * @param {string} content
452
+ */
453
+ async saveContext(doc, content) {
454
+ return this._fetch(`/api/context/${encodeURIComponent(doc)}`, {
455
+ method: 'PUT',
456
+ body: JSON.stringify({ content }),
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Initialize context markdown for a document if missing.
462
+ * @param {string} doc
463
+ */
464
+ async initContext(doc) {
465
+ return this._fetch(`/api/context/init/${encodeURIComponent(doc)}`, {
466
+ method: 'POST',
467
+ });
468
+ }
469
+
470
+ /**
471
+ * Get project default context markdown.
472
+ */
473
+ async getDefaultContext() {
474
+ return this._fetch('/api/context');
475
+ }
476
+
477
+ /**
478
+ * Save project default context markdown.
479
+ * @param {string} content
480
+ */
481
+ async saveDefaultContext(content) {
482
+ return this._fetch('/api/context', {
483
+ method: 'PUT',
484
+ body: JSON.stringify({ content }),
485
+ });
486
+ }
487
+
374
488
  // ===========================================================================
375
489
  // Project & Runtime Management
376
490
  // ===========================================================================
@@ -160,6 +160,24 @@ export const statusBarStyles = `
160
160
  margin-left: 4px;
161
161
  }
162
162
 
163
+ /* Active machine pill (simple status bar mode) */
164
+ .mrmd-statusbar__machine-pill {
165
+ display: inline-flex;
166
+ align-items: center;
167
+ padding: 1px 6px;
168
+ border-radius: 10px;
169
+ border: 1px solid var(--mrmd-border, #333);
170
+ background: var(--mrmd-hover-bg, rgba(255,255,255,0.06));
171
+ color: var(--mrmd-fg, #ccc);
172
+ font-size: 10px;
173
+ line-height: 1.2;
174
+ cursor: pointer;
175
+ }
176
+
177
+ .mrmd-statusbar__machine-pill:hover {
178
+ border-color: var(--mrmd-accent, #58a6ff);
179
+ }
180
+
163
181
  /* Unified files segment - takes more space */
164
182
  .mrmd-statusbar__segment--files {
165
183
  min-width: 120px;
@@ -505,6 +523,50 @@ export const filePickerStyles = `
505
523
  min-height: 300px;
506
524
  }
507
525
 
526
+ /* Machine tab bar */
527
+ .mrmd-filepicker__machines {
528
+ display: flex;
529
+ gap: 4px;
530
+ padding: 6px 0;
531
+ margin-bottom: 4px;
532
+ border-bottom: 1px solid var(--mrmd-border, #333);
533
+ overflow-x: auto;
534
+ white-space: nowrap;
535
+ -webkit-overflow-scrolling: touch;
536
+ }
537
+
538
+ .mrmd-filepicker__machine-tab {
539
+ display: inline-flex;
540
+ align-items: center;
541
+ gap: 4px;
542
+ padding: 4px 10px;
543
+ border-radius: 12px;
544
+ border: 1px solid var(--mrmd-border, #333);
545
+ background: transparent;
546
+ color: var(--mrmd-fg-muted, #888);
547
+ font-size: var(--mrmd-ui-font-size-sm, 11px);
548
+ font-family: inherit;
549
+ cursor: pointer;
550
+ white-space: nowrap;
551
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
552
+ }
553
+
554
+ .mrmd-filepicker__machine-tab:hover {
555
+ background: var(--mrmd-hover-bg, rgba(255, 255, 255, 0.08));
556
+ color: var(--mrmd-fg, #ccc);
557
+ }
558
+
559
+ .mrmd-filepicker__machine-tab--active {
560
+ background: var(--mrmd-selection-bg, rgba(0, 122, 204, 0.2));
561
+ color: var(--mrmd-fg, #ccc);
562
+ border-color: var(--mrmd-accent, #58a6ff);
563
+ }
564
+
565
+ .mrmd-filepicker__machine-tab--offline {
566
+ opacity: 0.7;
567
+ border-style: dashed;
568
+ }
569
+
508
570
  /* Path bar */
509
571
  .mrmd-filepicker__path {
510
572
  display: flex;