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
@@ -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...',
@@ -861,6 +869,80 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
861
869
  });
862
870
  }
863
871
 
872
+ // Runtime machine selection
873
+ if (machineState.supported) {
874
+ items.push({ type: 'divider' });
875
+ items.push({ type: 'header', label: 'Runtime Machine' });
876
+
877
+ items.push({
878
+ icon: activeMachineId ? '○' : '✓',
879
+ label: 'Auto-select',
880
+ active: !activeMachineId,
881
+ onClick: async () => {
882
+ try {
883
+ await orchestratorClient.setActiveMachine?.(null);
884
+ await refreshMachineState({ doRender: true });
885
+ } catch (err) {
886
+ console.error('Failed to set auto machine:', err);
887
+ }
888
+ },
889
+ });
890
+
891
+ for (const m of machineList) {
892
+ const online = m.status === 'online' || m.connected === true;
893
+ const isActiveMachine = activeMachineId === m.machineId;
894
+ items.push({
895
+ icon: isActiveMachine ? '✓' : (online ? '●' : '○'),
896
+ label: `${m.machineName || m.machineId}${online ? '' : ' (offline)'}`,
897
+ active: isActiveMachine,
898
+ onClick: async () => {
899
+ try {
900
+ await orchestratorClient.setActiveMachine?.(m.machineId);
901
+ await refreshMachineState({ doRender: true });
902
+ } catch (err) {
903
+ console.error('Failed to switch machine:', err);
904
+ }
905
+ },
906
+ });
907
+ }
908
+
909
+ if (projectRoot) {
910
+ const pref = getProjectMachinePreference(projectRoot);
911
+ items.push({ type: 'divider' });
912
+ items.push({ type: 'header', label: 'Project Machine Preference' });
913
+
914
+ items.push({
915
+ icon: pref === undefined ? '✓' : '○',
916
+ label: 'No preference (manual/global)',
917
+ active: pref === undefined,
918
+ onClick: () => {
919
+ setProjectMachinePreference(projectRoot, undefined);
920
+ },
921
+ });
922
+
923
+ items.push({
924
+ icon: pref === null ? '✓' : '○',
925
+ label: 'Prefer auto-select',
926
+ active: pref === null,
927
+ onClick: () => {
928
+ setProjectMachinePreference(projectRoot, null);
929
+ },
930
+ });
931
+
932
+ for (const m of machineList) {
933
+ const isPref = pref === m.machineId;
934
+ items.push({
935
+ icon: isPref ? '✓' : '📌',
936
+ label: `Prefer ${m.machineName || m.machineId}`,
937
+ active: isPref,
938
+ onClick: () => {
939
+ setProjectMachinePreference(projectRoot, m.machineId);
940
+ },
941
+ });
942
+ }
943
+ }
944
+ }
945
+
864
946
  // Refresh action
865
947
  items.push({ type: 'divider' });
866
948
  items.push({
@@ -870,6 +952,7 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
870
952
  cachedRuntimes = null;
871
953
  cachedVenvs = null;
872
954
  await fetchRuntimes();
955
+ await refreshMachineState({ doRender: false });
873
956
  render();
874
957
  },
875
958
  });
@@ -885,13 +968,30 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
885
968
  segment.addEventListener('click', openMenu);
886
969
 
887
970
  // Initial fetch
888
- fetchRuntimes().then(render);
971
+ Promise.all([
972
+ fetchRuntimes(),
973
+ refreshMachineState({ doRender: false }),
974
+ ]).then(async () => {
975
+ render();
976
+ await applyProjectPreference();
977
+ });
889
978
 
890
979
  // Subscribe to state changes
891
980
  const unsubscribe1 = shellState.onPath('runtimes', render);
892
981
  const unsubscribe2 = shellState.onPath('file', render);
