termbeam 1.2.2 → 1.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.2.2",
3
+ "version": "1.2.4",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -9,8 +9,8 @@
9
9
  "scripts": {
10
10
  "start": "node bin/termbeam.js",
11
11
  "dev": "node bin/termbeam.js --generate-password",
12
- "test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
13
- "test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
12
+ "test": "node -e \"require('child_process').execFileSync(process.execPath,['--test',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
13
+ "test:coverage": "c8 --exclude=src/tunnel.js --reporter=text --reporter=lcov --reporter=json-summary --reporter=json node -e \"require('child_process').execFileSync(process.execPath,['--test','--test-reporter=spec','--test-reporter-destination=stdout',...require('fs').readdirSync('test').filter(f=>f.endsWith('.test.js')&&!f.startsWith('e2e-')).map(f=>'test/'+f)],{stdio:'inherit'})\"",
14
14
  "prepare": "husky",
15
15
  "format": "prettier --write .",
16
16
  "lint": "node --check src/*.js bin/*.js",
@@ -73,6 +73,7 @@
73
73
  "ws": "^8.19.0"
74
74
  },
75
75
  "devDependencies": {
76
+ "@playwright/test": "^1.58.2",
76
77
  "c8": "^11.0.0",
77
78
  "husky": "^9.1.7",
78
79
  "lint-staged": "^16.2.7",
@@ -591,6 +591,34 @@
591
591
  touch-action: none;
592
592
  }
593
593
 
594
+ /* ===== Scroll-to-bottom indicator ===== */
595
+ .scroll-bottom-btn {
596
+ display: none;
597
+ position: absolute;
598
+ bottom: 8px;
599
+ right: 16px;
600
+ width: 36px;
601
+ height: 36px;
602
+ border-radius: 50%;
603
+ background: var(--accent);
604
+ color: #fff;
605
+ border: none;
606
+ font-size: 18px;
607
+ line-height: 36px;
608
+ text-align: center;
609
+ cursor: pointer;
610
+ z-index: 50;
611
+ opacity: 0.85;
612
+ transition: opacity 0.15s;
613
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
614
+ }
615
+ .scroll-bottom-btn:hover {
616
+ opacity: 1;
617
+ }
618
+ .scroll-bottom-btn.visible {
619
+ display: block;
620
+ }
621
+
594
622
  /* ===== Toasts & Overlays ===== */
595
623
  #copy-toast {
596
624
  position: fixed;
@@ -1245,6 +1273,7 @@
1245
1273
  border-radius: 10px;
1246
1274
  cursor: pointer;
1247
1275
  overflow: hidden;
1276
+ flex-shrink: 0;
1248
1277
  transition:
1249
1278
  background 0.15s,
1250
1279
  border-color 0.15s;
@@ -2118,6 +2147,7 @@
2118
2147
  document.documentElement.scrollTop = 0;
2119
2148
  document.body.scrollTop = 0;
2120
2149
  }
2150
+ let wasKeyboardOpen = false;
2121
2151
  function onViewportResize() {
2122
2152
  const vv = window.visualViewport;
2123
2153
  const keyboardHeight = window.innerHeight - vv.height;
@@ -2133,11 +2163,22 @@
2133
2163
  keyBar.style.paddingBottom = '';
2134
2164
  terminalsWrapper.style.bottom = '';
2135
2165
  }
2166
+ const keyboardJustClosed = wasKeyboardOpen && !keyboardOpen;
2167
+ wasKeyboardOpen = keyboardOpen;
2136
2168
  resetScroll();
2137
2169
  clearTimeout(vpResizeTimer);
2138
2170
  vpResizeTimer = setTimeout(() => {
2139
2171
  resetScroll();
2140
2172
  doResize();
2173
+ // When keyboard closes, the terminal grows taller — scroll to
2174
+ // bottom so the cursor stays visible instead of appearing offset.
2175
+ if (keyboardJustClosed) {
2176
+ for (const [, ms] of managed) {
2177
+ if (ms.container.classList.contains('visible')) {
2178
+ ms.term.scrollToBottom();
2179
+ }
2180
+ }
2181
+ }
2141
2182
  }, 150);
2142
2183
  }
2143
2184
  function onViewportScroll() {
@@ -2227,6 +2268,7 @@
2227
2268
  theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
2228
2269
  allowProposedApi: true,
2229
2270
  scrollback: 10000,
2271
+ scrollOnOutput: false,
2230
2272
  });
2231
2273
 
2232
2274
  const fitAddon = new window.FitAddon.FitAddon();
@@ -2236,9 +2278,44 @@
2236
2278
 
2237
2279
  const container = document.createElement('div');
2238
2280
  container.className = 'terminal-pane';
