termbeam 0.0.4 → 0.0.6

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.
@@ -8,13 +8,53 @@
8
8
  />
9
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
10
10
  <meta name="mobile-web-app-capable" content="yes" />
11
- <meta name="theme-color" content="#1a1a2e" />
11
+ <meta name="theme-color" content="#1e1e1e" />
12
12
  <title>TermBeam — Terminal</title>
13
13
  <link
14
14
  rel="stylesheet"
15
15
  href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
16
16
  />
17
17
  <style>
18
+ :root {
19
+ --bg: #1e1e1e;
20
+ --surface: #252526;
21
+ --border: #3c3c3c;
22
+ --border-subtle: #474747;
23
+ --text: #d4d4d4;
24
+ --text-secondary: #858585;
25
+ --text-dim: #6e6e6e;
26
+ --text-muted: #555555;
27
+ --accent: #0078d4;
28
+ --accent-hover: #1a8ae8;
29
+ --accent-active: #005a9e;
30
+ --danger: #f14c4c;
31
+ --danger-hover: #d73a3a;
32
+ --success: #89d185;
33
+ --key-bg: #2d2d2d;
34
+ --key-border: #404040;
35
+ --key-shadow: rgba(0,0,0,0.4);
36
+ --overlay-bg: rgba(0,0,0,0.85);
37
+ }
38
+ [data-theme="light"] {
39
+ --bg: #ffffff;
40
+ --surface: #f3f3f3;
41
+ --border: #e0e0e0;
42
+ --border-subtle: #d0d0d0;
43
+ --text: #1e1e1e;
44
+ --text-secondary: #616161;
45
+ --text-dim: #767676;
46
+ --text-muted: #a0a0a0;
47
+ --accent: #0078d4;
48
+ --accent-hover: #106ebe;
49
+ --accent-active: #005a9e;
50
+ --danger: #e51400;
51
+ --danger-hover: #c20000;
52
+ --success: #16825d;
53
+ --key-bg: #e8e8e8;
54
+ --key-border: #d0d0d0;
55
+ --key-shadow: rgba(0,0,0,0.08);
56
+ --overlay-bg: rgba(0,0,0,0.5);
57
+ }
18
58
  @font-face {
19
59
  font-family: 'NerdFont';
20
60
  src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
@@ -41,53 +81,117 @@
41
81
  body {
42
82
  height: 100%;
43
83
  width: 100%;
44
- background: #1a1a2e;
45
- color: #e0e0e0;
84
+ background: var(--bg);
85
+ color: var(--text);
46
86
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
47
87
  overflow: hidden;
48
88
  touch-action: manipulation;
49
- /* Use dvh to account for mobile browser chrome + keyboard */
50
89
  height: 100dvh;
90
+ transition: background 0.3s, color 0.3s;
51
91
  }
52
92
 
53
93
  #status-bar {
54
- height: 36px;
94
+ height: 40px;
55
95
  display: flex;
56
96
  align-items: center;
57
97
  justify-content: space-between;
58
- padding: 0 12px;
59
- background: #16213e;
60
- border-bottom: 1px solid #0f3460;
98
+ padding: 0 8px;
99
+ background: var(--surface);
100
+ border-bottom: 1px solid var(--border);
61
101
  font-size: 13px;
102
+ transition: background 0.3s, border-color 0.3s;
62
103
  }
63
104
  #status-bar .left {
64
105
  display: flex;
65
106
  align-items: center;
66
- gap: 8px;
107
+ gap: 6px;
108
+ }
109
+ #status-bar .right {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: 4px;
67
113
  }
68
- #back-btn {
114
+ .bar-btn {
69
115
  background: none;
70
116
  border: none;
71
- color: #888;
72
- font-size: 18px;
117
+ color: var(--text-dim);
118
+ width: 30px;
119
+ height: 30px;
120
+ border-radius: 8px;
73
121
  cursor: pointer;
74
- padding: 0 4px;
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ transition: background 0.15s, color 0.15s;
126
+ -webkit-tap-highlight-color: transparent;
127
+ }
128
+ .bar-btn:hover {
129
+ background: var(--border);
130
+ color: var(--text);
131
+ }
132
+ .bar-btn:active {
133
+ background: var(--accent);
134
+ color: #ffffff;
135
+ }
136
+ .bar-btn svg {
137
+ display: block;
138
+ }
139
+ .bar-group {
140
+ display: flex;
141
+ align-items: center;
142
+ background: var(--bg);
143
+ border-radius: 8px;
144
+ padding: 2px;
145
+ gap: 1px;
146
+ transition: background 0.3s;
75
147
  }
