termbeam 1.0.6 → 1.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.
@@ -2,15 +2,24 @@
2
2
  <html lang="en">
3
3
  <head>
4
4
  <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
8
+ />
6
9
  <meta name="apple-mobile-web-app-capable" content="yes" />
7
10
  <meta name="mobile-web-app-capable" content="yes" />
8
11
  <meta name="theme-color" content="#1e1e1e" />
9
- <meta name="description" content="TermBeam terminal session — access your terminal remotely from any browser with a mobile-optimized touch interface." />
12
+ <meta
13
+ name="description"
14
+ content="TermBeam terminal session — access your terminal remotely from any browser with a mobile-optimized touch interface."
15
+ />
10
16
  <link rel="manifest" href="/manifest.json" />
11
17
  <link rel="apple-touch-icon" href="/icons/icon-192.png" />
12
18
  <title>TermBeam — Terminal</title>
13
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css" />
19
+ <link
20
+ rel="stylesheet"
21
+ href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"
22
+ />
14
23
  <style>
15
24
  :root {
16
25
  --bg: #1e1e1e;
@@ -29,10 +38,10 @@
29
38
  --success: #89d185;
30
39
  --key-bg: #2d2d2d;
31
40
  --key-border: #404040;
32
- --key-shadow: rgba(0,0,0,0.4);
33
- --overlay-bg: rgba(0,0,0,0.85);
41
+ --key-shadow: rgba(0, 0, 0, 0.4);
42
+ --overlay-bg: rgba(0, 0, 0, 0.85);
34
43
  }
35
- [data-theme="light"] {
44
+ [data-theme='light'] {
36
45
  --bg: #ffffff;
37
46
  --surface: #f3f3f3;
38
47
  --border: #e0e0e0;
@@ -49,27 +58,47 @@
49
58
  --success: #16825d;
50
59
  --key-bg: #e8e8e8;
51
60
  --key-border: #d0d0d0;
52
- --key-shadow: rgba(0,0,0,0.08);
53
- --overlay-bg: rgba(0,0,0,0.5);
61
+ --key-shadow: rgba(0, 0, 0, 0.08);
62
+ --overlay-bg: rgba(0, 0, 0, 0.5);
54
63
  }
55
64
  @font-face {
56
65
  font-family: 'NerdFont';
57
- src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf') format('truetype');
58
- font-weight: normal; font-style: normal; font-display: swap;
66
+ src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
67
+ format('truetype');
68
+ font-weight: normal;
69
+ font-style: normal;
70
+ font-display: swap;
59
71
  }
60
72
  @font-face {
61
73
  font-family: 'NerdFont';
62
- src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Bold/JetBrainsMonoNerdFont-Bold.ttf') format('truetype');
63
- font-weight: bold; font-style: normal; font-display: swap;
74
+ src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Bold/JetBrainsMonoNerdFont-Bold.ttf')
75
+ format('truetype');
76
+ font-weight: bold;
77
+ font-style: normal;
78
+ font-display: swap;
64
79
  }
65
- :root { --sab: env(safe-area-inset-bottom, 0px); }
66
- * { margin: 0; padding: 0; box-sizing: border-box; }
67
- html, body {
68
- height: 100%; width: 100%; background: var(--bg); color: var(--text);
80
+ :root {
81
+ --sab: env(safe-area-inset-bottom, 0px);
82
+ }
83
+ * {
84
+ margin: 0;
85
+ padding: 0;
86
+ box-sizing: border-box;
87
+ }
88
+ html,
89
+ body {
90
+ height: 100%;
91
+ width: 100%;
92
+ background: var(--bg);
93
+ color: var(--text);
69
94
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
70
- overflow: hidden; touch-action: manipulation; height: 100dvh;
95
+ overflow: hidden;
96
+ touch-action: manipulation;
97
+ height: 100dvh;
71
98
  overscroll-behavior: none;
72
- transition: background 0.3s, color 0.3s;
99
+ transition:
100
+ background 0.3s,
101
+ color 0.3s;
73
102
  }
74
103
 
75
104
  /* ===== Top Bar (unified) ===== */
@@ -79,20 +108,51 @@
79
108
  align-items: center;
80
109
  background: var(--surface);
81
110
  border-bottom: 1px solid var(--border);
82
- padding: 0 calc(4px + env(safe-area-inset-right, 0px)) 0 calc(4px + env(safe-area-inset-left, 0px));
111
+ padding: 0 calc(4px + env(safe-area-inset-right, 0px)) 0
112
+ calc(4px + env(safe-area-inset-left, 0px));
83
113
  padding-top: env(safe-area-inset-top, 0px);
84
114
  gap: 2px;
85
- transition: background 0.3s, border-color 0.3s;
115
+ transition:
116
+ background 0.3s,
117
+ border-color 0.3s;
118
+ }
119
+ #top-bar .left {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 4px;
123
+ flex: 1;
124
+ min-width: 0;
125
+ }
126
+ #top-bar .right {
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 2px;
130
+ flex-shrink: 0;
86
131
  }
87
- #top-bar .left { display: flex; align-items: center; gap: 4px; flex: 1; min-width: 0; }
88
- #top-bar .right { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
89
132
  #status-dot {
90
- width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
91
- display: inline-block; transition: background 0.3s;
133
+ width: 8px;
134
+ height: 8px;
135
+ border-radius: 50%;
136
+ background: var(--danger);
137
+ display: inline-block;
138
+ transition: background 0.3s;
139
+ }
140
+ #status-dot.connected {
141
+ background: var(--success);
142
+ }
143
+ #session-name {
144
+ font-weight: 600;
145
+ font-size: 13px;
146
+ white-space: nowrap;
147
+ overflow: hidden;
148
+ text-overflow: ellipsis;
149
+ max-width: 120px;
150
+ }
151
+ #status-text {
152
+ color: var(--text-secondary);
153
+ font-size: 11px;
154
+ white-space: nowrap;
92
155
  }
93
- #status-dot.connected { background: var(--success); }
94
- #session-name { font-weight: 600; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px; }
95
- #status-text { color: var(--text-secondary); font-size: 11px; white-space: nowrap; }
96
156
  #tab-list {
97
157
  display: flex;
98
158
  align-items: center;
@@ -105,7 +165,9 @@
105
165
  height: 100%;
106
166
  min-width: 0;
107
167
  }
108
- #tab-list::-webkit-scrollbar { display: none; }
168
+ #tab-list::-webkit-scrollbar {
169
+ display: none;
170
+ }
109
171
  .session-tab {
110
172
  display: flex;
111
173
  align-items: center;
@@ -122,40 +184,68 @@
122
184
  white-space: nowrap;
123
185
  flex-shrink: 0;
124
186
  height: 30px;
125
- transition: background 0.15s, color 0.15s;
187
+ transition:
188
+ background 0.15s,
189
+ color 0.15s;
126
190
  -webkit-tap-highlight-color: transparent;
127
191
  user-select: none;
128
192
  }
129
- .session-tab:hover { background: var(--border); color: var(--text); }
193
+ .session-tab:hover {
194
+ background: var(--border);
195
+ color: var(--text);
196
+ }
130
197
  .session-tab.active {
131
198
  background: var(--bg);
132
199
  color: var(--text);
133
200
  font-weight: 600;
134
201
  }
135
202
  .session-tab.in-split {
136
- background: rgba(0,120,212,0.1);
203
+ background: rgba(0, 120, 212, 0.1);
137
204
  color: var(--text);
138
205
  }
139
- .session-tab.dragging { opacity: 0.4; }
206
+ .session-tab.dragging {
207
+ opacity: 0.4;
208
+ }
140
209
  .tab-dot {
141
- width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
210
+ width: 8px;
211
+ height: 8px;
212
+ border-radius: 50%;
213
+ flex-shrink: 0;
214
+ }
215
+ .tab-name {
216
+ max-width: 100px;
217
+ overflow: hidden;
218
+ text-overflow: ellipsis;
142
219
  }
143
- .tab-name { max-width: 100px; overflow: hidden; text-overflow: ellipsis; }
144
220
  .tab-activity {
145
- font-size: 10px; color: var(--text-muted); flex-shrink: 0;
221
+ font-size: 10px;
222
+ color: var(--text-muted);
223
+ flex-shrink: 0;
146
224
  }
147
225
  .tab-status {
148
- width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
226
+ width: 6px;
227
+ height: 6px;
228
+ border-radius: 50%;
229
+ flex-shrink: 0;
149
230
  }
150
231
  .tab-close {
151
232
  display: none;
152
- background: none; border: none; color: var(--text-muted);
153
- font-size: 14px; cursor: pointer; padding: 0 2px; line-height: 1;
233
+ background: none;
234
+ border: none;
235
+ color: var(--text-muted);
236
+ font-size: 14px;
237
+ cursor: pointer;
238
+ padding: 0 2px;
239
+ line-height: 1;
154
240
  transition: color 0.15s;
155
241
  }
156
242
  .session-tab:hover .tab-close,
157
- .session-tab.active .tab-close { display: block; }
158
- .tab-close:hover { color: var(--danger); }
243
+ .session-tab.active .tab-close {
244
+ display: block;
245
+ }
246
+ .tab-close:hover {
247
+ color: var(--danger);
248
+ }
159
249
  /* ===== Tab Preview ===== */
160
250
  #tab-preview {
161
251
  display: none;
@@ -165,18 +255,28 @@
165
255
  background: var(--bg);
166
256
  border: 1px solid var(--border);
167
257
  border-radius: 10px;
168
- box-shadow: 0 8px 24px rgba(0,0,0,0.35);
258
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
169
259
  overflow: hidden;
170
260
  pointer-events: none;
171
261
  }
172
- #tab-preview.visible { display: block; }
262
+ #tab-preview.visible {
263
+ display: block;
264
+ }
173
265
  .preview-header {
174
- display: flex; align-items: center; gap: 6px;
266
+ display: flex;
267
+ align-items: center;
268
+ gap: 6px;
175
269
  padding: 6px 10px;
176
270
  border-bottom: 1px solid var(--border);
177
- font-size: 12px; font-weight: 600;
271
+ font-size: 12px;
272
+ font-weight: 600;
273
+ }
274
+ .preview-dot {
275
+ width: 8px;
276
+ height: 8px;
277
+ border-radius: 50%;
278
+ flex-shrink: 0;
178
279
  }
179
- .preview-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
180
280
  .preview-body {
181
281
  padding: 6px 8px;
182
282
  font-family: 'NerdFont', 'JetBrains Mono', monospace;
@@ -197,46 +297,118 @@
197
297
  }
198
298
 
199
299
  .tab-bar-btn {
200
- background: none; border: none; color: var(--text-dim);
201
- width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
202
- display: flex; align-items: center; justify-content: center;
203
- flex-shrink: 0; font-size: 18px; font-weight: 600;
204
- transition: background 0.15s, color 0.15s;
300
+ background: none;
301
+ border: none;
302
+ color: var(--text-dim);
303
+ width: 30px;
304
+ height: 30px;
305
+ border-radius: 8px;
306
+ cursor: pointer;
307
+ display: flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ flex-shrink: 0;
311
+ font-size: 18px;
312
+ font-weight: 600;
313
+ transition:
314
+ background 0.15s,
315
+ color 0.15s;
205
316
  -webkit-tap-highlight-color: transparent;
206
317
  }
207
- .tab-bar-btn:hover { background: var(--border); color: var(--text); }
208
- .tab-bar-btn:active { background: var(--accent); color: #fff; }
209
- .tab-bar-btn.active { color: var(--accent); }
318
+ .tab-bar-btn:hover {
319
+ background: var(--border);
320
+ color: var(--text);
321
+ }
322
+ .tab-bar-btn:active {
323
+ background: var(--accent);
324
+ color: #fff;
325
+ }
326
+ .tab-bar-btn.active {
327
+ color: var(--accent);
328
+ }
210
329
  #tab-new-btn {
211
- gap: 3px; width: auto; padding: 0 10px; font-size: 12px;
330
+ gap: 3px;
331
+ width: auto;
332
+ padding: 0 10px;
333
+ font-size: 12px;
334
+ }
335
+ .new-btn-label {
336
+ font-size: 11px;
337
+ font-weight: 600;
212
338
  }
213
- .new-btn-label { font-size: 11px; font-weight: 600; }
214
339
 
215
340
  .bar-btn {
216
- background: none; border: none; color: var(--text-dim);
217
- width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
218
- display: flex; align-items: center; justify-content: center;
219
- transition: background 0.15s, color 0.15s;
341
+ background: none;
342
+ border: none;
343
+ color: var(--text-dim);
344
+ width: 30px;
345
+ height: 30px;
346
+ border-radius: 8px;
347
+ cursor: pointer;
348
+ display: flex;
349
+ align-items: center;
350
+ justify-content: center;
351
+ transition:
352
+ background 0.15s,
353
+ color 0.15s;
220
354
  -webkit-tap-highlight-color: transparent;
221
355
  }
222
- .bar-btn:hover { background: var(--border); color: var(--text); }
223
- .bar-btn:active { background: var(--accent); color: #fff; }
224
- .bar-btn svg { display: block; }
356
+ .bar-btn:hover {
357
+ background: var(--border);
358
+ color: var(--text);
359
+ }
360
+ .bar-btn:active {
361
+ background: var(--accent);
362
+ color: #fff;
363
+ }
364
+ .bar-btn svg {
365
+ display: block;
366
+ }
225
367
  .bar-group {
226
- display: flex; align-items: center; background: var(--bg);
227
- border-radius: 8px; padding: 2px; gap: 1px; transition: background 0.3s;
368
+ display: flex;
369
+ align-items: center;
370
+ background: var(--bg);
371
+ border-radius: 8px;
372
+ padding: 2px;
373
+ gap: 1px;
374
+ transition: background 0.3s;
375
+ }
376
+ .bar-group .bar-btn {
377
+ width: 26px;
378
+ height: 26px;
379
+ border-radius: 6px;
380
+ font-size: 14px;
381
+ font-weight: 600;
228
382
  }
229
- .bar-group .bar-btn { width: 26px; height: 26px; border-radius: 6px; font-size: 14px; font-weight: 600; }
230
383
  #stop-btn {
231
- background: none; border: none; color: var(--danger); height: 30px;
232
- border-radius: 8px; cursor: pointer; display: flex; align-items: center;
233
- justify-content: center; gap: 4px; padding: 0 8px; font-size: 11px;
384
+ background: none;
385
+ border: none;
386
+ color: var(--danger);
387
+ height: 30px;
388
+ border-radius: 8px;
389
+ cursor: pointer;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ gap: 4px;
394
+ padding: 0 8px;
395
+ font-size: 11px;
234
396
  font-weight: 600;
235
- transition: background 0.15s, color 0.15s, transform 0.1s;
397
+ transition:
398
+ background 0.15s,
399
+ color 0.15s,
400
+ transform 0.1s;
236
401
  -webkit-tap-highlight-color: transparent;
237
402
  }
238
- #stop-btn:hover { background: var(--danger); color: #fff; }
239
- #stop-btn:active { background: var(--danger-hover); color: #fff; transform: scale(0.9); }
403
+ #stop-btn:hover {
404
+ background: var(--danger);
405
+ color: #fff;
406
+ }
407
+ #stop-btn:active {
408
+ background: var(--danger-hover);
409
+ color: #fff;
410
+ transform: scale(0.9);
411
+ }
240
412
 
241
413
  /* ===== Terminals Wrapper ===== */
242
414
  #terminals-wrapper {
@@ -248,13 +420,22 @@
248
420
  display: flex;
249
421
  overflow: hidden;
250
422
  }
251
- #terminals-wrapper.split-h { flex-direction: row; }
252
- #terminals-wrapper.split-v { flex-direction: column; }
423
+ #terminals-wrapper.split-h {
424
+ flex-direction: row;
425
+ }
426
+ #terminals-wrapper.split-v {
427
+ flex-direction: column;
428
+ }
253
429
  .terminal-pane {
254
- flex: 1; padding: 2px; overflow: hidden; position: relative;
430
+ flex: 1;
431
+ padding: 2px;
432
+ overflow: hidden;
433
+ position: relative;
255
434
  display: none;
256
435
  }
