mrmd-editor 0.6.0 → 0.7.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 +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 +4 -10
- package/src/frontmatter-updater.js +224 -0
- package/src/index.js +127 -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 +5 -5
- package/src/shell/dialogs/file-picker.js +167 -6
- package/src/shell/layouts/studio.js +8 -9
- package/src/shell/orchestrator-client.js +60 -18
- package/src/shell/state.js +63 -42
- package/src/shell/styles.js +266 -0
package/src/output-widget.js
CHANGED
|
@@ -2423,6 +2423,60 @@ ${ansiStyles}
|
|
|
2423
2423
|
.cm-stdin-widget[data-password="true"] .cm-stdin-content {
|
|
2424
2424
|
letter-spacing: 0.2em;
|
|
2425
2425
|
}
|
|
2426
|
+
|
|
2427
|
+
/* ==========================================================================
|
|
2428
|
+
MOBILE RESPONSIVE
|
|
2429
|
+
|
|
2430
|
+
Output blocks need to be readable on narrow screens.
|
|
2431
|
+
Horizontal scroll for wide output, larger text for readability.
|
|
2432
|
+
========================================================================== */
|
|
2433
|
+
|
|
2434
|
+
@media (max-width: 768px) {
|
|
2435
|
+
.cm-output-widget {
|
|
2436
|
+
font-size: max(var(--output-font-size, 0.7em), 12px);
|
|
2437
|
+
left: 0; /* No inset on mobile — use full width */
|
|
2438
|
+
right: 0;
|
|
2439
|
+
padding: 8px 12px;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
/* Output content lines: ensure they don't overflow */
|
|
2443
|
+
.cm-output-content-line {
|
|
2444
|
+
font-size: max(var(--output-font-size, 0.8em), 12px);
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
/* Rich output (HTML renders, plots): full width */
|
|
2448
|
+
.cm-output-rich-widget {
|
|
2449
|
+
max-width: 100%;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
.cm-output-rich-widget iframe {
|
|
2453
|
+
max-width: 100%;
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
/* Stdin widget: bigger input on mobile */
|
|
2457
|
+
.cm-stdin-widget .cm-stdin-input {
|
|
2458
|
+
font-size: 16px; /* Prevents iOS zoom */
|
|
2459
|
+
padding: 10px;
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
.cm-stdin-widget .cm-stdin-prompt {
|
|
2463
|
+
font-size: 14px;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
@media (pointer: coarse) {
|
|
2468
|
+
/* Output container: larger tap target for focus/interaction */
|
|
2469
|
+
.cm-output-widget {
|
|
2470
|
+
padding: 10px 14px;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/* Collapsed output: easy to tap to expand */
|
|
2474
|
+
.cm-output-collapsed {
|
|
2475
|
+
min-height: 44px;
|
|
2476
|
+
display: flex;
|
|
2477
|
+
align-items: center;
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2426
2480
|
`;
|
|
2427
2481
|
|
|
2428
2482
|
// #endregion STYLES
|
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
* const extensions = createRuntimeCodeLensExtensions({
|
|
12
12
|
* projectName: 'my-project',
|
|
13
13
|
* getSessionStatus: (name) => shellState.get(`runtimes.sessions.${name}`),
|
|
14
|
-
* onStart: async (runtime) => { await electronAPI.
|
|
15
|
-
* onStop: async (name) => { await electronAPI.
|
|
16
|
-
* onRestart: async (name) => { await electronAPI.
|
|
14
|
+
* onStart: async (runtime) => { await electronAPI.runtime.start(runtime); },
|
|
15
|
+
* onStop: async (name) => { await electronAPI.runtime.stop(name); },
|
|
16
|
+
* onRestart: async (name) => { await electronAPI.runtime.restart(name); },
|
|
17
17
|
* onRestartAll: async () => { ... },
|
|
18
18
|
* });
|
|
19
19
|
* ```
|
package/src/shell/ai-menu.js
CHANGED
|
@@ -414,6 +414,76 @@ export const AI_MENU_STYLES = `
|
|
|
414
414
|
color: #808080;
|
|
415
415
|
font-style: italic;
|
|
416
416
|
}
|
|
417
|
+
|
|
418
|
+
/* Mobile: AI menu becomes a bottom sheet */
|
|
419
|
+
@media (max-width: 768px) {
|
|
420
|
+
.ai-menu {
|
|
421
|
+
position: fixed !important;
|
|
422
|
+
left: 0 !important;
|
|
423
|
+
right: 0 !important;
|
|
424
|
+
bottom: 0 !important;
|
|
425
|
+
top: auto !important;
|
|
426
|
+
min-width: 100%;
|
|
427
|
+
max-width: 100%;
|
|
428
|
+
border-radius: 16px 16px 0 0;
|
|
429
|
+
border: none;
|
|
430
|
+
border-top: 1px solid #3c3c3c;
|
|
431
|
+
box-shadow: 0 -4px 32px rgba(0, 0, 0, 0.4);
|
|
432
|
+
animation: ai-menu-slide-up 0.25s cubic-bezier(0.4, 0, 0.2, 1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@keyframes ai-menu-slide-up {
|
|
436
|
+
from { transform: translateY(100%); }
|
|
437
|
+
to { transform: translateY(0); }
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
.ai-menu-header {
|
|
441
|
+
padding: 16px 20px;
|
|
442
|
+
position: relative;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.ai-menu-header::before {
|
|
446
|
+
content: '';
|
|
447
|
+
position: absolute;
|
|
448
|
+
top: 8px;
|
|
449
|
+
left: 50%;
|
|
450
|
+
transform: translateX(-50%);
|
|
451
|
+
width: 36px;
|
|
452
|
+
height: 4px;
|
|
453
|
+
background: #666;
|
|
454
|
+
border-radius: 2px;
|
|
455
|
+
opacity: 0.4;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
.ai-menu-title {
|
|
459
|
+
font-size: 16px;
|
|
460
|
+
padding-top: 4px;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
.ai-menu-juice {
|
|
464
|
+
font-size: 14px;
|
|
465
|
+
padding: 6px 12px;
|
|
466
|
+
border-radius: 8px;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
.ai-menu-commands {
|
|
470
|
+
max-height: 50vh;
|
|
471
|
+
padding: 8px 0;
|
|
472
|
+
padding-bottom: calc(8px + env(safe-area-inset-bottom, 0px));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.ai-menu-item {
|
|
476
|
+
padding: 14px 20px;
|
|
477
|
+
min-height: 48px;
|
|
478
|
+
font-size: 16px;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
.ai-menu-item-icon {
|
|
482
|
+
width: 28px;
|
|
483
|
+
font-size: 18px;
|
|
484
|
+
margin-right: 12px;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
417
487
|
`;
|
|
418
488
|
|
|
419
489
|
/**
|
|
@@ -55,8 +55,21 @@ export function createMenu(options) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Detect mobile/narrow viewport
|
|
59
|
+
const isMobile = () => window.matchMedia('(max-width: 768px)').matches;
|
|
60
|
+
|
|
61
|
+
// Position menu relative to anchor (desktop) or as bottom sheet (mobile)
|
|
59
62
|
function updatePosition() {
|
|
63
|
+
// On mobile, CSS handles the positioning (bottom sheet via @media queries).
|
|
64
|
+
// We just need to not set inline position styles that would conflict.
|
|
65
|
+
if (isMobile()) {
|
|
66
|
+
menu.style.top = '';
|
|
67
|
+
menu.style.left = '';
|
|
68
|
+
menu.style.right = '';
|
|
69
|
+
menu.style.bottom = '';
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
const anchorRect = anchor.getBoundingClientRect();
|
|
61
74
|
const menuRect = menu.getBoundingClientRect();
|
|
62
75
|
|
|
@@ -96,10 +109,18 @@ export function createMenu(options) {
|
|
|
96
109
|
menu.style.left = `${left}px`;
|
|
97
110
|
}
|
|
98
111
|
|
|
112
|
+
// Mobile scrim backdrop (for bottom sheet pattern)
|
|
113
|
+
let scrim = null;
|
|
114
|
+
|
|
99
115
|
// Close menu
|
|
100
116
|
function close() {
|
|
101
117
|
menu.remove();
|
|
118
|
+
if (scrim) {
|
|
119
|
+
scrim.remove();
|
|
120
|
+
scrim = null;
|
|
121
|
+
}
|
|
102
122
|
document.removeEventListener('mousedown', handleOutsideClick);
|
|
123
|
+
document.removeEventListener('touchstart', handleOutsideClick);
|
|
103
124
|
document.removeEventListener('keydown', handleKeydown);
|
|
104
125
|
onClose?.();
|
|
105
126
|
}
|
|
@@ -120,11 +141,28 @@ export function createMenu(options) {
|
|
|
120
141
|
|
|
121
142
|
// Initialize
|
|
122
143
|
render(items);
|
|
144
|
+
|
|
145
|
+
// On mobile, add a scrim backdrop behind the bottom sheet
|
|
146
|
+
if (isMobile()) {
|
|
147
|
+
scrim = document.createElement('div');
|
|
148
|
+
scrim.style.cssText = `
|
|
149
|
+
position: fixed;
|
|
150
|
+
inset: 0;
|
|
151
|
+
background: rgba(0, 0, 0, 0.4);
|
|
152
|
+
z-index: 999;
|
|
153
|
+
animation: mrmd-fade-in 0.15s ease;
|
|
154
|
+
`;
|
|
155
|
+
scrim.addEventListener('click', close);
|
|
156
|
+
scrim.addEventListener('touchstart', close);
|
|
157
|
+
document.body.appendChild(scrim);
|
|
158
|
+
}
|
|
159
|
+
|
|
123
160
|
document.body.appendChild(menu);
|
|
124
161
|
updatePosition();
|
|
125
162
|
|
|
126
163
|
// Add event listeners
|
|
127
164
|
document.addEventListener('mousedown', handleOutsideClick);
|
|
165
|
+
document.addEventListener('touchstart', handleOutsideClick);
|
|
128
166
|
document.addEventListener('keydown', handleKeydown);
|
|
129
167
|
|
|
130
168
|
return {
|
|
@@ -1402,7 +1402,7 @@ function createRuntimesSegment({ shellState, orchestratorClient, handlers, onCle
|
|
|
1402
1402
|
let runtimeCount = 0;
|
|
1403
1403
|
if (python?.running || python?.status === 'ready') runtimeCount++;
|
|
1404
1404
|
|
|
1405
|
-
const sessions = shellState.
|
|
1405
|
+
const sessions = shellState.getRuntimes();
|
|
1406
1406
|
const dedicatedCount = sessions.filter(s => s.info?.dedicated).length;
|
|
1407
1407
|
runtimeCount += dedicatedCount;
|
|
1408
1408
|
|
|
@@ -1650,7 +1650,7 @@ function createRuntimesSegment({ shellState, orchestratorClient, handlers, onCle
|
|
|
1650
1650
|
items.push({ type: 'divider' });
|
|
1651
1651
|
items.push({
|
|
1652
1652
|
type: 'header',
|
|
1653
|
-
label: `Active
|
|
1653
|
+
label: `Active Runtime Attachments (${runtimes.sessions.length})`,
|
|
1654
1654
|
});
|
|
1655
1655
|
|
|
1656
1656
|
for (const session of runtimes.sessions) {
|
|
@@ -1671,16 +1671,16 @@ function createRuntimesSegment({ shellState, orchestratorClient, handlers, onCle
|
|
|
1671
1671
|
});
|
|
1672
1672
|
items.push({
|
|
1673
1673
|
icon: '✖',
|
|
1674
|
-
label: `
|
|
1674
|
+
label: `Detach runtime from "${session.doc}"`,
|
|
1675
1675
|
description: 'Stops monitor',
|
|
1676
1676
|
onClick: async () => {
|
|
1677
1677
|
try {
|
|
1678
|
-
await orchestratorClient.
|
|
1678
|
+
await orchestratorClient.destroyRuntimeAttachment(session.doc);
|
|
1679
1679
|
cachedRuntimes = null;
|
|
1680
1680
|
lastFetchTime = 0;
|
|
1681
1681
|
render();
|
|
1682
1682
|
} catch (err) {
|
|
1683
|
-
console.error('Failed to
|
|
1683
|
+
console.error('Failed to detach runtime attachment:', err);
|
|
1684
1684
|
}
|
|
1685
1685
|
},
|
|
1686
1686
|
});
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import { createDialog } from './base-dialog.js';
|
|
9
9
|
|
|
10
|
+
const FILE_PICKER_HISTORY_KEY = 'mrmd:file-picker-history:v1';
|
|
11
|
+
const FILE_PICKER_HISTORY_LIMIT = 400;
|
|
12
|
+
|
|
10
13
|
// =============================================================================
|
|
11
14
|
// FILE PICKER
|
|
12
15
|
// =============================================================================
|
|
@@ -43,6 +46,7 @@ export function showFilePicker(options) {
|
|
|
43
46
|
let entries = [];
|
|
44
47
|
let selectedEntry = null;
|
|
45
48
|
let isLoading = false;
|
|
49
|
+
let pickerHistory = loadFilePickerHistory();
|
|
46
50
|
|
|
47
51
|
// Create content
|
|
48
52
|
const content = document.createElement('div');
|
|
@@ -139,10 +143,20 @@ export function showFilePicker(options) {
|
|
|
139
143
|
return;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
// Sort: directories first, then
|
|
146
|
+
// Sort: directories first, then recency/frequency score, then name
|
|
147
|
+
const nowMs = Date.now();
|
|
143
148
|
const sorted = [...entries].sort((a, b) => {
|
|
144
149
|
if (a.type === 'directory' && b.type !== 'directory') return -1;
|
|
145
150
|
if (a.type !== 'directory' && b.type === 'directory') return 1;
|
|
151
|
+
|
|
152
|
+
const aScore = getHistoryScore(pickerHistory.get(a.path), nowMs);
|
|
153
|
+
const bScore = getHistoryScore(pickerHistory.get(b.path), nowMs);
|
|
154
|
+
if (aScore !== bScore) return bScore - aScore;
|
|
155
|
+
|
|
156
|
+
const aModified = Number(a.modified || 0);
|
|
157
|
+
const bModified = Number(b.modified || 0);
|
|
158
|
+
if (aModified !== bModified) return bModified - aModified;
|
|
159
|
+
|
|
146
160
|
return a.name.localeCompare(b.name);
|
|
147
161
|
});
|
|
148
162
|
|
|
@@ -164,11 +178,26 @@ export function showFilePicker(options) {
|
|
|
164
178
|
name.textContent = entry.name;
|
|
165
179
|
item.appendChild(name);
|
|
166
180
|
|
|
167
|
-
if (entry.type === 'file'
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
181
|
+
if (entry.type === 'file') {
|
|
182
|
+
const infoParts = [];
|
|
183
|
+
if (entry.size !== undefined) {
|
|
184
|
+
infoParts.push(formatSize(entry.size));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const history = pickerHistory.get(entry.path);
|
|
188
|
+
if (history?.lastOpened) {
|
|
189
|
+
infoParts.push(formatTimeAgo(history.lastOpened));
|
|
190
|
+
}
|
|
191
|
+
if ((history?.count || 0) > 1) {
|
|
192
|
+
infoParts.push(`${Math.round(history.count)}x`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (infoParts.length > 0) {
|
|
196
|
+
const info = document.createElement('span');
|
|
197
|
+
info.className = 'mrmd-filepicker__item-info';
|
|
198
|
+
info.textContent = infoParts.join(' • ');
|
|
199
|
+
item.appendChild(info);
|
|
200
|
+
}
|
|
172
201
|
}
|
|
173
202
|
|
|
174
203
|
// Click handler
|
|
@@ -206,6 +235,13 @@ export function showFilePicker(options) {
|
|
|
206
235
|
const result = await orchestratorClient.browse({ path, type: 'all' });
|
|
207
236
|
currentPath = result.path;
|
|
208
237
|
entries = result.entries;
|
|
238
|
+
|
|
239
|
+
// Track directory navigation with a lighter weight than file opens
|
|
240
|
+
pickerHistory = touchFilePickerHistory(pickerHistory, currentPath, {
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
weight: 0.25,
|
|
243
|
+
});
|
|
244
|
+
saveFilePickerHistory(pickerHistory);
|
|
209
245
|
} catch (error) {
|
|
210
246
|
console.error('Failed to browse:', error);
|
|
211
247
|
entries = [];
|
|
@@ -218,6 +254,12 @@ export function showFilePicker(options) {
|
|
|
218
254
|
|
|
219
255
|
// Select a file
|
|
220
256
|
function selectFile(path) {
|
|
257
|
+
pickerHistory = touchFilePickerHistory(pickerHistory, path, {
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
weight: 1,
|
|
260
|
+
});
|
|
261
|
+
saveFilePickerHistory(pickerHistory);
|
|
262
|
+
|
|
221
263
|
dialog.close();
|
|
222
264
|
onSelect(path);
|
|
223
265
|
}
|
|
@@ -449,3 +491,122 @@ function formatSize(bytes) {
|
|
|
449
491
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
450
492
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
451
493
|
}
|
|
494
|
+
|
|
495
|
+
function formatTimeAgo(value) {
|
|
496
|
+
if (!value) return '';
|
|
497
|
+
const ts = Date.parse(value);
|
|
498
|
+
if (!Number.isFinite(ts)) return '';
|
|
499
|
+
|
|
500
|
+
const diffMs = Math.max(0, Date.now() - ts);
|
|
501
|
+
const mins = Math.floor(diffMs / 60000);
|
|
502
|
+
if (mins < 1) return 'just now';
|
|
503
|
+
if (mins < 60) return `${mins}m ago`;
|
|
504
|
+
|
|
505
|
+
const hours = Math.floor(mins / 60);
|
|
506
|
+
if (hours < 24) return `${hours}h ago`;
|
|
507
|
+
|
|
508
|
+
const days = Math.floor(hours / 24);
|
|
509
|
+
if (days < 30) return `${days}d ago`;
|
|
510
|
+
|
|
511
|
+
const months = Math.floor(days / 30);
|
|
512
|
+
return `${months}mo ago`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function getStorage() {
|
|
516
|
+
if (typeof window === 'undefined') return null;
|
|
517
|
+
try {
|
|
518
|
+
return window.localStorage || null;
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function loadFilePickerHistory() {
|
|
525
|
+
const storage = getStorage();
|
|
526
|
+
if (!storage) return new Map();
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const raw = storage.getItem(FILE_PICKER_HISTORY_KEY);
|
|
530
|
+
if (!raw) return new Map();
|
|
531
|
+
|
|
532
|
+
const parsed = JSON.parse(raw);
|
|
533
|
+
if (!parsed || typeof parsed !== 'object') return new Map();
|
|
534
|
+
|
|
535
|
+
const byPath = new Map();
|
|
536
|
+
for (const [filePath, value] of Object.entries(parsed)) {
|
|
537
|
+
if (!filePath || !value || typeof value !== 'object') continue;
|
|
538
|
+
|
|
539
|
+
const count = Number(value.count || 0);
|
|
540
|
+
byPath.set(filePath, {
|
|
541
|
+
path: filePath,
|
|
542
|
+
lastOpened: typeof value.lastOpened === 'string' ? value.lastOpened : null,
|
|
543
|
+
count: Number.isFinite(count) && count > 0 ? count : 1,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
return byPath;
|
|
548
|
+
} catch {
|
|
549
|
+
return new Map();
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function saveFilePickerHistory(historyMap) {
|
|
554
|
+
const storage = getStorage();
|
|
555
|
+
if (!storage) return;
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const items = Array.from(historyMap.values())
|
|
559
|
+
.filter(item => item?.path)
|
|
560
|
+
.sort((a, b) => {
|
|
561
|
+
const aTs = Date.parse(a.lastOpened || 0) || 0;
|
|
562
|
+
const bTs = Date.parse(b.lastOpened || 0) || 0;
|
|
563
|
+
if (aTs !== bTs) return bTs - aTs;
|
|
564
|
+
return (b.count || 0) - (a.count || 0);
|
|
565
|
+
})
|
|
566
|
+
.slice(0, FILE_PICKER_HISTORY_LIMIT);
|
|
567
|
+
|
|
568
|
+
const serialized = {};
|
|
569
|
+
for (const item of items) {
|
|
570
|
+
serialized[item.path] = {
|
|
571
|
+
lastOpened: item.lastOpened || null,
|
|
572
|
+
count: item.count || 1,
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
storage.setItem(FILE_PICKER_HISTORY_KEY, JSON.stringify(serialized));
|
|
577
|
+
} catch {
|
|
578
|
+
// Best-effort persistence only
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function touchFilePickerHistory(historyMap, filePath, { timestamp, weight = 1 } = {}) {
|
|
583
|
+
if (!filePath) return historyMap;
|
|
584
|
+
|
|
585
|
+
const next = new Map(historyMap);
|
|
586
|
+
const existing = next.get(filePath);
|
|
587
|
+
const nextCount = Math.max(0.1, Number(existing?.count || 0) + Math.max(0.1, Number(weight) || 0.1));
|
|
588
|
+
|
|
589
|
+
next.set(filePath, {
|
|
590
|
+
path: filePath,
|
|
591
|
+
lastOpened: timestamp || new Date().toISOString(),
|
|
592
|
+
count: nextCount,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
return next;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function getHistoryScore(historyEntry, nowMs = Date.now()) {
|
|
599
|
+
if (!historyEntry) return 0;
|
|
600
|
+
|
|
601
|
+
const count = Math.max(1, Number(historyEntry.count) || 1);
|
|
602
|
+
const openedMs = Date.parse(historyEntry.lastOpened || '');
|
|
603
|
+
|
|
604
|
+
let recencyScore = 0;
|
|
605
|
+
if (Number.isFinite(openedMs)) {
|
|
606
|
+
const ageHours = Math.max(0, (nowMs - openedMs) / 3600000);
|
|
607
|
+
recencyScore = Math.max(0, 220 - Math.log2(ageHours + 1) * 36);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const frequencyScore = Math.min(180, Math.log2(count + 1) * 60);
|
|
611
|
+
return recencyScore + frequencyScore;
|
|
612
|
+
}
|
|
@@ -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,11 @@ export async function createStudio(target, options = {}) {
|
|
|
519
518
|
editor = createEditorForDocument(handle, normalizedName);
|
|
520
519
|
currentDocName = normalizedName;
|
|
521
520
|
|
|
522
|
-
// Ensure
|
|
521
|
+
// Ensure runtime attachment exists (starts monitor if needed)
|
|
523
522
|
try {
|
|
524
|
-
await orchestratorClient.
|
|
523
|
+
await orchestratorClient.createRuntimeAttachment(normalizedName, 'shared');
|
|
525
524
|
} catch (e) {
|
|
526
|
-
console.warn('Failed to create
|
|
525
|
+
console.warn('Failed to create runtime attachment for', normalizedName, e);
|
|
527
526
|
}
|
|
528
527
|
|
|
529
528
|
// Update shell state
|
|
@@ -558,11 +557,11 @@ export async function createStudio(target, options = {}) {
|
|
|
558
557
|
editor = createEditorForDocument(handle, docToOpen);
|
|
559
558
|
currentDocName = docToOpen;
|
|
560
559
|
|
|
561
|
-
// Ensure
|
|
560
|
+
// Ensure runtime attachment exists (starts monitor if needed)
|
|
562
561
|
try {
|
|
563
|
-
await orchestratorClient.
|
|
562
|
+
await orchestratorClient.createRuntimeAttachment(docToOpen, 'shared');
|
|
564
563
|
} catch (e) {
|
|
565
|
-
console.warn('Failed to create
|
|
564
|
+
console.warn('Failed to create runtime attachment for initial doc:', e);
|
|
566
565
|
}
|
|
567
566
|
} catch (e) {
|
|
568
567
|
console.error('Failed to open initial document:', e);
|
|
@@ -280,53 +280,95 @@ export class OrchestratorClient {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
// ===========================================================================
|
|
283
|
-
//
|
|
283
|
+
// Runtime Attachments
|
|
284
284
|
// ===========================================================================
|
|
285
285
|
|
|
286
286
|
/**
|
|
287
|
-
* Create a
|
|
287
|
+
* Create a runtime attachment for a document
|
|
288
288
|
* @param {string} doc - Document name
|
|
289
289
|
* @param {'shared'|'dedicated'} python - Python runtime mode
|
|
290
290
|
* @param {string} [venv] - Path to virtual environment (for dedicated runtimes)
|
|
291
291
|
* @returns {Promise<Object>}
|
|
292
292
|
*/
|
|
293
|
-
async
|
|
293
|
+
async createRuntimeAttachment(doc, python = 'shared', venv = null) {
|
|
294
294
|
const body = { doc, python };
|
|
295
295
|
if (venv) {
|
|
296
296
|
body.venv = venv;
|
|
297
297
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
return await this._fetch('/api/runtimes', {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
body: JSON.stringify(body),
|
|
303
|
+
});
|
|
304
|
+
} catch (err) {
|
|
305
|
+
// Legacy orchestrator compatibility
|
|
306
|
+
return this._fetch('/api/sessions', {
|
|
307
|
+
method: 'POST',
|
|
308
|
+
body: JSON.stringify(body),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
302
311
|
}
|
|
303
312
|
|
|
304
313
|
/**
|
|
305
|
-
* Get
|
|
314
|
+
* Get runtime attachment info for a document
|
|
306
315
|
* @param {string} doc - Document name
|
|
307
316
|
* @returns {Promise<Object>}
|
|
308
317
|
*/
|
|
309
|
-
async
|
|
310
|
-
|
|
318
|
+
async getRuntimeAttachment(doc) {
|
|
319
|
+
const encoded = encodeURIComponent(doc);
|
|
320
|
+
try {
|
|
321
|
+
return await this._fetch(`/api/runtimes/${encoded}`);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return this._fetch(`/api/sessions/${encoded}`);
|
|
324
|
+
}
|
|
311
325
|
}
|
|
312
326
|
|
|
313
327
|
/**
|
|
314
|
-
* Destroy a
|
|
328
|
+
* Destroy a runtime attachment
|
|
315
329
|
* @param {string} doc - Document name
|
|
316
330
|
* @returns {Promise<{doc: string, status: string}>}
|
|
317
331
|
*/
|
|
318
|
-
async
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
332
|
+
async destroyRuntimeAttachment(doc) {
|
|
333
|
+
const encoded = encodeURIComponent(doc);
|
|
334
|
+
try {
|
|
335
|
+
return await this._fetch(`/api/runtimes/${encoded}`, {
|
|
336
|
+
method: 'DELETE',
|
|
337
|
+
});
|
|
338
|
+
} catch (err) {
|
|
339
|
+
return this._fetch(`/api/sessions/${encoded}`, {
|
|
340
|
+
method: 'DELETE',
|
|
341
|
+
});
|
|
342
|
+
}
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
/**
|
|
325
|
-
* List all
|
|
326
|
-
* @returns {Promise<{sessions
|
|
346
|
+
* List all runtime attachments
|
|
347
|
+
* @returns {Promise<{runtimes?: Array, sessions?: Array}>}
|
|
327
348
|
*/
|
|
349
|
+
async listRuntimeAttachments() {
|
|
350
|
+
try {
|
|
351
|
+
return await this._fetch('/api/runtimes');
|
|
352
|
+
} catch (err) {
|
|
353
|
+
return this._fetch('/api/sessions');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Legacy aliases
|
|
358
|
+
async createSession(doc, python = 'shared', venv = null) {
|
|
359
|
+
return this.createRuntimeAttachment(doc, python, venv);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async getSession(doc) {
|
|
363
|
+
return this.getRuntimeAttachment(doc);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async destroySession(doc) {
|
|
367
|
+
return this.destroyRuntimeAttachment(doc);
|
|
368
|
+
}
|
|
369
|
+
|
|
328
370
|
async listSessions() {
|
|
329
|
-
return this.
|
|
371
|
+
return this.listRuntimeAttachments();
|
|
330
372
|
}
|
|
331
373
|
|
|
332
374
|
// ===========================================================================
|