982
+ const unsubscribe3 = shellState.onPath('projectRoot', () => {
983
+ applyProjectPreference();
984
+ render();
985
+ });
986
+
987
+ const machinePoll = setInterval(() => {
988
+ refreshMachineState({ doRender: true });
989
+ }, 15000);
990
+
893
991
  onCleanup(unsubscribe1);
894
992
  onCleanup(unsubscribe2);
993
+ onCleanup(unsubscribe3);
994
+ onCleanup(() => clearInterval(machinePoll));
895
995
  onCleanup(() => currentMenu?.close());
896
996
 
897
997
  render();
@@ -1032,7 +1132,7 @@ function createAiSegment({ shellState, handlers, onCleanup }) {
1032
1132
  // Known dark themes for proper icon display
1033
1133
  const DARK_THEMES = new Set([
1034
1134
  'midnight', 'moonlight', 'github', 'nord', 'nord-outputs',
1035
- 'grayscale-dark',
1135
+ 'grayscale-dark', 'newsprint-dark', 'plain-dark',
1036
1136
  ]);
1037
1137
 
1038
1138
  // Custom themes storage key
@@ -1185,7 +1285,7 @@ function createThemeSegment({ editorRef, shellState, handlers, onCleanup }) {
1185
1285
  }
1186
1286
 
1187
1287
  // Check for name conflicts with built-in themes
1188
- 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'];
1189
1289
  if (builtInThemes.includes(theme.name)) {
1190
1290
  alert(`Cannot use reserved theme name "${theme.name}". Please rename your theme.`);
1191
1291
  return;
@@ -1746,6 +1846,101 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
1746
1846
 
1747
1847
  let currentMenu = null;
1748
1848
  let runtimeState = 'disconnected'; // 'connected', 'disconnected', 'error'
1849
+ let machineState = {
1850
+ supported: false,
1851
+ activeMachineId: null,
1852
+ activeMachineName: null,
1853
+ machines: [],
1854
+ };
1855
+
1856
+ const PROJECT_MACHINE_PREF_KEY = 'mrmd:project-machine-pref:v1';
1857
+ let projectMachinePrefs = loadProjectMachinePrefs();
1858
+ let lastAppliedProjectPrefKey = null;
1859
+
1860
+ function loadProjectMachinePrefs() {
1861
+ try {
1862
+ const raw = window.localStorage?.getItem(PROJECT_MACHINE_PREF_KEY);
1863
+ const parsed = raw ? JSON.parse(raw) : {};
1864
+ return parsed && typeof parsed === 'object' ? parsed : {};
1865
+ } catch {
1866
+ return {};
1867
+ }
1868
+ }
1869
+
1870
+ function saveProjectMachinePrefs() {
1871
+ try {
1872
+ window.localStorage?.setItem(PROJECT_MACHINE_PREF_KEY, JSON.stringify(projectMachinePrefs));
1873
+ } catch {
1874
+ // ignore
1875
+ }
1876
+ }
1877
+
1878
+ function getProjectMachinePreference(projectRoot) {
1879
+ if (!projectRoot || !(projectRoot in projectMachinePrefs)) return undefined;
1880
+ const value = projectMachinePrefs[projectRoot];
1881
+ return value === '__auto__' ? null : value;
1882
+ }
1883
+
1884
+ function setProjectMachinePreference(projectRoot, machineIdOrNullOrUndefined) {
1885
+ if (!projectRoot) return;
1886
+ if (machineIdOrNullOrUndefined === undefined) {
1887
+ delete projectMachinePrefs[projectRoot];
1888
+ } else if (machineIdOrNullOrUndefined === null) {
1889
+ projectMachinePrefs[projectRoot] = '__auto__';
1890
+ } else {
1891
+ projectMachinePrefs[projectRoot] = machineIdOrNullOrUndefined;
1892
+ }
1893
+ saveProjectMachinePrefs();
1894
+ }
1895
+
1896
+ async function refreshMachineState({ doRender = true } = {}) {
1897
+ try {
1898
+ const [machinesRes, activeRes] = await Promise.all([
1899
+ orchestratorClient.getMachines?.(),
1900
+ orchestratorClient.getActiveMachine?.(),
1901
+ ]);
1902
+
1903
+ const machines = machinesRes?.machines || [];
1904
+ const activeMachineId = activeRes?.activeMachineId || null;
1905
+ const activeMachine = machines.find(m => m.machineId === activeMachineId) || null;
1906
+
1907
+ machineState = {
1908
+ supported: true,
1909
+ activeMachineId,
1910
+ activeMachineName: activeMachine?.machineName || activeMachine?.machineId || null,
1911
+ machines,
1912
+ };
1913
+ } catch {
1914
+ machineState = {
1915
+ supported: false,
1916
+ activeMachineId: null,
1917
+ activeMachineName: null,
1918
+ machines: [],
1919
+ };
1920
+ }
1921
+
1922
+ if (doRender) render();
1923
+ return machineState;
1924
+ }
1925
+
1926
+ async function applyProjectPreference() {
1927
+ const projectRoot = shellState.get('projectRoot');
1928
+ if (!projectRoot || !machineState.supported) return;
1929
+
1930
+ const pref = getProjectMachinePreference(projectRoot);
1931
+ if (pref === undefined) return; // no preference set
1932
+
1933
+ const key = `${projectRoot}|${pref === null ? '__auto__' : pref}`;
1934
+ if (key === lastAppliedProjectPrefKey) return;
1935
+ lastAppliedProjectPrefKey = key;
1936
+
1937
+ try {
1938
+ await orchestratorClient.setActiveMachine?.(pref);
1939
+ await refreshMachineState({ doRender: true });
1940
+ } catch {
1941
+ // ignore
1942
+ }
1943
+ }
1749
1944
 
1750
1945
  function render() {
1751
1946
  const file = shellState.get('file');
@@ -1777,11 +1972,18 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
1777
1972
  error: 'No venv selected - click to pick one',
1778
1973
  }[runtimeState];
1779
1974
 
1975
+ const machinePill = machineState.supported
1976
+ ? `<span class="mrmd-statusbar__machine-pill" title="Active runtime machine — click to change">⚡ ${machineState.activeMachineName || 'Auto'}</span>`
1977
+ : '';
1978
+
1780
1979
  segment.innerHTML = `
1781
1980
  <span class="mrmd-statusbar__filename" style="font-weight: 500;">${filename}</span>
1782
- <span class="mrmd-statusbar__runtime-dot"
1783
- style="width: 10px; height: 10px; border-radius: 50%; background: ${dotColor}; cursor: pointer; ${runtimeState === 'error' ? 'animation: blink 1s infinite;' : ''}"
1784
- title="${dotTitle}"></span>
1981
+ <span style="display:inline-flex; align-items:center; gap:8px;">
1982
+ ${machinePill}
1983
+ <span class="mrmd-statusbar__runtime-dot"
1984
+ style="width: 10px; height: 10px; border-radius: 50%; background: ${dotColor}; cursor: pointer; ${runtimeState === 'error' ? 'animation: blink 1s infinite;' : ''}"
1985
+ title="${dotTitle}"></span>
1986
+ </span>
1785
1987
  `;
1786
1988
 
1787
1989
  // Add blink animation if needed
@@ -1794,8 +1996,10 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
1794
1996
  }
1795
1997
 
1796
1998
  async function openMenu(e) {
1797
- // Only open menu when clicking the dot
1798
- if (!e.target.classList.contains('mrmd-statusbar__runtime-dot')) {
1999
+ // Only open menu when clicking the dot or machine pill
2000
+ const isRuntimeDot = e.target.classList.contains('mrmd-statusbar__runtime-dot');
2001
+ const isMachinePill = e.target.classList.contains('mrmd-statusbar__machine-pill');
2002
+ if (!isRuntimeDot && !isMachinePill) {
1799
2003
  return;
1800
2004
  }
1801
2005
 
@@ -1832,6 +2036,12 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
1832
2036
  console.error('Failed to fetch runtimes/venvs:', err);
1833
2037
  }
1834
2038
 
2039
+ // Fetch machine state (if available in cloud mode)
2040
+ await refreshMachineState({ doRender: false });
2041
+ const machineList = machineState.machines || [];
2042
+ const activeMachineId = machineState.activeMachineId || null;
2043
+ const projectRoot = shellState.get('projectRoot');
2044
+
1835
2045
  // Section 1: Running Runtimes
1836
2046
  items.push({
1837
2047
  type: 'header',
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * A dialog for browsing and selecting files.
5
5
  * Supports both project files and system-wide browsing.
6
+ * When connected to cloud machines, shows a machine tab bar for multi-machine browsing.
6
7
  */
7
8
 
8
9
  import { createDialog } from './base-dialog.js';
@@ -48,10 +49,22 @@ export function showFilePicker(options) {
48
49
  let isLoading = false;
49
50
  let pickerHistory = loadFilePickerHistory();
50
51
 
52
+ // ── Machine catalog state ──
53
+ let catalogData = null; // {machines: [...], cloudOnlyProjects: [...]}
54
+ let activeMachineTab = null; // machineId or 'local' or 'cloud'
55
+ let catalogView = false; // true when showing catalog project/doc tree
56
+ let catalogRoot = ''; // absolute docs root (e.g. /home/ubuntu)
57
+
51
58
  // Create content
52
59
  const content = document.createElement('div');
53
60
  content.className = 'mrmd-filepicker';
54
61
 
62
+ // Machine tab bar (hidden until catalog loads)
63
+ const machineBar = document.createElement('div');
64
+ machineBar.className = 'mrmd-filepicker__machines';
65
+ machineBar.style.display = 'none';
66
+ content.appendChild(machineBar);
67
+
55
68
  // Path bar
56
69
  const pathBar = document.createElement('div');
57
70
  pathBar.className = 'mrmd-filepicker__path';
@@ -62,6 +75,20 @@ export function showFilePicker(options) {
62
75
  fileList.className = 'mrmd-filepicker__list';
63
76
  content.appendChild(fileList);
64
77
 
78
+ // Fetch catalog in background (non-blocking)
79
+ Promise.all([
80
+ orchestratorClient.getCatalog().catch(() => null),
81
+ orchestratorClient.listFiles().catch(() => null),
82
+ ]).then(([data, files]) => {
83
+ if (files?.root) {
84
+ catalogRoot = String(files.root).replace(/\/$/, '');
85
+ }
86
+ if (data?.machines?.length > 0 || data?.cloudOnlyProjects?.length > 0) {
87
+ catalogData = data;
88
+ renderMachineBar();
89
+ }
90
+ }).catch(() => { /* catalog unavailable — filesystem-only mode */ });
91
+
65
92
  // New file input (for save mode)
66
93
  let filenameInput = null;
67
94
  if (mode === 'save') {
@@ -85,6 +112,190 @@ export function showFilePicker(options) {
85
112
  content.appendChild(newFileRow);
86
113
  }
87
114
 
115
+ function catalogDocAbsolutePath(projectName, docPath) {
116
+ const root = catalogRoot || '';
117
+ const normalizedRoot = root.replace(/\/$/, '');
118
+ if (normalizedRoot) return `${normalizedRoot}/${projectName}/${docPath}.md`;
119
+ return `/${projectName}/${docPath}.md`;
120
+ }
121
+
122
+ // ── Machine tab bar ──
123
+ function renderMachineBar() {
124
+ if (!catalogData?.machines?.length) {
125
+ machineBar.style.display = 'none';
126
+ return;
127
+ }
128
+
129
+ machineBar.style.display = 'flex';
130
+ machineBar.innerHTML = '';
131
+
132
+ // "Local" tab (filesystem browsing — current behavior)
133
+ const localTab = document.createElement('button');
134
+ localTab.className = 'mrmd-filepicker__machine-tab' + (!catalogView ? ' mrmd-filepicker__machine-tab--active' : '');
135
+ localTab.textContent = '📁 Files';
136
+ localTab.title = 'Browse local filesystem';
137
+ localTab.addEventListener('click', () => {
138
+ catalogView = false;
139
+ activeMachineTab = null;
140
+ renderMachineBar();
141
+ navigateTo(currentPath);
142
+ });
143
+ machineBar.appendChild(localTab);
144
+
145
+ // One tab per connected machine
146
+ for (const machine of catalogData.machines) {
147
+ const tab = document.createElement('button');
148
+ const isActive = catalogView && activeMachineTab === machine.machineId;
149
+ const online = machine.connected !== false && machine.status !== 'offline';
150
+ tab.className = 'mrmd-filepicker__machine-tab'
151
+ + (isActive ? ' mrmd-filepicker__machine-tab--active' : '')
152
+ + (!online ? ' mrmd-filepicker__machine-tab--offline' : '');
153
+
154
+ const dot = online ? '🟢' : '⚫';
155
+ tab.textContent = `${dot} ${machine.machineName || machine.machineId}`;
156
+ tab.title = `${machine.hostname || machine.machineId}\n${(machine.capabilities || []).join(', ')}\n${online ? 'online' : 'offline (opens cached snapshot)'}`;
157
+
158
+ tab.addEventListener('click', () => {
159
+ catalogView = true;
160
+ activeMachineTab = machine.machineId;
161
+ renderMachineBar();
162
+ renderCatalogView(machine);
163
+ });
164
+ machineBar.appendChild(tab);
165
+ }
166
+
167
+ // Cloud tab (cloud-only projects)
168
+ if (catalogData.cloudOnlyProjects?.length > 0) {
169
+ const cloudTab = document.createElement('button');
170
+ const isActive = catalogView && activeMachineTab === 'cloud';
171
+ cloudTab.className = 'mrmd-filepicker__machine-tab' + (isActive ? ' mrmd-filepicker__machine-tab--active' : '');
172
+ cloudTab.textContent = '☁️ Cloud';
173
+ cloudTab.title = 'Projects only in cloud (not on any machine)';
174
+ cloudTab.addEventListener('click', () => {
175
+ catalogView = true;
176
+ activeMachineTab = 'cloud';
177
+ renderMachineBar();
178
+ renderCloudOnlyView();
179
+ });
180
+ machineBar.appendChild(cloudTab);
181
+ }
182
+ }
183
+
184
+ // ── Catalog view: show projects/docs for a machine ──
185
+ function renderCatalogView(machine) {
186
+ pathBar.innerHTML = '';
187
+ const label = document.createElement('span');
188
+ label.className = 'mrmd-filepicker__path-segment';
189
+ const online = machine.connected !== false && machine.status !== 'offline';
190
+ label.textContent = `${machine.machineName || machine.machineId} — ${machine.projects?.length || 0} projects${online ? '' : ' (offline snapshots)'}`;
191
+ pathBar.appendChild(label);
192
+
193
+ fileList.innerHTML = '';
194
+ const projects = machine.projects || [];
195
+
196
+ if (projects.length === 0) {
197
+ fileList.innerHTML = '<div class="mrmd-filepicker__empty">No projects on this machine</div>';
198
+ return;
199
+ }
200
+
201
+ for (const project of projects) {
202
+ // Project header
203
+ const projectItem = document.createElement('div');
204
+ projectItem.className = 'mrmd-filepicker__item mrmd-filepicker__item--project';
205
+ projectItem.style.cursor = 'default';
206
+
207
+ const icon = document.createElement('span');
208
+ icon.className = 'mrmd-filepicker__item-icon';
209
+ icon.textContent = '📁';
210
+ projectItem.appendChild(icon);
211
+
212
+ const name = document.createElement('span');
213
+ name.className = 'mrmd-filepicker__item-name';
214
+ name.style.fontWeight = '600';
215
+ name.textContent = project.name;
216
+ projectItem.appendChild(name);
217
+
218
+ const info = document.createElement('span');
219
+ info.className = 'mrmd-filepicker__item-info';
220
+ info.textContent = `${project.docCount || project.documents?.length || 0} docs`;
221
+ projectItem.appendChild(info);
222
+
223
+ fileList.appendChild(projectItem);
224
+
225
+ // Documents under this project
226
+ for (const doc of (project.documents || [])) {
227
+ const docItem = document.createElement('div');
228
+ docItem.className = 'mrmd-filepicker__item';
229
+ docItem.style.paddingLeft = '30px';
230
+
231
+ const docIcon = document.createElement('span');
232
+ docIcon.className = 'mrmd-filepicker__item-icon';
233
+ docIcon.textContent = '📄';
234
+ docItem.appendChild(docIcon);
235
+
236
+ const docName = document.createElement('span');
237
+ docName.className = 'mrmd-filepicker__item-name';
238
+ docName.textContent = doc.docPath;
239
+ docItem.appendChild(docName);
240
+
241
+ // Click to select, double-click to open
242
+ docItem.addEventListener('click', () => {
243
+ const fullPath = catalogDocAbsolutePath(project.name, doc.docPath);
244
+ selectedEntry = { name: doc.docPath, path: fullPath, type: 'file' };
245
+ fileList.querySelectorAll('.mrmd-filepicker__item--selected').forEach(el => el.classList.remove('mrmd-filepicker__item--selected'));
246
+ docItem.classList.add('mrmd-filepicker__item--selected');
247
+ });
248
+ docItem.addEventListener('dblclick', () => {
249
+ selectFile(catalogDocAbsolutePath(project.name, doc.docPath));
250
+ });
251
+
252
+ fileList.appendChild(docItem);
253
+ }
254
+ }
255
+ }
256
+
257
+ // ── Cloud-only view ──
258
+ function renderCloudOnlyView() {
259
+ pathBar.innerHTML = '';
260
+ const label = document.createElement('span');
261
+ label.className = 'mrmd-filepicker__path-segment';
262
+ label.textContent = 'Cloud-only projects (not on any machine)';
263
+ pathBar.appendChild(label);
264
+
265
+ fileList.innerHTML = '';
266
+ const projects = catalogData?.cloudOnlyProjects || [];
267
+
268
+ if (projects.length === 0) {
269
+ fileList.innerHTML = '<div class="mrmd-filepicker__empty">No cloud-only projects</div>';
270
+ return;
271
+ }
272
+
273
+ for (const projectName of projects) {
274
+ const item = document.createElement('div');
275
+ item.className = 'mrmd-filepicker__item';
276
+
277
+ const icon = document.createElement('span');
278
+ icon.className = 'mrmd-filepicker__item-icon';
279
+ icon.textContent = '📁';
280
+ item.appendChild(icon);
281
+
282
+ const name = document.createElement('span');
283
+ name.className = 'mrmd-filepicker__item-name';
284
+ name.textContent = projectName;
285
+ item.appendChild(name);
286
+
287
+ item.addEventListener('dblclick', () => {
288
+ const root = catalogRoot || '~';
289
+ navigateTo(`${root.replace(/\/$/, '')}/${projectName}`);
290
+ catalogView = false;
291
+ activeMachineTab = null;
292
+ renderMachineBar();
293
+ });
294
+
295
+ fileList.appendChild(item);
296
+ }
297
+ }
298
+
88
299
  // Render path bar
89
300
  function renderPathBar() {
90
301
  pathBar.innerHTML = '';