2281
+ container.style.position = 'relative';
2239
2282
  terminalsWrapper.appendChild(container);
2240
2283
  term.open(container);
2241
2284
 
2285
+ // Scroll-to-bottom button
2286
+ const scrollBtn = document.createElement('button');
2287
+ scrollBtn.className = 'scroll-bottom-btn';
2288
+ scrollBtn.textContent = '↓';
2289
+ scrollBtn.title = 'Scroll to bottom';
2290
+ scrollBtn.addEventListener('click', (e) => {
2291
+ e.stopPropagation();
2292
+ term.scrollToBottom();
2293
+ });
2294
+ container.appendChild(scrollBtn);
2295
+
2296
+ // Write coalescer — batch rapid term.write() calls into one per frame
2297
+ // so ANSI cursor-up/down pairs within a frame are processed atomically
2298
+ let writeBuf = '';
2299
+ let writeRaf = null;
2300
+ function coalescedWrite(data) {
2301
+ writeBuf += data;
2302
+ if (!writeRaf) {
2303
+ writeRaf = requestAnimationFrame(() => {
2304
+ term.write(writeBuf);
2305
+ writeBuf = '';
2306
+ writeRaf = null;
2307
+ });
2308
+ }
2309
+ }
2310
+
2311
+ // Track whether user has scrolled away from bottom
2312
+ let userScrolledUp = false;
2313
+ term.onScroll(() => {
2314
+ const buf = term.buffer.active;
2315
+ userScrolledUp = buf.viewportY < buf.baseY;
2316
+ scrollBtn.classList.toggle('visible', userScrolledUp);
2317
+ });
2318
+
2242
2319
  // Pointer-event scroll handler — uses setPointerCapture so scrolling
2243
2320
  // survives xterm DOM re-renders under the finger (touch on a letter).
2244
2321
  (function () {
@@ -2348,6 +2425,8 @@
2348
2425
  term,
2349
2426
  fitAddon,
2350
2427
  container,
2428
+ coalescedWrite,
2429
+ scrollBtn,
2351
2430
  ws: null,
2352
2431
  exited: false,
2353
2432
  reconnectTimer: null,
@@ -2360,6 +2439,10 @@
2360
2439
  // Terminal input → WebSocket
2361
2440
  term.onData((input) => {
2362
2441
  if (ms.ws && ms.ws.readyState === 1) {
2442
+ // Auto-scroll to bottom on user input
2443
+ if (userScrolledUp) {
2444
+ ms.term.scrollToBottom();
2445
+ }
2363
2446
  let data = input;
2364
2447
  if (ctrlActive && input.length === 1) {
2365
2448
  const code = input.toLowerCase().charCodeAt(0);
@@ -2411,7 +2494,7 @@
2411
2494
  try {
2412
2495
  const msg = JSON.parse(event.data);
2413
2496
  if (msg.type === 'output') {
2414
- ms.term.write(msg.data);
2497
+ ms.coalescedWrite(msg.data);
2415
2498
  ms.lastActivity = Date.now();
2416
2499
  } else if (msg.type === 'attached') {
2417
2500
  if (ms.container.classList.contains('visible')) {
@@ -2447,7 +2530,7 @@
2447
2530
  }
2448
2531
  }
2449
2532
  } catch {
2450
- ms.term.write(event.data);
2533
+ ms.coalescedWrite(event.data);
2451
2534
  }
2452
2535
  };
2453
2536
 
package/src/server.js CHANGED
@@ -53,7 +53,7 @@ function createTermBeamServer(overrides = {}) {
53
53
  res.setHeader('Cache-Control', 'no-store');
54
54
  res.setHeader(
55
55
  'Content-Security-Policy',
56
- "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; connect-src 'self' ws: wss: https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net",
56
+ "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; img-src 'self' data:; connect-src 'self' ws: wss: https://cdn.jsdelivr.net; font-src 'self' https://cdn.jsdelivr.net",
57
57
  );
58
58
  next();
59
59
  });
package/src/shells.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const os = require('os');
2
2
  const fs = require('fs');
3
- const { execFileSync } = require('child_process');
3
+ const child_process = require('child_process');
4
4
 
5
5
  const KNOWN_WINDOWS_SHELLS = [
6
6
  { name: 'PowerShell (Core)', cmd: 'pwsh.exe' },
@@ -21,7 +21,7 @@ function detectWindowsShells() {
21
21
  const shells = [];
22
22
  for (const { name, cmd } of KNOWN_WINDOWS_SHELLS) {
23
23
  try {
24
- const result = execFileSync('where', [cmd], {
24
+ const result = child_process.execFileSync('where', [cmd], {
25
25
  stdio: ['pipe', 'pipe', 'ignore'],
26
26
  encoding: 'utf8',
27
27
  timeout: 3000,