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.
@@ -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;
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rms-devremote",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Control your terminal remotely from your phone — mobile PWA with push notifications and zero open ports",
5
5
  "type": "module",
6
6
  "bin": {