termbeam 0.0.5 → 0.0.7

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,55 @@
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
+ <link rel="manifest" href="/manifest.json" />
13
+ <link rel="apple-touch-icon" href="/icons/icon-192.png" />
12
14
  <title>TermBeam — Terminal</title>
13
15
  <link
14
16
  rel="stylesheet"
15
17
  href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
16
18
  />
17
19
  <style>
20
+ :root {
21
+ --bg: #1e1e1e;
22
+ --surface: #252526;
23
+ --border: #3c3c3c;
24
+ --border-subtle: #474747;
25
+ --text: #d4d4d4;
26
+ --text-secondary: #858585;
27
+ --text-dim: #6e6e6e;
28
+ --text-muted: #555555;
29
+ --accent: #0078d4;
30
+ --accent-hover: #1a8ae8;
31
+ --accent-active: #005a9e;
32
+ --danger: #f14c4c;
33
+ --danger-hover: #d73a3a;
34
+ --success: #89d185;
35
+ --key-bg: #2d2d2d;
36
+ --key-border: #404040;
37
+ --key-shadow: rgba(0,0,0,0.4);
38
+ --overlay-bg: rgba(0,0,0,0.85);
39
+ }
40
+ [data-theme="light"] {
41
+ --bg: #ffffff;
42
+ --surface: #f3f3f3;
43
+ --border: #e0e0e0;
44
+ --border-subtle: #d0d0d0;
45
+ --text: #1e1e1e;
46
+ --text-secondary: #616161;
47
+ --text-dim: #767676;
48
+ --text-muted: #a0a0a0;
49
+ --accent: #0078d4;
50
+ --accent-hover: #106ebe;
51
+ --accent-active: #005a9e;
52
+ --danger: #e51400;
53
+ --danger-hover: #c20000;
54
+ --success: #16825d;
55
+ --key-bg: #e8e8e8;
56
+ --key-border: #d0d0d0;
57
+ --key-shadow: rgba(0,0,0,0.08);
58
+ --overlay-bg: rgba(0,0,0,0.5);
59
+ }
18
60
  @font-face {
19
61
  font-family: 'NerdFont';
20
62
  src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
@@ -41,74 +83,117 @@
41
83
  body {
42
84
  height: 100%;
43
85
  width: 100%;
44
- background: #1a1a2e;
45
- color: #e0e0e0;
86
+ background: var(--bg);
87
+ color: var(--text);
46
88
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
47
89
  overflow: hidden;
48
90
  touch-action: manipulation;
49
- /* Use dvh to account for mobile browser chrome + keyboard */
50
91
  height: 100dvh;
92
+ transition: background 0.3s, color 0.3s;
51
93
  }
52
94
 
53
95
  #status-bar {
54
- height: 36px;
96
+ height: 40px;
55
97
  display: flex;
56
98
  align-items: center;
57
99
  justify-content: space-between;
58
- padding: 0 12px;
59
- background: #16213e;
60
- border-bottom: 1px solid #0f3460;
100
+ padding: 0 8px;
101
+ background: var(--surface);
102
+ border-bottom: 1px solid var(--border);
61
103
  font-size: 13px;
104
+ transition: background 0.3s, border-color 0.3s;
62
105
  }
63
106
  #status-bar .left {
64
107
  display: flex;
65
108
  align-items: center;
66
- gap: 8px;
109
+ gap: 6px;
67
110
  }
68
111
  #status-bar .right {
69
112
  display: flex;
70
113
  align-items: center;
71
- gap: 8px;
114
+ gap: 4px;
72
115
  }
73
- #back-btn {
116
+ .bar-btn {
74
117
  background: none;
75
118
  border: none;
76
- color: #888;
77
- font-size: 18px;
119
+ color: var(--text-dim);
120
+ width: 30px;
121
+ height: 30px;
122
+ border-radius: 8px;
78
123
  cursor: pointer;
79
- padding: 0 4px;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ transition: background 0.15s, color 0.15s;
128
+ -webkit-tap-highlight-color: transparent;
129
+ }
130
+ .bar-btn:hover {
131
+ background: var(--border);
132
+ color: var(--text);
80
133
  }