76
- #back-btn:active {
77
- color: #e0e0e0;
148
+ .bar-group .bar-btn {
149
+ width: 26px;
150
+ height: 26px;
151
+ border-radius: 6px;
152
+ font-size: 14px;
153
+ font-weight: 600;
154
+ }
155
+ #stop-btn {
156
+ background: none;
157
+ border: none;
158
+ color: var(--danger);
159
+ height: 30px;
160
+ border-radius: 8px;
161
+ cursor: pointer;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ gap: 4px;
166
+ padding: 0 10px;
167
+ font-size: 11px;
168
+ font-weight: 600;
169
+ margin-left: 2px;
170
+ transition: background 0.15s, color 0.15s, transform 0.1s;
171
+ -webkit-tap-highlight-color: transparent;
172
+ }
173
+ #stop-btn:hover {
174
+ background: var(--danger);
175
+ color: #ffffff;
176
+ }
177
+ #stop-btn:active {
178
+ background: var(--danger-hover);
179
+ color: #ffffff;
180
+ transform: scale(0.9);
78
181
  }
79
182
  #status-dot {
80
183
  width: 8px;
81
184
  height: 8px;
82
185
  border-radius: 50%;
83
- background: #e74c3c;
186
+ background: var(--danger);
84
187
  display: inline-block;
188
+ transition: background 0.3s;
85
189
  }
86
190
  #status-dot.connected {
87
- background: #2ecc71;
191
+ background: var(--success);
88
192
  }
89
193
  #status-text {
90
- color: #aaa;
194
+ color: var(--text-secondary);
91
195
  }
92
196
  #session-name {
93
197
  font-weight: 600;
@@ -95,10 +199,10 @@
95
199
 
96
200
  #terminal-container {
97
201
  position: absolute;
98
- top: 36px;
202
+ top: 40px;
99
203
  left: 0;
100
204
  right: 0;
101
- bottom: 44px;
205
+ bottom: 52px;
102
206
  padding: 2px;
103
207
  overflow: hidden;
104
208
  }
@@ -108,46 +212,67 @@
108
212
  bottom: 0;
109
213
  left: 0;
110
214
  right: 0;
111
- height: 44px;
215
+ height: 52px;
112
216
  display: flex;
113
217
  align-items: center;
114
- background: #16213e;
115
- border-top: 1px solid #0f3460;
116
- padding: 0 3px;
218
+ background: var(--surface);
219
+ border-top: 1px solid var(--border);
220
+ padding: 0 4px;
117
221
  padding-bottom: env(safe-area-inset-bottom, 0);
118
- gap: 3px;
119
- overflow-x: auto;
222
+ gap: 4px;
120
223
  z-index: 50;
224
+ transition: background 0.3s, border-color 0.3s;
121
225
  }
122
226
  .key-btn {
123
- min-width: 40px;
124
- height: 32px;
125
- background: #0f3460;
126
- color: #e0e0e0;
127
- border: 1px solid #1a1a5e;
128
- border-radius: 6px;
129
- font-size: 11px;
227
+ min-width: 0;
228
+ height: 40px;
229
+ background: var(--key-bg);
230
+ color: var(--text);
231
+ border: 1px solid var(--key-border);
232
+ border-radius: 8px;
233
+ font-size: 12px;
130
234
  font-weight: 600;
131
235
  cursor: pointer;
132
236
  display: flex;
237
+ flex-direction: column;
133
238
  align-items: center;
134
239
  justify-content: center;
135
240
  -webkit-tap-highlight-color: transparent;
136
241
  user-select: none;
137
242
  white-space: nowrap;
138
- padding: 0 6px;
139
- flex-shrink: 0;
243
+ padding: 2px 8px;
244
+ flex: 1 1 0;
245
+ gap: 0;
246
+ line-height: 1;
247
+ transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
248
+ box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
249
+ }
250
+ .key-btn .hint {
251
+ font-size: 8px;
252
+ font-weight: 400;
253
+ opacity: 0.5;
254
+ margin-top: 1px;
255
+ letter-spacing: 0.02em;
256
+ }
257
+ .key-btn:hover {
258
+ background: var(--border);
259
+ border-color: var(--accent);
260
+ box-shadow: 0 2px 6px var(--key-shadow);
140
261
  }
