rms-devremote 3.0.0 → 3.1.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/dist/server/frontend.js +177 -2
- package/dist/server/index.js +42 -0
- package/package.json +1 -1
package/dist/server/frontend.js
CHANGED
|
@@ -46,6 +46,14 @@ export function buildFrontendHTML() {
|
|
|
46
46
|
<div class="info-row"><span class="info-label">Uptime</span><span class="info-value" id="info-uptime">--</span></div>
|
|
47
47
|
</div>
|
|
48
48
|
|
|
49
|
+
<!-- Tab bar (tmux windows) -->
|
|
50
|
+
<div id="tab-bar" class="hidden">
|
|
51
|
+
<div id="tabs-scroll">
|
|
52
|
+
<div id="tabs-list"></div>
|
|
53
|
+
<button id="tab-add" title="New window">+</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
49
57
|
<!-- Terminal -->
|
|
50
58
|
<div id="terminal-container" class="hidden"></div>
|
|
51
59
|
|
|
@@ -125,6 +133,7 @@ const CSS = `
|
|
|
125
133
|
--muted: #888888;
|
|
126
134
|
--border: #333333;
|
|
127
135
|
--header-h: 44px;
|
|
136
|
+
--tabbar-h: 38px;
|
|
128
137
|
--input-h: 48px;
|
|
129
138
|
--toolbar-h: 104px;
|
|
130
139
|
--safe-t: env(safe-area-inset-top, 0px);
|
|
@@ -200,7 +209,7 @@ body { touch-action: none; }
|
|
|
200
209
|
/* ── Info panel ──────────────────────────── */
|
|
201
210
|
#info-panel {
|
|
202
211
|
position: fixed;
|
|
203
|
-
top: calc(var(--header-h) + var(--safe-t));
|
|
212
|
+
top: calc(var(--header-h) + var(--tabbar-h) + var(--safe-t));
|
|
204
213
|
left: 0; right: 0;
|
|
205
214
|
background: var(--surface);
|
|
206
215
|
border-bottom: 2px solid var(--border);
|
|
@@ -221,10 +230,76 @@ body { touch-action: none; }
|
|
|
221
230
|
.info-value.err { color: var(--danger); }
|
|
222
231
|
.info-value.info-path { font-size: 12px; max-width: 60%; text-align: right; word-break: break-all; }
|
|
223
232
|
|
|
233
|
+
/* ── Tab bar ────────────────────────────── */
|
|
234
|
+
#tab-bar {
|
|
235
|
+
position: fixed;
|
|
236
|
+
top: calc(var(--header-h) + var(--safe-t));
|
|
237
|
+
left: 0; right: 0;
|
|
238
|
+
height: var(--tabbar-h);
|
|
239
|
+
background: var(--surface);
|
|
240
|
+
border-bottom: 1px solid var(--border);
|
|
241
|
+
z-index: 95;
|
|
242
|
+
display: flex; align-items: center;
|
|
243
|
+
}
|
|
244
|
+
#tab-bar.hidden { display: none !important; }
|
|
245
|
+
#tabs-scroll {
|
|
246
|
+
display: flex; align-items: center;
|
|
247
|
+
overflow-x: auto; overflow-y: hidden;
|
|
248
|
+
width: 100%; height: 100%;
|
|
249
|
+
padding: 0 calc(6px + var(--safe-l)) 0 calc(6px + var(--safe-r));
|
|
250
|
+
gap: 4px;
|
|
251
|
+
scrollbar-width: none;
|
|
252
|
+
-webkit-overflow-scrolling: touch;
|
|
253
|
+
}
|
|
254
|
+
#tabs-scroll::-webkit-scrollbar { display: none; }
|
|
255
|
+
#tabs-list {
|
|
256
|
+
display: flex; align-items: center; gap: 4px;
|
|
257
|
+
flex-shrink: 0;
|
|
258
|
+
}
|
|
259
|
+
.tab-item {
|
|
260
|
+
display: flex; align-items: center; gap: 6px;
|
|
261
|
+
flex-shrink: 0;
|
|
262
|
+
height: 28px; padding: 0 10px;
|
|
263
|
+
background: var(--surface2); border: 1px solid var(--border);
|
|
264
|
+
border-radius: 6px;
|
|
265
|
+
font-size: 12px; font-weight: 600;
|
|
266
|
+
color: var(--muted); cursor: pointer;
|
|
267
|
+
-webkit-tap-highlight-color: transparent;
|
|
268
|
+
transition: background 0.12s, border-color 0.12s, color 0.12s;
|
|
269
|
+
white-space: nowrap;
|
|
270
|
+
}
|
|
271
|
+
.tab-item:active { background: #222; }
|
|
272
|
+
.tab-item.active {
|
|
273
|
+
background: rgba(0,255,170,0.1);
|
|
274
|
+
border-color: var(--accent);
|
|
275
|
+
color: var(--accent);
|
|
276
|
+
}
|
|
277
|
+
.tab-close {
|
|
278
|
+
display: flex; align-items: center; justify-content: center;
|
|
279
|
+
width: 16px; height: 16px;
|
|
280
|
+
font-size: 13px; font-weight: 700;
|
|
281
|
+
color: var(--muted); border: none; background: none;
|
|
282
|
+
border-radius: 3px; cursor: pointer; padding: 0;
|
|
283
|
+
-webkit-tap-highlight-color: transparent;
|
|
284
|
+
line-height: 1;
|
|
285
|
+
}
|
|
286
|
+
.tab-close:active { background: rgba(255,51,85,0.3); color: var(--danger); }
|
|
287
|
+
#tab-add {
|
|
288
|
+
flex-shrink: 0;
|
|
289
|
+
width: 28px; height: 28px;
|
|
290
|
+
background: var(--surface2); border: 1px dashed var(--border);
|
|
291
|
+
border-radius: 6px;
|
|
292
|
+
color: var(--muted); font-size: 18px; font-weight: 400;
|
|
293
|
+
cursor: pointer; display: flex; align-items: center; justify-content: center;
|
|
294
|
+
-webkit-tap-highlight-color: transparent;
|
|
295
|
+
transition: border-color 0.12s, color 0.12s;
|
|
296
|
+
}
|
|
297
|
+
#tab-add:active { border-color: var(--accent); color: var(--accent); }
|
|
298
|
+
|
|
224
299
|
/* ── Terminal ───────────────────────────── */
|
|
225
300
|
#terminal-container {
|
|
226
301
|
position: fixed;
|
|
227
|
-
top: calc(var(--header-h) + var(--safe-t)); left: 0; right: 0;
|
|
302
|
+
top: calc(var(--header-h) + var(--tabbar-h) + var(--safe-t)); left: 0; right: 0;
|
|
228
303
|
bottom: calc(var(--input-h) + var(--toolbar-h) + var(--safe-b));
|
|
229
304
|
background: var(--bg);
|
|
230
305
|
opacity: 0; animation: fadeIn 0.3s 0.2s forwards;
|
|
@@ -366,6 +441,7 @@ body.keyboard-open #terminal-container { bottom: calc(var(--input-h) + var(--saf
|
|
|
366
441
|
@media (orientation: landscape) {
|
|
367
442
|
:root {
|
|
368
443
|
--header-h: 36px;
|
|
444
|
+
--tabbar-h: 32px;
|
|
369
445
|
--input-h: 40px;
|
|
370
446
|
--toolbar-h: 52px;
|
|
371
447
|
}
|
|
@@ -472,6 +548,7 @@ const JS = `
|
|
|
472
548
|
inputBar.classList.remove('hidden');
|
|
473
549
|
toolbar.classList.remove('hidden');
|
|
474
550
|
connected = true;
|
|
551
|
+
startWindowsPoll();
|
|
475
552
|
setTimeout(doResize, 50);
|
|
476
553
|
showGestureHint();
|
|
477
554
|
}
|
|
@@ -480,11 +557,13 @@ const JS = `
|
|
|
480
557
|
container.classList.add('hidden');
|
|
481
558
|
inputBar.classList.add('hidden');
|
|
482
559
|
toolbar.classList.add('hidden');
|
|
560
|
+
tabBar.classList.add('hidden');
|
|
483
561
|
disconnect.classList.remove('hidden');
|
|
484
562
|
dcTitle.textContent = title;
|
|
485
563
|
dcReason.textContent = reason;
|
|
486
564
|
dcIcon.className = 'dc-icon' + (iconClass ? ' ' + iconClass : '');
|
|
487
565
|
connected = false;
|
|
566
|
+
stopWindowsPoll();
|
|
488
567
|
}
|
|
489
568
|
|
|
490
569
|
function connect() {
|
|
@@ -509,6 +588,7 @@ const JS = `
|
|
|
509
588
|
try {
|
|
510
589
|
var msg = JSON.parse(evt.data);
|
|
511
590
|
if (msg.type === 'output') term.write(msg.data);
|
|
591
|
+
else if (msg.type === 'windows-updated') fetchWindows();
|
|
512
592
|
else if (msg.type === 'exit') {
|
|
513
593
|
showDisconnect('Session Ended', 'The tmux session has ended.', 'error');
|
|
514
594
|
setStatus('session ended', false);
|
|
@@ -829,6 +909,101 @@ const JS = `
|
|
|
829
909
|
navigator.serviceWorker.register('/sw.js').catch(function() {});
|
|
830
910
|
}
|
|
831
911
|
|
|
912
|
+
// ── Tab bar (tmux windows) ──────────────────────────────
|
|
913
|
+
var tabBar = document.getElementById('tab-bar');
|
|
914
|
+
var tabsList = document.getElementById('tabs-list');
|
|
915
|
+
var tabAdd = document.getElementById('tab-add');
|
|
916
|
+
var windowsCache = [];
|
|
917
|
+
var windowsPollTimer = null;
|
|
918
|
+
|
|
919
|
+
function fetchWindows() {
|
|
920
|
+
fetch('/windows', { credentials: 'same-origin' })
|
|
921
|
+
.then(function(r) { return r.json(); })
|
|
922
|
+
.then(function(wins) {
|
|
923
|
+
windowsCache = wins;
|
|
924
|
+
renderTabs(wins);
|
|
925
|
+
})
|
|
926
|
+
.catch(function() {});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function renderTabs(wins) {
|
|
930
|
+
// Clear tabs safely (no innerHTML)
|
|
931
|
+
while (tabsList.firstChild) tabsList.removeChild(tabsList.firstChild);
|
|
932
|
+
if (!wins || wins.length === 0) return;
|
|
933
|
+
|
|
934
|
+
// Show tab bar only when connected
|
|
935
|
+
if (connected) tabBar.classList.remove('hidden');
|
|
936
|
+
|
|
937
|
+
wins.forEach(function(w) {
|
|
938
|
+
var tab = document.createElement('div');
|
|
939
|
+
tab.className = 'tab-item' + (w.active ? ' active' : '');
|
|
940
|
+
|
|
941
|
+
var label = document.createElement('span');
|
|
942
|
+
label.textContent = w.index + ':' + (w.name || 'bash');
|
|
943
|
+
tab.appendChild(label);
|
|
944
|
+
|
|
945
|
+
// Close button (only if more than 1 window)
|
|
946
|
+
if (wins.length > 1) {
|
|
947
|
+
var closeBtn = document.createElement('button');
|
|
948
|
+
closeBtn.className = 'tab-close';
|
|
949
|
+
closeBtn.textContent = '\\u00d7';
|
|
950
|
+
closeBtn.setAttribute('tabindex', '-1');
|
|
951
|
+
closeBtn.addEventListener('touchstart', function(e) {
|
|
952
|
+
e.stopPropagation();
|
|
953
|
+
e.preventDefault();
|
|
954
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
955
|
+
sendWsMsg({ type: 'window-close', index: w.index });
|
|
956
|
+
}, { passive: false });
|
|
957
|
+
closeBtn.addEventListener('click', function(e) {
|
|
958
|
+
e.stopPropagation();
|
|
959
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
960
|
+
sendWsMsg({ type: 'window-close', index: w.index });
|
|
961
|
+
});
|
|
962
|
+
tab.appendChild(closeBtn);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Switch on tap
|
|
966
|
+
tab.addEventListener('touchstart', function(e) {
|
|
967
|
+
e.preventDefault();
|
|
968
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
969
|
+
sendWsMsg({ type: 'window-switch', index: w.index });
|
|
970
|
+
}, { passive: false });
|
|
971
|
+
tab.addEventListener('click', function() {
|
|
972
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
973
|
+
sendWsMsg({ type: 'window-switch', index: w.index });
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
tabsList.appendChild(tab);
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// New window button
|
|
981
|
+
tabAdd.addEventListener('touchstart', function(e) {
|
|
982
|
+
e.preventDefault();
|
|
983
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
984
|
+
sendWsMsg({ type: 'window-create' });
|
|
985
|
+
}, { passive: false });
|
|
986
|
+
tabAdd.addEventListener('click', function() {
|
|
987
|
+
if (navigator.vibrate) navigator.vibrate(VIBRATE);
|
|
988
|
+
sendWsMsg({ type: 'window-create' });
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
function sendWsMsg(obj) {
|
|
992
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
993
|
+
ws.send(JSON.stringify(obj));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function startWindowsPoll() {
|
|
998
|
+
stopWindowsPoll();
|
|
999
|
+
fetchWindows();
|
|
1000
|
+
windowsPollTimer = setInterval(fetchWindows, 2000);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function stopWindowsPoll() {
|
|
1004
|
+
if (windowsPollTimer) { clearInterval(windowsPollTimer); windowsPollTimer = null; }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
832
1007
|
// ── Info panel ────────────────────────────────────────────
|
|
833
1008
|
var infoPanel = document.getElementById('info-panel');
|
|
834
1009
|
var infoOpen = false;
|
package/dist/server/index.js
CHANGED
|
@@ -107,6 +107,23 @@ app.get('/', (_req, res) => {
|
|
|
107
107
|
app.get('/health', (_req, res) => {
|
|
108
108
|
res.json({ status: 'ok', uptime: process.uptime() });
|
|
109
109
|
});
|
|
110
|
+
// ── tmux windows list ──────────────────────────────────────────────────
|
|
111
|
+
app.get('/windows', (_req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const raw = execFileSync('tmux', [
|
|
114
|
+
'list-windows', '-t', 'devremote',
|
|
115
|
+
'-F', '#{window_index}\t#{window_name}\t#{window_active}',
|
|
116
|
+
], { encoding: 'utf8', timeout: 3000 }).trim();
|
|
117
|
+
const windows = raw.split('\n').map((line) => {
|
|
118
|
+
const [index, name, active] = line.split('\t');
|
|
119
|
+
return { index: Number(index), name, active: active === '1' };
|
|
120
|
+
});
|
|
121
|
+
res.json(windows);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
res.json([]);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
110
127
|
app.get('/status', (_req, res) => {
|
|
111
128
|
// Get tmux current pane path
|
|
112
129
|
let tmuxPath = '';
|
|
@@ -201,6 +218,31 @@ wss.on('connection', (ws) => {
|
|
|
201
218
|
activeSession.resize(parsed.cols, parsed.rows);
|
|
202
219
|
return;
|
|
203
220
|
}
|
|
221
|
+
// ── Window management (tmux) ──────────────────────────
|
|
222
|
+
if (parsed.type === 'window-create') {
|
|
223
|
+
try {
|
|
224
|
+
execFileSync('tmux', ['new-window', '-t', 'devremote'], { timeout: 3000 });
|
|
225
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
226
|
+
}
|
|
227
|
+
catch { /* ignore */ }
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (parsed.type === 'window-switch' && parsed.index !== undefined) {
|
|
231
|
+
try {
|
|
232
|
+
execFileSync('tmux', ['select-window', '-t', `devremote:${parsed.index}`], { timeout: 3000 });
|
|
233
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
234
|
+
}
|
|
235
|
+
catch { /* ignore */ }
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
if (parsed.type === 'window-close' && parsed.index !== undefined) {
|
|
239
|
+
try {
|
|
240
|
+
execFileSync('tmux', ['kill-window', '-t', `devremote:${parsed.index}`], { timeout: 3000 });
|
|
241
|
+
ws.send(JSON.stringify({ type: 'windows-updated' }));
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
204
246
|
}
|
|
205
247
|
catch {
|
|
206
248
|
// Ignore malformed messages
|