81
- #back-btn:active {
82
- color: #e0e0e0;
134
+ .bar-btn:active {
135
+ background: var(--accent);
136
+ color: #ffffff;
137
+ }
138
+ .bar-btn svg {
139
+ display: block;
140
+ }
141
+ .bar-group {
142
+ display: flex;
143
+ align-items: center;
144
+ background: var(--bg);
145
+ border-radius: 8px;
146
+ padding: 2px;
147
+ gap: 1px;
148
+ transition: background 0.3s;
149
+ }
150
+ .bar-group .bar-btn {
151
+ width: 26px;
152
+ height: 26px;
153
+ border-radius: 6px;
154
+ font-size: 14px;
155
+ font-weight: 600;
83
156
  }
84
157
  #stop-btn {
85
- background: #e74c3c;
158
+ background: none;
86
159
  border: none;
87
- color: white;
88
- font-size: 11px;
89
- font-weight: 600;
160
+ color: var(--danger);
161
+ height: 30px;
162
+ border-radius: 8px;
90
163
  cursor: pointer;
91
- padding: 4px 10px;
92
- border-radius: 6px;
93
164
  display: flex;
94
165
  align-items: center;
166
+ justify-content: center;
95
167
  gap: 4px;
168
+ padding: 0 10px;
169
+ font-size: 11px;
170
+ font-weight: 600;
171
+ margin-left: 2px;
172
+ transition: background 0.15s, color 0.15s, transform 0.1s;
173
+ -webkit-tap-highlight-color: transparent;
174
+ }
175
+ #stop-btn:hover {
176
+ background: var(--danger);
177
+ color: #ffffff;
96
178
  }
97
179
  #stop-btn:active {
98
- background: #c0392b;
180
+ background: var(--danger-hover);
181
+ color: #ffffff;
182
+ transform: scale(0.9);
99
183
  }
100
184
  #status-dot {
101
185
  width: 8px;
102
186
  height: 8px;
103
187
  border-radius: 50%;
104
- background: #e74c3c;
188
+ background: var(--danger);
105
189
  display: inline-block;
190
+ transition: background 0.3s;
106
191
  }
107
192
  #status-dot.connected {
108
- background: #2ecc71;
193
+ background: var(--success);
109
194
  }
110
195
  #status-text {
111
- color: #aaa;
196
+ color: var(--text-secondary);
112
197
  }
113
198
  #session-name {
114
199
  font-weight: 600;
@@ -116,10 +201,10 @@
116
201
 
117
202
  #terminal-container {
118
203
  position: absolute;
119
- top: 36px;
204
+ top: 40px;
120
205
  left: 0;
121
206
  right: 0;
122
- bottom: 44px;
207
+ bottom: 52px;
123
208
  padding: 2px;
124
209
  overflow: hidden;
125
210
  }
@@ -129,46 +214,67 @@
129
214
  bottom: 0;
130
215
  left: 0;
131
216
  right: 0;
132
- height: 44px;
217
+ height: 52px;
133
218
  display: flex;
134
219
  align-items: center;
135
- background: #16213e;
136
- border-top: 1px solid #0f3460;
137
- padding: 0 3px;
220
+ background: var(--surface);
221
+ border-top: 1px solid var(--border);
222
+ padding: 0 4px;
138
223
  padding-bottom: env(safe-area-inset-bottom, 0);
139
- gap: 3px;
140
- overflow-x: auto;
224
+ gap: 4px;
141
225
  z-index: 50;
226
+ transition: background 0.3s, border-color 0.3s;
142
227
  }