141
262
  .key-btn:active {
142
- background: #533483;
263
+ background: var(--accent);
264
+ color: #ffffff;
265
+ border-color: var(--accent);
266
+ transform: scale(0.93);
267
+ box-shadow: none;
143
268
  }
144
269
  .key-btn.wide {
145
- min-width: 52px;
270
+ flex: 1.4 1 0;
146
271
  }
147
272
  .key-sep {
148
273
  width: 1px;
149
274
  height: 20px;
150
- background: #0f3460;
275
+ background: var(--border);
151
276
  flex-shrink: 0;
152
277
  }
153
278
 
@@ -165,7 +290,7 @@
165
290
  left: 0;
166
291
  right: 0;
167
292
  bottom: 0;
168
- background: rgba(0, 0, 0, 0.85);
293
+ background: var(--overlay-bg);
169
294
  z-index: 100;
170
295
  flex-direction: column;
171
296
  align-items: center;
@@ -177,6 +302,7 @@
177
302
  }
178
303
  #reconnect-overlay .msg {
179
304
  font-size: 17px;
305
+ color: #ffffff;
180
306
  }
181
307
  .overlay-actions {
182
308
  display: flex;
@@ -189,47 +315,60 @@
189
315
  font-size: 15px;
190
316
  font-weight: 600;
191
317
  cursor: pointer;
318
+ transition: background 0.15s, transform 0.1s;
319
+ }
320
+ .overlay-actions button:active {
321
+ transform: scale(0.95);
192
322
  }
193
323
  #reconnect-btn {
194
- background: #533483;
195
- color: white;
324
+ background: var(--accent);
325
+ color: #ffffff;
326
+ }
327
+ #reconnect-btn:hover {
328
+ background: var(--accent-hover);
196
329
  }
197
330
  #back-to-sessions {
198
- background: #0f3460;
199
- color: #e0e0e0;
331
+ background: rgba(255,255,255,0.15);
332
+ color: #ffffff;
333
+ }
334
+ #back-to-sessions:hover {
335
+ background: rgba(255,255,255,0.25);
200
336
  }
337
+
201
338
  </style>
202
339
  </head>
203
340
  <body>
204
341
  <div id="status-bar">
205
342
  <div class="left">
206
- <button id="back-btn" onclick="location.href = '/'">‹</button>
343
+ <button class="bar-btn" id="back-btn" onclick="location.href = '/'" title="Back to sessions"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg></button>
207
344
  <span id="status-dot"></span>
208
345
  <span id="session-name">…</span>
209
346
  </div>
210
- <span id="status-text">Connecting…</span>
211
- <span id="version-text" style="font-size: 11px; color: #555; margin-left: 8px"></span>
347
+ <div class="right">
348
+ <span id="status-text">Connecting…</span>
349
+ <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
350
+ <div class="bar-group">
351
+ <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
352
+ <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
353
+ </div>
354
+ <button class="bar-btn" id="theme-toggle" title="Toggle theme"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg></button>
355
+ <button id="stop-btn" title="Stop session"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>Stop</button>
356
+ </div>
212
357
  </div>
213
358
 
214
359
  <div id="terminal-container"></div>
215
360
 
216
361
  <div id="key-bar">
217
- <button class="key-btn" data-key="&#x1b;[A">↑</button>
218
- <button class="key-btn" data-key="&#x1b;[B">↓</button>
219
- <button class="key-btn" data-key="&#x1b;[D">←</button>
220
- <button class="key-btn" data-key="&#x1b;[C">→</button>
362
+ <button class="key-btn" data-key="&#x1b;[A" title="Previous command">↑<span class="hint">prev</span></button>
363
+ <button class="key-btn" data-key="&#x1b;[B" title="Next command">↓<span class="hint">next</span></button>
364
+ <button class="key-btn" data-key="&#x1b;[D" title="Left">←</button>
365
+ <button class="key-btn" data-key="&#x1b;[C" title="Right">→</button>
366
+ <button class="key-btn" data-key="&#x1b;[H" title="Home">Home</button>
367
+ <button class="key-btn" data-key="&#x1b;[F" title="End">End</button>
221
368
  <div class="key-sep"></div>
