mrmd-editor 0.6.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/cell-controls/widgets.js +30 -0
- package/src/cells.js +9 -9
- package/src/ctrl-k-modal.js +190 -14
- package/src/document-languages.js +105 -0
- package/src/execution.js +73 -25
- package/src/frontmatter-updater.js +224 -0
- package/src/index.js +173 -86
- package/src/markdown/renderer.js +52 -3
- package/src/markdown/styles.js +126 -0
- package/src/monitor-coordination.js +1 -3
- package/src/mrp-client.js +36 -169
- package/src/mrp-types.js +1 -37
- package/src/output-widget.js +54 -0
- package/src/runtime-codelens/index.js +3 -3
- package/src/shell/ai-menu.js +70 -0
- package/src/shell/components/menu.js +39 -1
- package/src/shell/components/status-bar.js +213 -11
- package/src/shell/dialogs/file-picker.js +378 -6
- package/src/shell/layouts/studio.js +31 -9
- package/src/shell/orchestrator-client.js +105 -18
- package/src/shell/state.js +63 -42
- package/src/shell/styles.js +328 -0
- package/src/term-pty-client.js +62 -7
- package/src/widgets/theme-utils.js +12 -12
- package/src/widgets/theme.js +520 -0
|
@@ -3,10 +3,14 @@
|
|
|
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';
|
|
9
10
|
|
|
11
|
+
const FILE_PICKER_HISTORY_KEY = 'mrmd:file-picker-history:v1';
|
|
12
|
+
const FILE_PICKER_HISTORY_LIMIT = 400;
|
|
13
|
+
|
|
10
14
|
// =============================================================================
|
|
11
15
|
// FILE PICKER
|
|
12
16
|
// =============================================================================
|
|
@@ -43,11 +47,24 @@ export function showFilePicker(options) {
|
|
|
43
47
|
let entries = [];
|
|
44
48
|
let selectedEntry = null;
|
|
45
49
|
let isLoading = false;
|
|
50
|
+
let pickerHistory = loadFilePickerHistory();
|
|
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)
|
|
46
57
|
|
|
47
58
|
// Create content
|
|
48
59
|
const content = document.createElement('div');
|
|
49
60
|
content.className = 'mrmd-filepicker';
|
|
50
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
|
+
|
|
51
68
|
// Path bar
|
|
52
69
|
const pathBar = document.createElement('div');
|
|
53
70
|
pathBar.className = 'mrmd-filepicker__path';
|
|
@@ -58,6 +75,20 @@ export function showFilePicker(options) {
|
|
|
58
75
|
fileList.className = 'mrmd-filepicker__list';
|
|
59
76
|
content.appendChild(fileList);
|
|
60
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
|
+
|
|
61
92
|
// New file input (for save mode)
|
|
62
93
|
let filenameInput = null;
|
|
63
94
|
if (mode === 'save') {
|
|
@@ -81,6 +112,190 @@ export function showFilePicker(options) {
|
|
|
81
112
|
content.appendChild(newFileRow);
|
|
82
113
|
}
|
|
83
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
|
+
|
|
84
299
|
// Render path bar
|
|
85
300
|
function renderPathBar() {
|
|
86
301
|
pathBar.innerHTML = '';
|
|
@@ -139,10 +354,20 @@ export function showFilePicker(options) {
|
|
|
139
354
|
return;
|
|
140
355
|
}
|
|
141
356
|
|
|
142
|
-
// Sort: directories first, then
|
|
357
|
+
// Sort: directories first, then recency/frequency score, then name
|
|
358
|
+
const nowMs = Date.now();
|
|
143
359
|
const sorted = [...entries].sort((a, b) => {
|
|
144
360
|
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
145
361
|
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
362
|
+
|
|
363
|
+
const aScore = getHistoryScore(pickerHistory.get(a.path), nowMs);
|
|
364
|
+
const bScore = getHistoryScore(pickerHistory.get(b.path), nowMs);
|
|
365
|
+
if (aScore !== bScore) return bScore - aScore;
|
|
366
|
+
|
|
367
|
+
const aModified = Number(a.modified || 0);
|
|
368
|
+
const bModified = Number(b.modified || 0);
|
|
369
|
+
if (aModified !== bModified) return bModified - aModified;
|
|
370
|
+
|
|
146
371
|
return a.name.localeCompare(b.name);
|
|
147
372
|
});
|
|
148
373
|
|
|
@@ -164,11 +389,26 @@ export function showFilePicker(options) {
|
|
|
164
389
|
name.textContent = entry.name;
|
|
165
390
|
item.appendChild(name);
|
|
166
391
|
|
|
167
|
-
if (entry.type === 'file'
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
392
|
+
if (entry.type === 'file') {
|
|
393
|
+
const infoParts = [];
|
|
394
|
+
if (entry.size !== undefined) {
|
|
395
|
+
infoParts.push(formatSize(entry.size));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const history = pickerHistory.get(entry.path);
|
|
399
|
+
if (history?.lastOpened) {
|
|
400
|
+
infoParts.push(formatTimeAgo(history.lastOpened));
|
|
401
|
+
}
|
|
402
|
+
if ((history?.count || 0) > 1) {
|
|
403
|
+
infoParts.push(`${Math.round(history.count)}x`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (infoParts.length > 0) {
|
|
407
|
+
const info = document.createElement('span');
|
|
408
|
+
info.className = 'mrmd-filepicker__item-info';
|
|
409
|
+
info.textContent = infoParts.join(' • ');
|
|
410
|
+
item.appendChild(info);
|
|
411
|
+
}
|
|
172
412
|
}
|
|
173
413
|
|
|
174
414
|
// Click handler
|
|
@@ -206,6 +446,13 @@ export function showFilePicker(options) {
|
|
|
206
446
|
const result = await orchestratorClient.browse({ path, type: 'all' });
|
|
207
447
|
currentPath = result.path;
|
|
208
448
|
entries = result.entries;
|
|
449
|
+
|
|
450
|
+
// Track directory navigation with a lighter weight than file opens
|
|
451
|
+
pickerHistory = touchFilePickerHistory(pickerHistory, currentPath, {
|
|
452
|
+
timestamp: new Date().toISOString(),
|
|
453
|
+
weight: 0.25,
|
|
454
|
+
});
|
|
455
|
+
saveFilePickerHistory(pickerHistory);
|
|
209
456
|
} catch (error) {
|
|
210
457
|
console.error('Failed to browse:', error);
|
|
211
458
|
entries = [];
|
|
@@ -218,6 +465,12 @@ export function showFilePicker(options) {
|
|
|
218
465
|
|
|
219
466
|
// Select a file
|
|
220
467
|
function selectFile(path) {
|
|
468
|
+
pickerHistory = touchFilePickerHistory(pickerHistory, path, {
|
|
469
|
+
timestamp: new Date().toISOString(),
|
|
470
|
+
weight: 1,
|
|
471
|
+
});
|
|
472
|
+
saveFilePickerHistory(pickerHistory);
|
|
473
|
+
|
|
221
474
|
dialog.close();
|
|
222
475
|
onSelect(path);
|
|
223
476
|
}
|
|
@@ -449,3 +702,122 @@ function formatSize(bytes) {
|
|
|
449
702
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
450
703
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
451
704
|
}
|
|
705
|
+
|
|
706
|
+
function formatTimeAgo(value) {
|
|
707
|
+
if (!value) return '';
|
|
708
|
+
const ts = Date.parse(value);
|
|
709
|
+
if (!Number.isFinite(ts)) return '';
|
|
710
|
+
|
|
711
|
+
const diffMs = Math.max(0, Date.now() - ts);
|
|
712
|
+
const mins = Math.floor(diffMs / 60000);
|
|
713
|
+
if (mins < 1) return 'just now';
|
|
714
|
+
if (mins < 60) return `${mins}m ago`;
|
|
715
|
+
|
|
716
|
+
const hours = Math.floor(mins / 60);
|
|
717
|
+
if (hours < 24) return `${hours}h ago`;
|
|
718
|
+
|
|
719
|
+
const days = Math.floor(hours / 24);
|
|
720
|
+
if (days < 30) return `${days}d ago`;
|
|
721
|
+
|
|
722
|
+
const months = Math.floor(days / 30);
|
|
723
|
+
return `${months}mo ago`;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function getStorage() {
|
|
727
|
+
if (typeof window === 'undefined') return null;
|
|
728
|
+
try {
|
|
729
|
+
return window.localStorage || null;
|
|
730
|
+
} catch {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function loadFilePickerHistory() {
|
|
736
|
+
const storage = getStorage();
|
|
737
|
+
if (!storage) return new Map();
|
|
738
|
+
|
|
739
|
+
try {
|
|
740
|
+
const raw = storage.getItem(FILE_PICKER_HISTORY_KEY);
|
|
741
|
+
if (!raw) return new Map();
|
|
742
|
+
|
|
743
|
+
const parsed = JSON.parse(raw);
|
|
744
|
+
if (!parsed || typeof parsed !== 'object') return new Map();
|
|
745
|
+
|
|
746
|
+
const byPath = new Map();
|
|
747
|
+
for (const [filePath, value] of Object.entries(parsed)) {
|
|
748
|
+
if (!filePath || !value || typeof value !== 'object') continue;
|
|
749
|
+
|
|
750
|
+
const count = Number(value.count || 0);
|
|
751
|
+
byPath.set(filePath, {
|
|
752
|
+
path: filePath,
|
|
753
|
+
lastOpened: typeof value.lastOpened === 'string' ? value.lastOpened : null,
|
|
754
|
+
count: Number.isFinite(count) && count > 0 ? count : 1,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return byPath;
|
|
759
|
+
} catch {
|
|
760
|
+
return new Map();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function saveFilePickerHistory(historyMap) {
|
|
765
|
+
const storage = getStorage();
|
|
766
|
+
if (!storage) return;
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
const items = Array.from(historyMap.values())
|
|
770
|
+
.filter(item => item?.path)
|
|
771
|
+
.sort((a, b) => {
|
|
772
|
+
const aTs = Date.parse(a.lastOpened || 0) || 0;
|
|
773
|
+
const bTs = Date.parse(b.lastOpened || 0) || 0;
|
|
774
|
+
if (aTs !== bTs) return bTs - aTs;
|
|
775
|
+
return (b.count || 0) - (a.count || 0);
|
|
776
|
+
})
|
|
777
|
+
.slice(0, FILE_PICKER_HISTORY_LIMIT);
|
|
778
|
+
|
|
779
|
+
const serialized = {};
|
|
780
|
+
for (const item of items) {
|
|
781
|
+
serialized[item.path] = {
|
|
782
|
+
lastOpened: item.lastOpened || null,
|
|
783
|
+
count: item.count || 1,
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
storage.setItem(FILE_PICKER_HISTORY_KEY, JSON.stringify(serialized));
|
|
788
|
+
} catch {
|
|
789
|
+
// Best-effort persistence only
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function touchFilePickerHistory(historyMap, filePath, { timestamp, weight = 1 } = {}) {
|
|
794
|
+
if (!filePath) return historyMap;
|
|
795
|
+
|
|
796
|
+
const next = new Map(historyMap);
|
|
797
|
+
const existing = next.get(filePath);
|
|
798
|
+
const nextCount = Math.max(0.1, Number(existing?.count || 0) + Math.max(0.1, Number(weight) || 0.1));
|
|
799
|
+
|
|
800
|
+
next.set(filePath, {
|
|
801
|
+
path: filePath,
|
|
802
|
+
lastOpened: timestamp || new Date().toISOString(),
|
|
803
|
+
count: nextCount,
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
return next;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function getHistoryScore(historyEntry, nowMs = Date.now()) {
|
|
810
|
+
if (!historyEntry) return 0;
|
|
811
|
+
|
|
812
|
+
const count = Math.max(1, Number(historyEntry.count) || 1);
|
|
813
|
+
const openedMs = Date.parse(historyEntry.lastOpened || '');
|
|
814
|
+
|
|
815
|
+
let recencyScore = 0;
|
|
816
|
+
if (Number.isFinite(openedMs)) {
|
|
817
|
+
const ageHours = Math.max(0, (nowMs - openedMs) / 3600000);
|
|
818
|
+
recencyScore = Math.max(0, 220 - Math.log2(ageHours + 1) * 36);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const frequencyScore = Math.min(180, Math.log2(count + 1) * 60);
|
|
822
|
+
return recencyScore + frequencyScore;
|
|
823
|
+
}
|
|
@@ -212,9 +212,9 @@ export async function createStudio(target, options = {}) {
|
|
|
212
212
|
throw new Error('No sync URL returned from orchestrator');
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
// Register the shared
|
|
215
|
+
// Register the shared runtime in shell state
|
|
216
216
|
if (runtimeUrls.python) {
|
|
217
|
-
shellState.
|
|
217
|
+
shellState.registerRuntime('shared', {
|
|
218
218
|
id: 'shared',
|
|
219
219
|
url: runtimeUrls.python,
|
|
220
220
|
status: 'ready',
|
|
@@ -329,7 +329,6 @@ export async function createStudio(target, options = {}) {
|
|
|
329
329
|
ydoc: handle.ydoc,
|
|
330
330
|
awareness: handle.awareness,
|
|
331
331
|
runtimeUrl: monitorRuntimeUrl,
|
|
332
|
-
session: docName,
|
|
333
332
|
});
|
|
334
333
|
}
|
|
335
334
|
|
|
@@ -519,11 +518,23 @@ export async function createStudio(target, options = {}) {
|
|
|
519
518
|
editor = createEditorForDocument(handle, normalizedName);
|
|
520
519
|
currentDocName = normalizedName;
|
|
521
520
|
|
|
522
|
-
//
|
|
521
|
+
// Place cursor on first empty line (after frontmatter) for a clean first impression.
|
|
522
|
+
// Without this, cursor lands at position 0, showing raw YAML frontmatter.
|
|
523
|
+
if (mrmd.findInitialCursorPosition) {
|
|
524
|
+
const pos = mrmd.findInitialCursorPosition(editor.view.state.doc.toString());
|
|
525
|
+
if (pos > 0) {
|
|
526
|
+
editor.view.dispatch({
|
|
527
|
+
selection: { anchor: pos },
|
|
528
|
+
scrollIntoView: true,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Ensure runtime attachment exists (starts monitor if needed)
|
|
523
534
|
try {
|
|
524
|
-
await orchestratorClient.
|
|
535
|
+
await orchestratorClient.createRuntimeAttachment(normalizedName, 'shared');
|
|
525
536
|
} catch (e) {
|
|
526
|
-
console.warn('Failed to create
|
|
537
|
+
console.warn('Failed to create runtime attachment for', normalizedName, e);
|
|
527
538
|
}
|
|
528
539
|
|
|
529
540
|
// Update shell state
|
|
@@ -558,11 +569,22 @@ export async function createStudio(target, options = {}) {
|
|
|
558
569
|
editor = createEditorForDocument(handle, docToOpen);
|
|
559
570
|
currentDocName = docToOpen;
|
|
560
571
|
|
|
561
|
-
//
|
|
572
|
+
// Place cursor on first empty line (after frontmatter) for a clean first impression
|
|
573
|
+
if (mrmd.findInitialCursorPosition) {
|
|
574
|
+
const pos = mrmd.findInitialCursorPosition(editor.view.state.doc.toString());
|
|
575
|
+
if (pos > 0) {
|
|
576
|
+
editor.view.dispatch({
|
|
577
|
+
selection: { anchor: pos },
|
|
578
|
+
scrollIntoView: true,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Ensure runtime attachment exists (starts monitor if needed)
|
|
562
584
|
try {
|
|
563
|
-
await orchestratorClient.
|
|
585
|
+
await orchestratorClient.createRuntimeAttachment(docToOpen, 'shared');
|
|
564
586
|
} catch (e) {
|
|
565
|
-
console.warn('Failed to create
|
|
587
|
+
console.warn('Failed to create runtime attachment for initial doc:', e);
|
|
566
588
|
}
|
|
567
589
|
} catch (e) {
|
|
568
590
|
console.error('Failed to open initial document:', e);
|
|
@@ -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
|
// ===========================================================================
|
|
@@ -280,53 +325,95 @@ export class OrchestratorClient {
|
|
|
280
325
|
}
|
|
281
326
|
|
|
282
327
|
// ===========================================================================
|
|
283
|
-
//
|
|
328
|
+
// Runtime Attachments
|
|
284
329
|
// ===========================================================================
|
|
285
330
|
|
|
286
331
|
/**
|
|
287
|
-
* Create a
|
|
332
|
+
* Create a runtime attachment for a document
|
|
288
333
|
* @param {string} doc - Document name
|
|
289
334
|
* @param {'shared'|'dedicated'} python - Python runtime mode
|
|
290
335
|
* @param {string} [venv] - Path to virtual environment (for dedicated runtimes)
|
|
291
336
|
* @returns {Promise<Object>}
|
|
292
337
|
*/
|
|
293
|
-
async
|
|
338
|
+
async createRuntimeAttachment(doc, python = 'shared', venv = null) {
|
|
294
339
|
const body = { doc, python };
|
|
295
340
|
if (venv) {
|
|
296
341
|
body.venv = venv;
|
|
297
342
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
return await this._fetch('/api/runtimes', {
|
|
346
|
+
method: 'POST',
|
|
347
|
+
body: JSON.stringify(body),
|
|
348
|
+
});
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// Legacy orchestrator compatibility
|
|
351
|
+
return this._fetch('/api/sessions', {
|
|
352
|
+
method: 'POST',
|
|
353
|
+
body: JSON.stringify(body),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
302
356
|
}
|
|
303
357
|
|
|
304
358
|
/**
|
|
305
|
-
* Get
|
|
359
|
+
* Get runtime attachment info for a document
|
|
306
360
|
* @param {string} doc - Document name
|
|
307
361
|
* @returns {Promise<Object>}
|
|
308
362
|
*/
|
|
309
|
-
async
|
|
310
|
-
|
|
363
|
+
async getRuntimeAttachment(doc) {
|
|
364
|
+
const encoded = encodeURIComponent(doc);
|
|
365
|
+
try {
|
|
366
|
+
return await this._fetch(`/api/runtimes/${encoded}`);
|
|
367
|
+
} catch (err) {
|
|
368
|
+
return this._fetch(`/api/sessions/${encoded}`);
|
|
369
|
+
}
|
|
311
370
|
}
|
|
312
371
|
|
|
313
372
|
/**
|
|
314
|
-
* Destroy a
|
|
373
|
+
* Destroy a runtime attachment
|
|
315
374
|
* @param {string} doc - Document name
|
|
316
375
|
* @returns {Promise<{doc: string, status: string}>}
|
|
317
376
|
*/
|
|
318
|
-
async
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
377
|
+
async destroyRuntimeAttachment(doc) {
|
|
378
|
+
const encoded = encodeURIComponent(doc);
|
|
379
|
+
try {
|
|
380
|
+
return await this._fetch(`/api/runtimes/${encoded}`, {
|
|
381
|
+
method: 'DELETE',
|
|
382
|
+
});
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return this._fetch(`/api/sessions/${encoded}`, {
|
|
385
|
+
method: 'DELETE',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
322
388
|
}
|
|
323
389
|
|
|
324
390
|
/**
|
|
325
|
-
* List all
|
|
326
|
-
* @returns {Promise<{sessions
|
|
391
|
+
* List all runtime attachments
|
|
392
|
+
* @returns {Promise<{runtimes?: Array, sessions?: Array}>}
|
|
327
393
|
*/
|
|
394
|
+
async listRuntimeAttachments() {
|
|
395
|
+
try {
|
|
396
|
+
return await this._fetch('/api/runtimes');
|
|
397
|
+
} catch (err) {
|
|
398
|
+
return this._fetch('/api/sessions');
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Legacy aliases
|
|
403
|
+
async createSession(doc, python = 'shared', venv = null) {
|
|
404
|
+
return this.createRuntimeAttachment(doc, python, venv);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async getSession(doc) {
|
|
408
|
+
return this.getRuntimeAttachment(doc);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async destroySession(doc) {
|
|
412
|
+
return this.destroyRuntimeAttachment(doc);
|
|
413
|
+
}
|
|
414
|
+
|
|
328
415
|
async listSessions() {
|
|
329
|
-
return this.
|
|
416
|
+
return this.listRuntimeAttachments();
|
|
330
417
|
}
|
|
331
418
|
|
|
332
419
|
// ===========================================================================
|