143
228
  .key-btn {
144
- min-width: 40px;
145
- height: 32px;
146
- background: #0f3460;
147
- color: #e0e0e0;
148
- border: 1px solid #1a1a5e;
149
- border-radius: 6px;
150
- font-size: 11px;
229
+ min-width: 0;
230
+ height: 40px;
231
+ background: var(--key-bg);
232
+ color: var(--text);
233
+ border: 1px solid var(--key-border);
234
+ border-radius: 8px;
235
+ font-size: 12px;
151
236
  font-weight: 600;
152
237
  cursor: pointer;
153
238
  display: flex;
239
+ flex-direction: column;
154
240
  align-items: center;
155
241
  justify-content: center;
156
242
  -webkit-tap-highlight-color: transparent;
157
243
  user-select: none;
158
244
  white-space: nowrap;
159
- padding: 0 6px;
160
- flex-shrink: 0;
245
+ padding: 2px 8px;
246
+ flex: 1 1 0;
247
+ gap: 0;
248
+ line-height: 1;
249
+ transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
250
+ box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
251
+ }
252
+ .key-btn .hint {
253
+ font-size: 8px;
254
+ font-weight: 400;
255
+ opacity: 0.5;
256
+ margin-top: 1px;
257
+ letter-spacing: 0.02em;
258
+ }
259
+ .key-btn:hover {
260
+ background: var(--border);
261
+ border-color: var(--accent);
262
+ box-shadow: 0 2px 6px var(--key-shadow);
161
263
  }
162
264
  .key-btn:active {
163
- background: #533483;
265
+ background: var(--accent);
266
+ color: #ffffff;
267
+ border-color: var(--accent);
268
+ transform: scale(0.93);
269
+ box-shadow: none;
164
270
  }
165
271
  .key-btn.wide {
166
- min-width: 52px;
272
+ flex: 1.4 1 0;
167
273
  }
168
274
  .key-sep {
169
275
  width: 1px;
170
276
  height: 20px;
171
- background: #0f3460;
277
+ background: var(--border);
172
278
  flex-shrink: 0;
173
279
  }
174
280
 
@@ -179,6 +285,64 @@
179
285
  overflow-y: hidden !important;
180
286
  }
181
287
 
288
+ #copy-toast {
289
+ position: fixed;
290
+ top: 56px;
291
+ left: 50%;
292
+ transform: translateX(-50%) translateY(-8px);
293
+ background: var(--surface);
294
+ color: var(--text);
295
+ border: 1px solid var(--border);
296
+ padding: 6px 16px;
297
+ border-radius: 8px;
298
+ font-size: 13px;
299
+ font-weight: 600;
300
+ opacity: 0;
301
+ pointer-events: none;
302
+ transition: opacity 0.2s, transform 0.2s;
303
+ z-index: 200;
304
+ }
305
+ #copy-toast.visible {
306
+ opacity: 1;
307
+ transform: translateX(-50%) translateY(0);
308
+ }
309
+
310
+ #paste-overlay {
311
+ display: none;
312
+ position: fixed;
313
+ top: 0;
314
+ left: 0;
315
+ right: 0;
316
+ bottom: 0;
317
+ background: var(--overlay-bg);
318
+ z-index: 150;
319
+ flex-direction: column;
320
+ align-items: center;
321
+ justify-content: center;
322
+ gap: 12px;
323
+ }
324
+ #paste-overlay.visible {
325
+ display: flex;
326
+ }
327
+ #paste-overlay label {
328
+ font-size: 15px;
329
+ color: #ffffff;
330
+ font-weight: 600;
331
+ }
332
+ #paste-input {
333
+ width: 80%;
334
+ max-width: 400px;
335
+ min-height: 80px;
336
+ background: var(--surface);
337
+ color: var(--text);
338
+ border: 1px solid var(--border);
339
+ border-radius: 8px;
340
+ padding: 10px;
341
+ font-size: 14px;
342
+ font-family: 'NerdFont', 'JetBrains Mono', monospace;
343
+ resize: vertical;
344
+ }
345
+
182
346
  #reconnect-overlay {
183
347
  display: none;
184
348
  position: fixed;
@@ -186,7 +350,7 @@
186
350
  left: 0;
187
351
  right: 0;
188
352
  bottom: 0;