222
- <button class="key-btn wide" data-key="&#x09;">Tab</button>
223
- <button class="key-btn wide" data-key="&#x0d;">Enter</button>
224
- <button class="key-btn" data-key="&#x1b;">Esc</button>
369
+ <button class="key-btn wide" data-key="&#x09;" title="Autocomplete">Tab</button>
225
370
  <div class="key-sep"></div>
226
- <button class="key-btn" data-key="&#x03;">^C</button>
227
- <button class="key-btn" data-key="&#x04;">^D</button>
228
- <button class="key-btn" data-key="&#x1a;">^Z</button>
229
- <button class="key-btn" data-key="&#x0c;">^L</button>
230
- <div class="key-sep"></div>
231
- <button class="key-btn" id="zoom-out">A-</button>
232
- <button class="key-btn" id="zoom-in">A+</button>
371
+ <button class="key-btn" data-key="&#x03;" title="Interrupt process">^C<span class="hint">stop</span></button>
233
372
  </div>
234
373
 
235
374
  <div id="reconnect-overlay">
@@ -253,6 +392,50 @@
253
392
  const statusText = document.getElementById('status-text');
254
393
  const sessionName = document.getElementById('session-name');
255
394
  const reconnectOverlay = document.getElementById('reconnect-overlay');
395
+ let sessionExited = false;
396
+ let reconnectTimer = null;
397
+ let reconnectDelay = 3000;
398
+ const MAX_RECONNECT_DELAY = 30000;
399
+
400
+ // Terminal themes
401
+ const darkTermTheme = {
402
+ background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
403
+ selectionBackground: 'rgba(38, 79, 120, 0.5)',
404
+ black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
405
+ blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
406
+ brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
407
+ brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
408
+ brightCyan: '#29b8db', brightWhite: '#e5e5e5',
409
+ };
410
+ const lightTermTheme = {
411
+ background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
412
+ selectionBackground: 'rgba(0, 120, 215, 0.3)',
413
+ black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
414
+ blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
415
+ brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
416
+ brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
417
+ brightCyan: '#0598bc', brightWhite: '#a5a5a5',
418
+ };
419
+
420
+ function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
421
+ function applyTheme(theme) {
422
+ document.documentElement.setAttribute('data-theme', theme);
423
+ document.querySelector('meta[name="theme-color"]').content =
424
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
425
+ const btn = document.getElementById('theme-toggle');
426
+ if (btn) btn.innerHTML = theme === 'light'
427
+ ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>'
428
+ : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>';
429
+ localStorage.setItem('termbeam-theme', theme);
430
+ if (window._term) {
431
+ window._term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
432
+ }
433
+ }
434
+ applyTheme(getTheme());
435
+
436
+ document.getElementById('theme-toggle').addEventListener('click', () => {
437
+ applyTheme(getTheme() === 'light' ? 'dark' : 'light');
438
+ });
256
439
 
257
440
  // Load Nerd Font, then init terminal
258
441
  const nerdFont = new FontFace(
@@ -283,32 +466,11 @@
283
466
  fontWeightBold: 'bold',
284
467
  letterSpacing: 0,
285
468
  lineHeight: 1.1,
286
- theme: {
287
- background: '#1a1a2e',
288
- foreground: '#e0e0e0',
289
- cursor: '#533483',
290
- cursorAccent: '#1a1a2e',
291
- selectionBackground: 'rgba(83, 52, 131, 0.4)',
292
- black: '#1a1a2e',
293
- red: '#e74c3c',
294
- green: '#2ecc71',
295
- yellow: '#f1c40f',
296
- blue: '#3498db',
297
- magenta: '#9b59b6',
298
- cyan: '#1abc9c',
299
- white: '#ecf0f1',
300
- brightBlack: '#636e72',
301
- brightRed: '#ff6b6b',
302
- brightGreen: '#55efc4',
303
- brightYellow: '#ffeaa7',
304
- brightBlue: '#74b9ff',
305
- brightMagenta: '#a29bfe',
306
- brightCyan: '#81ecec',
307
- brightWhite: '#ffffff',
308
- },
469
+ theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
309
470
  allowProposedApi: true,
310
471
  scrollback: 10000,
311
472
  });