257
- .terminal-pane.visible { display: block; }
436
+ .terminal-pane.visible {
437
+ display: block;
438
+ }
258
439
  #terminals-wrapper.split-h .terminal-pane.visible + .terminal-pane.visible {
259
440
  border-left: 2px solid var(--accent);
260
441
  }
@@ -264,112 +445,284 @@
264
445
 
265
446
  /* ===== Key Bar ===== */
266
447
  #key-bar {
267
- position: fixed; bottom: 0; left: 0; right: 0;
448
+ position: fixed;
449
+ bottom: 0;
450
+ left: 0;
451
+ right: 0;
268
452
  height: calc(52px + env(safe-area-inset-bottom, 0px));
269
- display: flex; align-items: center; background: var(--surface);
453
+ display: flex;
454
+ align-items: center;
455
+ background: var(--surface);
270
456
  border-top: 1px solid var(--border);
271
- padding: 0 calc(4px + env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px) calc(4px + env(safe-area-inset-left, 0px));
272
- gap: 4px; z-index: 50;
273
- transition: background 0.3s, border-color 0.3s;
457
+ padding: 0 calc(4px + env(safe-area-inset-right, 0px)) env(safe-area-inset-bottom, 0px)
458
+ calc(4px + env(safe-area-inset-left, 0px));
459
+ gap: 4px;
460
+ z-index: 50;
461
+ transition:
462
+ background 0.3s,
463
+ border-color 0.3s;
274
464
  }
275
465
  .key-btn {
276
- min-width: 0; height: 40px; background: var(--key-bg); color: var(--text);
277
- border: 1px solid var(--key-border); border-radius: 8px; font-size: 12px;
278
- font-weight: 600; cursor: pointer; display: flex; flex-direction: column;
279
- align-items: center; justify-content: center;
280
- -webkit-tap-highlight-color: transparent; user-select: none;
281
- white-space: nowrap; padding: 2px 8px; flex: 1 1 0; gap: 0; line-height: 1;
282
- transition: background 0.15s, color 0.15s, border-color 0.15s, transform 0.1s, box-shadow 0.15s;
283
- box-shadow: 0 1px 3px var(--key-shadow), inset 0 1px 0 rgba(255,255,255,0.05);
284
- }
285
- .key-btn .hint { font-size: 8px; font-weight: 400; opacity: 0.5; margin-top: 1px; letter-spacing: 0.02em; }
286
- .key-btn:hover { background: var(--border); border-color: var(--accent); box-shadow: 0 2px 6px var(--key-shadow); }
287
- .key-btn:active { background: var(--accent); color: #fff; border-color: var(--accent); transform: scale(0.93); box-shadow: none; }
288
- .key-btn.wide { flex: 1.4 1 0; }
289
- .key-sep { width: 1px; height: 20px; background: var(--border); flex-shrink: 0; }
290
-
291
- .xterm { height: 100% !important; }
292
- .xterm-viewport { overflow-y: auto !important; scrollbar-width: none; overscroll-behavior: contain; }
293
- .xterm-viewport::-webkit-scrollbar { display: none; }
466
+ min-width: 0;
467
+ height: 40px;
468
+ background: var(--key-bg);
469
+ color: var(--text);
470
+ border: 1px solid var(--key-border);
471
+ border-radius: 8px;
472
+ font-size: 12px;
473
+ font-weight: 600;
474
+ cursor: pointer;
475
+ display: flex;
476
+ flex-direction: column;
477
+ align-items: center;
478
+ justify-content: center;
479
+ -webkit-tap-highlight-color: transparent;
480
+ user-select: none;
481
+ white-space: nowrap;
482
+ padding: 2px 8px;
483
+ flex: 1 1 0;
484
+ gap: 0;
485
+ line-height: 1;
486
+ transition:
487
+ background 0.15s,
488
+ color 0.15s,
489
+ border-color 0.15s,
490
+ transform 0.1s,
491
+ box-shadow 0.15s;
492
+ box-shadow:
493
+ 0 1px 3px var(--key-shadow),
494
+ inset 0 1px 0 rgba(255, 255, 255, 0.05);
495
+ }
496
+ .key-btn .hint {
497
+ font-size: 8px;
498
+ font-weight: 400;
499
+ opacity: 0.5;
500
+ margin-top: 1px;
501
+ letter-spacing: 0.02em;
502
+ }
503
+ .key-btn:hover {
504
+ background: var(--border);
505
+ border-color: var(--accent);
506
+ box-shadow: 0 2px 6px var(--key-shadow);
507
+ }
508
+ .key-btn:active {
509
+ background: var(--accent);
510
+ color: #fff;
511
+ border-color: var(--accent);
512
+ transform: scale(0.93);
513
+ box-shadow: none;
514
+ }
515
+ .key-btn.wide {
516
+ flex: 1.4 1 0;
517
+ }
518
+ .key-sep {
519
+ width: 1px;
520
+ height: 20px;
521
+ background: var(--border);
522
+ flex-shrink: 0;
523
+ }
524
+
525
+ .xterm {
526
+ height: 100% !important;
527
+ }
528
+ .xterm-viewport {
529
+ overflow-y: auto !important;
530
+ scrollbar-width: none;
531
+ overscroll-behavior: contain;
532
+ }
533
+ .xterm-viewport::-webkit-scrollbar {
534
+ display: none;
535
+ }
294
536
  .terminal-pane,
295
537
  .terminal-pane .xterm,
296
538
  .terminal-pane .xterm-screen,
297
539
  .terminal-pane .xterm-viewport,
298
- .terminal-pane .xterm-helper-textarea { touch-action: none; }
540
+ .terminal-pane .xterm-helper-textarea {
541
+ touch-action: none;
542
+ }
299
543
 
300
544
  /* ===== Toasts & Overlays ===== */
301
545
  #copy-toast {
302
- position: fixed; top: 48px; left: 50%;
546
+ position: fixed;
547
+ top: 48px;
548
+ left: 50%;
303
549
  transform: translateX(-50%) translateY(-8px);
304
- background: var(--surface); color: var(--text); border: 1px solid var(--border);
305
- padding: 6px 16px; border-radius: 8px; font-size: 13px; font-weight: 600;
306
- opacity: 0; pointer-events: none; transition: opacity 0.2s, transform 0.2s; z-index: 200;
550
+ background: var(--surface);
551
+ color: var(--text);
552
+ border: 1px solid var(--border);
553
+ padding: 6px 16px;
554
+ border-radius: 8px;
555
+ font-size: 13px;
556
+ font-weight: 600;
557
+ opacity: 0;
558
+ pointer-events: none;
559
+ transition:
560
+ opacity 0.2s,
561
+ transform 0.2s;
562
+ z-index: 200;
563
+ }
564
+ #copy-toast.visible {
565
+ opacity: 1;
566
+ transform: translateX(-50%) translateY(0);
307
567
  }
308
- #copy-toast.visible { opacity: 1; transform: translateX(-50%) translateY(0); }
309
568
 
310
569
  #paste-overlay {
311
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
312
- background: var(--overlay-bg); z-index: 150;
313
- flex-direction: column; align-items: center; justify-content: flex-start;
314
- padding-top: calc(80px + env(safe-area-inset-top, 0px)); gap: 12px;
570
+ display: none;
571
+ position: fixed;
572
+ top: 0;
573
+ left: 0;
574
+ right: 0;
575
+ bottom: 0;
576
+ background: var(--overlay-bg);
577
+ z-index: 150;
578
+ flex-direction: column;
579
+ align-items: center;
580
+ justify-content: flex-start;
581
+ padding-top: calc(80px + env(safe-area-inset-top, 0px));
582
+ gap: 12px;
583
+ }
584
+ #paste-overlay.visible {
585
+ display: flex;
586
+ }
587
+ #paste-overlay label {
588
+ font-size: 15px;
589
+ color: #fff;
590
+ font-weight: 600;
315
591
  }
316
- #paste-overlay.visible { display: flex; }
317
- #paste-overlay label { font-size: 15px; color: #fff; font-weight: 600; }
318
592
  #paste-input {
319
- width: 80%; max-width: 400px; min-height: 80px; background: var(--surface);
320
- color: var(--text); border: 1px solid var(--border); border-radius: 8px;
321
- padding: 10px; font-size: 14px; font-family: 'NerdFont', 'JetBrains Mono', monospace; resize: vertical;
593
+ width: 80%;
594
+ max-width: 400px;
595
+ min-height: 80px;
596
+ background: var(--surface);
597
+ color: var(--text);
598
+ border: 1px solid var(--border);
599
+ border-radius: 8px;
600
+ padding: 10px;
601
+ font-size: 14px;
602
+ font-family: 'NerdFont', 'JetBrains Mono', monospace;
603
+ resize: vertical;
322
604
  }
323
605
 
324
606
  #select-overlay {
325
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
326
- background: var(--bg); z-index: 160;
607
+ display: none;
608
+ position: fixed;
609
+ top: 0;
610
+ left: 0;
611
+ right: 0;
612
+ bottom: 0;
613
+ background: var(--bg);
614
+ z-index: 160;
327
615
  flex-direction: column;
328
616
  }
329
- #select-overlay.visible { display: flex; }
617
+ #select-overlay.visible {
618
+ display: flex;
619
+ }
330
620
  .select-overlay-header {
331
- display: flex; align-items: center; justify-content: space-between;
332
- padding: 12px 12px; border-bottom: 1px solid var(--border);
333
- font-size: 15px; font-weight: 600; color: var(--text);
621
+ display: flex;
622
+ align-items: center;
623
+ justify-content: space-between;
624
+ padding: 12px 12px;
625
+ border-bottom: 1px solid var(--border);
626
+ font-size: 15px;
627
+ font-weight: 600;
628
+ color: var(--text);
334
629
  padding-top: calc(12px + env(safe-area-inset-top, 0px));
335
- min-height: 48px; box-sizing: border-box;
630
+ min-height: 48px;
631
+ box-sizing: border-box;
336
632
  }
337
633
  .select-overlay-header button {
338
- padding: 6px 12px; border: none; border-radius: 8px;
339
- font-size: 13px; font-weight: 600; cursor: pointer;
340
- white-space: nowrap; flex-shrink: 0;
341
- transition: background 0.15s, transform 0.1s;
342
- }
343
- .select-overlay-header button:active { transform: scale(0.95); }
344
- #select-copy { background: var(--accent); color: #fff; }
345
- #select-close { background: var(--border); color: var(--text); }
634
+ padding: 6px 12px;
635
+ border: none;
636
+ border-radius: 8px;
637
+ font-size: 13px;
638
+ font-weight: 600;
639
+ cursor: pointer;
640
+ white-space: nowrap;
641
+ flex-shrink: 0;
642
+ transition:
643
+ background 0.15s,
644
+ transform 0.1s;
645
+ }
646
+ .select-overlay-header button:active {
647
+ transform: scale(0.95);
648
+ }
649
+ #select-copy {
650
+ background: var(--accent);
651
+ color: #fff;
652
+ }
653
+ #select-close {
654
+ background: var(--border);
655
+ color: var(--text);
656
+ }
346
657
  #select-content {
347
- flex: 1; overflow: auto; padding: 12px 16px;
658
+ flex: 1;
659
+ overflow: auto;
660
+ padding: 12px 16px;
348
661
  font-family: 'NerdFont', 'JetBrains Mono', monospace;
349
- font-size: 13px; line-height: 1.4; color: var(--text);
350
- white-space: pre; word-break: normal;
351
- -webkit-user-select: text; user-select: text;
662
+ font-size: 13px;
663
+ line-height: 1.4;
664
+ color: var(--text);
665
+ white-space: pre;
666
+ word-break: normal;
667
+ -webkit-user-select: text;
668
+ user-select: text;
352
669
  margin: 0;
353
670
  padding-bottom: calc(12px + env(safe-area-inset-bottom, 0px));
354
671
  }
355
672
 
356
673
  #reconnect-overlay {
357
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
358
- background: var(--overlay-bg); z-index: 100;
359
- flex-direction: column; align-items: center; justify-content: center; gap: 16px;
674
+ display: none;
675
+ position: fixed;
676
+ top: 0;
677
+ left: 0;
678
+ right: 0;
679
+ bottom: 0;
680
+ background: var(--overlay-bg);
681
+ z-index: 100;
682
+ flex-direction: column;
683
+ align-items: center;
684
+ justify-content: center;
685
+ gap: 16px;
686
+ }
687
+ #reconnect-overlay.visible {
688
+ display: flex;
689
+ }
690
+ #reconnect-overlay .msg {
691
+ font-size: 17px;
692
+ color: #fff;
693
+ }
694
+ .overlay-actions {
695
+ display: flex;
696
+ gap: 12px;
360
697
  }
361
- #reconnect-overlay.visible { display: flex; }
362
- #reconnect-overlay .msg { font-size: 17px; color: #fff; }
363
- .overlay-actions { display: flex; gap: 12px; }
364
698
  .overlay-actions button {
365
- padding: 10px 24px; border: none; border-radius: 8px; font-size: 15px;
366
- font-weight: 600; cursor: pointer; transition: background 0.15s, transform 0.1s;
699
+ padding: 10px 24px;
700
+ border: none;
701
+ border-radius: 8px;
702
+ font-size: 15px;
703
+ font-weight: 600;
704
+ cursor: pointer;
705
+ transition:
706
+ background 0.15s,
707
+ transform 0.1s;
708
+ }
709
+ .overlay-actions button:active {
710
+ transform: scale(0.95);
711
+ }
712
+ #reconnect-btn {
713
+ background: var(--accent);
714
+ color: #fff;
715
+ }
716
+ #reconnect-btn:hover {
717
+ background: var(--accent-hover);
718
+ }
719
+ #back-to-sessions {
720
+ background: rgba(255, 255, 255, 0.15);
721
+ color: #fff;
722
+ }
723
+ #back-to-sessions:hover {
724
+ background: rgba(255, 255, 255, 0.25);
367
725
  }
368
- .overlay-actions button:active { transform: scale(0.95); }
369
- #reconnect-btn { background: var(--accent); color: #fff; }
370
- #reconnect-btn:hover { background: var(--accent-hover); }
371
- #back-to-sessions { background: rgba(255,255,255,0.15); color: #fff; }
372
- #back-to-sessions:hover { background: rgba(255,255,255,0.25); }
373
726
 
374
727
  /* ===== Folder Browser ===== */
375
728
  .cwd-picker {
@@ -390,7 +743,9 @@
390
743
  flex-shrink: 0;
391
744
  display: flex;
392
745
  align-items: center;
393
- transition: background 0.15s, border-color 0.15s;
746
+ transition:
747
+ background 0.15s,
748
+ border-color 0.15s;
394
749
  }
