mrmd-editor 0.7.0 → 0.7.1
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 +1 -1
- package/src/execution.js +69 -15
- package/src/index.js +46 -0
- package/src/shell/components/status-bar.js +208 -6
- package/src/shell/dialogs/file-picker.js +211 -0
- package/src/shell/layouts/studio.js +23 -0
- package/src/shell/orchestrator-client.js +45 -0
- package/src/shell/styles.js +62 -0
- package/src/term-pty-client.js +62 -7
- package/src/widgets/theme-utils.js +12 -12
- package/src/widgets/theme.js +520 -0
package/package.json
CHANGED
package/src/execution.js
CHANGED
|
@@ -737,6 +737,9 @@ export class ExecutionManager {
|
|
|
737
737
|
// Emit start event
|
|
738
738
|
this._emit('cellRun', index, cell, execId);
|
|
739
739
|
|
|
740
|
+
// Direct execution output write throttling (reduces CRDT churn for progress bars)
|
|
741
|
+
let clearChunkFlushTimer = null;
|
|
742
|
+
|
|
740
743
|
try {
|
|
741
744
|
// Prepare output position
|
|
742
745
|
// Re-read content as it may have changed
|
|
@@ -815,22 +818,11 @@ export class ExecutionManager {
|
|
|
815
818
|
|
|
816
819
|
// Track current output length in document for replacement
|
|
817
820
|
let currentDocOutputLen = 0;
|
|
821
|
+
const chunkFlushMs = 100;
|
|
822
|
+
let chunkFlushTimer = null;
|
|
823
|
+
let latestProcessedOutput = '';
|
|
818
824
|
|
|
819
|
-
const
|
|
820
|
-
if (controller.signal.aborted) return;
|
|
821
|
-
|
|
822
|
-
// Process through terminal buffer (handles \r, cursor movement, ANSI)
|
|
823
|
-
buffer.write(chunk);
|
|
824
|
-
|
|
825
|
-
// Get processed output with ANSI codes preserved
|
|
826
|
-
let processedOutput = buffer.toAnsi();
|
|
827
|
-
|
|
828
|
-
// Ensure output ends with newline so closing ``` stays on its own line
|
|
829
|
-
// This is critical for maintaining valid markdown structure
|
|
830
|
-
if (processedOutput && !processedOutput.endsWith('\n')) {
|
|
831
|
-
processedOutput += '\n';
|
|
832
|
-
}
|
|
833
|
-
|
|
825
|
+
const applyProcessedOutput = (processedOutput) => {
|
|
834
826
|
// Get current position - prefer finding by execId for robustness
|
|
835
827
|
let currentOutputStart = outputContentStart;
|
|
836
828
|
|
|
@@ -865,6 +857,61 @@ export class ExecutionManager {
|
|
|
865
857
|
});
|
|
866
858
|
|
|
867
859
|
currentDocOutputLen = processedOutput.length;
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const flushChunkOutputNow = () => {
|
|
863
|
+
if (chunkFlushTimer) {
|
|
864
|
+
clearTimeout(chunkFlushTimer);
|
|
865
|
+
chunkFlushTimer = null;
|
|
866
|
+
}
|
|
867
|
+
applyProcessedOutput(latestProcessedOutput);
|
|
868
|
+
};
|
|
869
|
+
|
|
870
|
+
clearChunkFlushTimer = () => {
|
|
871
|
+
if (chunkFlushTimer) {
|
|
872
|
+
clearTimeout(chunkFlushTimer);
|
|
873
|
+
chunkFlushTimer = null;
|
|
874
|
+
}
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
const scheduleChunkOutputFlush = () => {
|
|
878
|
+
if (chunkFlushMs === 0) {
|
|
879
|
+
flushChunkOutputNow();
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
if (chunkFlushTimer) return;
|
|
883
|
+
chunkFlushTimer = setTimeout(() => {
|
|
884
|
+
chunkFlushTimer = null;
|
|
885
|
+
applyProcessedOutput(latestProcessedOutput);
|
|
886
|
+
}, chunkFlushMs);
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
controller.signal.addEventListener('abort', () => {
|
|
890
|
+
clearChunkFlushTimer?.();
|
|
891
|
+
}, { once: true });
|
|
892
|
+
|
|
893
|
+
const onChunk = (chunk, accumulatedRaw, done) => {
|
|
894
|
+
if (controller.signal.aborted) return;
|
|
895
|
+
|
|
896
|
+
// Process through terminal buffer (handles \r, cursor movement, ANSI)
|
|
897
|
+
buffer.write(chunk);
|
|
898
|
+
|
|
899
|
+
// Get processed output with ANSI codes preserved
|
|
900
|
+
let processedOutput = buffer.toAnsi();
|
|
901
|
+
|
|
902
|
+
// Ensure output ends with newline so closing ``` stays on its own line
|
|
903
|
+
// This is critical for maintaining valid markdown structure
|
|
904
|
+
if (processedOutput && !processedOutput.endsWith('\n')) {
|
|
905
|
+
processedOutput += '\n';
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
latestProcessedOutput = processedOutput;
|
|
909
|
+
|
|
910
|
+
if (done) {
|
|
911
|
+
flushChunkOutputNow();
|
|
912
|
+
} else {
|
|
913
|
+
scheduleChunkOutputFlush();
|
|
914
|
+
}
|
|
868
915
|
|
|
869
916
|
this._emit('cellOutput', index, chunk, processedOutput, execId);
|
|
870
917
|
};
|
|
@@ -883,6 +930,9 @@ export class ExecutionManager {
|
|
|
883
930
|
return;
|
|
884
931
|
}
|
|
885
932
|
|
|
933
|
+
// Flush pending output so prompt context is visible immediately
|
|
934
|
+
flushChunkOutputNow();
|
|
935
|
+
|
|
886
936
|
const stdinExecId = execId;
|
|
887
937
|
|
|
888
938
|
// Wait for any pending output to be written to the document
|
|
@@ -1009,6 +1059,9 @@ export class ExecutionManager {
|
|
|
1009
1059
|
onAsset,
|
|
1010
1060
|
});
|
|
1011
1061
|
|
|
1062
|
+
// Flush any pending throttled output before final normalization
|
|
1063
|
+
flushChunkOutputNow();
|
|
1064
|
+
|
|
1012
1065
|
// Final update - find output block by execId for robustness
|
|
1013
1066
|
content = this.editor.getContent();
|
|
1014
1067
|
const finalOutputBlock = findOutputBlockByExecId(content, execId);
|
|
@@ -1127,6 +1180,7 @@ export class ExecutionManager {
|
|
|
1127
1180
|
}
|
|
1128
1181
|
return execId;
|
|
1129
1182
|
} finally {
|
|
1183
|
+
clearChunkFlushTimer?.();
|
|
1130
1184
|
this.running.delete(execId);
|
|
1131
1185
|
this.buffers.delete(execId);
|
|
1132
1186
|
|
package/src/index.js
CHANGED
|
@@ -791,6 +791,50 @@ class Writer {
|
|
|
791
791
|
}
|
|
792
792
|
// #endregion WRITER
|
|
793
793
|
|
|
794
|
+
// #region INITIAL_CURSOR
|
|
795
|
+
/**
|
|
796
|
+
* Find the ideal initial cursor position for a markdown document.
|
|
797
|
+
*
|
|
798
|
+
* When opening a file, placing the cursor at position 0 shows raw frontmatter
|
|
799
|
+
* YAML which looks ugly. Instead, we find the first empty line after any
|
|
800
|
+
* frontmatter block — this causes the frontmatter to render as a nice widget
|
|
801
|
+
* and gives a clean first impression.
|
|
802
|
+
*
|
|
803
|
+
* @param {string} content - Document content
|
|
804
|
+
* @returns {number} Character position for the cursor
|
|
805
|
+
*/
|
|
806
|
+
function findInitialCursorPosition(content) {
|
|
807
|
+
if (!content) return 0;
|
|
808
|
+
|
|
809
|
+
const lines = content.split('\n');
|
|
810
|
+
let i = 0;
|
|
811
|
+
|
|
812
|
+
// Skip YAML frontmatter if present (--- ... ---)
|
|
813
|
+
if (lines[0]?.trim() === '---') {
|
|
814
|
+
i = 1;
|
|
815
|
+
while (i < lines.length && lines[i]?.trim() !== '---') {
|
|
816
|
+
i++;
|
|
817
|
+
}
|
|
818
|
+
if (i < lines.length) i++; // skip closing ---
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// Find first empty line from current position
|
|
822
|
+
while (i < lines.length) {
|
|
823
|
+
if (lines[i]?.trim() === '') {
|
|
824
|
+
// Calculate character position (start of this empty line)
|
|
825
|
+
let pos = 0;
|
|
826
|
+
for (let j = 0; j < i; j++) {
|
|
827
|
+
pos += lines[j].length + 1; // +1 for \n
|
|
828
|
+
}
|
|
829
|
+
return pos;
|
|
830
|
+
}
|
|
831
|
+
i++;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
return 0; // fallback to start
|
|
835
|
+
}
|
|
836
|
+
// #endregion INITIAL_CURSOR
|
|
837
|
+
|
|
794
838
|
// #region CREATE
|
|
795
839
|
/**
|
|
796
840
|
* Create a standalone markdown editor
|
|
@@ -3076,6 +3120,7 @@ const mrmd = {
|
|
|
3076
3120
|
create,
|
|
3077
3121
|
drive,
|
|
3078
3122
|
runtime,
|
|
3123
|
+
findInitialCursorPosition,
|
|
3079
3124
|
yjs,
|
|
3080
3125
|
codemirror,
|
|
3081
3126
|
terminal,
|
|
@@ -3134,6 +3179,7 @@ export {
|
|
|
3134
3179
|
create,
|
|
3135
3180
|
drive,
|
|
3136
3181
|
runtime,
|
|
3182
|
+
findInitialCursorPosition,
|
|
3137
3183
|
yjs,
|
|
3138
3184
|
codemirror,
|
|
3139
3185
|
terminal,
|
|
@@ -861,6 +861,80 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
|
|
|
861
861
|
});
|
|
862
862
|
}
|
|
863
863
|
|
|
864
|
+
// Runtime machine selection
|
|
865
|
+
if (machineState.supported) {
|
|
866
|
+
items.push({ type: 'divider' });
|
|
867
|
+
items.push({ type: 'header', label: 'Runtime Machine' });
|
|
868
|
+
|
|
869
|
+
items.push({
|
|
870
|
+
icon: activeMachineId ? '○' : '✓',
|
|
871
|
+
label: 'Auto-select',
|
|
872
|
+
active: !activeMachineId,
|
|
873
|
+
onClick: async () => {
|
|
874
|
+
try {
|
|
875
|
+
await orchestratorClient.setActiveMachine?.(null);
|
|
876
|
+
await refreshMachineState({ doRender: true });
|
|
877
|
+
} catch (err) {
|
|
878
|
+
console.error('Failed to set auto machine:', err);
|
|
879
|
+
}
|
|
880
|
+
},
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
for (const m of machineList) {
|
|
884
|
+
const online = m.status === 'online' || m.connected === true;
|
|
885
|
+
const isActiveMachine = activeMachineId === m.machineId;
|
|
886
|
+
items.push({
|
|
887
|
+
icon: isActiveMachine ? '✓' : (online ? '●' : '○'),
|
|
888
|
+
label: `${m.machineName || m.machineId}${online ? '' : ' (offline)'}`,
|
|
889
|
+
active: isActiveMachine,
|
|
890
|
+
onClick: async () => {
|
|
891
|
+
try {
|
|
892
|
+
await orchestratorClient.setActiveMachine?.(m.machineId);
|
|
893
|
+
await refreshMachineState({ doRender: true });
|
|
894
|
+
} catch (err) {
|
|
895
|
+
console.error('Failed to switch machine:', err);
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (projectRoot) {
|
|
902
|
+
const pref = getProjectMachinePreference(projectRoot);
|
|
903
|
+
items.push({ type: 'divider' });
|
|
904
|
+
items.push({ type: 'header', label: 'Project Machine Preference' });
|
|
905
|
+
|
|
906
|
+
items.push({
|
|
907
|
+
icon: pref === undefined ? '✓' : '○',
|
|
908
|
+
label: 'No preference (manual/global)',
|
|
909
|
+
active: pref === undefined,
|
|
910
|
+
onClick: () => {
|
|
911
|
+
setProjectMachinePreference(projectRoot, undefined);
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
items.push({
|
|
916
|
+
icon: pref === null ? '✓' : '○',
|
|
917
|
+
label: 'Prefer auto-select',
|
|
918
|
+
active: pref === null,
|
|
919
|
+
onClick: () => {
|
|
920
|
+
setProjectMachinePreference(projectRoot, null);
|
|
921
|
+
},
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
for (const m of machineList) {
|
|
925
|
+
const isPref = pref === m.machineId;
|
|
926
|
+
items.push({
|
|
927
|
+
icon: isPref ? '✓' : '📌',
|
|
928
|
+
label: `Prefer ${m.machineName || m.machineId}`,
|
|
929
|
+
active: isPref,
|
|
930
|
+
onClick: () => {
|
|
931
|
+
setProjectMachinePreference(projectRoot, m.machineId);
|
|
932
|
+
},
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
864
938
|
// Refresh action
|
|
865
939
|
items.push({ type: 'divider' });
|
|
866
940
|
items.push({
|
|
@@ -870,6 +944,7 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
|
|
|
870
944
|
cachedRuntimes = null;
|
|
871
945
|
cachedVenvs = null;
|
|
872
946
|
await fetchRuntimes();
|
|
947
|
+
await refreshMachineState({ doRender: false });
|
|
873
948
|
render();
|
|
874
949
|
},
|
|
875
950
|
});
|
|
@@ -885,13 +960,30 @@ function createRuntimeSegment({ shellState, orchestratorClient, handlers, onClea
|
|
|
885
960
|
segment.addEventListener('click', openMenu);
|
|
886
961
|
|
|
887
962
|
// Initial fetch
|
|
888
|
-
|
|
963
|
+
Promise.all([
|
|
964
|
+
fetchRuntimes(),
|
|
965
|
+
refreshMachineState({ doRender: false }),
|
|
966
|
+
]).then(async () => {
|
|
967
|
+
render();
|
|
968
|
+
await applyProjectPreference();
|
|
969
|
+
});
|
|
889
970
|
|
|
890
971
|
// Subscribe to state changes
|
|
891
972
|
const unsubscribe1 = shellState.onPath('runtimes', render);
|
|
892
973
|
const unsubscribe2 = shellState.onPath('file', render);
|
|
974
|
+
const unsubscribe3 = shellState.onPath('projectRoot', () => {
|
|
975
|
+
applyProjectPreference();
|
|
976
|
+
render();
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const machinePoll = setInterval(() => {
|
|
980
|
+
refreshMachineState({ doRender: true });
|
|
981
|
+
}, 15000);
|
|
982
|
+
|
|
893
983
|
onCleanup(unsubscribe1);
|
|
894
984
|
onCleanup(unsubscribe2);
|
|
985
|
+
onCleanup(unsubscribe3);
|
|
986
|
+
onCleanup(() => clearInterval(machinePoll));
|
|
895
987
|
onCleanup(() => currentMenu?.close());
|
|
896
988
|
|
|
897
989
|
render();
|
|
@@ -1746,6 +1838,101 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
|
|
|
1746
1838
|
|
|
1747
1839
|
let currentMenu = null;
|
|
1748
1840
|
let runtimeState = 'disconnected'; // 'connected', 'disconnected', 'error'
|
|
1841
|
+
let machineState = {
|
|
1842
|
+
supported: false,
|
|
1843
|
+
activeMachineId: null,
|
|
1844
|
+
activeMachineName: null,
|
|
1845
|
+
machines: [],
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
const PROJECT_MACHINE_PREF_KEY = 'mrmd:project-machine-pref:v1';
|
|
1849
|
+
let projectMachinePrefs = loadProjectMachinePrefs();
|
|
1850
|
+
let lastAppliedProjectPrefKey = null;
|
|
1851
|
+
|
|
1852
|
+
function loadProjectMachinePrefs() {
|
|
1853
|
+
try {
|
|
1854
|
+
const raw = window.localStorage?.getItem(PROJECT_MACHINE_PREF_KEY);
|
|
1855
|
+
const parsed = raw ? JSON.parse(raw) : {};
|
|
1856
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
1857
|
+
} catch {
|
|
1858
|
+
return {};
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function saveProjectMachinePrefs() {
|
|
1863
|
+
try {
|
|
1864
|
+
window.localStorage?.setItem(PROJECT_MACHINE_PREF_KEY, JSON.stringify(projectMachinePrefs));
|
|
1865
|
+
} catch {
|
|
1866
|
+
// ignore
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function getProjectMachinePreference(projectRoot) {
|
|
1871
|
+
if (!projectRoot || !(projectRoot in projectMachinePrefs)) return undefined;
|
|
1872
|
+
const value = projectMachinePrefs[projectRoot];
|
|
1873
|
+
return value === '__auto__' ? null : value;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
function setProjectMachinePreference(projectRoot, machineIdOrNullOrUndefined) {
|
|
1877
|
+
if (!projectRoot) return;
|
|
1878
|
+
if (machineIdOrNullOrUndefined === undefined) {
|
|
1879
|
+
delete projectMachinePrefs[projectRoot];
|
|
1880
|
+
} else if (machineIdOrNullOrUndefined === null) {
|
|
1881
|
+
projectMachinePrefs[projectRoot] = '__auto__';
|
|
1882
|
+
} else {
|
|
1883
|
+
projectMachinePrefs[projectRoot] = machineIdOrNullOrUndefined;
|
|
1884
|
+
}
|
|
1885
|
+
saveProjectMachinePrefs();
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
async function refreshMachineState({ doRender = true } = {}) {
|
|
1889
|
+
try {
|
|
1890
|
+
const [machinesRes, activeRes] = await Promise.all([
|
|
1891
|
+
orchestratorClient.getMachines?.(),
|
|
1892
|
+
orchestratorClient.getActiveMachine?.(),
|
|
1893
|
+
]);
|
|
1894
|
+
|
|
1895
|
+
const machines = machinesRes?.machines || [];
|
|
1896
|
+
const activeMachineId = activeRes?.activeMachineId || null;
|
|
1897
|
+
const activeMachine = machines.find(m => m.machineId === activeMachineId) || null;
|
|
1898
|
+
|
|
1899
|
+
machineState = {
|
|
1900
|
+
supported: true,
|
|
1901
|
+
activeMachineId,
|
|
1902
|
+
activeMachineName: activeMachine?.machineName || activeMachine?.machineId || null,
|
|
1903
|
+
machines,
|
|
1904
|
+
};
|
|
1905
|
+
} catch {
|
|
1906
|
+
machineState = {
|
|
1907
|
+
supported: false,
|
|
1908
|
+
activeMachineId: null,
|
|
1909
|
+
activeMachineName: null,
|
|
1910
|
+
machines: [],
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
if (doRender) render();
|
|
1915
|
+
return machineState;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
async function applyProjectPreference() {
|
|
1919
|
+
const projectRoot = shellState.get('projectRoot');
|
|
1920
|
+
if (!projectRoot || !machineState.supported) return;
|
|
1921
|
+
|
|
1922
|
+
const pref = getProjectMachinePreference(projectRoot);
|
|
1923
|
+
if (pref === undefined) return; // no preference set
|
|
1924
|
+
|
|
1925
|
+
const key = `${projectRoot}|${pref === null ? '__auto__' : pref}`;
|
|
1926
|
+
if (key === lastAppliedProjectPrefKey) return;
|
|
1927
|
+
lastAppliedProjectPrefKey = key;
|
|
1928
|
+
|
|
1929
|
+
try {
|
|
1930
|
+
await orchestratorClient.setActiveMachine?.(pref);
|
|
1931
|
+
await refreshMachineState({ doRender: true });
|
|
1932
|
+
} catch {
|
|
1933
|
+
// ignore
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1749
1936
|
|
|
1750
1937
|
function render() {
|
|
1751
1938
|
const file = shellState.get('file');
|
|
@@ -1777,11 +1964,18 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
|
|
|
1777
1964
|
error: 'No venv selected - click to pick one',
|
|
1778
1965
|
}[runtimeState];
|
|
1779
1966
|
|
|
1967
|
+
const machinePill = machineState.supported
|
|
1968
|
+
? `<span class="mrmd-statusbar__machine-pill" title="Active runtime machine — click to change">⚡ ${machineState.activeMachineName || 'Auto'}</span>`
|
|
1969
|
+
: '';
|
|
1970
|
+
|
|
1780
1971
|
segment.innerHTML = `
|
|
1781
1972
|
<span class="mrmd-statusbar__filename" style="font-weight: 500;">${filename}</span>
|
|
1782
|
-
<span
|
|
1783
|
-
|
|
1784
|
-
|
|
1973
|
+
<span style="display:inline-flex; align-items:center; gap:8px;">
|
|
1974
|
+
${machinePill}
|
|
1975
|
+
<span class="mrmd-statusbar__runtime-dot"
|
|
1976
|
+
style="width: 10px; height: 10px; border-radius: 50%; background: ${dotColor}; cursor: pointer; ${runtimeState === 'error' ? 'animation: blink 1s infinite;' : ''}"
|
|
1977
|
+
title="${dotTitle}"></span>
|
|
1978
|
+
</span>
|
|
1785
1979
|
`;
|
|
1786
1980
|
|
|
1787
1981
|
// Add blink animation if needed
|
|
@@ -1794,8 +1988,10 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
|
|
|
1794
1988
|
}
|
|
1795
1989
|
|
|
1796
1990
|
async function openMenu(e) {
|
|
1797
|
-
// Only open menu when clicking the dot
|
|
1798
|
-
|
|
1991
|
+
// Only open menu when clicking the dot or machine pill
|
|
1992
|
+
const isRuntimeDot = e.target.classList.contains('mrmd-statusbar__runtime-dot');
|
|
1993
|
+
const isMachinePill = e.target.classList.contains('mrmd-statusbar__machine-pill');
|
|
1994
|
+
if (!isRuntimeDot && !isMachinePill) {
|
|
1799
1995
|
return;
|
|
1800
1996
|
}
|
|
1801
1997
|
|
|
@@ -1832,6 +2028,12 @@ function createSimpleSegment({ shellState, orchestratorClient, handlers, onClean
|
|
|
1832
2028
|
console.error('Failed to fetch runtimes/venvs:', err);
|
|
1833
2029
|
}
|
|
1834
2030
|
|
|
2031
|
+
// Fetch machine state (if available in cloud mode)
|
|
2032
|
+
await refreshMachineState({ doRender: false });
|
|
2033
|
+
const machineList = machineState.machines || [];
|
|
2034
|
+
const activeMachineId = machineState.activeMachineId || null;
|
|
2035
|
+
const projectRoot = shellState.get('projectRoot');
|
|
2036
|
+
|
|
1835
2037
|
// Section 1: Running Runtimes
|
|
1836
2038
|
items.push({
|
|
1837
2039
|
type: 'header',
|