webtun 1.5.2 → 1.5.3
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/README.md +5 -1
- package/package.json +1 -1
- package/public/index.html +5 -2
- package/server.js +86 -21
package/README.md
CHANGED
|
@@ -68,7 +68,7 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
68
68
|
- **Multi-tab** — drag to reorder, side-by-side sessions
|
|
69
69
|
- **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop)
|
|
70
70
|
- **Command history** — keystroke-based capture, strips ANSI/control sequences
|
|
71
|
-
- **
|
|
71
|
+
- **Session persistence** — tmux or in-memory PTY; tabs survive page reload (all platforms)
|
|
72
72
|
- **Keyboard Shortcuts** — reference dialog in the overflow menu (Ctrl+P/T/W/B/F, etc.)
|
|
73
73
|
|
|
74
74
|
### File Explorer
|
|
@@ -175,6 +175,10 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
175
175
|
|
|
176
176
|
## Changelog
|
|
177
177
|
|
|
178
|
+
### v1.5.3
|
|
179
|
+
- Terminal session persistence without tmux (in-memory PTY on Windows + Linux)
|
|
180
|
+
- Session IDs always generated; tabs now survive page reload on all platforms
|
|
181
|
+
|
|
178
182
|
### v1.5.2
|
|
179
183
|
- UI/UX audit fixes: theme-aware xterm selection color, `--fg3` across all 6 themes
|
|
180
184
|
- Replaced native `confirm()` with a theme-aware, focus-trapped dialog
|
package/package.json
CHANGED
package/public/index.html
CHANGED
|
@@ -2068,7 +2068,7 @@ function hideTermLoading(tab) {
|
|
|
2068
2068
|
|
|
2069
2069
|
function newTab(title, sessionId, dir) {
|
|
2070
2070
|
const id = ++tabCounter;
|
|
2071
|
-
const sid = sessionId ||
|
|
2071
|
+
const sid = sessionId || uuid();
|
|
2072
2072
|
const tab = { id, sessionId: sid, title: title || `Term ${nextTermNumber()}`, term: null, fitAddon: null, searchAddon: null, ws: null, el: null, wrapper: null, closed: false, reconnectDelay: 1000, dataDisposable: null, resizeDisposable: null, resizeObserver: null, cwd: dir || currentPath };
|
|
2073
2073
|
tabs.push(tab);
|
|
2074
2074
|
createTabButton(tab);
|
|
@@ -2348,7 +2348,7 @@ async function closeTab(e, id) {
|
|
|
2348
2348
|
tab.term?.dispose();
|
|
2349
2349
|
tab.el?.remove();
|
|
2350
2350
|
tab.wrapper?.remove();
|
|
2351
|
-
// Kill the backing tmux
|
|
2351
|
+
// Kill the backing session (tmux or in-memory) so it doesn't pile up
|
|
2352
2352
|
if (tab.sessionId) api(`/api/sessions/${tab.sessionId}`, { method: 'DELETE' });
|
|
2353
2353
|
tabs = tabs.filter(t => t.id !== id);
|
|
2354
2354
|
saveTabState();
|
|
@@ -5387,6 +5387,9 @@ function setupDragDrop() {
|
|
|
5387
5387
|
const traversed = await traverseDirectoryEntry(entry);
|
|
5388
5388
|
filesToUpload.push(...traversed);
|
|
5389
5389
|
} catch(e) { console.warn(e); }
|
|
5390
|
+
} else {
|
|
5391
|
+
const file = item.getAsFile();
|
|
5392
|
+
if (file) filesToUpload.push({ file, path: file.name });
|
|
5390
5393
|
}
|
|
5391
5394
|
}
|
|
5392
5395
|
}
|
package/server.js
CHANGED
|
@@ -1272,9 +1272,12 @@ app.delete('/api/history/:index', checkPin, (req, res) => {
|
|
|
1272
1272
|
res.json({ success: true, history: cmdHistory });
|
|
1273
1273
|
});
|
|
1274
1274
|
|
|
1275
|
-
// ── Session persistence
|
|
1275
|
+
// ── Session persistence ──────────────────────────────────────────────
|
|
1276
1276
|
const TMUX = (() => { try { return execSync('command -v tmux', { stdio: ['ignore','pipe','ignore'] }).toString().trim(); } catch { return null; } })();
|
|
1277
1277
|
|
|
1278
|
+
// In-memory PTY session store — enables persistence without tmux (Windows + Linux)
|
|
1279
|
+
const ptySessions = new Map(); // sessionId -> { proc, drainCheck, createdAt }
|
|
1280
|
+
|
|
1278
1281
|
function isValidPID(pid) {
|
|
1279
1282
|
return typeof pid === 'number' && Number.isInteger(pid) && pid > 0;
|
|
1280
1283
|
}
|
|
@@ -1301,22 +1304,38 @@ function tmuxSessionExists(name) {
|
|
|
1301
1304
|
}
|
|
1302
1305
|
|
|
1303
1306
|
app.get('/api/sessions', checkPin, (req, res) => {
|
|
1304
|
-
if (
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1307
|
+
if (TMUX) {
|
|
1308
|
+
try {
|
|
1309
|
+
const out = execFileSync(TMUX, ['list-sessions', '-F', '#{session_name}'], { encoding: 'utf8' }).trim();
|
|
1310
|
+
const sessions = out.split('\n')
|
|
1311
|
+
.filter(s => s.startsWith('wt-'))
|
|
1312
|
+
.map(s => ({ id: s.replace(/^wt-/, ''), name: s }));
|
|
1313
|
+
return res.json({ tmux: true, sessions });
|
|
1314
|
+
} catch {
|
|
1315
|
+
return res.json({ tmux: true, sessions: [] });
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
// In-memory sessions (no tmux)
|
|
1319
|
+
const sessions = [];
|
|
1320
|
+
for (const [id] of ptySessions) {
|
|
1321
|
+
sessions.push({ id, name: 'wt-' + id });
|
|
1313
1322
|
}
|
|
1323
|
+
res.json({ tmux: false, sessions });
|
|
1314
1324
|
});
|
|
1315
1325
|
|
|
1316
1326
|
app.delete('/api/sessions/:id', checkPin, (req, res) => {
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1327
|
+
const id = req.params.id.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
1328
|
+
if (TMUX) {
|
|
1329
|
+
const name = 'wt-' + id;
|
|
1330
|
+
try { execFileSync(TMUX, ['kill-session', '-t', name], { stdio: 'ignore' }); } catch {}
|
|
1331
|
+
return res.json({ success: true });
|
|
1332
|
+
}
|
|
1333
|
+
// In-memory session
|
|
1334
|
+
const entry = ptySessions.get(id);
|
|
1335
|
+
if (entry) {
|
|
1336
|
+
try { entry.proc.kill(); } catch {}
|
|
1337
|
+
ptySessions.delete(id);
|
|
1338
|
+
}
|
|
1320
1339
|
res.json({ success: true });
|
|
1321
1340
|
});
|
|
1322
1341
|
|
|
@@ -1378,8 +1397,28 @@ wss.on('connection', (ws, req) => {
|
|
|
1378
1397
|
};
|
|
1379
1398
|
|
|
1380
1399
|
let proc;
|
|
1400
|
+
let reattached = false;
|
|
1381
1401
|
try {
|
|
1382
|
-
if (
|
|
1402
|
+
if (sessionId && !TMUX) {
|
|
1403
|
+
// ── In-memory PTY persistence (no tmux needed) ──
|
|
1404
|
+
const existing = ptySessions.get(sessionId);
|
|
1405
|
+
if (existing && existing.proc && !existing.exited) {
|
|
1406
|
+
// Reattach: remove old listeners, reuse the running PTY
|
|
1407
|
+
proc = existing.proc;
|
|
1408
|
+
proc.removeAllListeners('data');
|
|
1409
|
+
proc.removeAllListeners('exit');
|
|
1410
|
+
proc.resize(cols, rows);
|
|
1411
|
+
reattached = true;
|
|
1412
|
+
} else {
|
|
1413
|
+
// New in-memory session
|
|
1414
|
+
if (existing) ptySessions.delete(sessionId);
|
|
1415
|
+
const shellArgs = os.platform() === 'win32' ? ['-NoLogo'] : ['-l'];
|
|
1416
|
+
proc = pty.spawn(SHELL, shellArgs, {
|
|
1417
|
+
name: 'xterm-256color', cols, rows, cwd,
|
|
1418
|
+
env: sessionEnv
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
} else if (TMUX && sessionId) {
|
|
1383
1422
|
const tmuxName = 'wt-' + sessionId;
|
|
1384
1423
|
const exists = tmuxSessionExists(tmuxName);
|
|
1385
1424
|
|
|
@@ -1413,6 +1452,12 @@ wss.on('connection', (ws, req) => {
|
|
|
1413
1452
|
const HIGH_WATER = 4 * 1024 * 1024; // 4MB — pause PTY above this
|
|
1414
1453
|
const LOW_WATER = 1 * 1024 * 1024; // 1MB — resume PTY below this
|
|
1415
1454
|
|
|
1455
|
+
const drainCheck = setInterval(() => {
|
|
1456
|
+
if (paused && ws.bufferedAmount < LOW_WATER) {
|
|
1457
|
+
try { proc.resume(); paused = false; } catch (_) {}
|
|
1458
|
+
}
|
|
1459
|
+
}, 50);
|
|
1460
|
+
|
|
1416
1461
|
proc.onData(data => {
|
|
1417
1462
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
1418
1463
|
send(0x00, data);
|
|
@@ -1422,19 +1467,29 @@ wss.on('connection', (ws, req) => {
|
|
|
1422
1467
|
}
|
|
1423
1468
|
});
|
|
1424
1469
|
|
|
1425
|
-
|
|
1426
|
-
const
|
|
1427
|
-
if (paused && ws.bufferedAmount < LOW_WATER) {
|
|
1428
|
-
try { proc.resume(); paused = false; } catch (_) {}
|
|
1429
|
-
}
|
|
1430
|
-
}, 50);
|
|
1470
|
+
const useInMemory = sessionId && !TMUX;
|
|
1471
|
+
const useTmux = TMUX && sessionId;
|
|
1431
1472
|
|
|
1432
1473
|
proc.onExit(() => {
|
|
1433
1474
|
clearInterval(drainCheck);
|
|
1434
|
-
if (
|
|
1475
|
+
if (useInMemory) ptySessions.delete(sessionId);
|
|
1476
|
+
if (!useTmux) send(0x01, Buffer.from([0]));
|
|
1435
1477
|
ws.close();
|
|
1436
1478
|
});
|
|
1437
1479
|
|
|
1480
|
+
// If this is a new in-memory session, register it now (after onExit is wired)
|
|
1481
|
+
if (useInMemory && !rehattached) {
|
|
1482
|
+
ptySessions.set(sessionId, { proc, exited: false, createdAt: Date.now() });
|
|
1483
|
+
// Track exit so stale sessions are detected on reconnect
|
|
1484
|
+
proc.onExit(() => {
|
|
1485
|
+
const entry = ptySessions.get(sessionId);
|
|
1486
|
+
if (entry) entry.exited = true;
|
|
1487
|
+
});
|
|
1488
|
+
} else if (useInMemory && reattached) {
|
|
1489
|
+
const entry = ptySessions.get(sessionId);
|
|
1490
|
+
if (entry) { entry.proc = proc; entry.exited = false; }
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1438
1493
|
ws.isAlive = true;
|
|
1439
1494
|
const pingInterval = setInterval(() => {
|
|
1440
1495
|
if (!ws.isAlive) { clearInterval(pingInterval); ws.terminate(); return; }
|
|
@@ -1467,6 +1522,11 @@ wss.on('connection', (ws, req) => {
|
|
|
1467
1522
|
const cleanup = () => {
|
|
1468
1523
|
clearInterval(pingInterval);
|
|
1469
1524
|
clearInterval(drainCheck);
|
|
1525
|
+
if (useInMemory && sessionId) {
|
|
1526
|
+
// Keep the PTY alive for reattachment — just detach listeners
|
|
1527
|
+
try { proc.removeAllListeners('data'); } catch {}
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1470
1530
|
try { proc.kill(); } catch {}
|
|
1471
1531
|
};
|
|
1472
1532
|
ws.on('close', cleanup);
|
|
@@ -1928,6 +1988,11 @@ function cleanup() {
|
|
|
1928
1988
|
}
|
|
1929
1989
|
} catch {}
|
|
1930
1990
|
}
|
|
1991
|
+
// Kill all in-memory PTY sessions
|
|
1992
|
+
for (const [id, entry] of ptySessions) {
|
|
1993
|
+
try { entry.proc.kill(); } catch {}
|
|
1994
|
+
}
|
|
1995
|
+
ptySessions.clear();
|
|
1931
1996
|
}
|
|
1932
1997
|
|
|
1933
1998
|
function startServer(opts = {}) {
|