395
750
  .cwd-browse-btn:hover {
396
751
  border-color: var(--accent);
@@ -402,7 +757,10 @@
402
757
  .browser-overlay {
403
758
  display: none;
404
759
  position: fixed;
405
- top: 0; left: 0; right: 0; bottom: 0;
760
+ top: 0;
761
+ left: 0;
762
+ right: 0;
763
+ bottom: 0;
406
764
  background: var(--overlay-bg);
407
765
  z-index: 300;
408
766
  justify-content: center;
@@ -443,8 +801,12 @@
443
801
  line-height: 1;
444
802
  transition: color 0.15s;
445
803
  }
446
- .browser-close:hover { color: var(--text); }
447
- .browser-close:active { color: var(--text); }
804
+ .browser-close:hover {
805
+ color: var(--text);
806
+ }
807
+ .browser-close:active {
808
+ color: var(--text);
809
+ }
448
810
  .browser-breadcrumb {
449
811
  padding: 8px 16px;
450
812
  display: flex;
@@ -458,195 +820,418 @@
458
820
  -webkit-overflow-scrolling: touch;
459
821
  }
460
822
  .crumb {
461
- background: none; border: none; color: var(--text-dim);
462
- font-size: 13px; cursor: pointer; padding: 4px 6px;
463
- border-radius: 4px; flex-shrink: 0;
464
- transition: background 0.15s, color 0.15s;
465
- }
466
- .crumb:active, .crumb:hover { background: var(--border); color: var(--text); }
467
- .crumb.current { color: var(--text); font-weight: 600; }
468
- .crumb-sep { color: var(--border-subtle); flex-shrink: 0; }
823
+ background: none;
824
+ border: none;
825
+ color: var(--text-dim);
826
+ font-size: 13px;
827
+ cursor: pointer;
828
+ padding: 4px 6px;
829
+ border-radius: 4px;
830
+ flex-shrink: 0;
831
+ transition:
832
+ background 0.15s,
833
+ color 0.15s;
834
+ }
835
+ .crumb:active,
836
+ .crumb:hover {
837
+ background: var(--border);
838
+ color: var(--text);
839
+ }
840
+ .crumb.current {
841
+ color: var(--text);
842
+ font-weight: 600;
843
+ }
844
+ .crumb-sep {
845
+ color: var(--border-subtle);
846
+ flex-shrink: 0;
847
+ }
469
848
  .browser-list {
470
- flex: 1; overflow-y: auto; padding: 4px 0;
849
+ flex: 1;
850
+ overflow-y: auto;
851
+ padding: 4px 0;
471
852
  -webkit-overflow-scrolling: touch;
472
853
  }
473
854
  .browser-empty {
474
- text-align: center; padding: 40px 20px;
475
- color: var(--text-muted); font-size: 14px;
855
+ text-align: center;
856
+ padding: 40px 20px;
857
+ color: var(--text-muted);
858
+ font-size: 14px;
476
859
  }
477
860
  .folder-item {
478
- display: flex; align-items: center; gap: 12px;
479
- padding: 12px 16px; cursor: pointer;
480
- border-bottom: 1px solid rgba(60,60,60,0.5);
861
+ display: flex;
862
+ align-items: center;
863
+ gap: 12px;
864
+ padding: 12px 16px;
865
+ cursor: pointer;
866
+ border-bottom: 1px solid rgba(60, 60, 60, 0.5);
481
867
  transition: background 0.1s;
482
868
  -webkit-tap-highlight-color: transparent;
483
869
  }
484
- .folder-item:active { background: rgba(0,120,212,0.2); }
485
- .folder-item:hover { background: rgba(0,120,212,0.1); }
486
- .folder-icon { font-size: 22px; flex-shrink: 0; width: 28px; text-align: center; }
870
+ .folder-item:active {
871
+ background: rgba(0, 120, 212, 0.2);
872
+ }
873
+ .folder-item:hover {
874
+ background: rgba(0, 120, 212, 0.1);
875
+ }
876
+ .folder-icon {
877
+ font-size: 22px;
878
+ flex-shrink: 0;
879
+ width: 28px;
880
+ text-align: center;
881
+ }
487
882
  .folder-name {
488
- font-size: 15px; color: var(--text); flex: 1;
489
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
883
+ font-size: 15px;
884
+ color: var(--text);
885
+ flex: 1;
886
+ overflow: hidden;
887
+ text-overflow: ellipsis;
888
+ white-space: nowrap;
889
+ }
890
+ .folder-arrow {
891
+ color: var(--border-subtle);
892
+ font-size: 18px;
893
+ flex-shrink: 0;
490
894
  }
491
- .folder-arrow { color: var(--border-subtle); font-size: 18px; flex-shrink: 0; }
492
895
  .browser-footer {
493
896
  padding: 12px 16px calc(env(safe-area-inset-bottom, 8px) + 8px);
494
897
  border-top: 1px solid var(--border);
495
- display: flex; flex-direction: column; gap: 8px;
898
+ display: flex;
899
+ flex-direction: column;
900
+ gap: 8px;
496
901
  }
497
902
  .browser-current-path {
498
- font-size: 12px; color: var(--text-dim);
499
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
903
+ font-size: 12px;
904
+ color: var(--text-dim);
905
+ overflow: hidden;
906
+ text-overflow: ellipsis;
907
+ white-space: nowrap;
500
908
  }
501
909
  .browser-select-btn {
502
- width: 100%; padding: 12px; background: var(--accent);
503
- color: #ffffff; border: none; border-radius: 10px;
504
- font-size: 16px; font-weight: 600; cursor: pointer;
505
- transition: background 0.15s, transform 0.1s;
910
+ width: 100%;
911
+ padding: 12px;
912
+ background: var(--accent);
913
+ color: #ffffff;
914
+ border: none;
915
+ border-radius: 10px;
916
+ font-size: 16px;
917
+ font-weight: 600;
918
+ cursor: pointer;
919
+ transition:
920
+ background 0.15s,
921
+ transform 0.1s;
922
+ }
923
+ .browser-select-btn:hover {
924
+ background: var(--accent-hover);
925
+ }
926
+ .browser-select-btn:active {
927
+ background: var(--accent-active);
928
+ transform: scale(0.98);
506
929
  }
507
- .browser-select-btn:hover { background: var(--accent-hover); }
508
- .browser-select-btn:active { background: var(--accent-active); transform: scale(0.98); }
509
930
 
510
931
  /* ===== New Session Modal ===== */
511
932
  .modal-overlay {
512
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
513
- background: var(--overlay-bg); z-index: 200;
514
- justify-content: center; align-items: center;
933
+ display: none;
934
+ position: fixed;
935
+ top: 0;
936
+ left: 0;
937
+ right: 0;
938
+ bottom: 0;
939
+ background: var(--overlay-bg);
940
+ z-index: 200;
941
+ justify-content: center;
942
+ align-items: center;
943
+ }
944
+ .modal-overlay.visible {
945
+ display: flex;
515
946
  }
516
- .modal-overlay.visible { display: flex; }
517
947
  .modal {
518
- background: var(--surface); border-radius: 16px; width: 90%; max-width: 500px;
519
- padding: 24px 20px; transition: background 0.3s; max-height: 90vh; overflow-y: auto;
948
+ background: var(--surface);
949
+ border-radius: 16px;
950
+ width: 90%;
951
+ max-width: 500px;
952
+ padding: 24px 20px;
953
+ transition: background 0.3s;
954
+ max-height: 90vh;
955
+ overflow-y: auto;
956
+ }
957
+ .modal h2 {
958
+ font-size: 18px;
959
+ margin-bottom: 16px;
520
960
  }
521
- .modal h2 { font-size: 18px; margin-bottom: 16px; }
522
961
  .modal label {
523
- display: block; font-size: 13px; color: var(--text-secondary);
524
- margin-bottom: 4px; margin-top: 12px;
962
+ display: block;
963
+ font-size: 13px;
964
+ color: var(--text-secondary);
965
+ margin-bottom: 4px;
966
+ margin-top: 12px;
525
967
  }
526
- .modal input, .modal select {
527
- width: 100%; padding: 10px 12px; background: var(--bg); border: 1px solid var(--border);
528
- border-radius: 8px; color: var(--text); font-size: 15px; outline: none;
529
- -webkit-appearance: none; appearance: none;
530
- transition: background 0.3s, border-color 0.15s, color 0.3s;
968
+ .modal input,
969
+ .modal select {
970
+ width: 100%;
971
+ padding: 10px 12px;
972
+ background: var(--bg);
973
+ border: 1px solid var(--border);
974
+ border-radius: 8px;
975
+ color: var(--text);
976
+ font-size: 15px;
977
+ outline: none;
978
+ -webkit-appearance: none;
979
+ appearance: none;
980
+ transition:
981
+ background 0.3s,
982
+ border-color 0.15s,
983
+ color 0.3s;
531
984
  }
532
985
  .modal select {
533
986
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23888' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
534
- background-repeat: no-repeat; background-position: right 12px center;
535
- padding-right: 32px; cursor: pointer;
987
+ background-repeat: no-repeat;
988
+ background-position: right 12px center;
989
+ padding-right: 32px;
990
+ cursor: pointer;
991
+ }
992
+ .modal input:focus,
993
+ .modal select:focus {
994
+ border-color: var(--accent);
995
+ }
996
+ .modal-actions {
997
+ display: flex;
998
+ gap: 12px;
999
+ margin-top: 20px;
536
1000
  }
537
- .modal input:focus, .modal select:focus { border-color: var(--accent); }
538
- .modal-actions { display: flex; gap: 12px; margin-top: 20px; }
539
1001
  .modal-actions button {
540
- flex: 1; padding: 12px; border: none; border-radius: 8px;
541
- font-size: 15px; font-weight: 600; cursor: pointer;
542
- transition: background 0.15s, transform 0.1s;
1002
+ flex: 1;
1003
+ padding: 12px;
1004
+ border: none;
1005
+ border-radius: 8px;
1006
+ font-size: 15px;
1007
+ font-weight: 600;
1008
+ cursor: pointer;
1009
+ transition:
1010
+ background 0.15s,
1011
+ transform 0.1s;
1012
+ }
1013
+ .modal-actions button:active {
1014
+ transform: scale(0.95);
1015
+ }
1016
+ .btn-cancel {
1017
+ background: var(--border);
1018
+ color: var(--text);
1019
+ }
1020
+ .btn-cancel:hover {
1021
+ background: var(--border-subtle);
1022
+ }
1023
+ .btn-create {
1024
+ background: var(--accent);
1025
+ color: #fff;
1026
+ }
1027
+ .btn-create:hover {
1028
+ background: var(--accent-hover);
543
1029
  }
544
- .modal-actions button:active { transform: scale(0.95); }
545
- .btn-cancel { background: var(--border); color: var(--text); }
546
- .btn-cancel:hover { background: var(--border-subtle); }
547
- .btn-create { background: var(--accent); color: #fff; }
548
- .btn-create:hover { background: var(--accent-hover); }
549
1030
 
550
- .color-picker { display: flex; gap: 8px; padding: 6px 0; flex-wrap: wrap; }
1031
+ .color-picker {
1032
+ display: flex;
1033
+ gap: 8px;
1034
+ padding: 6px 0;
1035
+ flex-wrap: wrap;
1036
+ }
551
1037
  .color-swatch {
552
- width: 32px; height: 32px; border-radius: 50%; border: 3px solid transparent;
553
- cursor: pointer; padding: 0; outline: none;
554
- transition: border-color 0.15s, transform 0.1s;
1038
+ width: 32px;
1039
+ height: 32px;
1040
+ border-radius: 50%;
1041
+ border: 3px solid transparent;
1042
+ cursor: pointer;
1043
+ padding: 0;
1044
+ outline: none;
1045
+ transition:
1046
+ border-color 0.15s,
1047
+ transform 0.1s;
555
1048
  -webkit-tap-highlight-color: transparent;
556
1049
  }
557
- .color-swatch:hover { transform: scale(1.1); }
558
- .color-swatch.selected { border-color: var(--text); transform: scale(1.15); }
1050
+ .color-swatch:hover {
1051
+ transform: scale(1.1);
1052
+ }
1053
+ .color-swatch.selected {
1054
+ border-color: var(--text);
1055
+ transform: scale(1.15);
1056
+ }
559
1057
 
560
1058
  /* ===== Sessions Side Panel (mobile) ===== */
561
1059
  #panel-toggle {
562
1060
  display: none;
563
- background: none; border: none; color: var(--text-dim);
564
- width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
565
- align-items: center; justify-content: center;
1061
+ background: none;
1062
+ border: none;
1063
+ color: var(--text-dim);
1064
+ width: 30px;
1065
+ height: 30px;
1066
+ border-radius: 8px;
1067
+ cursor: pointer;
1068
+ align-items: center;
1069
+ justify-content: center;
566
1070
  flex-shrink: 0;
567
- transition: background 0.15s, color 0.15s;
1071
+ transition:
1072
+ background 0.15s,
1073
+ color 0.15s;
568
1074
  -webkit-tap-highlight-color: transparent;
569
1075
  }
570
- #panel-toggle:hover { background: var(--border); color: var(--text); }
1076
+ #panel-toggle:hover {
1077
+ background: var(--border);
1078
+ color: var(--text);
1079
+ }
571
1080
 
572
1081
  #side-panel-backdrop {
573
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
574
- background: rgba(0,0,0,0.5); z-index: 400;
575
- opacity: 0; transition: opacity 0.25s;
1082
+ display: none;
1083
+ position: fixed;
1084
+ top: 0;
1085
+ left: 0;
1086
+ right: 0;
1087
+ bottom: 0;
1088
+ background: rgba(0, 0, 0, 0.5);
1089
+ z-index: 400;
1090
+ opacity: 0;
1091
+ transition: opacity 0.25s;
1092
+ }
1093
+ #side-panel-backdrop.visible {
1094
+ display: block;
1095
+ opacity: 1;
576
1096
  }
577
- #side-panel-backdrop.visible { display: block; opacity: 1; }
578
1097
 
579
1098
  #side-panel {
580
- position: fixed; top: 0; left: 0; bottom: 0;
1099
+ position: fixed;
1100
+ top: 0;
1101
+ left: 0;
1102
+ bottom: 0;
581
1103
  width: min(85vw, 380px);
582
1104
  background: var(--surface);
583
1105
  border-right: 1px solid var(--border);
584
1106
  z-index: 410;
585
1107
  transform: translateX(-100%);
586
- transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), background 0.3s;
587
- display: flex; flex-direction: column;
1108
+ transition:
1109
+ transform 0.25s cubic-bezier(0.4, 0, 0.2, 1),
1110
+ background 0.3s;
1111
+ display: flex;
1112
+ flex-direction: column;
588
1113
  overflow: hidden;
589
1114
  padding-top: env(safe-area-inset-top, 0px);
590
1115
  padding-left: env(safe-area-inset-left, 0px);
591
1116
  padding-bottom: env(safe-area-inset-bottom, 0px);
592
1117
  }
593
- #side-panel.open { transform: translateX(0); }
1118
+ #side-panel.open {
1119
+ transform: translateX(0);
1120
+ }
594
1121
 
595
1122
  .side-panel-header {
596
- display: flex; align-items: center; justify-content: space-between;
597
- padding: 12px 14px; border-bottom: 1px solid var(--border);
598
- font-size: 15px; font-weight: 700;
1123
+ display: flex;
1124
+ align-items: center;
1125
+ justify-content: space-between;
1126
+ padding: 12px 14px;
1127
+ border-bottom: 1px solid var(--border);
1128
+ font-size: 15px;
1129
+ font-weight: 700;
599
1130
  }
600
1131
  .side-panel-close {
601
- background: none; border: none; color: var(--text-dim);
602
- width: 30px; height: 30px; border-radius: 8px; cursor: pointer;
603
- display: flex; align-items: center; justify-content: center;
604
- font-size: 20px; transition: background 0.15s, color 0.15s;
1132
+ background: none;
1133
+ border: none;
1134
+ color: var(--text-dim);
1135
+ width: 30px;
1136
+ height: 30px;
1137
+ border-radius: 8px;
1138
+ cursor: pointer;
1139
+ display: flex;
1140
+ align-items: center;
1141
+ justify-content: center;
1142
+ font-size: 20px;
1143
+ transition:
1144
+ background 0.15s,
1145
+ color 0.15s;
605
1146
  -webkit-tap-highlight-color: transparent;
606
1147
  }
607
- .side-panel-close:hover { background: var(--border); color: var(--text); }
1148
+ .side-panel-close:hover {
1149
+ background: var(--border);
1150
+ color: var(--text);
1151
+ }
608
1152
 
609
1153
  .side-panel-list {
610
- flex: 1; overflow-y: auto; padding: 8px;
1154
+ flex: 1;
1155
+ overflow-y: auto;
1156
+ padding: 8px;
611
1157
  -webkit-overflow-scrolling: touch;
612
- display: flex; flex-direction: column; gap: 6px;
1158
+ display: flex;
1159
+ flex-direction: column;
1160
+ gap: 6px;
613
1161
  }
614
1162
 