189
- background: rgba(0, 0, 0, 0.85);
353
+ background: var(--overlay-bg);
190
354
  z-index: 100;
191
355
  flex-direction: column;
192
356
  align-items: center;
@@ -198,6 +362,7 @@
198
362
  }
199
363
  #reconnect-overlay .msg {
200
364
  font-size: 17px;
365
+ color: #ffffff;
201
366
  }
202
367
  .overlay-actions {
203
368
  display: flex;
@@ -210,50 +375,63 @@
210
375
  font-size: 15px;
211
376
  font-weight: 600;
212
377
  cursor: pointer;
378
+ transition: background 0.15s, transform 0.1s;
379
+ }
380
+ .overlay-actions button:active {
381
+ transform: scale(0.95);
213
382
  }
214
383
  #reconnect-btn {
215
- background: #533483;
216
- color: white;
384
+ background: var(--accent);
385
+ color: #ffffff;
386
+ }
387
+ #reconnect-btn:hover {
388
+ background: var(--accent-hover);
217
389
  }
218
390
  #back-to-sessions {
219
- background: #0f3460;
220
- color: #e0e0e0;
391
+ background: rgba(255,255,255,0.15);
392
+ color: #ffffff;
393
+ }
394
+ #back-to-sessions:hover {
395
+ background: rgba(255,255,255,0.25);
221
396
  }
397
+
222
398
  </style>
223
399
  </head>
224
400
  <body>
225
401
  <div id="status-bar">
226
402
  <div class="left">
227
- <button id="back-btn" onclick="location.href = '/'">‹</button>
403
+ <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>
228
404
  <span id="status-dot"></span>
229
405
  <span id="session-name">…</span>
230
406
  </div>
231
407
  <div class="right">
232
408
  <span id="status-text">Connecting…</span>
233
- <span id="version-text" style="font-size: 11px; color: #555"></span>
234
- <button id="stop-btn" title="Stop session">■ Stop</button>
409
+ <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
410
+ <div class="bar-group">
411
+ <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
412
+ <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
413
+ </div>
414
+ <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>
415
+ <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>
235
416
  </div>
236
417
  </div>
237
418
 
238
419
  <div id="terminal-container"></div>
420
+ <div id="copy-toast">Copied!</div>
239
421
 
240
422
  <div id="key-bar">
241
- <button class="key-btn" data-key="&#x1b;[A">↑</button>
242
- <button class="key-btn" data-key="&#x1b;[B">↓</button>
243
- <button class="key-btn" data-key="&#x1b;[D">←</button>
244
- <button class="key-btn" data-key="&#x1b;[C">→</button>
423
+ <button class="key-btn" data-key="&#x1b;[A" title="Previous command">↑<span class="hint">prev</span></button>
424
+ <button class="key-btn" data-key="&#x1b;[B" title="Next command">↓<span class="hint">next</span></button>
425
+ <button class="key-btn" data-key="&#x1b;[D" title="Left">←</button>
426
+ <button class="key-btn" data-key="&#x1b;[C" title="Right">→</button>
427
+ <button class="key-btn" data-key="&#x1b;[H" title="Home">Home</button>
428
+ <button class="key-btn" data-key="&#x1b;[F" title="End">End</button>
245
429
  <div class="key-sep"></div>
246
- <button class="key-btn wide" data-key="&#x09;">Tab</button>
247
- <button class="key-btn wide" data-key="&#x0d;">Enter</button>
248
- <button class="key-btn" data-key="&#x1b;">Esc</button>
430
+ <button class="key-btn" id="paste-btn" title="Paste from clipboard">Paste</button>
249
431
  <div class="key-sep"></div>
250
- <button class="key-btn" data-key="&#x03;">^C</button>
251
- <button class="key-btn" data-key="&#x04;">^D</button>
252
- <button class="key-btn" data-key="&#x1a;">^Z</button>
253
- <button class="key-btn" data-key="&#x0c;">^L</button>
432
+ <button class="key-btn wide" data-key="&#x09;" title="Autocomplete">Tab</button>
254
433
  <div class="key-sep"></div>
