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.
- package/package.json +3 -1
- package/src/commands.js +112 -4
- package/src/comment-syntax.js +364 -39
- package/src/config/handlers.js +1 -2
- package/src/config/schema.js +46 -4
- package/src/document-template.js +2236 -0
- package/src/execution.js +69 -15
- package/src/frontmatter-updater.js +204 -74
- package/src/grammar.js +758 -0
- package/src/index.js +1120 -55
- package/src/keymap.js +11 -2
- package/src/markdown/block-decorations.js +108 -5
- package/src/markdown/facets.js +37 -0
- package/src/markdown/html-inline.js +9 -5
- package/src/markdown/index.js +13 -3
- package/src/markdown/inline-commands.js +256 -0
- package/src/markdown/inline-model.js +578 -0
- package/src/markdown/inline-state.js +103 -0
- package/src/markdown/renderer.js +219 -12
- package/src/markdown/styles.js +290 -3
- package/src/markdown/widgets/alert-title.js +10 -8
- package/src/markdown/widgets/frontmatter.js +0 -6
- package/src/markdown/widgets/index.js +1 -0
- package/src/markdown/widgets/list-marker.js +29 -0
- package/src/markdown/wysiwyg.js +1158 -0
- package/src/mrp-types.js +2 -0
- package/src/output-widget.js +532 -18
- package/src/page-view-pagination.js +127 -0
- package/src/runtime-lsp.js +1757 -150
- package/src/section-controls/commands.js +617 -0
- package/src/section-controls/index.js +63 -0
- package/src/section-controls/plugin.js +165 -0
- package/src/section-controls/widgets.js +936 -0
- package/src/shell/ai-menu.js +11 -0
- package/src/shell/components/context-panel.js +572 -0
- package/src/shell/components/status-bar.js +218 -8
- package/src/shell/dialogs/file-picker.js +211 -0
- package/src/shell/layouts/studio.js +229 -14
- package/src/shell/orchestrator-client.js +114 -0
- package/src/shell/styles.js +62 -0
- package/src/spellcheck.js +166 -0
- package/src/tables/README.md +97 -0
- package/src/tables/commands/insert-linked-table.js +122 -0
- package/src/tables/commands/open-table-workspace.js +43 -0
- package/src/tables/index.js +24 -0
- package/src/tables/jobs/client.js +158 -0
- package/src/tables/parsing/anchors.js +82 -0
- package/src/tables/parsing/linked-table-blocks.js +61 -0
- package/src/tables/state/linked-table-state.js +68 -0
- package/src/tables/widgets/linked-table-source-banner.js +77 -0
- package/src/tables/widgets/linked-table-widget.js +256 -0
- package/src/tables/workspace/controller.js +616 -0
- package/src/term-pty-client.js +111 -7
- package/src/term-widget.js +43 -3
- package/src/widgets/theme-utils.js +24 -16
- package/src/widgets/theme.js +1535 -1
- package/src/runtime-codelens/detector.js +0 -279
- package/src/runtime-codelens/index.js +0 -76
- package/src/runtime-codelens/plugin.js +0 -142
- package/src/runtime-codelens/styles.js +0 -184
- 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
|
-
|
|
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
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
|
|
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 = '';
|