473
+ window._term = term;
312
474
 
313
475
  const fitAddon = new window.FitAddon.FitAddon();
314
476
  const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
@@ -327,8 +489,9 @@
327
489
 
328
490
  ws.onopen = () => {
329
491
  statusDot.className = 'connected';
330
- statusText.textContent = 'Connected';
492
+ statusText.textContent = '';
331
493
  reconnectOverlay.classList.remove('visible');
494
+ reconnectDelay = 3000;
332
495
  // Attach to session
333
496
  ws.send(JSON.stringify({ type: 'attach', sessionId }));
334
497
  };
@@ -345,6 +508,8 @@
345
508
  ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
346
509
  }
347
510
  } else if (msg.type === 'exit') {
511
+ sessionExited = true;
512
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
348
513
  statusText.textContent = `Exited (code ${msg.code})`;
349
514
  statusDot.className = '';
350
515
  reconnectOverlay.querySelector('.msg').textContent =
@@ -364,6 +529,9 @@
364
529
  statusDot.className = '';
365
530
  statusText.textContent = 'Disconnected';
366
531
  reconnectOverlay.classList.add('visible');
532
+ if (!sessionExited) {
533
+ scheduleReconnect();
534
+ }
367
535
  };
368
536
 
369
537
  ws.onerror = () => {
@@ -391,7 +559,7 @@
391
559
  window.addEventListener('resize', doResize);
392
560
  screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
393
561
 
394
- // Key bar (skip zoom buttons — handled separately)
562
+ // Key bar
395
563
  // Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
396
564
  document.getElementById('key-bar').addEventListener('mousedown', (e) => {
397
565
  // Only prevent default on buttons, not the scrollable bar itself
@@ -405,38 +573,56 @@
405
573
  .addEventListener('touchstart', () => {}, { passive: true });
406
574
  document.getElementById('key-bar').addEventListener('click', (e) => {
407
575
  const btn = e.target.closest('.key-btn');
408
- if (!btn || btn.id === 'zoom-in' || btn.id === 'zoom-out') return;
576
+ if (!btn) return;
409
577
  if (ws && ws.readyState === 1) {
410
578
  ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
411
579
  }
412
580
  // Don't call term.focus() here — it opens the soft keyboard
413
581
  });
414
582
 
415
- // Zoom
416
- const MIN_FONT = 2,
417
- MAX_FONT = 28;
418
- let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
419
-
583
+ // Zoom (status bar)
584
+ const MIN_FONT = 2, MAX_FONT = 28;
585
+ let fontSize = savedFontSize;
420
586
  function applyZoom(size) {
421
587
  fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
422
588
  term.options.fontSize = fontSize;
423
589
  localStorage.setItem('termbeam-fontsize', fontSize);
424
590
  doResize();
425
591
  }
426
-
427
- document.getElementById('zoom-in').addEventListener('click', () => {
428
- applyZoom(fontSize + 2);
429
- });
430
- document.getElementById('zoom-out').addEventListener('click', () => {
431
- applyZoom(fontSize - 2);
432
- });
592
+ document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
593
+ document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
594
+
595
+ function scheduleReconnect() {
596
+ if (reconnectTimer) clearTimeout(reconnectTimer);
597
+ const seconds = Math.round(reconnectDelay / 1000);
598
+ reconnectOverlay.querySelector('.msg').textContent =
599
+ `Disconnected — reconnecting in ${seconds}s…`;
600
+ reconnectTimer = setTimeout(() => {
601
+ reconnectTimer = null;
602
+ reconnectOverlay.querySelector('.msg').textContent = 'Reconnecting…';
603
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
604
+ connect();
605
+ }, reconnectDelay);
606
+ }
433
607
 
434
608
  // Reconnect
435
609
  document.getElementById('reconnect-btn').addEventListener('click', () => {
610
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
611
+ sessionExited = false;
612
+ reconnectDelay = 3000;
436
613
  term.clear();
437
614
  connect();
438
615
  });
439
616
 
617
+ // Stop session
618
+ document.getElementById('stop-btn').addEventListener('click', async () => {
619
+ if (!confirm('Stop this session? The process will be killed.')) return;
620
+ try {
621
+ await fetch(`/api/sessions/${sessionId}`, { method: 'DELETE' });
622
+ } catch {}
623
+ location.href = '/';
624
+ });
625
+
440
626
  // Tap terminal area to toggle keyboard (intentional user action)
441
627
  container.addEventListener('click', () => term.focus());
442
628
 
@@ -453,11 +639,11 @@
453
639
  if (keyboardHeight > 50) {
454
640
  // Keyboard is open — move key bar above it
455
641
  keyBar.style.bottom = keyboardHeight + 'px';
456
- container.style.bottom = 44 + keyboardHeight + 'px';
642
+ container.style.bottom = 52 + keyboardHeight + 'px';
457
643
  } else {
458
644
  // Keyboard closed
459
645
  keyBar.style.bottom = '0px';
460
- container.style.bottom = '44px';
646
+ container.style.bottom = '52px';
461
647
  }
462
648
  // Refit terminal to new available space
463
649
  setTimeout(() => doResize(), 50);
package/src/cli.js CHANGED
@@ -32,10 +32,61 @@ Environment:
32
32
  `);
33
33
  }
34
34
 
35
+ function getDefaultShell() {
36
+ const { execFileSync } = require('child_process');
37
+ const ppid = process.ppid;
38
+ console.log(`[termbeam] Detecting shell (parent PID: ${ppid}, platform: ${os.platform()})`);
39
+
40
+ if (os.platform() === 'win32') {
41
+ // Detect parent process on Windows via WMIC
42
+ try {
43
+ const result = execFileSync(
44
+ 'wmic',
45
+ ['process', 'where', `ProcessId=${ppid}`, 'get', 'Name', '/value'],
46
+ { stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', timeout: 3000 },
47
+ );
48
+ const match = result.match(/Name=(.+)/);
49
+ if (match) {
50
+ const name = match[1].trim().toLowerCase();
51
+ console.log(`[termbeam] Detected parent process: ${name}`);
52
+ if (name === 'pwsh.exe') return 'pwsh.exe';
53
+ if (name === 'powershell.exe') return 'powershell.exe';
54
+ }
55
+ } catch (err) {
56
+ console.log(`[termbeam] Could not detect parent process: ${err.message}`);
57
+ }
58
+ const fallback = process.env.COMSPEC || 'cmd.exe';
59
+ console.log(`[termbeam] Falling back to: ${fallback}`);
60
+ return fallback;
61
+ }
62
+
63
+ // Unix: detect parent shell via ps
64
+ try {
65
+ const result = execFileSync('ps', ['-o', 'comm=', '-p', String(ppid)], {
66
+ stdio: ['pipe', 'pipe', 'ignore'],
67
+ encoding: 'utf8',
68
+ timeout: 3000,
69
+ });
70
+ const comm = result.trim();
71
+ if (comm) {
72
+ const shell = comm.startsWith('-') ? comm.slice(1) : comm;
73
+ console.log(`[termbeam] Detected parent shell: ${shell}`);
74
+ return shell;
75
+ }
76
+ } catch (err) {
77
+ console.log(`[termbeam] Could not detect parent shell: ${err.message}`);
78
+ }
79
+
80
+ // Fallback to SHELL env or /bin/sh
81
+ const fallback = process.env.SHELL || '/bin/sh';
82
+ console.log(`[termbeam] Falling back to: ${fallback}`);
83
+ return fallback;
84
+ }
85
+
35
86
  function parseArgs() {
36
87
  let port = parseInt(process.env.PORT || '3456', 10);
37
88
  let host = '0.0.0.0';
38
- const defaultShell = process.env.SHELL || '/bin/zsh';
89
+ const defaultShell = getDefaultShell();
39
90
  const cwd = process.env.TERMBEAM_CWD || process.env.PTY_CWD || process.cwd();
40
91
  let password = process.env.TERMBEAM_PASSWORD || process.env.PTY_PASSWORD || null;
41
92
  let useTunnel = false;