255
- <button class="key-btn" id="zoom-out">A-</button>
256
- <button class="key-btn" id="zoom-in">A+</button>
434
+ <button class="key-btn" data-key="&#x03;" title="Interrupt process">^C<span class="hint">stop</span></button>
257
435
  </div>
258
436
 
259
437
  <div id="reconnect-overlay">
@@ -264,6 +442,15 @@
264
442
  </div>
265
443
  </div>
266
444
 
445
+ <div id="paste-overlay">
446
+ <label for="paste-input">Paste your text below</label>
447
+ <textarea id="paste-input" placeholder="Long-press here and paste…"></textarea>
448
+ <div class="overlay-actions">
449
+ <button id="paste-cancel" style="background: rgba(255,255,255,0.15); color: #ffffff;">Cancel</button>
450
+ <button id="paste-send" style="background: var(--accent); color: #ffffff;">Send</button>
451
+ </div>
452
+ </div>
453
+
267
454
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
268
455
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
269
456
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
@@ -277,6 +464,50 @@
277
464
  const statusText = document.getElementById('status-text');
278
465
  const sessionName = document.getElementById('session-name');
279
466
  const reconnectOverlay = document.getElementById('reconnect-overlay');
467
+ let sessionExited = false;
468
+ let reconnectTimer = null;
469
+ let reconnectDelay = 3000;
470
+ const MAX_RECONNECT_DELAY = 30000;
471
+
472
+ // Terminal themes
473
+ const darkTermTheme = {
474
+ background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
475
+ selectionBackground: 'rgba(38, 79, 120, 0.5)',
476
+ black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
477
+ blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
478
+ brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
479
+ brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
480
+ brightCyan: '#29b8db', brightWhite: '#e5e5e5',
481
+ };
482
+ const lightTermTheme = {
483
+ background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
484
+ selectionBackground: 'rgba(0, 120, 215, 0.3)',
485
+ black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
486
+ blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
487
+ brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
488
+ brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
489
+ brightCyan: '#0598bc', brightWhite: '#a5a5a5',
490
+ };
491
+
492
+ function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
493
+ function applyTheme(theme) {
494
+ document.documentElement.setAttribute('data-theme', theme);
495
+ document.querySelector('meta[name="theme-color"]').content =
496
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
497
+ const btn = document.getElementById('theme-toggle');
498
+ if (btn) btn.innerHTML = theme === 'light'
499
+ ? '<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>'
500
+ : '<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>';
501
+ localStorage.setItem('termbeam-theme', theme);
502
+ if (window._term) {
503
+ window._term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
504
+ }
505
+ }
506
+ applyTheme(getTheme());
507
+
508
+ document.getElementById('theme-toggle').addEventListener('click', () => {
509
+ applyTheme(getTheme() === 'light' ? 'dark' : 'light');
510
+ });
280
511
 
281
512
  // Load Nerd Font, then init terminal
282
513
  const nerdFont = new FontFace(
@@ -307,32 +538,11 @@
307
538
  fontWeightBold: 'bold',
308
539
  letterSpacing: 0,
309
540
  lineHeight: 1.1,
310
- theme: {
311
- background: '#1a1a2e',
312
- foreground: '#e0e0e0',
313
- cursor: '#533483',
314
- cursorAccent: '#1a1a2e',
315
- selectionBackground: 'rgba(83, 52, 131, 0.4)',
316
- black: '#1a1a2e',
317
- red: '#e74c3c',
318
- green: '#2ecc71',
319
- yellow: '#f1c40f',
320
- blue: '#3498db',
321
- magenta: '#9b59b6',
322
- cyan: '#1abc9c',
323
- white: '#ecf0f1',
324
- brightBlack: '#636e72',
325
- brightRed: '#ff6b6b',
326
- brightGreen: '#55efc4',
327
- brightYellow: '#ffeaa7',
328
- brightBlue: '#74b9ff',
329
- brightMagenta: '#a29bfe',
330
- brightCyan: '#81ecec',
331
- brightWhite: '#ffffff',
332
- },
541
+ theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
333
542
  allowProposedApi: true,
334
543
  scrollback: 10000,
335
544
  });