615
1163
  .side-panel-card {
616
- background: var(--bg); border: 1px solid var(--border);
617
- border-radius: 10px; cursor: pointer; overflow: hidden;
618
- transition: background 0.15s, border-color 0.15s;
1164
+ background: var(--bg);
1165
+ border: 1px solid var(--border);
1166
+ border-radius: 10px;
1167
+ cursor: pointer;
1168
+ overflow: hidden;
1169
+ transition:
1170
+ background 0.15s,
1171
+ border-color 0.15s;
619
1172
  -webkit-tap-highlight-color: transparent;
620
1173
  }
621
- .side-panel-card:hover { border-color: var(--accent); }
622
- .side-panel-card.active { border-color: var(--accent); border-width: 2px; }
1174
+ .side-panel-card:hover {
1175
+ border-color: var(--accent);
1176
+ }
1177
+ .side-panel-card.active {
1178
+ border-color: var(--accent);
1179
+ border-width: 2px;
1180
+ }
623
1181
 
624
1182
  .side-panel-card-header {
625
- display: flex; align-items: center; gap: 8px;
1183
+ display: flex;
1184
+ align-items: center;
1185
+ gap: 8px;
626
1186
  padding: 10px 12px 6px;
627
1187
  }
628
1188
  .side-panel-card-dot {
629
- width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
1189
+ width: 10px;
1190
+ height: 10px;
1191
+ border-radius: 50%;
1192
+ flex-shrink: 0;
630
1193
  }
631
1194
  .side-panel-card-name {
632
- font-size: 13px; font-weight: 600; flex: 1;
633
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1195
+ font-size: 13px;
1196
+ font-weight: 600;
1197
+ flex: 1;
1198
+ overflow: hidden;
1199
+ text-overflow: ellipsis;
1200
+ white-space: nowrap;
634
1201
  }
635
1202
  .side-panel-card-status {
636
- width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
1203
+ width: 7px;
1204
+ height: 7px;
1205
+ border-radius: 50%;
1206
+ flex-shrink: 0;
637
1207
  }
638
1208
  .side-panel-card-close {
639
- background: none; border: none; color: var(--text-muted);
640
- width: 26px; height: 26px; border-radius: 6px; cursor: pointer;
641
- display: flex; align-items: center; justify-content: center;
642
- font-size: 16px; flex-shrink: 0; padding: 0;
643
- transition: background 0.15s, color 0.15s;
1209
+ background: none;
1210
+ border: none;
1211
+ color: var(--text-muted);
1212
+ width: 26px;
1213
+ height: 26px;
1214
+ border-radius: 6px;
1215
+ cursor: pointer;
1216
+ display: flex;
1217
+ align-items: center;
1218
+ justify-content: center;
1219
+ font-size: 16px;
1220
+ flex-shrink: 0;
1221
+ padding: 0;
1222
+ transition:
1223
+ background 0.15s,
1224
+ color 0.15s;
644
1225
  -webkit-tap-highlight-color: transparent;
645
1226
  }
646
- .side-panel-card-close:hover { background: var(--danger); color: #fff; }
1227
+ .side-panel-card-close:hover {
1228
+ background: var(--danger);
1229
+ color: #fff;
1230
+ }
647
1231
  .side-panel-card-meta {
648
1232
  padding: 0 12px 4px;
649
- font-size: 10px; color: var(--text-muted);
1233
+ font-size: 10px;
1234
+ color: var(--text-muted);
650
1235
  }
651
1236
 
652
1237
  .side-panel-card-preview {
@@ -664,22 +1249,47 @@
664
1249
  -webkit-text-size-adjust: none;
665
1250
  }
666
1251
  .side-panel-card-preview.empty {
667
- font-style: italic; color: var(--text-muted);
668
- text-align: center; font-family: inherit; font-size: 11px;
1252
+ font-style: italic;
1253
+ color: var(--text-muted);
1254
+ text-align: center;
1255
+ font-family: inherit;
1256
+ font-size: 11px;
669
1257
  white-space: normal;
670
1258
  }
671
1259
 
672
1260
  @media (max-width: 640px) {
673
- #panel-toggle { display: flex; }
674
- #tab-list { display: none; }
675
- #split-toggle { display: none; }
676
- #version-text { display: none; }
677
- #back-btn { display: none; }
678
- #theme-toggle { display: flex; }
679
- #stop-btn { padding: 0 8px; }
680
- #session-name { max-width: 30vw; }
681
- #status-text { display: none; }
682
- #tab-new-btn { font-size: 13px; padding: 0 8px; width: auto; }
1261
+ #panel-toggle {
1262
+ display: flex;
1263
+ }
1264
+ #tab-list {
1265
+ display: none;
1266
+ }
1267
+ #split-toggle {
1268
+ display: none;
1269
+ }
1270
+ #version-text {
1271
+ display: none;
1272
+ }
1273
+ #back-btn {
1274
+ display: none;
1275
+ }
1276
+ #theme-toggle {
1277
+ display: flex;
1278
+ }
1279
+ #stop-btn {
1280
+ padding: 0 8px;
1281
+ }
1282
+ #session-name {
1283
+ max-width: 30vw;
1284
+ }
1285
+ #status-text {
1286
+ display: none;
1287
+ }
1288
+ #tab-new-btn {
1289
+ font-size: 13px;
1290
+ padding: 0 8px;
1291
+ width: auto;
1292
+ }
683
1293
  }
684
1294
  </style>
685
1295
  </head>
@@ -692,8 +1302,28 @@
692
1302
  <button class="side-panel-close" id="side-panel-close" title="Close">×</button>
693
1303
  </div>
694
1304
  <div class="side-panel-list" id="side-panel-list"></div>
695
- <div style="padding:8px;border-top:1px solid var(--border)">
696
- <button id="side-panel-new-btn" style="width:100%;padding:10px;border:1px dashed var(--border);border-radius:8px;background:none;color:var(--accent);font-size:14px;font-weight:600;cursor:pointer;display:flex;align-items:center;justify-content:center;gap:6px;transition:background 0.15s">+ New Session</button>
1305
+ <div style="padding: 8px; border-top: 1px solid var(--border)">
1306
+ <button
1307
+ id="side-panel-new-btn"
1308
+ style="
1309
+ width: 100%;
1310
+ padding: 10px;
1311
+ border: 1px dashed var(--border);
1312
+ border-radius: 8px;
1313
+ background: none;
1314
+ color: var(--accent);
1315
+ font-size: 14px;
1316
+ font-weight: 600;
1317
+ cursor: pointer;
1318
+ display: flex;
1319
+ align-items: center;
1320
+ justify-content: center;
1321
+ gap: 6px;
1322
+ transition: background 0.15s;
1323
+ "
1324
+ >
1325
+ + New Session
1326
+ </button>
697
1327
  </div>
698
1328
  </div>
699
1329
 
@@ -701,28 +1331,141 @@
701
1331
  <div id="top-bar">
702
1332
  <div class="left">
703
1333
  <button id="panel-toggle" title="Sessions">
704
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
1334
+ <svg
1335
+ width="18"
1336
+ height="18"
1337
+ viewBox="0 0 24 24"
1338
+ fill="none"
1339
+ stroke="currentColor"
1340
+ stroke-width="2"
1341
+ stroke-linecap="round"
1342
+ stroke-linejoin="round"
1343
+ >
1344
+ <line x1="3" y1="6" x2="21" y2="6" />
1345
+ <line x1="3" y1="12" x2="21" y2="12" />
1346
+ <line x1="3" y1="18" x2="21" y2="18" />
1347
+ </svg>
1348
+ </button>
1349
+ <button
1350
+ class="bar-btn"
1351
+ id="back-btn"
1352
+ onclick="location.href = '/'"
1353
+ title="Back to sessions"
1354
+ >
1355
+ <svg
1356
+ width="18"
1357
+ height="18"
1358
+ viewBox="0 0 24 24"
1359
+ fill="none"
1360
+ stroke="currentColor"
1361
+ stroke-width="2"
1362
+ stroke-linecap="round"
1363
+ stroke-linejoin="round"
1364
+ >
1365
+ <polyline points="15 18 9 12 15 6" />
1366
+ </svg>
705
1367
  </button>
706
- <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>
707
1368
  <span id="status-dot"></span>
708
1369
  <span id="session-name">…</span>
709
1370
  <span id="status-text">Connecting…</span>
710
1371
  </div>
711
1372
  <div id="tab-list"></div>
712
1373
  <div class="right">
713
- <span id="version-text" style="font-size:11px;color:var(--text-muted)"></span>
714
- <button class="tab-bar-btn" id="tab-new-btn" title="New session"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span class="new-btn-label">New</span></button>
1374
+ <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
1375
+ <button class="tab-bar-btn" id="tab-new-btn" title="New session">
1376
+ <svg
1377
+ width="14"
1378
+ height="14"
1379
+ viewBox="0 0 24 24"
1380
+ fill="none"
1381
+ stroke="currentColor"
1382
+ stroke-width="2.5"
1383
+ stroke-linecap="round"
1384
+ >
1385
+ <line x1="12" y1="5" x2="12" y2="19" />
1386
+ <line x1="5" y1="12" x2="19" y2="12" /></svg
1387
+ ><span class="new-btn-label">New</span>
1388
+ </button>
715
1389
  <button class="tab-bar-btn" id="split-toggle" title="Split view">
716
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/></svg>
1390
+ <svg
1391
+ width="16"
1392
+ height="16"
1393
+ viewBox="0 0 24 24"
1394
+ fill="none"
1395
+ stroke="currentColor"
1396
+ stroke-width="2"
1397
+ stroke-linecap="round"
1398
+ stroke-linejoin="round"
1399
+ >
1400
+ <rect x="3" y="3" width="18" height="18" rx="2" />
1401
+ <line x1="12" y1="3" x2="12" y2="21" />
1402
+ </svg>
717
1403
  </button>
718
1404
  <div class="bar-group">
719
1405
  <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
720
1406
  <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
721
1407
  </div>
722
- <button class="bar-btn" id="share-btn" title="Share link"><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="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg></button>
723
- <button class="bar-btn" id="refresh-btn" title="Refresh app"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg></button>
724
- <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>
725
- <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>
1408
+ <button class="bar-btn" id="share-btn" title="Share link">
1409
+ <svg
1410
+ width="16"
1411
+ height="16"
1412
+ viewBox="0 0 24 24"
1413
+ fill="none"
1414
+ stroke="currentColor"
1415
+ stroke-width="2"
1416
+ stroke-linecap="round"
1417
+ stroke-linejoin="round"
1418
+ >
1419
+ <circle cx="18" cy="5" r="3" />
1420
+ <circle cx="6" cy="12" r="3" />
1421
+ <circle cx="18" cy="19" r="3" />
1422
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
1423
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
1424
+ </svg>
1425
+ </button>
1426
+ <button class="bar-btn" id="refresh-btn" title="Refresh app">
1427
+ <svg
1428
+ width="16"
1429
+ height="16"
1430
+ viewBox="0 0 24 24"
1431
+ fill="none"
1432
+ stroke="currentColor"
1433
+ stroke-width="2"
1434
+ stroke-linecap="round"
1435
+ stroke-linejoin="round"
1436
+ >
1437
+ <polyline points="23 4 23 10 17 10" />
1438
+ <polyline points="1 20 1 14 7 14" />
1439
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15" />
1440
+ </svg>
1441
+ </button>
1442
+ <button class="bar-btn" id="theme-toggle" title="Toggle theme">
1443
+ <svg
1444
+ width="16"
1445
+ height="16"
1446
+ viewBox="0 0 24 24"
1447
+ fill="none"
1448
+ stroke="currentColor"
1449
+ stroke-width="2"
1450
+ stroke-linecap="round"
1451
+ stroke-linejoin="round"
1452
+ >
1453
+ <circle cx="12" cy="12" r="5" />
1454
+ <line x1="12" y1="1" x2="12" y2="3" />
1455
+ <line x1="12" y1="21" x2="12" y2="23" />
1456
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
1457
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
1458
+ <line x1="1" y1="12" x2="3" y2="12" />
1459
+ <line x1="21" y1="12" x2="23" y2="12" />
1460
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
1461
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
1462
+ </svg>
1463
+ </button>
1464
+ <button id="stop-btn" title="Stop session">
1465
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none">
1466
+ <rect x="6" y="6" width="12" height="12" rx="2" /></svg
1467
+ >Stop
1468
+ </button>
726
1469
  </div>
727
1470
  </div>
728
1471
 
@@ -741,8 +1484,12 @@
741
1484
  <div id="copy-toast">Copied!</div>
742
1485
 
743
1486
  <div id="key-bar">
744
- <button class="key-btn" data-key="&#x1b;[A" title="Previous command">↑<span class="hint">prev</span></button>
745
- <button class="key-btn" data-key="&#x1b;[B" title="Next command">↓<span class="hint">next</span></button>
1487
+ <button class="key-btn" data-key="&#x1b;[A" title="Previous command">
1488
+ ↑<span class="hint">prev</span>
1489
+ </button>
1490
+ <button class="key-btn" data-key="&#x1b;[B" title="Next command">
1491
+ ↓<span class="hint">next</span>
1492
+ </button>
746
1493
  <button class="key-btn" data-key="&#x1b;[D" title="Left">←</button>
747
1494
  <button class="key-btn" data-key="&#x1b;[C" title="Right">→</button>
748
1495
  <button class="key-btn" data-key="&#x1b;[H" title="Home">Home</button>
@@ -753,15 +1500,19 @@
753
1500
  <div class="key-sep"></div>
754
1501
  <button class="key-btn wide" data-key="&#x09;" title="Autocomplete">Tab</button>
755
1502
  <div class="key-sep"></div>
756
- <button class="key-btn" data-key="&#x03;" title="Interrupt process">^C<span class="hint">stop</span></button>
1503
+ <button class="key-btn" data-key="&#x03;" title="Interrupt process">
1504
+ ^C<span class="hint">stop</span>
1505
+ </button>
757
1506
  <div class="key-sep"></div>
758
- <button class="key-btn" data-key="enter" title="Enter / Return">↵<span class="hint">enter</span></button>
1507
+ <button class="key-btn" data-key="enter" title="Enter / Return">
1508
+ ↵<span class="hint">enter</span>
1509
+ </button>
759
1510
  </div>
760
1511
 
761
1512
  <div id="reconnect-overlay">
762
1513
  <div class="msg">Session disconnected</div>
763
1514
  <div class="overlay-actions">
764
- <button id="back-to-sessions" onclick="location.href='/'">Sessions</button>
1515
+ <button id="back-to-sessions" onclick="location.href = '/'">Sessions</button>
765
1516
  <button id="reconnect-btn">Reconnect</button>
766
1517
  </div>
767
1518
  </div>
@@ -770,20 +1521,37 @@
770
1521
  <label for="paste-input">Paste your text below</label>
771
1522
  <textarea id="paste-input" placeholder="Long-press here and paste…"></textarea>
772
1523
  <div class="overlay-actions">
773
- <button id="paste-cancel" style="background:rgba(255,255,255,0.15);color:#fff;">Cancel</button>
774
- <button id="paste-send" style="background:var(--accent);color:#fff;">Send</button>
1524
+ <button id="paste-cancel" style="background: rgba(255, 255, 255, 0.15); color: #fff">
1525
+ Cancel
1526
+ </button>
1527
+ <button id="paste-send" style="background: var(--accent); color: #fff">Send</button>
775
1528
  </div>
776
1529
  </div>
777
1530
 
778
1531
  <div id="select-overlay">
779
1532
  <div class="select-overlay-header">
780
1533
  <span id="select-title">Copy Text</span>
781
- <div style="display:flex;gap:8px">
1534
+ <div style="display: flex; gap: 8px">
782
1535
  <button id="select-copy">Copy</button>
783
1536
  <button id="select-close">Done</button>
784
1537
  </div>
785
1538
  </div>