545
+ window._term = term;
336
546
 
337
547
  const fitAddon = new window.FitAddon.FitAddon();
338
548
  const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
@@ -351,8 +561,9 @@
351
561
 
352
562
  ws.onopen = () => {
353
563
  statusDot.className = 'connected';
354
- statusText.textContent = 'Connected';
564
+ statusText.textContent = '';
355
565
  reconnectOverlay.classList.remove('visible');
566
+ reconnectDelay = 3000;
356
567
  // Attach to session
357
568
  ws.send(JSON.stringify({ type: 'attach', sessionId }));
358
569
  };
@@ -369,6 +580,8 @@
369
580
  ws.send(JSON.stringify({ type: 'resize', cols: dims.cols, rows: dims.rows }));
370
581
  }
371
582
  } else if (msg.type === 'exit') {
583
+ sessionExited = true;
584
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
372
585
  statusText.textContent = `Exited (code ${msg.code})`;
373
586
  statusDot.className = '';
374
587
  reconnectOverlay.querySelector('.msg').textContent =
@@ -388,6 +601,9 @@
388
601
  statusDot.className = '';
389
602
  statusText.textContent = 'Disconnected';
390
603
  reconnectOverlay.classList.add('visible');
604
+ if (!sessionExited) {
605
+ scheduleReconnect();
606
+ }
391
607
  };
392
608
 
393
609
  ws.onerror = () => {
@@ -415,7 +631,7 @@
415
631
  window.addEventListener('resize', doResize);
416
632
  screen.orientation?.addEventListener('change', () => setTimeout(doResize, 150));
417
633
 
418
- // Key bar (skip zoom buttons — handled separately)
634
+ // Key bar
419
635
  // Use mousedown + preventDefault to stop buttons from stealing focus/opening keyboard
420
636
  document.getElementById('key-bar').addEventListener('mousedown', (e) => {
421
637
  // Only prevent default on buttons, not the scrollable bar itself
@@ -429,34 +645,103 @@
429
645
  .addEventListener('touchstart', () => {}, { passive: true });
430
646
  document.getElementById('key-bar').addEventListener('click', (e) => {
431
647
  const btn = e.target.closest('.key-btn');
432
- if (!btn || btn.id === 'zoom-in' || btn.id === 'zoom-out') return;
648
+ if (!btn || !btn.dataset.key) return;
433
649
  if (ws && ws.readyState === 1) {
434
650
  ws.send(JSON.stringify({ type: 'input', data: btn.dataset.key }));
435
651
  }
436
652
  // Don't call term.focus() here — it opens the soft keyboard
437
653
  });
438
654
 
439
- // Zoom
440
- const MIN_FONT = 2,
441
- MAX_FONT = 28;
442
- let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
443
-
655
+ // Zoom (status bar)
656
+ const MIN_FONT = 2, MAX_FONT = 28;
657
+ let fontSize = savedFontSize;
444
658
  function applyZoom(size) {
445
659
  fontSize = Math.max(MIN_FONT, Math.min(MAX_FONT, size));
446
660
  term.options.fontSize = fontSize;
447
661
  localStorage.setItem('termbeam-fontsize', fontSize);
448
662
  doResize();
449
663
  }
664
+ document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
665
+ document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
666
+
667
+ // Clipboard: copy on selection
668
+ function showToast(msg) {
669
+ const toast = document.getElementById('copy-toast');
670
+ toast.textContent = msg;
671
+ toast.classList.add('visible');
672
+ clearTimeout(toast._timer);
673
+ toast._timer = setTimeout(() => toast.classList.remove('visible'), 1500);
674
+ }
675
+
676
+ term.onSelectionChange(() => {
677
+ const sel = term.getSelection();
678
+ if (sel && navigator.clipboard && navigator.clipboard.writeText) {
679
+ navigator.clipboard.writeText(sel).then(() => {
680
+ showToast('Copied!');
681
+ }).catch(() => {});
682
+ }
683
+ });
684
+
685
+ // Clipboard: paste button
686
+ const pasteOverlay = document.getElementById('paste-overlay');
687
+ const pasteInput = document.getElementById('paste-input');
688
+
689
+ function openPasteModal() {
690
+ pasteInput.value = '';
691
+ pasteOverlay.classList.add('visible');
692
+ pasteInput.focus();
693
+ }
694
+
695
+ function closePasteModal() {
696
+ pasteOverlay.classList.remove('visible');
697
+ pasteInput.value = '';
698
+ }
450
699
 
451
- document.getElementById('zoom-in').addEventListener('click', () => {
452
- applyZoom(fontSize + 2);
700
+ document.getElementById('paste-btn').addEventListener('mousedown', (e) => {
701
+ e.preventDefault();
453
702
  });
454
- document.getElementById('zoom-out').addEventListener('click', () => {
455
- applyZoom(fontSize - 2);
703
+ document.getElementById('paste-btn').addEventListener('click', () => {
704
+ if (navigator.clipboard && navigator.clipboard.readText) {
705
+ navigator.clipboard.readText().then((text) => {
706
+ if (text && ws && ws.readyState === 1) {
707
+ ws.send(JSON.stringify({ type: 'input', data: text }));
708
+ showToast('Pasted!');
709
+ }
710
+ }).catch(() => {
711
+ openPasteModal();
712
+ });
713
+ } else {
714
+ openPasteModal();
715
+ }
456
716
  });
457
717
 
718
+ document.getElementById('paste-send').addEventListener('click', () => {
719
+ const text = pasteInput.value;
720
+ if (text && ws && ws.readyState === 1) {
721
+ ws.send(JSON.stringify({ type: 'input', data: text }));
722
+ }
723
+ closePasteModal();
724
+ });
725
+ document.getElementById('paste-cancel').addEventListener('click', closePasteModal);
726
+
727
+ function scheduleReconnect() {
728
+ if (reconnectTimer) clearTimeout(reconnectTimer);
729
+ const seconds = Math.round(reconnectDelay / 1000);
730
+ reconnectOverlay.querySelector('.msg').textContent =
731
+ `Disconnected — reconnecting in ${seconds}s…`;
732
+ reconnectTimer = setTimeout(() => {
733
+ reconnectTimer = null;
734
+ reconnectOverlay.querySelector('.msg').textContent = 'Reconnecting…';
735
+ reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT_DELAY);
736
+ connect();
737
+ }, reconnectDelay);
738
+ }
739
+
458
740
  // Reconnect
459
741
  document.getElementById('reconnect-btn').addEventListener('click', () => {
742
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
743
+ sessionExited = false;
744
+ reconnectDelay = 3000;
460
745
  term.clear();
461
746
  connect();
462
747
  });
@@ -486,11 +771,11 @@
486
771
  if (keyboardHeight > 50) {
487
772
  // Keyboard is open — move key bar above it
488
773
  keyBar.style.bottom = keyboardHeight + 'px';
489
- container.style.bottom = 44 + keyboardHeight + 'px';
774
+ container.style.bottom = 52 + keyboardHeight + 'px';
490
775
  } else {
491
776
  // Keyboard closed
492
777
  keyBar.style.bottom = '0px';
493
- container.style.bottom = '44px';
778
+ container.style.bottom = '52px';
494
779
  }
495
780
  // Refit terminal to new available space
496
781
  setTimeout(() => doResize(), 50);
@@ -518,6 +803,10 @@
518
803
 
519
804
  connect();
520
805
  }
806
+
807
+ if ('serviceWorker' in navigator) {
808
+ navigator.serviceWorker.register('/sw.js').catch(() => {});
809
+ }
521
810
  </script>
522
811
  </body>
523
812
  </html>