786
- <button id="select-load-more" style="display:none;width:100%;padding:8px;background:var(--surface);color:var(--accent);border:1px solid var(--border);border-radius:6px;font-size:13px;cursor:pointer;">▲ Load more</button>
1539
+ <button
1540
+ id="select-load-more"
1541
+ style="
1542
+ display: none;
1543
+ width: 100%;
1544
+ padding: 8px;
1545
+ background: var(--surface);
1546
+ color: var(--accent);
1547
+ border: 1px solid var(--border);
1548
+ border-radius: 6px;
1549
+ font-size: 13px;
1550
+ cursor: pointer;
1551
+ "
1552
+ >
1553
+ ▲ Load more
1554
+ </button>
787
1555
  <pre id="select-content"></pre>
788
1556
  </div>
789
1557
 
@@ -794,25 +1562,94 @@
794
1562
  <label for="ns-name">Name</label>
795
1563
  <input type="text" id="ns-name" placeholder="My Session" />
796
1564
  <label for="ns-shell">Shell</label>
797
- <select id="ns-shell"><option value="">Loading shells…</option></select>
798
- <label for="ns-cmd">Initial Command <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
1565
+ <select id="ns-shell">
1566
+ <option value="">Loading shells…</option>
1567
+ </select>
1568
+ <label for="ns-cmd"
1569
+ >Initial Command
1570
+ <span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
1571
+ >
799
1572
  <input type="text" id="ns-cmd" placeholder="e.g. htop, vim" />
800
1573
  <label>Color</label>
801
1574
  <div class="color-picker" id="ns-color-picker">
802
- <button type="button" class="color-swatch selected" data-color="#4a9eff" style="background:#4a9eff" title="Blue"></button>
803
- <button type="button" class="color-swatch" data-color="#4ade80" style="background:#4ade80" title="Green"></button>
804
- <button type="button" class="color-swatch" data-color="#fbbf24" style="background:#fbbf24" title="Amber"></button>
805
- <button type="button" class="color-swatch" data-color="#c084fc" style="background:#c084fc" title="Purple"></button>
806
- <button type="button" class="color-swatch" data-color="#f87171" style="background:#f87171" title="Red"></button>
807
- <button type="button" class="color-swatch" data-color="#22d3ee" style="background:#22d3ee" title="Cyan"></button>
808
- <button type="button" class="color-swatch" data-color="#fb923c" style="background:#fb923c" title="Orange"></button>
809
- <button type="button" class="color-swatch" data-color="#f472b6" style="background:#f472b6" title="Pink"></button>
1575
+ <button
1576
+ type="button"
1577
+ class="color-swatch selected"
1578
+ data-color="#4a9eff"
1579
+ style="background: #4a9eff"
1580
+ title="Blue"
1581
+ ></button>
1582
+ <button
1583
+ type="button"
1584
+ class="color-swatch"
1585
+ data-color="#4ade80"
1586
+ style="background: #4ade80"
1587
+ title="Green"
1588
+ ></button>
1589
+ <button
1590
+ type="button"
1591
+ class="color-swatch"
1592
+ data-color="#fbbf24"
1593
+ style="background: #fbbf24"
1594
+ title="Amber"
1595
+ ></button>
1596
+ <button
1597
+ type="button"
1598
+ class="color-swatch"
1599
+ data-color="#c084fc"
1600
+ style="background: #c084fc"
1601
+ title="Purple"
1602
+ ></button>
1603
+ <button
1604
+ type="button"
1605
+ class="color-swatch"
1606
+ data-color="#f87171"
1607
+ style="background: #f87171"
1608
+ title="Red"
1609
+ ></button>
1610
+ <button
1611
+ type="button"
1612
+ class="color-swatch"
1613
+ data-color="#22d3ee"
1614
+ style="background: #22d3ee"
1615
+ title="Cyan"
1616
+ ></button>
1617
+ <button
1618
+ type="button"
1619
+ class="color-swatch"
1620
+ data-color="#fb923c"
1621
+ style="background: #fb923c"
1622
+ title="Orange"
1623
+ ></button>
1624
+ <button
1625
+ type="button"
1626
+ class="color-swatch"
1627
+ data-color="#f472b6"
1628
+ style="background: #f472b6"
1629
+ title="Pink"
1630
+ ></button>
810
1631
  </div>
811
- <label for="ns-cwd">Working Directory <span style="color:var(--text-muted);font-weight:normal">(optional)</span></label>
1632
+ <label for="ns-cwd"
1633
+ >Working Directory
1634
+ <span style="color: var(--text-muted); font-weight: normal">(optional)</span></label
1635
+ >
812
1636
  <div class="cwd-picker">
813
1637
  <input type="text" id="ns-cwd" placeholder="Uses server default" />
814
1638
  <button type="button" class="cwd-browse-btn" id="ns-browse-btn" title="Browse folders">
815
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
1639
+ <svg
1640
+ width="18"
1641
+ height="18"
1642
+ viewBox="0 0 24 24"
1643
+ fill="none"
1644
+ stroke="currentColor"
1645
+ stroke-width="2"
1646
+ stroke-linecap="round"
1647
+ stroke-linejoin="round"
1648
+ >
1649
+ <path
1650
+ d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
1651
+ />
1652
+ </svg>
816
1653
  </button>
817
1654
  </div>
818
1655
  <div class="modal-actions">
@@ -827,7 +1664,21 @@
827
1664
  <div class="browser-sheet">
828
1665
  <div class="browser-header">
829
1666
  <h3>
830
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align: -3px; margin-right: 6px"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>Choose Folder
1667
+ <svg
1668
+ width="18"
1669
+ height="18"
1670
+ viewBox="0 0 24 24"
1671
+ fill="none"
1672
+ stroke="currentColor"
1673
+ stroke-width="2"
1674
+ stroke-linecap="round"
1675
+ stroke-linejoin="round"
1676
+ style="vertical-align: -3px; margin-right: 6px"
1677
+ >
1678
+ <path
1679
+ d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"
1680
+ /></svg
1681
+ >Choose Folder
831
1682
  </h3>
832
1683
  <button class="browser-close" id="ns-browser-close">×</button>
833
1684
  </div>
@@ -845,7 +1696,16 @@
845
1696
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
846
1697
  <script>
847
1698
  // ===== Constants =====
848
- const SESSION_COLORS = ['#4a9eff','#4ade80','#fbbf24','#c084fc','#f87171','#22d3ee','#fb923c','#f472b6'];
1699
+ const SESSION_COLORS = [
1700
+ '#4a9eff',
1701
+ '#4ade80',
1702
+ '#fbbf24',
1703
+ '#c084fc',
1704
+ '#f87171',
1705
+ '#22d3ee',
1706
+ '#fb923c',
1707
+ '#f472b6',
1708
+ ];
849
1709
 
850
1710
  // ===== State =====
851
1711
  const managed = new Map(); // sessionId -> ManagedSession
@@ -861,7 +1721,10 @@
861
1721
  ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
862
1722
  document.body.appendChild(ta);
863
1723
  ta.select();
864
- try { document.execCommand('copy'); showToast('Copied!'); } catch {}
1724
+ try {
1725
+ document.execCommand('copy');
1726
+ showToast('Copied!');
1727
+ } catch {}
865
1728
  document.body.removeChild(ta);
866
1729
  }
867
1730
 
@@ -875,33 +1738,66 @@
875
1738
 
876
1739
  // ===== Terminal Themes =====
877
1740
  const darkTermTheme = {
878
- background: '#1e1e1e', foreground: '#d4d4d4', cursor: '#aeafad', cursorAccent: '#1e1e1e',
1741
+ background: '#1e1e1e',
1742
+ foreground: '#d4d4d4',
1743
+ cursor: '#aeafad',
1744
+ cursorAccent: '#1e1e1e',
879
1745
  selectionBackground: 'rgba(38, 79, 120, 0.5)',
880
- black: '#000000', red: '#cd3131', green: '#0dbc79', yellow: '#e5e510',
881
- blue: '#2472c8', magenta: '#bc3fbc', cyan: '#11a8cd', white: '#e5e5e5',
882
- brightBlack: '#666666', brightRed: '#f14c4c', brightGreen: '#23d18b',
883
- brightYellow: '#f5f543', brightBlue: '#3b8eea', brightMagenta: '#d670d6',
884
- brightCyan: '#29b8db', brightWhite: '#e5e5e5',
1746
+ black: '#000000',
1747
+ red: '#cd3131',
1748
+ green: '#0dbc79',
1749
+ yellow: '#e5e510',
1750
+ blue: '#2472c8',
1751
+ magenta: '#bc3fbc',
1752
+ cyan: '#11a8cd',
1753
+ white: '#e5e5e5',
1754
+ brightBlack: '#666666',
1755
+ brightRed: '#f14c4c',
1756
+ brightGreen: '#23d18b',
1757
+ brightYellow: '#f5f543',
1758
+ brightBlue: '#3b8eea',
1759
+ brightMagenta: '#d670d6',
1760
+ brightCyan: '#29b8db',
1761
+ brightWhite: '#e5e5e5',
885
1762
  };
886
1763
  const lightTermTheme = {
887
- background: '#ffffff', foreground: '#1e1e1e', cursor: '#000000', cursorAccent: '#ffffff',
1764
+ background: '#ffffff',
1765
+ foreground: '#1e1e1e',
1766
+ cursor: '#000000',
1767
+ cursorAccent: '#ffffff',
888
1768
  selectionBackground: 'rgba(0, 120, 215, 0.3)',
889
- black: '#000000', red: '#cd3131', green: '#00bc7c', yellow: '#949800',
890
- blue: '#0451a5', magenta: '#bc05bc', cyan: '#0598bc', white: '#555555',
891
- brightBlack: '#666666', brightRed: '#cd3131', brightGreen: '#14ce14',
892
- brightYellow: '#b5ba00', brightBlue: '#0451a5', brightMagenta: '#bc05bc',
893
- brightCyan: '#0598bc', brightWhite: '#a5a5a5',
1769
+ black: '#000000',
1770
+ red: '#cd3131',
1771
+ green: '#00bc7c',
1772
+ yellow: '#949800',
1773
+ blue: '#0451a5',
1774
+ magenta: '#bc05bc',
1775
+ cyan: '#0598bc',
1776
+ white: '#555555',
1777
+ brightBlack: '#666666',
1778
+ brightRed: '#cd3131',
1779
+ brightGreen: '#14ce14',
1780
+ brightYellow: '#b5ba00',
1781
+ brightBlue: '#0451a5',
1782
+ brightMagenta: '#bc05bc',
1783
+ brightCyan: '#0598bc',
1784
+ brightWhite: '#a5a5a5',
894
1785
  };
895
1786
 
896
1787
  // ===== Theme =====
897
- function getTheme() { return localStorage.getItem('termbeam-theme') || 'dark'; }
1788
+ function getTheme() {
1789
+ return localStorage.getItem('termbeam-theme') || 'dark';
1790
+ }
898
1791
  function applyTheme(theme) {
899
1792
  document.documentElement.setAttribute('data-theme', theme);
900
- document.querySelector('meta[name="theme-color"]').content = theme === 'light' ? '#f3f3f3' : '#1e1e1e';
1793
+ document.querySelector('meta[name="theme-color"]').content =
1794
+ theme === 'light' ? '#f3f3f3' : '#1e1e1e';
901
1795
  const btn = document.getElementById('theme-toggle');
902
- if (btn) btn.innerHTML = theme === 'light'
903
- ? '<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>'
904
- : '<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>';
1796
+ if (btn)
1797
+ btn.innerHTML =
1798
+ theme === 'light'
1799
+ ? '<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>'
1800
+ : '<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>';
905
1801
  localStorage.setItem('termbeam-theme', theme);
906
1802
  for (const [, ms] of managed) {
907
1803
  ms.term.options.theme = theme === 'light' ? lightTermTheme : darkTermTheme;
@@ -913,17 +1809,39 @@
913
1809
  });
914
1810
 
915
1811
  // ===== Font Loading (non-blocking) =====
916
- const nerdFont = new FontFace('NerdFont',
917
- "url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')");
918
- nerdFont.load().then(font => { document.fonts.add(font); }).catch(() => { console.warn('Nerd Font failed to load, using fallback'); });
1812
+ const nerdFont = new FontFace(
1813
+ 'NerdFont',
1814
+ "url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')",
1815
+ );
1816
+ nerdFont
1817
+ .load()
1818
+ .then((font) => {
1819
+ document.fonts.add(font);
1820
+ })
1821
+ .catch(() => {
1822
+ console.warn('Nerd Font failed to load, using fallback');
1823
+ });
919
1824
 
920
1825
  // Start immediately — don't wait for font
921
1826
  init();
922
1827
 
923
1828
  // ===== Helpers =====
924
- function esc(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; }
925
- function escAttr(str) { return String(str).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
926
- function safeColor(c) { return /^(#[0-9a-fA-F]{3,8}|var\(--[a-z-]+\)|[a-z]+)$/.test(c) ? c : 'var(--text-muted)'; }
1829
+ function esc(str) {
1830
+ const d = document.createElement('div');
1831
+ d.textContent = str;
1832
+ return d.innerHTML;
1833
+ }
1834
+ function escAttr(str) {
1835
+ return String(str)
1836
+ .replace(/&/g, '&amp;')
1837
+ .replace(/"/g, '&quot;')
1838
+ .replace(/'/g, '&#39;')
1839
+ .replace(/</g, '&lt;')
1840
+ .replace(/>/g, '&gt;');
1841
+ }
1842
+ function safeColor(c) {
1843
+ return /^(#[0-9a-fA-F]{3,8}|var\(--[a-z-]+\)|[a-z]+)$/.test(c) ? c : 'var(--text-muted)';
1844
+ }
927
1845
 
928
1846
  function showToast(msg) {
929
1847
  const toast = document.getElementById('copy-toast');
@@ -943,7 +1861,8 @@
943
1861
  }
944
1862
 
945
1863
  // ===== Zoom =====
946
- const MIN_FONT = 2, MAX_FONT = 28;
1864
+ const MIN_FONT = 2,
1865
+ MAX_FONT = 28;
947
1866
  let fontSize = parseInt(localStorage.getItem('termbeam-fontsize') || '8', 10);
948
1867
 
949
1868
  function applyZoom(size) {
@@ -973,25 +1892,36 @@
973
1892
  function getTabOrder() {
974
1893
  let order = JSON.parse(localStorage.getItem('termbeam-tab-order') || '[]');
975
1894
  const allIds = [...managed.keys()];
976
- for (const id of allIds) { if (!order.includes(id)) order.push(id); }
977
- order = order.filter(id => managed.has(id));
1895
+ for (const id of allIds) {
1896
+ if (!order.includes(id)) order.push(id);
1897
+ }
1898
+ order = order.filter((id) => managed.has(id));
978
1899
  return order;
979
1900
  }
980
- function saveTabOrder(order) { localStorage.setItem('termbeam-tab-order', JSON.stringify(order)); }
1901
+ function saveTabOrder(order) {
1902
+ localStorage.setItem('termbeam-tab-order', JSON.stringify(order));
1903
+ }
981
1904
 
982
1905
  // ===== Init =====
983
1906
  async function init() {
984
- const sessionList = await fetch('/api/sessions').then(r => r.json());
1907
+ const sessionList = await fetch('/api/sessions').then((r) => r.json());
985
1908
  const initialId = new URLSearchParams(location.search).get('id');
986
1909
 
987
1910
  for (const s of sessionList) addSession(s);
988
1911
 
989
- const startId = (initialId && managed.has(initialId))
990
- ? initialId
991
- : (sessionList.length > 0 ? sessionList[0].id : null);
1912
+ const startId =
1913
+ initialId && managed.has(initialId)
1914
+ ? initialId
1915
+ : sessionList.length > 0
1916
+ ? sessionList[0].id
1917
+ : null;
992
1918
 
993
1919
  if (startId) activateSession(startId);
994
- else { sessionNameEl.textContent = 'No sessions'; statusText.textContent = ''; document.getElementById('stop-btn').style.display = 'none'; }
1920
+ else {
1921
+ sessionNameEl.textContent = 'No sessions';
1922
+ statusText.textContent = '';
1923
+ document.getElementById('stop-btn').style.display = 'none';
1924
+ }
995
1925
 
996
1926
  renderTabs();
997
1927
  setupKeyBar();
@@ -1004,12 +1934,17 @@
1004
1934
 
1005
1935
  // Zoom
1006
1936
  document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
1007
- document.getElementById('zoom-out').addEventListener('click', () => applyZoom(fontSize - 2));
1937
+ document
1938
+ .getElementById('zoom-out')
1939
+ .addEventListener('click', () => applyZoom(fontSize - 2));
1008
1940
 
1009
1941
  // Resize
1010
1942
  function doResize() {
1011
1943
  for (const [, ms] of managed) {
1012
- if (ms.container.classList.contains('visible')) { ms.fitAddon.fit(); sendResize(ms); }
1944
+ if (ms.container.classList.contains('visible')) {
1945
+ ms.fitAddon.fit();
1946
+ sendResize(ms);
1947
+ }
1013
1948
  }
1014
1949
  }
1015
1950
  window.addEventListener('resize', doResize);
@@ -1032,7 +1967,7 @@
1032
1967
  keyBar.style.bottom = keyboardHeight + 'px';
1033
1968
  keyBar.style.height = '52px';
1034
1969
  keyBar.style.paddingBottom = '0px';
1035
- terminalsWrapper.style.bottom = (52 + keyboardHeight) + 'px';
1970
+ terminalsWrapper.style.bottom = 52 + keyboardHeight + 'px';
1036
1971
  } else {
1037
1972
  keyBar.style.bottom = '0px';
1038
1973
  keyBar.style.height = '';
@@ -1053,9 +1988,13 @@
1053
1988
  window.visualViewport.addEventListener('scroll', onViewportScroll);
1054
1989
  // Page should never scroll — catch any browser-initiated scroll
1055
1990
  // (e.g. iOS scrolling to show focused xterm textarea behind keyboard)
1056
- window.addEventListener('scroll', () => {
1057
- if (window.scrollY !== 0) resetScroll();
1058
- }, { passive: true });
1991
+ window.addEventListener(
1992
+ 'scroll',
1993
+ () => {
1994
+ if (window.scrollY !== 0) resetScroll();
1995
+ },
1996
+ { passive: true },
1997
+ );
1059
1998
  }
1060
1999
 
1061
2000
  // Split toggle
@@ -1085,7 +2024,10 @@
1085
2024
  document.getElementById('reconnect-btn').addEventListener('click', () => {
1086
2025
  const ms = managed.get(activeId);
1087
2026
  if (ms) {
1088
- if (ms.reconnectTimer) { clearTimeout(ms.reconnectTimer); ms.reconnectTimer = null; }
2027
+ if (ms.reconnectTimer) {
2028
+ clearTimeout(ms.reconnectTimer);
2029
+ ms.reconnectTimer = null;
2030
+ }
1089
2031
  ms.exited = false;
1090
2032
  ms.reconnectDelay = 3000;
1091
2033
  ms.term.clear();
@@ -1101,9 +2043,12 @@
1101
2043
  });
1102
2044
 
1103
2045
  // Version
1104
- fetch('/api/version').then(r => r.json()).then(d => {
1105
- document.getElementById('version-text').textContent = 'v' + d.version;
1106
- }).catch(() => {});
2046
+ fetch('/api/version')
2047
+ .then((r) => r.json())
2048
+ .then((d) => {
2049
+ document.getElementById('version-text').textContent = 'v' + d.version;
2050
+ })
2051
+ .catch(() => {});
1107
2052
  }
1108
2053
 
1109
2054
  // ===== Session Management =====
@@ -1111,11 +2056,17 @@
1111
2056
  if (managed.has(data.id)) return;
1112
2057
 
1113
2058
  const term = new window.Terminal({
1114
- cursorBlink: true, fontSize: fontSize,
1115
- fontFamily: "'NerdFont', 'JetBrains Mono', 'MesloLGS NF', 'Hack Nerd Font', 'Fira Code', Menlo, monospace",
1116
- fontWeight: 'normal', fontWeightBold: 'bold', letterSpacing: 0, lineHeight: 1.1,
2059
+ cursorBlink: true,
2060
+ fontSize: fontSize,
2061
+ fontFamily:
2062
+ "'NerdFont', 'JetBrains Mono', 'MesloLGS NF', 'Hack Nerd Font', 'Fira Code', Menlo, monospace",
2063
+ fontWeight: 'normal',
2064
+ fontWeightBold: 'bold',
2065
+ letterSpacing: 0,
2066
+ lineHeight: 1.1,
1117
2067
  theme: getTheme() === 'light' ? lightTermTheme : darkTermTheme,
1118
- allowProposedApi: true, scrollback: 10000,
2068
+ allowProposedApi: true,
2069
+ scrollback: 10000,
1119
2070
  });
1120
2071
 
1121
2072
  const fitAddon = new window.FitAddon.FitAddon();
@@ -1130,46 +2081,64 @@
1130
2081
 
1131
2082
  // Pointer-event scroll handler — uses setPointerCapture so scrolling
1132
2083
  // survives xterm DOM re-renders under the finger (touch on a letter).
1133
- (function() {
1134
- let startY = null, scrolling = false, accum = 0, ptrId = null;
1135
-
1136
- container.addEventListener('pointerdown', (e) => {
1137
- if (e.pointerType !== 'touch' || ptrId !== null) return;
1138
- startY = e.clientY;
1139
- scrolling = false;
1140
- accum = 0;
1141
- ptrId = e.pointerId;
1142
- }, { capture: true });
2084
+ (function () {
2085
+ let startY = null,
2086
+ scrolling = false,
2087
+ accum = 0,
2088
+ ptrId = null;
1143
2089
 
1144
- container.addEventListener('pointermove', (e) => {
1145
- if (e.pointerId !== ptrId) return;
1146
- const y = e.clientY;
1147
- const delta = startY - y;
1148
- if (!scrolling) {
1149
- if (Math.abs(delta) > 10) {
1150
- scrolling = true;
1151
- term.clearSelection();
1152
- // Lock all future pointer events to this element —
1153
- // immune to DOM mutations, xterm re-renders, etc.
1154
- try { container.setPointerCapture(ptrId); } catch (_) {}
1155
- } else {
1156
- return;
2090
+ container.addEventListener(
2091
+ 'pointerdown',
2092
+ (e) => {
2093
+ if (e.pointerType !== 'touch' || ptrId !== null) return;
2094
+ startY = e.clientY;
2095
+ scrolling = false;
2096
+ accum = 0;
2097
+ ptrId = e.pointerId;
2098
+ },
2099
+ { capture: true },
2100
+ );
2101
+
2102
+ container.addEventListener(
2103
+ 'pointermove',
2104
+ (e) => {
2105
+ if (e.pointerId !== ptrId) return;
2106
+ const y = e.clientY;
2107
+ const delta = startY - y;
2108
+ if (!scrolling) {
2109
+ if (Math.abs(delta) > 10) {
2110
+ scrolling = true;
2111
+ term.clearSelection();
2112
+ // Lock all future pointer events to this element —
2113
+ // immune to DOM mutations, xterm re-renders, etc.
2114
+ try {
2115
+ container.setPointerCapture(ptrId);
2116
+ } catch (_) {}
2117
+ } else {
2118
+ return;
2119
+ }
1157
2120
  }
1158
- }
1159
- e.preventDefault();
1160
- e.stopPropagation();
1161
- startY = y;
1162
- const lineH = term.options.fontSize * (term.options.lineHeight || 1);
1163
- accum += delta;
1164
- const lines = Math.trunc(accum / lineH);
1165
- if (lines !== 0) { term.scrollLines(lines); accum -= lines * lineH; }
1166
- }, { capture: true, passive: false });
2121
+ e.preventDefault();
2122
+ e.stopPropagation();
2123
+ startY = y;
2124
+ const lineH = term.options.fontSize * (term.options.lineHeight || 1);
2125
+ accum += delta;
2126
+ const lines = Math.trunc(accum / lineH);
2127
+ if (lines !== 0) {
2128
+ term.scrollLines(lines);
2129
+ accum -= lines * lineH;
2130
+ }
2131
+ },
2132
+ { capture: true, passive: false },
2133
+ );
1167
2134
 
1168
2135
  function endScroll(e) {
1169
2136
  if (e.pointerId !== ptrId) return;
1170
2137
  if (scrolling) {
1171
2138
  e.stopPropagation();
1172
- try { container.releasePointerCapture(ptrId); } catch (_) {}
2139
+ try {
2140
+ container.releasePointerCapture(ptrId);
2141
+ } catch (_) {}
1173
2142
  }
1174
2143
  startY = null;
1175
2144
  scrolling = false;
@@ -1184,10 +2153,13 @@
1184
2153
  const sel = term.getSelection();
1185
2154
  if (!sel) return;
1186
2155
  if (navigator.clipboard && navigator.clipboard.writeText) {
1187
- navigator.clipboard.writeText(sel).then(() => showToast('Copied!')).catch(() => {
1188
- // Fallback for non-secure contexts (HTTP over LAN)
1189
- copyFallback(sel);
1190
- });
2156
+ navigator.clipboard
2157
+ .writeText(sel)
2158
+ .then(() => showToast('Copied!'))
2159
+ .catch(() => {
2160
+ // Fallback for non-secure contexts (HTTP over LAN)
2161
+ copyFallback(sel);
2162
+ });
1191
2163
  } else {
1192
2164
  copyFallback(sel);
1193
2165
  }
@@ -1207,19 +2179,26 @@
1207
2179
  });
1208
2180
 
1209
2181
  const ms = {
1210
- id: data.id, name: data.name,
2182
+ id: data.id,
2183
+ name: data.name,
1211
2184
  color: data.color || SESSION_COLORS[managed.size % SESSION_COLORS.length],
1212
- shell: data.shell, cwd: data.cwd, pid: data.pid,
1213
- term, fitAddon, container,
1214
- ws: null, exited: false,
1215
- reconnectTimer: null, reconnectDelay: 3000,
2185
+ shell: data.shell,
2186
+ cwd: data.cwd,
2187
+ pid: data.pid,
2188
+ term,
2189
+ fitAddon,
2190
+ container,
2191
+ ws: null,
2192
+ exited: false,
2193
+ reconnectTimer: null,
2194
+ reconnectDelay: 3000,
1216
2195
  lastActivity: data.lastActivity || Date.now(),
1217
2196
  };
1218
2197
 
1219
2198
  managed.set(data.id, ms);
1220
2199
 
1221
2200
  // Terminal input → WebSocket
1222
- term.onData(input => {
2201
+ term.onData((input) => {
1223
2202
  if (ms.ws && ms.ws.readyState === 1) {
1224
2203
  ms.ws.send(JSON.stringify({ type: 'input', data: input }));
1225
2204
  }
@@ -1230,8 +2209,15 @@
1230
2209
  }
1231
2210
 
1232
2211
  function connectSession(ms) {
1233
- if (ms.reconnectTimer) { clearTimeout(ms.reconnectTimer); ms.reconnectTimer = null; }
1234
- if (ms.ws) { try { ms.ws.close(); } catch {} }
2212
+ if (ms.reconnectTimer) {
2213
+ clearTimeout(ms.reconnectTimer);
2214
+ ms.reconnectTimer = null;
2215
+ }
2216
+ if (ms.ws) {
2217
+ try {
2218
+ ms.ws.close();
2219
+ } catch {}
2220
+ }
1235
2221
 
1236
2222
  const proto = location.protocol === 'https:' ? 'wss' : 'ws';
1237
2223
  const ws = new WebSocket(`${proto}://${location.host}/ws`);
@@ -1260,20 +2246,27 @@
1260
2246
  if (ms.container.classList.contains('visible')) {
1261
2247
  sendResize(ms);
1262
2248
  }
1263
- if (ms.id === activeId) { statusDot.className = 'connected'; statusText.textContent = ''; }
2249
+ if (ms.id === activeId) {
2250
+ statusDot.className = 'connected';
2251
+ statusText.textContent = '';
2252
+ }
1264
2253
  } else if (msg.type === 'exit') {
1265
2254
  ms.exited = true;
1266
2255
  renderTabs();
1267
2256
  if (ms.id === activeId) {
1268
2257
  statusText.textContent = 'Exited (code ' + msg.code + ')';
1269
2258
  statusDot.className = '';
1270
- reconnectOverlay.querySelector('.msg').textContent = 'Session exited (code ' + msg.code + ')';
2259
+ reconnectOverlay.querySelector('.msg').textContent =
2260
+ 'Session exited (code ' + msg.code + ')';
1271
2261
  reconnectOverlay.classList.add('visible');
1272
2262
  }
1273
2263
  } else if (msg.type === 'error') {
1274
2264
  if (msg.message === 'Session not found') {
1275
2265
  ms.exited = true;
1276
- if (ms.reconnectTimer) { clearTimeout(ms.reconnectTimer); ms.reconnectTimer = null; }
2266
+ if (ms.reconnectTimer) {
2267
+ clearTimeout(ms.reconnectTimer);
2268
+ ms.reconnectTimer = null;
2269
+ }
1277
2270
  renderTabs();
1278
2271
  }
1279
2272
  if (ms.id === activeId) {
@@ -1289,7 +2282,10 @@
1289
2282
 
1290
2283
  ws.onclose = () => {
1291
2284
  if (ms.ws !== ws) return;
1292
- if (ms.id === activeId) { statusDot.className = ''; statusText.textContent = 'Disconnected'; }
2285
+ if (ms.id === activeId) {
2286
+ statusDot.className = '';
2287
+ statusText.textContent = 'Disconnected';
2288
+ }
1293
2289
  if (!ms.exited) {
1294
2290
  ms.reconnectTimer = setTimeout(() => {
1295
2291
  ms.reconnectDelay = Math.min(ms.reconnectDelay * 1.5, 30000);
@@ -1345,19 +2341,28 @@
1345
2341
  }
1346
2342
  stopBtn.style.display = '';
1347
2343
  sessionNameEl.textContent = ms.name;
1348
- statusDot.className = (ms.ws && ms.ws.readyState === 1) ? 'connected' : '';
1349
- statusText.textContent = (ms.ws && ms.ws.readyState === 1) ? '' : (ms.exited ? 'Exited' : 'Disconnected');
2344
+ statusDot.className = ms.ws && ms.ws.readyState === 1 ? 'connected' : '';
2345
+ statusText.textContent =
2346
+ ms.ws && ms.ws.readyState === 1 ? '' : ms.exited ? 'Exited' : 'Disconnected';
1350
2347
  }
1351
2348
 
1352
2349
  async function removeSession(id) {
1353
2350
  const ms = managed.get(id);
1354
2351
  if (ms) {
1355
2352
  ms.exited = true;
1356
- if (ms.reconnectTimer) { clearTimeout(ms.reconnectTimer); ms.reconnectTimer = null; }
1357
- if (ms.ws) try { ms.ws.close(); } catch {}
2353
+ if (ms.reconnectTimer) {
2354
+ clearTimeout(ms.reconnectTimer);
2355
+ ms.reconnectTimer = null;
2356
+ }
2357
+ if (ms.ws)
2358
+ try {
2359
+ ms.ws.close();
2360
+ } catch {}
1358
2361
  }
1359
2362
 
1360
- try { await fetch('/api/sessions/' + encodeURIComponent(id), { method: 'DELETE' }); } catch {}
2363
+ try {
2364
+ await fetch('/api/sessions/' + encodeURIComponent(id), { method: 'DELETE' });
2365
+ } catch {}
1361
2366
 
1362
2367
  if (ms) {
1363
2368
  ms.term.dispose();
@@ -1384,25 +2389,44 @@
1384
2389
  function renderTabs() {
1385
2390
  const order = getTabOrder();
1386
2391
 
1387
- tabListEl.innerHTML = order.map(id => {
1388
- const ms = managed.get(id);
1389
- if (!ms) return '';
1390
- const isActive = id === activeId;
1391
- const isSplit = splitMode && id === splitSecondId;
1392
- const statusColor = ms.exited ? 'var(--danger)'
1393
- : (ms.ws && ms.ws.readyState === 1 ? 'var(--success)' : 'var(--text-muted)');
1394
- const activity = getActivityLabel(ms.lastActivity);
1395
- let cls = 'session-tab';
1396
- if (isActive) cls += ' active';
1397
- if (isSplit) cls += ' in-split';
1398
- return '<button class="' + cls + '" data-id="' + escAttr(id) + '" draggable="true">'
1399
- + '<span class="tab-dot" style="background:' + safeColor(ms.color) + '"></span>'
1400
- + '<span class="tab-name">' + esc(ms.name) + '</span>'
1401
- + (activity ? '<span class="tab-activity">' + activity + '</span>' : '')
1402
- + '<span class="tab-status" style="background:' + safeColor(statusColor) + '"></span>'
1403
- + '<span class="tab-close" data-close="' + escAttr(id) + '">×</span>'
1404
- + '</button>';
1405
- }).join('');
2392
+ tabListEl.innerHTML = order
2393
+ .map((id) => {
2394
+ const ms = managed.get(id);
2395
+ if (!ms) return '';
2396
+ const isActive = id === activeId;
2397
+ const isSplit = splitMode && id === splitSecondId;
2398
+ const statusColor = ms.exited
2399
+ ? 'var(--danger)'
2400
+ : ms.ws && ms.ws.readyState === 1
2401
+ ? 'var(--success)'
2402
+ : 'var(--text-muted)';
2403
+ const activity = getActivityLabel(ms.lastActivity);
2404
+ let cls = 'session-tab';
2405
+ if (isActive) cls += ' active';
2406
+ if (isSplit) cls += ' in-split';
2407
+ return (
2408
+ '<button class="' +
2409
+ cls +
2410
+ '" data-id="' +
2411
+ escAttr(id) +
2412
+ '" draggable="true">' +
2413
+ '<span class="tab-dot" style="background:' +
2414
+ safeColor(ms.color) +
2415
+ '"></span>' +
2416
+ '<span class="tab-name">' +
2417
+ esc(ms.name) +
2418
+ '</span>' +
2419
+ (activity ? '<span class="tab-activity">' + activity + '</span>' : '') +
2420
+ '<span class="tab-status" style="background:' +
2421
+ safeColor(statusColor) +
2422
+ '"></span>' +
2423
+ '<span class="tab-close" data-close="' +
2424
+ escAttr(id) +
2425
+ '">×</span>' +
2426
+ '</button>'
2427
+ );
2428
+ })
2429
+ .join('');
1406
2430
 
1407
2431
  attachTabHandlers();
1408
2432
  initTabDrag();
@@ -1423,7 +2447,10 @@
1423
2447
  let lastNonEmpty = -1;
1424
2448
  for (let i = totalRows - 1; i >= 0; i--) {
1425
2449
  const line = buf.getLine(i);
1426
- if (line && line.translateToString(true).trim() !== '') { lastNonEmpty = i; break; }
2450
+ if (line && line.translateToString(true).trim() !== '') {
2451
+ lastNonEmpty = i;
2452
+ break;
2453
+ }
1427
2454
  }
1428
2455
  if (lastNonEmpty < 0) return [];
1429
2456
  const start = Math.max(0, lastNonEmpty - count + 1);
@@ -1465,12 +2492,15 @@
1465
2492
  function hidePreview() {
1466
2493
  previewEl.classList.remove('visible');
1467
2494
  previewVisible = false;
1468
- if (previewTimer) { clearTimeout(previewTimer); previewTimer = null; }
2495
+ if (previewTimer) {
2496
+ clearTimeout(previewTimer);
2497
+ previewTimer = null;
2498
+ }
1469
2499
  }
1470
2500
 
1471
2501
  function attachTabHandlers() {
1472
- tabListEl.querySelectorAll('.session-tab').forEach(tab => {
1473
- tab.addEventListener('click', e => {
2502
+ tabListEl.querySelectorAll('.session-tab').forEach((tab) => {
2503
+ tab.addEventListener('click', (e) => {
1474
2504
  if (e.target.dataset.close) return;
1475
2505
  hidePreview();
1476
2506
  activateSession(tab.dataset.id);
@@ -1483,8 +2513,8 @@
1483
2513
  });
1484
2514
  tab.addEventListener('mouseleave', () => hidePreview());
1485
2515
  });
1486
- tabListEl.querySelectorAll('.tab-close').forEach(btn => {
1487
- btn.addEventListener('click', e => {
2516
+ tabListEl.querySelectorAll('.tab-close').forEach((btn) => {
2517
+ btn.addEventListener('click', (e) => {
1488
2518
  e.stopPropagation();
1489
2519
  if (confirm('Close this session?')) removeSession(btn.dataset.close);
1490
2520
  });
@@ -1509,30 +2539,50 @@
1509
2539
 
1510
2540
  function renderSidePanel() {
1511
2541
  const order = getTabOrder();
1512
- sidePanelList.innerHTML = order.map(id => {
1513
- const ms = managed.get(id);
1514
- if (!ms) return '';
1515
- const isActive = id === activeId;
1516
- const statusColor = ms.exited ? 'var(--danger)'
1517
- : (ms.ws && ms.ws.readyState === 1 ? 'var(--success)' : 'var(--text-muted)');
1518
- const activity = getActivityLabel(ms.lastActivity);
1519
- const lines = getTerminalLines(ms, 6);
1520
- const previewContent = lines.length > 0
1521
- ? '<div class="side-panel-card-preview">' + esc(lines.join('\n')) + '</div>'
1522
- : '<div class="side-panel-card-preview empty">No output yet</div>';
1523
- return '<div class="side-panel-card' + (isActive ? ' active' : '') + '" data-id="' + escAttr(id) + '">'
1524
- + '<div class="side-panel-card-header">'
1525
- + '<span class="side-panel-card-dot" style="background:' + safeColor(ms.color) + '"></span>'
1526
- + '<span class="side-panel-card-name">' + esc(ms.name) + '</span>'
1527
- + '<span class="side-panel-card-status" style="background:' + safeColor(statusColor) + '"></span>'
1528
- + '<button class="side-panel-card-close" data-close-id="' + escAttr(id) + '" title="Close session">×</button>'
1529
- + '</div>'
1530
- + (activity ? '<div class="side-panel-card-meta">' + activity + ' ago</div>' : '')
1531
- + previewContent
1532
- + '</div>';
1533
- }).join('');
1534
-
1535
- sidePanelList.querySelectorAll('.side-panel-card-close').forEach(btn => {
2542
+ sidePanelList.innerHTML = order
2543
+ .map((id) => {
2544
+ const ms = managed.get(id);
2545
+ if (!ms) return '';
2546
+ const isActive = id === activeId;
2547
+ const statusColor = ms.exited
2548
+ ? 'var(--danger)'
2549
+ : ms.ws && ms.ws.readyState === 1
2550
+ ? 'var(--success)'
2551
+ : 'var(--text-muted)';
2552
+ const activity = getActivityLabel(ms.lastActivity);
2553
+ const lines = getTerminalLines(ms, 6);
2554
+ const previewContent =
2555
+ lines.length > 0
2556
+ ? '<div class="side-panel-card-preview">' + esc(lines.join('\n')) + '</div>'
2557
+ : '<div class="side-panel-card-preview empty">No output yet</div>';
2558
+ return (
2559
+ '<div class="side-panel-card' +
2560
+ (isActive ? ' active' : '') +
2561
+ '" data-id="' +
2562
+ escAttr(id) +
2563
+ '">' +
2564
+ '<div class="side-panel-card-header">' +
2565
+ '<span class="side-panel-card-dot" style="background:' +
2566
+ safeColor(ms.color) +
2567
+ '"></span>' +
2568
+ '<span class="side-panel-card-name">' +
2569
+ esc(ms.name) +
2570
+ '</span>' +
2571
+ '<span class="side-panel-card-status" style="background:' +
2572
+ safeColor(statusColor) +
2573
+ '"></span>' +
2574
+ '<button class="side-panel-card-close" data-close-id="' +
2575
+ escAttr(id) +
2576
+ '" title="Close session">×</button>' +
2577
+ '</div>' +
2578
+ (activity ? '<div class="side-panel-card-meta">' + activity + ' ago</div>' : '') +
2579
+ previewContent +
2580
+ '</div>'
2581
+ );
2582
+ })
2583
+ .join('');
2584
+
2585
+ sidePanelList.querySelectorAll('.side-panel-card-close').forEach((btn) => {
1536
2586
  btn.addEventListener('click', async (e) => {
1537
2587
  e.stopPropagation();
1538
2588
  const id = btn.dataset.closeId;
@@ -1543,7 +2593,7 @@
1543
2593
  });
1544
2594
  });
1545
2595
 
1546
- sidePanelList.querySelectorAll('.side-panel-card').forEach(card => {
2596
+ sidePanelList.querySelectorAll('.side-panel-card').forEach((card) => {
1547
2597
  card.addEventListener('click', () => {
1548
2598
  activateSession(card.dataset.id);
1549
2599
  closeSidePanel();
@@ -1560,40 +2610,45 @@
1560
2610
  let dragId = null;
1561
2611
  let dragEl = null;
1562
2612
 
1563
- tabListEl.querySelectorAll('.session-tab').forEach(tab => {
2613
+ tabListEl.querySelectorAll('.session-tab').forEach((tab) => {
1564
2614
  // --- Touch drag (long press) + preview ---
1565
2615
  let longPressTimer = null;
1566
2616
  let previewHoldTimer = null;
1567
- let startX = 0, startY = 0;
2617
+ let startX = 0,
2618
+ startY = 0;
1568
2619
  let isDragging = false;
1569
2620
  let didPreview = false;
1570
2621
 
1571
- tab.addEventListener('touchstart', e => {
1572
- startX = e.touches[0].clientX;
1573
- startY = e.touches[0].clientY;
1574
- didPreview = false;
1575
-
1576
- // 200ms: show preview (if not active tab)
1577
- if (tab.dataset.id !== activeId) {
1578
- previewHoldTimer = setTimeout(() => {
1579
- didPreview = true;
1580
- showPreview(tab, tab.dataset.id);
1581
- if (navigator.vibrate) navigator.vibrate(30);
1582
- }, 200);
1583
- }
2622
+ tab.addEventListener(
2623
+ 'touchstart',
2624
+ (e) => {
2625
+ startX = e.touches[0].clientX;
2626
+ startY = e.touches[0].clientY;
2627
+ didPreview = false;
2628
+
2629
+ // 200ms: show preview (if not active tab)
2630
+ if (tab.dataset.id !== activeId) {
2631
+ previewHoldTimer = setTimeout(() => {
2632
+ didPreview = true;
2633
+ showPreview(tab, tab.dataset.id);
2634
+ if (navigator.vibrate) navigator.vibrate(30);
2635
+ }, 200);
2636
+ }
1584
2637
 
1585
- // 600ms: enter drag mode (dismiss preview)
1586
- longPressTimer = setTimeout(() => {
1587
- hidePreview();
1588
- isDragging = true;
1589
- dragId = tab.dataset.id;
1590
- dragEl = tab;
1591
- tab.classList.add('dragging');
1592
- if (navigator.vibrate) navigator.vibrate(50);
1593
- }, 600);
1594
- }, { passive: true });
1595
-
1596
- tab.addEventListener('touchmove', e => {
2638
+ // 600ms: enter drag mode (dismiss preview)
2639
+ longPressTimer = setTimeout(() => {
2640
+ hidePreview();
2641
+ isDragging = true;
2642
+ dragId = tab.dataset.id;
2643
+ dragEl = tab;
2644
+ tab.classList.add('dragging');
2645
+ if (navigator.vibrate) navigator.vibrate(50);
2646
+ }, 600);
2647
+ },
2648
+ { passive: true },
2649
+ );
2650
+
2651
+ tab.addEventListener('touchmove', (e) => {
1597
2652
  const dx = Math.abs(e.touches[0].clientX - startX);
1598
2653
  const dy = Math.abs(e.touches[0].clientY - startY);
1599
2654
  if (!isDragging && (dx > 10 || dy > 10)) {
@@ -1626,7 +2681,9 @@
1626
2681
  isDragging = false;
1627
2682
  dragEl.classList.remove('dragging');
1628
2683
  // Save new order from DOM
1629
- const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(t => t.dataset.id);
2684
+ const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
2685
+ (t) => t.dataset.id,
2686
+ );
1630
2687
  saveTabOrder(newOrder);
1631
2688
  dragId = null;
1632
2689
  dragEl = null;
@@ -1634,19 +2691,19 @@
1634
2691
  });
1635
2692
 
1636
2693
  // --- Desktop drag ---
1637
- tab.addEventListener('dragstart', e => {
2694
+ tab.addEventListener('dragstart', (e) => {
1638
2695
  dragId = tab.dataset.id;
1639
2696
  dragEl = tab;
1640
2697
  tab.classList.add('dragging');
1641
2698
  e.dataTransfer.effectAllowed = 'move';
1642
2699
  });
1643
2700
 
1644
- tab.addEventListener('dragover', e => {
2701
+ tab.addEventListener('dragover', (e) => {
1645
2702
  e.preventDefault();
1646
2703
  e.dataTransfer.dropEffect = 'move';
1647
2704
  });
1648
2705
 
1649
- tab.addEventListener('drop', e => {
2706
+ tab.addEventListener('drop', (e) => {
1650
2707
  e.preventDefault();
1651
2708
  if (dragId && tab.dataset.id !== dragId) {
1652
2709
  const rect = tab.getBoundingClientRect();
@@ -1655,7 +2712,9 @@
1655
2712
  } else {
1656
2713
  tab.parentNode.insertBefore(dragEl, tab.nextSibling);
1657
2714
  }
1658
- const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(t => t.dataset.id);
2715
+ const newOrder = [...tabListEl.querySelectorAll('.session-tab')].map(
2716
+ (t) => t.dataset.id,
2717
+ );
1659
2718
  saveTabOrder(newOrder);
1660
2719
  }
1661
2720
  });
@@ -1676,7 +2735,7 @@
1676
2735
  if (splitMode) {
1677
2736
  // Find a second session to show
1678
2737
  const order = getTabOrder();
1679
- const others = order.filter(id => id !== activeId);
2738
+ const others = order.filter((id) => id !== activeId);
1680
2739
  splitSecondId = others.length > 0 ? others[0] : null;
1681
2740
 
1682
2741
  if (splitSecondId) {
@@ -1695,7 +2754,10 @@
1695
2754
 
1696
2755
  requestAnimationFrame(() => {
1697
2756
  for (const [, ms] of managed) {
1698
- if (ms.container.classList.contains('visible')) { ms.fitAddon.fit(); sendResize(ms); }
2757
+ if (ms.container.classList.contains('visible')) {
2758
+ ms.fitAddon.fit();
2759
+ sendResize(ms);
2760
+ }
1699
2761
  }
1700
2762
  });
1701
2763
 
@@ -1733,19 +2795,32 @@
1733
2795
  }
1734
2796
 
1735
2797
  let keyBarTouched = false;
1736
- keyBar.addEventListener('mousedown', e => {
1737
- if (keyBarTouched) { keyBarTouched = false; return; }
2798
+ keyBar.addEventListener('mousedown', (e) => {
2799
+ if (keyBarTouched) {
2800
+ keyBarTouched = false;
2801
+ return;
2802
+ }
1738
2803
  const btn = e.target.closest('.key-btn');
1739
- if (btn && btn.dataset.key) { e.preventDefault(); startRepeat(btn); }
2804
+ if (btn && btn.dataset.key) {
2805
+ e.preventDefault();
2806
+ startRepeat(btn);
2807
+ }
1740
2808
  });
1741
2809
  keyBar.addEventListener('mouseup', stopRepeat);
1742
2810
  keyBar.addEventListener('mouseleave', stopRepeat);
1743
2811
 
1744
- keyBar.addEventListener('touchstart', e => {
1745
- keyBarTouched = true;
1746
- const btn = e.target.closest('.key-btn');
1747
- if (btn && btn.dataset.key) { e.preventDefault(); startRepeat(btn); }
1748
- }, { passive: false });
2812
+ keyBar.addEventListener(
2813
+ 'touchstart',
2814
+ (e) => {
2815
+ keyBarTouched = true;
2816
+ const btn = e.target.closest('.key-btn');
2817
+ if (btn && btn.dataset.key) {
2818
+ e.preventDefault();
2819
+ startRepeat(btn);
2820
+ }
2821
+ },
2822
+ { passive: false },
2823
+ );
1749
2824
  keyBar.addEventListener('touchend', (e) => {
1750
2825
  const btn = e.target.closest('.key-btn');
1751
2826
  if (btn && btn.dataset.key) e.preventDefault();
@@ -1753,9 +2828,12 @@
1753
2828
  });
1754
2829
  keyBar.addEventListener('touchcancel', stopRepeat);
1755
2830
 
1756
- keyBar.addEventListener('click', e => {
2831
+ keyBar.addEventListener('click', (e) => {
1757
2832
  const btn = e.target.closest('.key-btn');
1758
- if (btn) { const ms = managed.get(activeId); if (ms) ms.term.focus(); }
2833
+ if (btn) {
2834
+ const ms = managed.get(activeId);
2835
+ if (ms) ms.term.focus();
2836
+ }
1759
2837
  });
1760
2838
  }
1761
2839
 
@@ -1793,7 +2871,7 @@
1793
2871
  try {
1794
2872
  const items = await navigator.clipboard.read();
1795
2873
  for (const item of items) {
1796
- const imageType = item.types.find(t => t.startsWith('image/'));
2874
+ const imageType = item.types.find((t) => t.startsWith('image/'));
1797
2875
  if (imageType) {
1798
2876
  const blob = await item.getType(imageType);
1799
2877
  const res = await fetch('/api/upload', {
@@ -1826,9 +2904,12 @@
1826
2904
  pasteTouched = true;
1827
2905
  handlePaste();
1828
2906
  });
1829
- pasteBtn.addEventListener('mousedown', e => e.preventDefault());
2907
+ pasteBtn.addEventListener('mousedown', (e) => e.preventDefault());
1830
2908
  pasteBtn.addEventListener('click', () => {
1831
- if (pasteTouched) { pasteTouched = false; return; }
2909
+ if (pasteTouched) {
2910
+ pasteTouched = false;
2911
+ return;
2912
+ }
1832
2913
  handlePaste();
1833
2914
  });
1834
2915
 
@@ -1885,9 +2966,10 @@
1885
2966
  // Show line count in title
1886
2967
  const title = document.getElementById('select-title');
1887
2968
  const shown = allLines.length - loadedFrom;
1888
- title.textContent = allLines.length <= PAGE_SIZE
1889
- ? `Copy Text (${allLines.length} lines)`
1890
- : `Copy Text (${shown}/${allLines.length} lines)`;
2969
+ title.textContent =
2970
+ allLines.length <= PAGE_SIZE
2971
+ ? `Copy Text (${allLines.length} lines)`
2972
+ : `Copy Text (${shown}/${allLines.length} lines)`;
1891
2973
 
1892
2974
  selectBtn.style.display = 'none';
1893
2975
  selectOverlay.classList.add('visible');
@@ -1911,18 +2993,21 @@
1911
2993
  title.textContent = `Copy Text (${shown}/${allLines.length} lines)`;
1912
2994
  });
1913
2995
 
1914
- selectBtn.addEventListener('mousedown', e => e.preventDefault());
2996
+ selectBtn.addEventListener('mousedown', (e) => e.preventDefault());
1915
2997
  selectBtn.addEventListener('click', openSelectOverlay);
1916
2998
 
1917
2999
  document.getElementById('select-copy').addEventListener('click', () => {
1918
3000
  // Copy finger selection if any, otherwise copy all loaded text
1919
3001
  const sel = window.getSelection();
1920
- const text = (sel && sel.toString()) ? sel.toString() : selectContent.textContent;
3002
+ const text = sel && sel.toString() ? sel.toString() : selectContent.textContent;
1921
3003
  if (!text) return;
1922
3004
  if (navigator.clipboard && navigator.clipboard.writeText) {
1923
- navigator.clipboard.writeText(text).then(() => showToast('Copied!')).catch(() => {
1924
- copyFallback(text);
1925
- });
3005
+ navigator.clipboard
3006
+ .writeText(text)
3007
+ .then(() => showToast('Copied!'))
3008
+ .catch(() => {
3009
+ copyFallback(text);
3010
+ });
1926
3011
  } else {
1927
3012
  copyFallback(text);
1928
3013
  }
@@ -1989,15 +3074,18 @@
1989
3074
  document.getElementById('ns-cancel').addEventListener('click', () => {
1990
3075
  document.getElementById('new-session-modal').classList.remove('visible');
1991
3076
  });
1992
- document.getElementById('new-session-modal').addEventListener('click', e => {
1993
- if (e.target.id === 'new-session-modal') document.getElementById('new-session-modal').classList.remove('visible');
3077
+ document.getElementById('new-session-modal').addEventListener('click', (e) => {
3078
+ if (e.target.id === 'new-session-modal')
3079
+ document.getElementById('new-session-modal').classList.remove('visible');
1994
3080
  });
1995
3081
 
1996
3082
  // Color picker
1997
- document.getElementById('ns-color-picker').addEventListener('click', e => {
3083
+ document.getElementById('ns-color-picker').addEventListener('click', (e) => {
1998
3084
  const swatch = e.target.closest('.color-swatch');
1999
3085
  if (!swatch) return;
2000
- document.querySelectorAll('#ns-color-picker .color-swatch').forEach(s => s.classList.remove('selected'));
3086
+ document
3087
+ .querySelectorAll('#ns-color-picker .color-swatch')
3088
+ .forEach((s) => s.classList.remove('selected'));
2001
3089
  swatch.classList.add('selected');
2002
3090
  });
2003
3091
 
@@ -2015,7 +3103,7 @@
2015
3103
  document.getElementById('ns-browse-btn').addEventListener('click', async () => {
2016
3104
  if (serverCwd === '/') {
2017
3105
  try {
2018
- const data = await fetch('/api/shells').then(r => r.json());
3106
+ const data = await fetch('/api/shells').then((r) => r.json());
2019
3107
  if (data.cwd) serverCwd = data.cwd;
2020
3108
  } catch {}
2021
3109
  }
@@ -2046,7 +3134,9 @@
2046
3134
  const data = await res.json();
2047
3135
  let items = '';
2048
3136
  // Add parent (..) entry unless at root
2049
- const parent = dir.replace(/[/\\][^/\\]+$/, '') || (dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
3137
+ const parent =
3138
+ dir.replace(/[/\\][^/\\]+$/, '') ||
3139
+ (dir.includes('\\') ? dir.match(/^[A-Za-z]:\\/)?.[0] : '/');
2050
3140
  if (parent && parent !== dir) {
2051
3141
  items += `<div class="folder-item" data-path="${escAttr(parent)}">
2052
3142
  <span class="folder-icon">📁</span>
@@ -2054,16 +3144,18 @@
2054
3144
  <span class="folder-arrow">›</span>
2055
3145
  </div>`;
2056
3146
  }
2057
- items += data.dirs.map(d => {
2058
- const name = d.split(/[/\\]/).pop();
2059
- return `<div class="folder-item" data-path="${escAttr(d)}">
3147
+ items += data.dirs
3148
+ .map((d) => {
3149
+ const name = d.split(/[/\\]/).pop();
3150
+ return `<div class="folder-item" data-path="${escAttr(d)}">
2060
3151
  <span class="folder-icon">📁</span>
2061
3152
  <span class="folder-name">${esc(name)}</span>
2062
3153
  <span class="folder-arrow">›</span>
2063
3154
  </div>`;
2064
- }).join('');
3155
+ })
3156
+ .join('');
2065
3157
  nsBrowserList.innerHTML = items || '<div class="browser-empty">No subfolders</div>';
2066
- nsBrowserList.querySelectorAll('.folder-item').forEach(el => {
3158
+ nsBrowserList.querySelectorAll('.folder-item').forEach((el) => {
2067
3159
  el.addEventListener('click', () => nsBrowseNavigate(el.dataset.path));
2068
3160
  });
2069
3161
  nsBrowserList.scrollTop = 0;
@@ -2085,7 +3177,7 @@
2085
3177
  html += `<button class="crumb${isCurrent ? ' current' : ''}" data-path="${escAttr(accumulated)}">${esc(part)}</button>`;
2086
3178
  });
2087
3179
  nsBrowserBreadcrumb.innerHTML = html;
2088
- nsBrowserBreadcrumb.querySelectorAll('.crumb').forEach(el => {
3180
+ nsBrowserBreadcrumb.querySelectorAll('.crumb').forEach((el) => {
2089
3181
  el.addEventListener('click', () => nsBrowseNavigate(el.dataset.path));
2090
3182
  });
2091
3183
  nsBrowserBreadcrumb.scrollLeft = nsBrowserBreadcrumb.scrollWidth;
@@ -2096,17 +3188,29 @@
2096
3188
  if (shellsLoaded) return;
2097
3189
  const sel = document.getElementById('ns-shell');
2098
3190
  try {
2099
- const data = await fetch('/api/shells').then(r => r.json());
3191
+ const data = await fetch('/api/shells').then((r) => r.json());
2100
3192
  if (data.cwd) {
2101
3193
  serverCwd = data.cwd;
2102
3194
  document.getElementById('ns-cwd').placeholder = data.cwd;
2103
3195
  }
2104
- sel.innerHTML = data.shells.map(s =>
2105
- '<option value="' + escAttr(s.cmd) + '"' + (s.cmd === data.default ? ' selected' : '') + '>'
2106
- + esc(s.name) + ' (' + esc(s.cmd) + ')</option>'
2107
- ).join('');
3196
+ sel.innerHTML = data.shells
3197
+ .map(
3198
+ (s) =>
3199
+ '<option value="' +
3200
+ escAttr(s.cmd) +
3201
+ '"' +
3202
+ (s.cmd === data.default ? ' selected' : '') +
3203
+ '>' +
3204
+ esc(s.name) +
3205
+ ' (' +
3206
+ esc(s.cmd) +
3207
+ ')</option>',
3208
+ )
3209
+ .join('');
2108
3210
  shellsLoaded = true;
2109
- } catch { sel.innerHTML = '<option value="">Could not detect shells</option>'; }
3211
+ } catch {
3212
+ sel.innerHTML = '<option value="">Could not detect shells</option>';
3213
+ }
2110
3214
  }
2111
3215
 
2112
3216
  async function createNewSession() {
@@ -2133,8 +3237,8 @@
2133
3237
  const data = await res.json();
2134
3238
 
2135
3239
  // Fetch full session list to get the new session data
2136
- const list = await fetch('/api/sessions').then(r => r.json());
2137
- const newSession = list.find(s => s.id === data.id);
3240
+ const list = await fetch('/api/sessions').then((r) => r.json());
3241
+ const newSession = list.find((s) => s.id === data.id);
2138
3242
  if (newSession) {
2139
3243
  addSession(newSession);
2140
3244
  activateSession(data.id);
@@ -2153,8 +3257,8 @@
2153
3257
  function startPolling() {
2154
3258
  setInterval(async () => {
2155
3259
  try {
2156
- const list = await fetch('/api/sessions').then(r => r.json());
2157
- const serverIds = new Set(list.map(s => s.id));
3260
+ const list = await fetch('/api/sessions').then((r) => r.json());
3261
+ const serverIds = new Set(list.map((s) => s.id));
2158
3262
 
2159
3263
  // Add new sessions created elsewhere
2160
3264
  for (const s of list) {
@@ -2174,8 +3278,14 @@
2174
3278
  if (!serverIds.has(id)) {
2175
3279
  const ms = managed.get(id);
2176
3280
  ms.exited = true;
2177
- if (ms.reconnectTimer) { clearTimeout(ms.reconnectTimer); ms.reconnectTimer = null; }
2178
- if (ms.ws) try { ms.ws.close(); } catch {}
3281
+ if (ms.reconnectTimer) {
3282
+ clearTimeout(ms.reconnectTimer);
3283
+ ms.reconnectTimer = null;
3284
+ }
3285
+ if (ms.ws)
3286
+ try {
3287
+ ms.ws.close();
3288
+ } catch {}
2179
3289
  ms.term.dispose();
2180
3290
  ms.container.remove();
2181
3291
  managed.delete(id);
@@ -2206,25 +3316,82 @@
2206
3316
  ta.value = text;
2207
3317
  ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
2208
3318
  document.body.appendChild(ta);
3319
+ ta.focus();
2209
3320
  ta.select();
2210
- try { document.execCommand('copy'); } catch {}
3321
+ let ok = false;
3322
+ try {
3323
+ ok = document.execCommand('copy');
3324
+ } catch {}
2211
3325
  document.body.removeChild(ta);
3326
+ return ok;
3327
+ }
3328
+
3329
+ function showShareUrlPrompt(url) {
3330
+ const overlay = document.createElement('div');
3331
+ overlay.style.cssText =
3332
+ 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
3333
+ const box = document.createElement('div');
3334
+ box.style.cssText =
3335
+ 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:90vw;width:360px;text-align:center;';
3336
+ box.innerHTML =
3337
+ '<div style="font-size:14px;font-weight:600;color:var(--text);margin-bottom:12px;">Copy this link</div>';
3338
+ const input = document.createElement('input');
3339
+ input.type = 'text';
3340
+ input.readOnly = true;
3341
+ input.value = url;
3342
+ input.style.cssText =
3343
+ 'width:100%;box-sizing:border-box;padding:8px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:13px;margin-bottom:12px;';
3344
+ box.appendChild(input);
3345
+ const btn = document.createElement('button');
3346
+ btn.textContent = 'Close';
3347
+ btn.style.cssText =
3348
+ 'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
3349
+ btn.onclick = () => overlay.remove();
3350
+ box.appendChild(btn);
3351
+ overlay.appendChild(box);
3352
+ overlay.addEventListener('click', (e) => {
3353
+ if (e.target === overlay) overlay.remove();
3354
+ });
3355
+ document.body.appendChild(overlay);
3356
+ input.focus();
3357
+ input.select();
2212
3358
  }
2213
3359
 
2214
3360
  document.getElementById('share-btn').addEventListener('click', async () => {
2215
- const url = location.href;
3361
+ const urlPromise = fetch('/api/share-token')
3362
+ .then((r) => (r.ok ? r.json() : null))
3363
+ .then((data) => (data && data.url) || location.href)
3364
+ .catch(() => location.href);
3365
+ // ClipboardItem with a promise preserves user activation across the fetch
3366
+ if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
3367
+ try {
3368
+ const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
3369
+ await navigator.clipboard.write([new ClipboardItem({ 'text/plain': blobPromise })]);
3370
+ showToast('Link copied!');
3371
+ return;
3372
+ } catch {}
3373
+ }
3374
+ // Fallback: resolve URL first, then try legacy methods
3375
+ const url = await urlPromise;
2216
3376
  if (navigator.clipboard && navigator.clipboard.writeText) {
2217
- try { await navigator.clipboard.writeText(url); showToast('Link copied!'); return; } catch {}
3377
+ try {
3378
+ await navigator.clipboard.writeText(url);
3379
+ showToast('Link copied!');
3380
+ return;
3381
+ } catch {}
3382
+ }
3383
+ if (copyToClipboardFallback(url)) {
3384
+ showToast('Link copied!');
3385
+ } else {
3386
+ showShareUrlPrompt(url);
2218
3387
  }
2219
- copyToClipboardFallback(url);
2220
- showToast('Link copied!');
2221
3388
  });
2222
3389
 
2223
3390
  // ===== Refresh Button =====
2224
3391
  document.getElementById('refresh-btn').addEventListener('click', async () => {
2225
3392
  if ('caches' in window) {
2226
3393
  const keys = await caches.keys();
2227
- await Promise.all(keys.map(k => caches.delete(k)));
3394
+ await Promise.all(keys.map((k) => caches.delete(k)));
2228
3395
  }
2229
3396
  if (navigator.serviceWorker) {
2230
3397
  const reg = await navigator.serviceWorker.getRegistration();