termbeam 1.4.0 → 1.7.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.
@@ -15,6 +15,7 @@
15
15
  />
16
16
  <link rel="manifest" href="/manifest.json" />
17
17
  <link rel="apple-touch-icon" href="/icons/icon-192.png" />
18
+ <link rel="stylesheet" href="/css/themes.css" />
18
19
  <title>TermBeam — Terminal</title>
19
20
  <link
20
21
  rel="stylesheet"
@@ -22,257 +23,23 @@
22
23
  />
23
24
  <style>
24
25
  :root {
25
- --bg: #1e1e1e;
26
- --surface: #252526;
27
- --border: #3c3c3c;
28
- --border-subtle: #474747;
29
- --text: #d4d4d4;
30
- --text-secondary: #858585;
31
- --text-dim: #6e6e6e;
32
- --text-muted: #555555;
33
- --accent: #0078d4;
34
- --accent-hover: #1a8ae8;
35
- --accent-active: #005a9e;
36
- --danger: #f14c4c;
37
- --danger-hover: #d73a3a;
38
- --success: #89d185;
39
26
  --key-bg: #4a4a4c;
40
27
  --key-border: #5a5a5c;
41
28
  --key-shadow: rgba(0, 0, 0, 0.5);
42
29
  --key-special-bg: #333335;
43
30
  --overlay-bg: rgba(0, 0, 0, 0.85);
44
31
  }
45
- [data-theme='light'] {
46
- --bg: #ffffff;
47
- --surface: #f3f3f3;
48
- --border: #e0e0e0;
49
- --border-subtle: #d0d0d0;
50
- --text: #1e1e1e;
51
- --text-secondary: #616161;
52
- --text-dim: #767676;
53
- --text-muted: #a0a0a0;
54
- --accent: #0078d4;
55
- --accent-hover: #106ebe;
56
- --accent-active: #005a9e;
57
- --danger: #e51400;
58
- --danger-hover: #c20000;
59
- --success: #16825d;
60
- --key-bg: #ffffff;
61
- --key-border: #b5b5b5;
62
- --key-shadow: rgba(0, 0, 0, 0.12);
63
- --key-special-bg: #adb5bd;
64
- --overlay-bg: rgba(0, 0, 0, 0.5);
65
- }
66
- [data-theme='monokai'] {
67
- --bg: #272822;
68
- --surface: #1e1f1c;
69
- --border: #49483e;
70
- --border-subtle: #5c5c4f;
71
- --text: #f8f8f2;
72
- --text-secondary: #a59f85;
73
- --text-dim: #75715e;
74
- --text-muted: #5a5854;
75
- --accent: #a6e22e;
76
- --accent-hover: #b8f53c;
77
- --accent-active: #8acc16;
78
- --danger: #f92672;
79
- --danger-hover: #e0155d;
80
- --success: #a6e22e;
81
- --key-bg: #49483e;
82
- --key-border: #5c5c4f;
83
- --key-shadow: rgba(0, 0, 0, 0.4);
84
- --key-special-bg: #3e3d32;
85
- --overlay-bg: rgba(0, 0, 0, 0.75);
86
- }
87
- [data-theme='solarized-dark'] {
88
- --bg: #002b36;
89
- --surface: #073642;
90
- --border: #586e75;
91
- --border-subtle: #657b83;
92
- --text: #839496;
93
- --text-secondary: #657b83;
94
- --text-dim: #586e75;
95
- --text-muted: #4a5a62;
96
- --accent: #268bd2;
97
- --accent-hover: #379ce3;
98
- --accent-active: #1a7abf;
99
- --danger: #dc322f;
100
- --danger-hover: #c8221f;
101
- --success: #859900;
102
- --key-bg: #073642;
103
- --key-border: #586e75;
104
- --key-shadow: rgba(0, 0, 0, 0.3);
105
- --key-special-bg: #002b36;
106
- --overlay-bg: rgba(0, 0, 0, 0.75);
107
- }
108
- [data-theme='solarized-light'] {
109
- --bg: #fdf6e3;
110
- --surface: #eee8d5;
111
- --border: #93a1a1;
112
- --border-subtle: #839496;
113
- --text: #657b83;
114
- --text-secondary: #93a1a1;
115
- --text-dim: #a0a0a0;
116
- --text-muted: #b0b0b0;
117
- --accent: #268bd2;
118
- --accent-hover: #379ce3;
119
- --accent-active: #1a7abf;
120
- --danger: #dc322f;
121
- --danger-hover: #c8221f;
122
- --success: #859900;
123
- --key-bg: #ffffff;
124
- --key-border: #b5b5b5;
125
- --key-shadow: rgba(0, 0, 0, 0.12);
126
- --key-special-bg: #adb5bd;
127
- --overlay-bg: rgba(0, 0, 0, 0.4);
128
- }
129
- [data-theme='nord'] {
130
- --bg: #2e3440;
131
- --surface: #3b4252;
132
- --border: #434c5e;
133
- --border-subtle: #4c566a;
134
- --text: #d8dee9;
135
- --text-secondary: #b0bac9;
136
- --text-dim: #7b88a1;
137
- --text-muted: #5c6a85;
138
- --accent: #88c0d0;
139
- --accent-hover: #9fd4e4;
140
- --accent-active: #6aafbf;
141
- --danger: #bf616a;
142
- --danger-hover: #a84d57;
143
- --success: #a3be8c;
144
- --key-bg: #434c5e;
145
- --key-border: #4c566a;
146
- --key-shadow: rgba(0, 0, 0, 0.3);
147
- --key-special-bg: #3b4252;
148
- --overlay-bg: rgba(0, 0, 0, 0.75);
149
- }
150
- [data-theme='dracula'] {
151
- --bg: #282a36;
152
- --surface: #343746;
153
- --border: #44475a;
154
- --border-subtle: #525568;
155
- --text: #f8f8f2;
156
- --text-secondary: #c1c4d2;
157
- --text-dim: #8e92a4;
158
- --text-muted: #6272a4;
159
- --accent: #bd93f9;
160
- --accent-hover: #d0b0ff;
161
- --accent-active: #a77de7;
162
- --danger: #ff5555;
163
- --danger-hover: #e03d3d;
164
- --success: #50fa7b;
165
- --key-bg: #44475a;
166
- --key-border: #525568;
167
- --key-shadow: rgba(0, 0, 0, 0.4);
168
- --key-special-bg: #343746;
169
- --overlay-bg: rgba(0, 0, 0, 0.75);
170
- }
171
- [data-theme='github-dark'] {
172
- --bg: #0d1117;
173
- --surface: #161b22;
174
- --border: #30363d;
175
- --border-subtle: #3d444d;
176
- --text: #c9d1d9;
177
- --text-secondary: #8b949e;
178
- --text-dim: #6e7681;
179
- --text-muted: #484f58;
180
- --accent: #58a6ff;
181
- --accent-hover: #79b8ff;
182
- --accent-active: #388bfd;
183
- --danger: #f85149;
184
- --danger-hover: #da3633;
185
- --success: #3fb950;
186
- --key-bg: #161b22;
187
- --key-border: #30363d;
188
- --key-shadow: rgba(0, 0, 0, 0.4);
189
- --key-special-bg: #0d1117;
190
- --overlay-bg: rgba(0, 0, 0, 0.75);
191
- }
192
- [data-theme='one-dark'] {
193
- --bg: #282c34;
194
- --surface: #21252b;
195
- --border: #3e4452;
196
- --border-subtle: #4b5263;
197
- --text: #abb2bf;
198
- --text-secondary: #7f848e;
199
- --text-dim: #5c6370;
200
- --text-muted: #4b5263;
201
- --accent: #61afef;
202
- --accent-hover: #7dc0ff;
203
- --accent-active: #4d9ede;
204
- --danger: #e06c75;
205
- --danger-hover: #c95c67;
206
- --success: #98c379;
207
- --key-bg: #3e4452;
208
- --key-border: #4b5263;
209
- --key-shadow: rgba(0, 0, 0, 0.3);
210
- --key-special-bg: #21252b;
211
- --overlay-bg: rgba(0, 0, 0, 0.75);
212
- }
213
- [data-theme='catppuccin'] {
214
- --bg: #1e1e2e;
215
- --surface: #313244;
216
- --border: #45475a;
217
- --border-subtle: #585b70;
218
- --text: #cdd6f4;
219
- --text-secondary: #a6adc8;
220
- --text-dim: #7f849c;
221
- --text-muted: #585b70;
222
- --accent: #89b4fa;
223
- --accent-hover: #b4d0ff;
224
- --accent-active: #5c9de3;
225
- --danger: #f38ba8;
226
- --danger-hover: #eb7c9d;
227
- --success: #a6e3a1;
228
- --key-bg: #45475a;
229
- --key-border: #585b70;
230
- --key-shadow: rgba(0, 0, 0, 0.3);
231
- --key-special-bg: #313244;
232
- --overlay-bg: rgba(0, 0, 0, 0.75);
233
- }
234
- [data-theme='gruvbox'] {
235
- --bg: #282828;
236
- --surface: #3c3836;
237
- --border: #504945;
238
- --border-subtle: #665c54;
239
- --text: #ebdbb2;
240
- --text-secondary: #d5c4a1;
241
- --text-dim: #a89984;
242
- --text-muted: #7c6f64;
243
- --accent: #83a598;
244
- --accent-hover: #9dbfb4;
245
- --accent-active: #6a8f8a;
246
- --danger: #fb4934;
247
- --danger-hover: #e33826;
248
- --success: #b8bb26;
249
- --key-bg: #504945;
250
- --key-border: #665c54;
251
- --key-shadow: rgba(0, 0, 0, 0.4);
252
- --key-special-bg: #3c3836;
253
- --overlay-bg: rgba(0, 0, 0, 0.75);
254
- }
255
- [data-theme='night-owl'] {
256
- --bg: #011627;
257
- --surface: #0d2a45;
258
- --border: #1d3b53;
259
- --border-subtle: #264863;
260
- --text: #d6deeb;
261
- --text-secondary: #8badc1;
262
- --text-dim: #5f7e97;
263
- --text-muted: #3f5f7d;
264
- --accent: #7fdbca;
265
- --accent-hover: #9ff0e0;
266
- --accent-active: #62c5b5;
267
- --danger: #ef5350;
268
- --danger-hover: #d83130;
269
- --success: #addb67;
270
- --key-bg: #1d3b53;
271
- --key-border: #264863;
272
- --key-shadow: rgba(0, 0, 0, 0.4);
273
- --key-special-bg: #0d2a45;
274
- --overlay-bg: rgba(0, 0, 0, 0.75);
275
- }
32
+ [data-theme='light'] { --key-bg: #ffffff; --key-border: #b5b5b5; --key-shadow: rgba(0, 0, 0, 0.12); --key-special-bg: #adb5bd; --overlay-bg: rgba(0, 0, 0, 0.5); }
33
+ [data-theme='monokai'] { --key-bg: #49483e; --key-border: #5c5c4f; --key-shadow: rgba(0, 0, 0, 0.4); --key-special-bg: #3e3d32; }
34
+ [data-theme='solarized-dark'] { --key-bg: #073642; --key-border: #586e75; --key-shadow: rgba(0, 0, 0, 0.3); --key-special-bg: #002b36; }
35
+ [data-theme='solarized-light'] { --key-bg: #ffffff; --key-border: #b5b5b5; --key-shadow: rgba(0, 0, 0, 0.12); --key-special-bg: #adb5bd; }
36
+ [data-theme='nord'] { --key-bg: #434c5e; --key-border: #4c566a; --key-shadow: rgba(0, 0, 0, 0.3); --key-special-bg: #3b4252; }
37
+ [data-theme='dracula'] { --key-bg: #44475a; --key-border: #525568; --key-shadow: rgba(0, 0, 0, 0.4); --key-special-bg: #343746; }
38
+ [data-theme='github-dark'] { --key-bg: #161b22; --key-border: #30363d; --key-shadow: rgba(0, 0, 0, 0.4); --key-special-bg: #0d1117; }
39
+ [data-theme='one-dark'] { --key-bg: #3e4452; --key-border: #4b5263; --key-shadow: rgba(0, 0, 0, 0.3); --key-special-bg: #21252b; }
40
+ [data-theme='catppuccin'] { --key-bg: #45475a; --key-border: #585b70; --key-shadow: rgba(0, 0, 0, 0.3); --key-special-bg: #313244; }
41
+ [data-theme='gruvbox'] { --key-bg: #504945; --key-border: #665c54; --key-shadow: rgba(0, 0, 0, 0.4); --key-special-bg: #3c3836; }
42
+ [data-theme='night-owl'] { --key-bg: #1d3b53; --key-border: #264863; --key-shadow: rgba(0, 0, 0, 0.4); --key-special-bg: #0d2a45; }
276
43
  @font-face {
277
44
  font-family: 'NerdFont';
278
45
  src: url('https://cdn.jsdelivr.net/gh/ryanoasis/nerd-fonts@latest/patched-fonts/JetBrainsMono/Ligatures/Regular/JetBrainsMonoNerdFont-Regular.ttf')
@@ -611,7 +378,7 @@
611
378
  }
612
379
  .theme-wrap {
613
380
  position: relative;
614
- display: flex;
381
+ display: none;
615
382
  align-items: center;
616
383
  }
617
384
  .theme-picker {
@@ -662,7 +429,7 @@
662
429
  height: 30px;
663
430
  border-radius: 8px;
664
431
  cursor: pointer;
665
- display: flex;
432
+ display: none;
666
433
  align-items: center;
667
434
  justify-content: center;
668
435
  gap: 4px;
@@ -685,6 +452,75 @@
685
452
  transform: scale(0.9);
686
453
  }
687
454
 
455
+ /* ===== Notification Toggle ===== */
456
+ .notify-btn.active {
457
+ color: var(--accent) !important;
458
+ }
459
+
460
+ /* ===== Search Bar ===== */
461
+ .search-bar {
462
+ display: none;
463
+ position: absolute;
464
+ top: 4px;
465
+ right: 12px;
466
+ z-index: 100;
467
+ background: var(--surface);
468
+ border: 1px solid var(--border);
469
+ border-radius: 8px;
470
+ padding: 4px 6px;
471
+ gap: 4px;
472
+ align-items: center;
473
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
474
+ font-size: 13px;
475
+ color: var(--text);
476
+ }
477
+ .search-bar.visible {
478
+ display: flex;
479
+ }
480
+ .search-bar input {
481
+ background: var(--bg);
482
+ border: 1px solid var(--border);
483
+ border-radius: 4px;
484
+ color: var(--text);
485
+ padding: 3px 6px;
486
+ font-size: 13px;
487
+ font-family: inherit;
488
+ width: 160px;
489
+ outline: none;
490
+ }
491
+ .search-bar input:focus {
492
+ border-color: var(--accent);
493
+ }
494
+ .search-bar .search-count {
495
+ color: var(--text-secondary);
496
+ font-size: 11px;
497
+ min-width: 40px;
498
+ text-align: center;
499
+ white-space: nowrap;
500
+ }
501
+ .search-bar button {
502
+ background: none;
503
+ border: 1px solid transparent;
504
+ color: var(--text-dim);
505
+ width: 24px;
506
+ height: 24px;
507
+ border-radius: 4px;
508
+ cursor: pointer;
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ font-size: 13px;
513
+ padding: 0;
514
+ }
515
+ .search-bar button:hover {
516
+ background: var(--border);
517
+ color: var(--text);
518
+ }
519
+ .search-bar button.active {
520
+ color: var(--accent);
521
+ border-color: var(--accent);
522
+ }
523
+
688
524
  /* ===== Terminals Wrapper ===== */
689
525
  #terminals-wrapper {
690
526
  position: absolute;
@@ -1661,6 +1497,30 @@
1661
1497
  white-space: normal;
1662
1498
  }
1663
1499
 
1500
+ .side-panel-card-git {
1501
+ display: flex;
1502
+ flex-wrap: wrap;
1503
+ gap: 3px 6px;
1504
+ padding: 0 12px 4px;
1505
+ font-size: 10px;
1506
+ color: var(--text-secondary);
1507
+ align-items: center;
1508
+ }
1509
+ .side-panel-card-git .git-badge {
1510
+ display: inline-flex;
1511
+ align-items: center;
1512
+ gap: 3px;
1513
+ background: var(--surface);
1514
+ padding: 1px 6px;
1515
+ border-radius: 3px;
1516
+ }
1517
+ .side-panel-card-git .git-status-clean {
1518
+ color: var(--success);
1519
+ }
1520
+ .side-panel-card-git .git-status-dirty {
1521
+ color: var(--warning, #fbbf24);
1522
+ }
1523
+
1664
1524
  @media (max-width: 640px) {
1665
1525
  #panel-toggle {
1666
1526
  display: flex;
@@ -1668,17 +1528,12 @@
1668
1528
  #tab-list {
1669
1529
  display: none;
1670
1530
  }
1671
- #split-toggle {
1672
- display: none;
1673
- }
1674
- #version-text {
1675
- display: none;
1676
- }
1531
+
1677
1532
  #back-btn {
1678
1533
  display: none;
1679
1534
  }
1680
1535
  #theme-wrap {
1681
- display: flex;
1536
+ display: none;
1682
1537
  }
1683
1538
  #stop-btn {
1684
1539
  padding: 0 8px;
@@ -1695,6 +1550,195 @@
1695
1550
  width: auto;
1696
1551
  }
1697
1552
  }
1553
+
1554
+ /* ===== Command Palette / Tool Panel ===== */
1555
+ .palette-backdrop {
1556
+ position: fixed;
1557
+ inset: 0;
1558
+ background: rgba(0, 0, 0, 0.4);
1559
+ z-index: 250;
1560
+ opacity: 0;
1561
+ pointer-events: none;
1562
+ transition: opacity 0.3s;
1563
+ }
1564
+ .palette-backdrop.open {
1565
+ opacity: 1;
1566
+ pointer-events: auto;
1567
+ }
1568
+ .palette-panel {
1569
+ position: fixed;
1570
+ top: 0;
1571
+ right: 0;
1572
+ width: 280px;
1573
+ max-width: 85vw;
1574
+ height: 100%;
1575
+ background: var(--surface);
1576
+ border-left: 1px solid var(--border);
1577
+ z-index: 260;
1578
+ transform: translateX(100%);
1579
+ transition: transform 0.3s ease;
1580
+ display: flex;
1581
+ flex-direction: column;
1582
+ overflow-y: auto;
1583
+ -webkit-overflow-scrolling: touch;
1584
+ }
1585
+ .palette-panel.open {
1586
+ transform: translateX(0);
1587
+ }
1588
+ .palette-header {
1589
+ display: flex;
1590
+ align-items: center;
1591
+ justify-content: space-between;
1592
+ padding: 14px 16px;
1593
+ border-bottom: 1px solid var(--border);
1594
+ font-weight: 600;
1595
+ font-size: 15px;
1596
+ color: var(--text);
1597
+ }
1598
+ .palette-close {
1599
+ background: none;
1600
+ border: none;
1601
+ color: var(--text-secondary);
1602
+ font-size: 18px;
1603
+ cursor: pointer;
1604
+ padding: 4px 8px;
1605
+ border-radius: 6px;
1606
+ }
1607
+ .palette-close:hover {
1608
+ background: var(--hover-bg, rgba(255, 255, 255, 0.08));
1609
+ color: var(--text);
1610
+ }
1611
+ .palette-body {
1612
+ padding: 8px 0;
1613
+ flex: 1;
1614
+ }
1615
+ .palette-category {
1616
+ padding: 8px 16px 4px;
1617
+ font-size: 11px;
1618
+ font-weight: 600;
1619
+ text-transform: uppercase;
1620
+ letter-spacing: 0.5px;
1621
+ color: var(--text-muted, var(--text-secondary));
1622
+ }
1623
+ .palette-action {
1624
+ display: flex;
1625
+ align-items: center;
1626
+ gap: 10px;
1627
+ width: 100%;
1628
+ padding: 10px 16px;
1629
+ background: none;
1630
+ border: none;
1631
+ color: var(--text);
1632
+ font-size: 13px;
1633
+ cursor: pointer;
1634
+ text-align: left;
1635
+ transition: background 0.15s;
1636
+ }
1637
+ .palette-action:hover {
1638
+ background: rgba(255, 255, 255, 0.06);
1639
+ }
1640
+ .palette-action:active {
1641
+ background: rgba(255, 255, 255, 0.1);
1642
+ }
1643
+ [data-theme='light'] .palette-action:hover,
1644
+ [data-theme='solarized-light'] .palette-action:hover {
1645
+ background: rgba(0, 0, 0, 0.06);
1646
+ }
1647
+ [data-theme='light'] .palette-action:active,
1648
+ [data-theme='solarized-light'] .palette-action:active {
1649
+ background: rgba(0, 0, 0, 0.1);
1650
+ }
1651
+ .palette-action-icon {
1652
+ width: 20px;
1653
+ height: 20px;
1654
+ display: flex;
1655
+ align-items: center;
1656
+ justify-content: center;
1657
+ flex-shrink: 0;
1658
+ color: var(--text-secondary);
1659
+ }
1660
+ .palette-action-icon svg {
1661
+ width: 16px;
1662
+ }
1663
+ /* ===== Theme Sub-Panel ===== */
1664
+ .theme-subpanel {
1665
+ display: none;
1666
+ position: fixed;
1667
+ top: 50%;
1668
+ left: 50%;
1669
+ transform: translate(-50%, -50%);
1670
+ width: 240px;
1671
+ max-height: 70vh;
1672
+ background: var(--surface);
1673
+ border: 1px solid var(--border);
1674
+ border-radius: 12px;
1675
+ z-index: 270;
1676
+ overflow-y: auto;
1677
+ -webkit-overflow-scrolling: touch;
1678
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
1679
+ }
1680
+ .theme-subpanel.open {
1681
+ display: block;
1682
+ }
1683
+ .theme-subpanel-header {
1684
+ display: flex;
1685
+ align-items: center;
1686
+ justify-content: space-between;
1687
+ padding: 12px 14px;
1688
+ border-bottom: 1px solid var(--border);
1689
+ font-weight: 600;
1690
+ font-size: 13px;
1691
+ color: var(--text);
1692
+ }
1693
+ .theme-subpanel-close {
1694
+ background: none;
1695
+ border: none;
1696
+ color: var(--text-secondary);
1697
+ font-size: 16px;
1698
+ cursor: pointer;
1699
+ padding: 2px 6px;
1700
+ border-radius: 6px;
1701
+ }
1702
+ .theme-subpanel-close:hover {
1703
+ background: var(--hover-bg, rgba(255, 255, 255, 0.08));
1704
+ color: var(--text);
1705
+ }
1706
+ .theme-subpanel-list {
1707
+ padding: 6px 0;
1708
+ }
1709
+ .theme-subpanel-item {
1710
+ display: flex;
1711
+ align-items: center;
1712
+ gap: 10px;
1713
+ width: 100%;
1714
+ padding: 9px 14px;
1715
+ background: none;
1716
+ border: none;
1717
+ color: var(--text);
1718
+ font-size: 13px;
1719
+ cursor: pointer;
1720
+ text-align: left;
1721
+ transition: background 0.15s;
1722
+ }
1723
+ .theme-subpanel-item:hover {
1724
+ background: rgba(255, 255, 255, 0.06);
1725
+ }
1726
+ [data-theme='light'] .theme-subpanel-item:hover,
1727
+ [data-theme='solarized-light'] .theme-subpanel-item:hover {
1728
+ background: rgba(0, 0, 0, 0.06);
1729
+ }
1730
+ .theme-subpanel-item.active {
1731
+ color: var(--accent);
1732
+ }
1733
+ .theme-subpanel-swatch {
1734
+ width: 14px;
1735
+ height: 14px;
1736
+ border-radius: 50%;
1737
+ flex-shrink: 0;
1738
+ border: 1px solid rgba(128, 128, 128, 0.3);
1739
+ }
1740
+ height: 16px;
1741
+ }
1698
1742
  </style>
1699
1743
  </head>
1700
1744
  <body>
@@ -1792,7 +1836,6 @@
1792
1836
  </div>
1793
1837
  <div id="tab-list"></div>
1794
1838
  <div class="right">
1795
- <span id="version-text" style="font-size: 11px; color: var(--text-muted)"></span>
1796
1839
  <button class="tab-bar-btn" id="tab-new-btn" title="New session">
1797
1840
  <svg
1798
1841
  width="14"
@@ -1807,67 +1850,6 @@
1807
1850
  <line x1="5" y1="12" x2="19" y2="12" /></svg
1808
1851
  ><span class="new-btn-label">New</span>
1809
1852
  </button>
1810
- <button class="tab-bar-btn" id="split-toggle" title="Split view">
1811
- <svg
1812
- width="16"
1813
- height="16"
1814
- viewBox="0 0 24 24"
1815
- fill="none"
1816
- stroke="currentColor"
1817
- stroke-width="2"
1818
- stroke-linecap="round"
1819
- stroke-linejoin="round"
1820
- >
1821
- <rect x="3" y="3" width="18" height="18" rx="2" />
1822
- <line x1="12" y1="3" x2="12" y2="21" />
1823
- </svg>
1824
- </button>
1825
- <div class="bar-group">
1826
- <button class="bar-btn" id="zoom-out" title="Decrease font size">−</button>
1827
- <button class="bar-btn" id="zoom-in" title="Increase font size">+</button>
1828
- </div>
1829
- <button
1830
- class="bar-btn"
1831
- id="preview-btn"
1832
- title="Preview local port"
1833
- onclick="openPreviewModal()"
1834
- >
1835
- 🌐
1836
- </button>
1837
- <button class="bar-btn" id="share-btn" title="Share link">
1838
- <svg
1839
- width="16"
1840
- height="16"
1841
- viewBox="0 0 24 24"
1842
- fill="none"
1843
- stroke="currentColor"
1844
- stroke-width="2"
1845
- stroke-linecap="round"
1846
- stroke-linejoin="round"
1847
- >
1848
- <circle cx="18" cy="5" r="3" />
1849
- <circle cx="6" cy="12" r="3" />
1850
- <circle cx="18" cy="19" r="3" />
1851
- <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
1852
- <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
1853
- </svg>
1854
- </button>
1855
- <button class="bar-btn" id="refresh-btn" title="Refresh app">
1856
- <svg
1857
- width="16"
1858
- height="16"
1859
- viewBox="0 0 24 24"
1860
- fill="none"
1861
- stroke="currentColor"
1862
- stroke-width="2"
1863
- stroke-linecap="round"
1864
- stroke-linejoin="round"
1865
- >
1866
- <polyline points="23 4 23 10 17 10" />
1867
- <polyline points="1 20 1 14 7 14" />
1868
- <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" />
1869
- </svg>
1870
- </button>
1871
1853
  <div class="theme-wrap" id="theme-wrap">
1872
1854
  <button class="bar-btn" id="theme-toggle" title="Switch theme">
1873
1855
  <svg
@@ -1928,6 +1910,23 @@
1928
1910
  </div>
1929
1911
  </div>
1930
1912
  </div>
1913
+ <button class="bar-btn" id="palette-trigger" title="Tools (Ctrl+K)">
1914
+ <svg
1915
+ width="16"
1916
+ height="16"
1917
+ viewBox="0 0 24 24"
1918
+ fill="none"
1919
+ stroke="currentColor"
1920
+ stroke-width="2"
1921
+ stroke-linecap="round"
1922
+ stroke-linejoin="round"
1923
+ >
1924
+ <rect x="3" y="3" width="7" height="7" rx="1" />
1925
+ <rect x="14" y="3" width="7" height="7" rx="1" />
1926
+ <rect x="3" y="14" width="7" height="7" rx="1" />
1927
+ <rect x="14" y="14" width="7" height="7" rx="1" />
1928
+ </svg>
1929
+ </button>
1931
1930
  <button id="stop-btn" title="Stop session">
1932
1931
  <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" stroke="none">
1933
1932
  <rect x="6" y="6" width="12" height="12" rx="2" /></svg
@@ -1946,7 +1945,16 @@
1946
1945
  </div>
1947
1946
 
1948
1947
  <!-- Terminals Wrapper (panes created dynamically) -->
1949
- <div id="terminals-wrapper"></div>
1948
+ <div id="terminals-wrapper">
1949
+ <div class="search-bar" id="search-bar">
1950
+ <input type="text" id="search-input" placeholder="Search…" autocomplete="off" />
1951
+ <span class="search-count" id="search-count"></span>
1952
+ <button id="search-regex" title="Regex">.*</button>
1953
+ <button id="search-prev" title="Previous">▲</button>
1954
+ <button id="search-next" title="Next">▼</button>
1955
+ <button id="search-close" title="Close">✕</button>
1956
+ </div>
1957
+ </div>
1950
1958
 
1951
1959
  <div id="copy-toast">Copied!</div>
1952
1960
 
@@ -1965,7 +1973,7 @@
1965
1973
  <div class="key-row">
1966
1974
  <button class="key-btn modifier" id="ctrl-btn" title="Toggle Ctrl modifier">Ctrl</button>
1967
1975
  <button class="key-btn modifier" id="shift-btn" title="Toggle Shift modifier">Shift</button>
1968
- <button class="key-btn special" data-key="&#x09;" title="Autocomplete">Tab</button>
1976
+ <button class="key-btn special" data-key="tab" title="Autocomplete">Tab</button>
1969
1977
  <button class="key-btn special key-danger" data-key="&#x03;" title="Interrupt process">
1970
1978
  ^C
1971
1979
  </button>
@@ -2212,9 +2220,34 @@
2212
2220
  </div>
2213
2221
  </div>
2214
2222
 
2223
+ <!-- Command Palette / Tool Panel -->
2224
+ <div id="palette-backdrop" class="palette-backdrop"></div>
2225
+ <div id="palette-panel" class="palette-panel">
2226
+ <div class="palette-header">
2227
+ <span>Tools</span>
2228
+ <button class="palette-close" id="palette-close">✕</button>
2229
+ </div>
2230
+ <div class="palette-body" id="palette-body"></div>
2231
+ </div>
2232
+
2233
+ <!-- Theme Sub-Panel -->
2234
+ <div class="theme-subpanel" id="theme-subpanel">
2235
+ <div class="theme-subpanel-header">
2236
+ <span>Theme</span>
2237
+ <button class="theme-subpanel-close" id="theme-subpanel-close">✕</button>
2238
+ </div>
2239
+ <div class="theme-subpanel-list" id="theme-subpanel-list"></div>
2240
+ </div>
2241
+
2215
2242
  <script src="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"></script>
2216
2243
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/lib/addon-fit.min.js"></script>
2217
2244
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0.11.0/lib/addon-web-links.min.js"></script>
2245
+ <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-search@0.15.0/lib/addon-search.min.js"></script>
2246
+ <script src="/js/shared.js"></script>
2247
+ <script src="/js/themes.js"></script>
2248
+ <script src="/js/terminal-themes.js"></script>
2249
+ <script src="/js/keybar.js"></script>
2250
+ <script src="/js/search.js"></script>
2218
2251
  <script>
2219
2252
  // ===== Constants =====
2220
2253
  const SESSION_COLORS = [
@@ -2235,6 +2268,33 @@
2235
2268
 
2236
2269
  let splitSecondId = null;
2237
2270
 
2271
+ // ===== Notification State =====
2272
+ let notificationsEnabled = localStorage.getItem('termbeam-notifications') !== 'false';
2273
+
2274
+ function updateNotifyToggle() {
2275
+ // notify-toggle button removed from top bar; function kept for palette use
2276
+ }
2277
+
2278
+ function sendCommandNotification(sessionName) {
2279
+ if (Notification.permission !== 'granted') return;
2280
+ try {
2281
+ new Notification('Command finished in ' + sessionName, {
2282
+ icon: '/icons/icon-192.png',
2283
+ tag: 'termbeam-cmd',
2284
+ });
2285
+ } catch {}
2286
+ }
2287
+
2288
+ function resetSilenceTimer(ms) {
2289
+ if (ms.silenceTimer) clearTimeout(ms.silenceTimer);
2290
+ ms.silenceTimer = setTimeout(() => {
2291
+ ms.silenceTimer = null;
2292
+ if (document.hidden && notificationsEnabled) {
2293
+ sendCommandNotification(ms.name || ms.id);
2294
+ }
2295
+ }, 3000);
2296
+ }
2297
+
2238
2298
  // Clipboard copy fallback for non-secure contexts (HTTP over LAN)
2239
2299
  function copyFallback(text) {
2240
2300
  const ta = document.createElement('textarea');
@@ -2249,6 +2309,14 @@
2249
2309
  document.body.removeChild(ta);
2250
2310
  }
2251
2311
 
2312
+ // Hook into shared theme system to update xterm terminal themes
2313
+ window.onThemeApplied = function (theme) {
2314
+ const termTheme = TERM_THEMES[theme] || darkTermTheme;
2315
+ for (const [, ms] of managed) {
2316
+ ms.term.options.theme = termTheme;
2317
+ }
2318
+ };
2319
+
2252
2320
  // ===== DOM Refs =====
2253
2321
  const statusDot = document.getElementById('status-dot');
2254
2322
  const statusText = document.getElementById('status-text');
@@ -2257,345 +2325,6 @@
2257
2325
  const tabListEl = document.getElementById('tab-list');
2258
2326
  const terminalsWrapper = document.getElementById('terminals-wrapper');
2259
2327
 
2260
- // ===== Terminal Themes =====
2261
- const darkTermTheme = {
2262
- background: '#1e1e1e',
2263
- foreground: '#d4d4d4',
2264
- cursor: '#aeafad',
2265
- cursorAccent: '#1e1e1e',
2266
- selectionBackground: 'rgba(38, 79, 120, 0.5)',
2267
- black: '#000000',
2268
- red: '#cd3131',
2269
- green: '#0dbc79',
2270
- yellow: '#e5e510',
2271
- blue: '#2472c8',
2272
- magenta: '#bc3fbc',
2273
- cyan: '#11a8cd',
2274
- white: '#e5e5e5',
2275
- brightBlack: '#666666',
2276
- brightRed: '#f14c4c',
2277
- brightGreen: '#23d18b',
2278
- brightYellow: '#f5f543',
2279
- brightBlue: '#3b8eea',
2280
- brightMagenta: '#d670d6',
2281
- brightCyan: '#29b8db',
2282
- brightWhite: '#e5e5e5',
2283
- };
2284
- const lightTermTheme = {
2285
- background: '#ffffff',
2286
- foreground: '#1e1e1e',
2287
- cursor: '#000000',
2288
- cursorAccent: '#ffffff',
2289
- selectionBackground: 'rgba(0, 120, 215, 0.3)',
2290
- black: '#000000',
2291
- red: '#cd3131',
2292
- green: '#00bc7c',
2293
- yellow: '#949800',
2294
- blue: '#0451a5',
2295
- magenta: '#bc05bc',
2296
- cyan: '#0598bc',
2297
- white: '#555555',
2298
- brightBlack: '#666666',
2299
- brightRed: '#cd3131',
2300
- brightGreen: '#14ce14',
2301
- brightYellow: '#b5ba00',
2302
- brightBlue: '#0451a5',
2303
- brightMagenta: '#bc05bc',
2304
- brightCyan: '#0598bc',
2305
- brightWhite: '#a5a5a5',
2306
- };
2307
- const monokaiTermTheme = {
2308
- background: '#272822',
2309
- foreground: '#f8f8f2',
2310
- cursor: '#f8f8f0',
2311
- cursorAccent: '#272822',
2312
- selectionBackground: 'rgba(166, 226, 46, 0.3)',
2313
- black: '#272822',
2314
- red: '#f92672',
2315
- green: '#a6e22e',
2316
- yellow: '#f4bf75',
2317
- blue: '#66d9e8',
2318
- magenta: '#ae81ff',
2319
- cyan: '#a1efe4',
2320
- white: '#f8f8f2',
2321
- brightBlack: '#75715e',
2322
- brightRed: '#f92672',
2323
- brightGreen: '#a6e22e',
2324
- brightYellow: '#f4bf75',
2325
- brightBlue: '#66d9e8',
2326
- brightMagenta: '#ae81ff',
2327
- brightCyan: '#a1efe4',
2328
- brightWhite: '#f9f8f5',
2329
- };
2330
- const solarizedDarkTermTheme = {
2331
- background: '#002b36',
2332
- foreground: '#839496',
2333
- cursor: '#839496',
2334
- cursorAccent: '#002b36',
2335
- selectionBackground: 'rgba(7, 54, 66, 0.8)',
2336
- black: '#073642',
2337
- red: '#dc322f',
2338
- green: '#859900',
2339
- yellow: '#b58900',
2340
- blue: '#268bd2',
2341
- magenta: '#d33682',
2342
- cyan: '#2aa198',
2343
- white: '#eee8d5',
2344
- brightBlack: '#002b36',
2345
- brightRed: '#cb4b16',
2346
- brightGreen: '#586e75',
2347
- brightYellow: '#657b83',
2348
- brightBlue: '#839496',
2349
- brightMagenta: '#6c71c4',
2350
- brightCyan: '#93a1a1',
2351
- brightWhite: '#fdf6e3',
2352
- };
2353
- const solarizedLightTermTheme = {
2354
- background: '#fdf6e3',
2355
- foreground: '#657b83',
2356
- cursor: '#586e75',
2357
- cursorAccent: '#fdf6e3',
2358
- selectionBackground: 'rgba(238, 232, 213, 0.8)',
2359
- black: '#073642',
2360
- red: '#dc322f',
2361
- green: '#859900',
2362
- yellow: '#b58900',
2363
- blue: '#268bd2',
2364
- magenta: '#d33682',
2365
- cyan: '#2aa198',
2366
- white: '#eee8d5',
2367
- brightBlack: '#002b36',
2368
- brightRed: '#cb4b16',
2369
- brightGreen: '#586e75',
2370
- brightYellow: '#657b83',
2371
- brightBlue: '#839496',
2372
- brightMagenta: '#6c71c4',
2373
- brightCyan: '#93a1a1',
2374
- brightWhite: '#fdf6e3',
2375
- };
2376
- const nordTermTheme = {
2377
- background: '#2e3440',
2378
- foreground: '#d8dee9',
2379
- cursor: '#d8dee9',
2380
- cursorAccent: '#2e3440',
2381
- selectionBackground: 'rgba(67, 76, 94, 0.5)',
2382
- black: '#3b4252',
2383
- red: '#bf616a',
2384
- green: '#a3be8c',
2385
- yellow: '#ebcb8b',
2386
- blue: '#81a1c1',
2387
- magenta: '#b48ead',
2388
- cyan: '#88c0d0',
2389
- white: '#e5e9f0',
2390
- brightBlack: '#4c566a',
2391
- brightRed: '#bf616a',
2392
- brightGreen: '#a3be8c',
2393
- brightYellow: '#ebcb8b',
2394
- brightBlue: '#81a1c1',
2395
- brightMagenta: '#b48ead',
2396
- brightCyan: '#8fbcbb',
2397
- brightWhite: '#eceff4',
2398
- };
2399
- const draculaTermTheme = {
2400
- background: '#282a36',
2401
- foreground: '#f8f8f2',
2402
- cursor: '#f8f8f2',
2403
- cursorAccent: '#282a36',
2404
- selectionBackground: 'rgba(68, 71, 90, 0.7)',
2405
- black: '#21222c',
2406
- red: '#ff5555',
2407
- green: '#50fa7b',
2408
- yellow: '#f1fa8c',
2409
- blue: '#bd93f9',
2410
- magenta: '#ff79c6',
2411
- cyan: '#8be9fd',
2412
- white: '#f8f8f2',
2413
- brightBlack: '#6272a4',
2414
- brightRed: '#ff6e6e',
2415
- brightGreen: '#69ff94',
2416
- brightYellow: '#ffffa5',
2417
- brightBlue: '#d6acff',
2418
- brightMagenta: '#ff92df',
2419
- brightCyan: '#a4ffff',
2420
- brightWhite: '#ffffff',
2421
- };
2422
- const githubDarkTermTheme = {
2423
- background: '#0d1117',
2424
- foreground: '#c9d1d9',
2425
- cursor: '#c9d1d9',
2426
- cursorAccent: '#0d1117',
2427
- selectionBackground: 'rgba(56, 139, 253, 0.3)',
2428
- black: '#484f58',
2429
- red: '#ff7b72',
2430
- green: '#3fb950',
2431
- yellow: '#d29922',
2432
- blue: '#58a6ff',
2433
- magenta: '#bc8cff',
2434
- cyan: '#39c5cf',
2435
- white: '#c9d1d9',
2436
- brightBlack: '#6e7681',
2437
- brightRed: '#ffa198',
2438
- brightGreen: '#56d364',
2439
- brightYellow: '#e3b341',
2440
- brightBlue: '#79c0ff',
2441
- brightMagenta: '#d2a8ff',
2442
- brightCyan: '#56d4dd',
2443
- brightWhite: '#f0f6fc',
2444
- };
2445
- const oneDarkTermTheme = {
2446
- background: '#282c34',
2447
- foreground: '#abb2bf',
2448
- cursor: '#528bff',
2449
- cursorAccent: '#282c34',
2450
- selectionBackground: 'rgba(62, 68, 82, 0.7)',
2451
- black: '#3f4451',
2452
- red: '#e06c75',
2453
- green: '#98c379',
2454
- yellow: '#e5c07b',
2455
- blue: '#61afef',
2456
- magenta: '#c678dd',
2457
- cyan: '#56b6c2',
2458
- white: '#d7dae0',
2459
- brightBlack: '#4f5666',
2460
- brightRed: '#e06c75',
2461
- brightGreen: '#98c379',
2462
- brightYellow: '#e5c07b',
2463
- brightBlue: '#61afef',
2464
- brightMagenta: '#c678dd',
2465
- brightCyan: '#56b6c2',
2466
- brightWhite: '#ffffff',
2467
- };
2468
- const catppuccinTermTheme = {
2469
- background: '#1e1e2e',
2470
- foreground: '#cdd6f4',
2471
- cursor: '#f5e0dc',
2472
- cursorAccent: '#1e1e2e',
2473
- selectionBackground: 'rgba(88, 91, 112, 0.5)',
2474
- black: '#45475a',
2475
- red: '#f38ba8',
2476
- green: '#a6e3a1',
2477
- yellow: '#f9e2af',
2478
- blue: '#89b4fa',
2479
- magenta: '#f5c2e7',
2480
- cyan: '#94e2d5',
2481
- white: '#bac2de',
2482
- brightBlack: '#585b70',
2483
- brightRed: '#f38ba8',
2484
- brightGreen: '#a6e3a1',
2485
- brightYellow: '#f9e2af',
2486
- brightBlue: '#89b4fa',
2487
- brightMagenta: '#f5c2e7',
2488
- brightCyan: '#94e2d5',
2489
- brightWhite: '#a6adc8',
2490
- };
2491
- const gruvboxTermTheme = {
2492
- background: '#282828',
2493
- foreground: '#ebdbb2',
2494
- cursor: '#ebdbb2',
2495
- cursorAccent: '#282828',
2496
- selectionBackground: 'rgba(80, 73, 69, 0.7)',
2497
- black: '#282828',
2498
- red: '#cc241d',
2499
- green: '#98971a',
2500
- yellow: '#d79921',
2501
- blue: '#458588',
2502
- magenta: '#b16286',
2503
- cyan: '#689d6a',
2504
- white: '#a89984',
2505
- brightBlack: '#928374',
2506
- brightRed: '#fb4934',
2507
- brightGreen: '#b8bb26',
2508
- brightYellow: '#fabd2f',
2509
- brightBlue: '#83a598',
2510
- brightMagenta: '#d3869b',
2511
- brightCyan: '#8ec07c',
2512
- brightWhite: '#ebdbb2',
2513
- };
2514
- const nightOwlTermTheme = {
2515
- background: '#011627',
2516
- foreground: '#d6deeb',
2517
- cursor: '#80a4c2',
2518
- cursorAccent: '#011627',
2519
- selectionBackground: 'rgba(29, 59, 83, 0.7)',
2520
- black: '#010e1a',
2521
- red: '#ef5350',
2522
- green: '#22da6e',
2523
- yellow: '#addb67',
2524
- blue: '#82aaff',
2525
- magenta: '#c792ea',
2526
- cyan: '#21c7a8',
2527
- white: '#d6deeb',
2528
- brightBlack: '#575656',
2529
- brightRed: '#ef5350',
2530
- brightGreen: '#22da6e',
2531
- brightYellow: '#ffeb95',
2532
- brightBlue: '#82aaff',
2533
- brightMagenta: '#c792ea',
2534
- brightCyan: '#7fdbca',
2535
- brightWhite: '#ffffff',
2536
- };
2537
- const TERM_THEMES = {
2538
- dark: darkTermTheme,
2539
- light: lightTermTheme,
2540
- monokai: monokaiTermTheme,
2541
- 'solarized-dark': solarizedDarkTermTheme,
2542
- 'solarized-light': solarizedLightTermTheme,
2543
- nord: nordTermTheme,
2544
- dracula: draculaTermTheme,
2545
- 'github-dark': githubDarkTermTheme,
2546
- 'one-dark': oneDarkTermTheme,
2547
- catppuccin: catppuccinTermTheme,
2548
- gruvbox: gruvboxTermTheme,
2549
- 'night-owl': nightOwlTermTheme,
2550
- };
2551
-
2552
- // ===== Theme =====
2553
- const THEMES = [
2554
- { id: 'dark', name: 'Dark', bg: '#1e1e1e' },
2555
- { id: 'light', name: 'Light', bg: '#f3f3f3' },
2556
- { id: 'monokai', name: 'Monokai', bg: '#272822' },
2557
- { id: 'solarized-dark', name: 'Solarized Dark', bg: '#002b36' },
2558
- { id: 'solarized-light', name: 'Solarized Light', bg: '#fdf6e3' },
2559
- { id: 'nord', name: 'Nord', bg: '#2e3440' },
2560
- { id: 'dracula', name: 'Dracula', bg: '#282a36' },
2561
- { id: 'github-dark', name: 'GitHub Dark', bg: '#0d1117' },
2562
- { id: 'one-dark', name: 'One Dark', bg: '#282c34' },
2563
- { id: 'catppuccin', name: 'Catppuccin', bg: '#1e1e2e' },
2564
- { id: 'gruvbox', name: 'Gruvbox', bg: '#282828' },
2565
- { id: 'night-owl', name: 'Night Owl', bg: '#011627' },
2566
- ];
2567
- function getTheme() {
2568
- return localStorage.getItem('termbeam-theme') || 'dark';
2569
- }
2570
- function applyTheme(theme) {
2571
- document.documentElement.setAttribute('data-theme', theme);
2572
- const t = THEMES.find((x) => x.id === theme) || THEMES[0];
2573
- document.querySelector('meta[name="theme-color"]').content = t.bg;
2574
- localStorage.setItem('termbeam-theme', theme);
2575
- document.querySelectorAll('.theme-option').forEach((el) => {
2576
- el.classList.toggle('active', el.dataset.themeOption === theme);
2577
- });
2578
- const termTheme = TERM_THEMES[theme] || darkTermTheme;
2579
- for (const [, ms] of managed) {
2580
- ms.term.options.theme = termTheme;
2581
- }
2582
- }
2583
- applyTheme(getTheme());
2584
- document.getElementById('theme-toggle').addEventListener('click', (e) => {
2585
- e.stopPropagation();
2586
- document.getElementById('theme-picker').classList.toggle('open');
2587
- });
2588
- document.addEventListener('click', () => {
2589
- document.getElementById('theme-picker').classList.remove('open');
2590
- });
2591
- document.querySelectorAll('.theme-option').forEach((el) => {
2592
- el.addEventListener('click', (e) => {
2593
- e.stopPropagation();
2594
- applyTheme(el.dataset.themeOption);
2595
- document.getElementById('theme-picker').classList.remove('open');
2596
- });
2597
- });
2598
-
2599
2328
  // ===== Font Loading (non-blocking) =====
2600
2329
  const nerdFont = new FontFace(
2601
2330
  'NerdFont',
@@ -2614,11 +2343,6 @@
2614
2343
  init();
2615
2344
 
2616
2345
  // ===== Helpers =====
2617
- function esc(str) {
2618
- const d = document.createElement('div');
2619
- d.textContent = str;
2620
- return d.innerHTML;
2621
- }
2622
2346
  function escAttr(str) {
2623
2347
  return String(str)
2624
2348
  .replace(/&/g, '&amp;')
@@ -2731,12 +2455,6 @@
2731
2455
  loadShellsForModal();
2732
2456
  startPolling();
2733
2457
 
2734
- // Zoom
2735
- document.getElementById('zoom-in').addEventListener('click', () => applyZoom(fontSize + 2));
2736
- document
2737
- .getElementById('zoom-out')
2738
- .addEventListener('click', () => applyZoom(fontSize - 2));
2739
-
2740
2458
  // Pinch-to-zoom
2741
2459
  (function setupPinchZoom() {
2742
2460
  const wrapper = document.getElementById('terminals-wrapper');
@@ -2864,9 +2582,6 @@
2864
2582
  );
2865
2583
  }
2866
2584
 
2867
- // Split toggle
2868
- document.getElementById('split-toggle').addEventListener('click', toggleSplit);
2869
-
2870
2585
  // Scroll to bottom when returning from idle / tab switch
2871
2586
  document.addEventListener('visibilitychange', () => {
2872
2587
  if (!document.hidden && activeId) {
@@ -2914,7 +2629,7 @@
2914
2629
  fetch('/api/version')
2915
2630
  .then((r) => r.json())
2916
2631
  .then((d) => {
2917
- document.getElementById('version-text').textContent = 'v' + d.version;
2632
+ window._termbeamVersion = 'v' + d.version;
2918
2633
  document.getElementById('side-panel-version').textContent = 'v' + d.version;
2919
2634
  })
2920
2635
  .catch(() => {});
@@ -2923,6 +2638,7 @@
2923
2638
  // ===== Session Management =====
2924
2639
  function addSession(data) {
2925
2640
  if (managed.has(data.id)) return;
2641
+ managed.set(data.id, null); // reserve slot to prevent race condition
2926
2642
 
2927
2643
  const term = new window.Terminal({
2928
2644
  cursorBlink: true,
@@ -2941,8 +2657,10 @@
2941
2657
 
2942
2658
  const fitAddon = new window.FitAddon.FitAddon();
2943
2659
  const webLinksAddon = new window.WebLinksAddon.WebLinksAddon();
2660
+ const searchAddon = new window.SearchAddon.SearchAddon();
2944
2661
  term.loadAddon(fitAddon);
2945
2662
  term.loadAddon(webLinksAddon);
2663
+ term.loadAddon(searchAddon);
2946
2664
 
2947
2665
  const container = document.createElement('div');
2948
2666
  container.className = 'terminal-pane';
@@ -3090,8 +2808,10 @@
3090
2808
  shell: data.shell,
3091
2809
  cwd: data.cwd,
3092
2810
  pid: data.pid,
2811
+ git: data.git || null,
3093
2812
  term,
3094
2813
  fitAddon,
2814
+ searchAddon,
3095
2815
  container,
3096
2816
  coalescedWrite,
3097
2817
  scrollBtn,
@@ -3100,6 +2820,7 @@
3100
2820
  reconnectTimer: null,
3101
2821
  reconnectDelay: 3000,
3102
2822
  lastActivity: data.lastActivity || Date.now(),
2823
+ silenceTimer: null,
3103
2824
  };
3104
2825
 
3105
2826
  managed.set(data.id, ms);
@@ -3211,6 +2932,7 @@
3211
2932
  if (msg.type === 'output') {
3212
2933
  ms.coalescedWrite(msg.data);
3213
2934
  ms.lastActivity = Date.now();
2935
+ resetSilenceTimer(ms);
3214
2936
  markUnreadOutput();
3215
2937
  if (ms.id !== activeId && !ms.hasUnread) {
3216
2938
  ms.hasUnread = true;
@@ -3331,6 +3053,10 @@
3331
3053
  clearTimeout(ms.reconnectTimer);
3332
3054
  ms.reconnectTimer = null;
3333
3055
  }
3056
+ if (ms.silenceTimer) {
3057
+ clearTimeout(ms.silenceTimer);
3058
+ ms.silenceTimer = null;
3059
+ }
3334
3060
  if (ms.ws)
3335
3061
  try {
3336
3062
  ms.ws.close();
@@ -3563,6 +3289,26 @@
3563
3289
  '" title="Close session">×</button>' +
3564
3290
  '</div>' +
3565
3291
  (activity ? '<div class="side-panel-card-meta">' + activity + ' ago</div>' : '') +
3292
+ (ms.git
3293
+ ? '<div class="side-panel-card-git">' +
3294
+ '<span class="git-badge"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg> ' +
3295
+ esc(ms.git.branch || 'detached') +
3296
+ '</span>' +
3297
+ (ms.git.provider
3298
+ ? '<span class="git-badge">' + esc(ms.git.provider) + '</span>'
3299
+ : '') +
3300
+ (ms.git.repoName
3301
+ ? '<span class="git-badge">' + esc(ms.git.repoName) + '</span>'
3302
+ : '') +
3303
+ (ms.git.status
3304
+ ? '<span class="git-badge ' +
3305
+ (ms.git.status.clean ? 'git-status-clean' : 'git-status-dirty') +
3306
+ '">' +
3307
+ (ms.git.status.clean ? '✓ clean' : esc(ms.git.status.summary)) +
3308
+ '</span>'
3309
+ : '') +
3310
+ '</div>'
3311
+ : '') +
3566
3312
  previewContent +
3567
3313
  '</div>'
3568
3314
  );
@@ -3717,7 +3463,6 @@
3717
3463
  // ===== Split View =====
3718
3464
  function toggleSplit() {
3719
3465
  splitMode = !splitMode;
3720
- document.getElementById('split-toggle').classList.toggle('active', splitMode);
3721
3466
 
3722
3467
  if (splitMode) {
3723
3468
  // Find a second session to show
@@ -3751,178 +3496,6 @@
3751
3496
  renderTabs();
3752
3497
  }
3753
3498
 
3754
- // ===== Key Bar =====
3755
- // Modifier state (shared with terminal onData)
3756
- let ctrlActive = false;
3757
- let shiftActive = false;
3758
- function clearModifiers() {
3759
- ctrlActive = false;
3760
- shiftActive = false;
3761
- const ctrlBtn = document.getElementById('ctrl-btn');
3762
- const shiftBtn = document.getElementById('shift-btn');
3763
- if (ctrlBtn) ctrlBtn.classList.remove('active');
3764
- if (shiftBtn) shiftBtn.classList.remove('active');
3765
- }
3766
-
3767
- function setupKeyBar() {
3768
- const keyBar = document.getElementById('key-bar');
3769
- const ctrlBtn = document.getElementById('ctrl-btn');
3770
- const shiftBtn = document.getElementById('shift-btn');
3771
- let repeatTimer = null;
3772
- let repeatInterval = null;
3773
-
3774
- function toggleModifier(which) {
3775
- if (which === 'ctrl') {
3776
- ctrlActive = !ctrlActive;
3777
- ctrlBtn.classList.toggle('active', ctrlActive);
3778
- } else {
3779
- shiftActive = !shiftActive;
3780
- shiftBtn.classList.toggle('active', shiftActive);
3781
- }
3782
- }
3783
-
3784
- function applyModifiers(key) {
3785
- if (!ctrlActive && !shiftActive) return key;
3786
- // Modifier param: Shift=2, Ctrl=5, Ctrl+Shift=6
3787
- const mod = ctrlActive && shiftActive ? 6 : ctrlActive ? 5 : 2;
3788
- // Arrow keys: \x1b[X → \x1b[1;{mod}X
3789
- const csiMatch = key.match(/^\x1b\[([ABCD])$/);
3790
- if (csiMatch) return '\x1b[1;' + mod + csiMatch[1];
3791
- // Home/End: \x1bOH/\x1bOF → \x1b[1;{mod}H/F
3792
- const ssMatch = key.match(/^\x1bO([HF])$/);
3793
- if (ssMatch) return '\x1b[1;' + mod + ssMatch[1];
3794
- // Tab with Shift → reverse tab
3795
- if (key === '\x09' && shiftActive && !ctrlActive) return '\x1b[Z';
3796
- return key;
3797
- }
3798
-
3799
- function flashBtn(btn) {
3800
- btn.classList.add('flash');
3801
- setTimeout(() => btn.classList.remove('flash'), 120);
3802
- }
3803
-
3804
- function sendKey(btn) {
3805
- if (!btn || !btn.dataset.key) return;
3806
- flashBtn(btn);
3807
- let data = btn.dataset.key === 'enter' ? '\r' : btn.dataset.key;
3808
- data = applyModifiers(data);
3809
- const ms = managed.get(activeId);
3810
- if (ms && ms.ws && ms.ws.readyState === 1) {
3811
- ms.ws.send(JSON.stringify({ type: 'input', data }));
3812
- }
3813
- clearModifiers();
3814
- }
3815
-
3816
- function stopRepeat() {
3817
- clearTimeout(repeatTimer);
3818
- clearInterval(repeatInterval);
3819
- repeatTimer = null;
3820
- repeatInterval = null;
3821
- }
3822
-
3823
- function startRepeat(btn) {
3824
- stopRepeat();
3825
- sendKey(btn);
3826
- repeatTimer = setTimeout(() => {
3827
- repeatInterval = setInterval(() => sendKey(btn), 80);
3828
- }, 400);
3829
- }
3830
-
3831
- ctrlBtn.addEventListener('click', (e) => {
3832
- e.preventDefault();
3833
- e.stopPropagation();
3834
- toggleModifier('ctrl');
3835
- });
3836
- shiftBtn.addEventListener('click', (e) => {
3837
- e.preventDefault();
3838
- e.stopPropagation();
3839
- toggleModifier('shift');
3840
- });
3841
- ctrlBtn.addEventListener('mousedown', (e) => e.preventDefault());
3842
- shiftBtn.addEventListener('mousedown', (e) => e.preventDefault());
3843
- ctrlBtn.addEventListener(
3844
- 'touchstart',
3845
- (e) => {
3846
- e.preventDefault();
3847
- keyBarTouched = true;
3848
- toggleModifier('ctrl');
3849
- },
3850
- { passive: false },
3851
- );
3852
- shiftBtn.addEventListener(
3853
- 'touchstart',
3854
- (e) => {
3855
- e.preventDefault();
3856
- keyBarTouched = true;
3857
- toggleModifier('shift');
3858
- },
3859
- { passive: false },
3860
- );
3861
-
3862
- let keyBarTouched = false;
3863
- keyBar.addEventListener('mousedown', (e) => {
3864
- if (keyBarTouched) {
3865
- keyBarTouched = false;
3866
- return;
3867
- }
3868
- const btn = e.target.closest('.key-btn');
3869
- if (btn && btn.dataset.key) {
3870
- e.preventDefault();
3871
- startRepeat(btn);
3872
- }
3873
- });
3874
- keyBar.addEventListener('mouseup', stopRepeat);
3875
- keyBar.addEventListener('mouseleave', stopRepeat);
3876
-
3877
- // Touch handling: allow native scroll when swiping, fire key only on tap
3878
- const SWIPE_THRESHOLD = 10;
3879
- let touchStartX = 0;
3880
- let touchBtn = null;
3881
- let touchMoved = false;
3882
-
3883
- keyBar.addEventListener(
3884
- 'touchstart',
3885
- (e) => {
3886
- keyBarTouched = true;
3887
- touchBtn = e.target.closest('.key-btn');
3888
- touchMoved = false;
3889
- touchStartX = e.touches[0].clientX;
3890
- // Don't preventDefault — let the browser handle scroll
3891
- },
3892
- { passive: true },
3893
- );
3894
- keyBar.addEventListener(
3895
- 'touchmove',
3896
- (e) => {
3897
- if (Math.abs(e.touches[0].clientX - touchStartX) > SWIPE_THRESHOLD) {
3898
- touchMoved = true;
3899
- stopRepeat();
3900
- }
3901
- },
3902
- { passive: true },
3903
- );
3904
- keyBar.addEventListener('touchend', (e) => {
3905
- if (!touchMoved && touchBtn && touchBtn.dataset.key) {
3906
- e.preventDefault();
3907
- sendKey(touchBtn);
3908
- }
3909
- stopRepeat();
3910
- touchBtn = null;
3911
- });
3912
- keyBar.addEventListener('touchcancel', () => {
3913
- stopRepeat();
3914
- touchBtn = null;
3915
- });
3916
-
3917
- keyBar.addEventListener('click', (e) => {
3918
- const btn = e.target.closest('.key-btn');
3919
- if (btn) {
3920
- const ms = managed.get(activeId);
3921
- if (ms) ms.term.focus();
3922
- }
3923
- });
3924
- }
3925
-
3926
3499
  // ===== Paste =====
3927
3500
  function setupPaste() {
3928
3501
  const pasteOverlay = document.getElementById('paste-overlay');
@@ -4337,6 +3910,18 @@
4337
3910
  if (cmd) body.initialCommand = cmd;
4338
3911
  if (color) body.color = color;
4339
3912
 
3913
+ // Include current terminal dimensions so the PTY spawns at the right
3914
+ // size — prevents oh-my-posh and other slow prompts from rendering
3915
+ // at the default 120×30 size and triggering a duplicate on SIGWINCH.
3916
+ const activeMs = managed.get(activeId);
3917
+ if (activeMs && activeMs.fitAddon) {
3918
+ const dims = activeMs.fitAddon.proposeDimensions();
3919
+ if (dims) {
3920
+ body.cols = dims.cols;
3921
+ body.rows = dims.rows;
3922
+ }
3923
+ }
3924
+
4340
3925
  try {
4341
3926
  const res = await fetch('/api/sessions', {
4342
3927
  method: 'POST',
@@ -4378,7 +3963,9 @@
4378
3963
  const ms = managed.get(s.id);
4379
3964
  ms.name = s.name;
4380
3965
  ms.color = s.color;
3966
+ ms.cwd = s.cwd;
4381
3967
  ms.lastActivity = s.lastActivity;
3968
+ ms.git = s.git || null;
4382
3969
  }
4383
3970
  }
4384
3971
 
@@ -4391,6 +3978,10 @@
4391
3978
  clearTimeout(ms.reconnectTimer);
4392
3979
  ms.reconnectTimer = null;
4393
3980
  }
3981
+ if (ms.silenceTimer) {
3982
+ clearTimeout(ms.silenceTimer);
3983
+ ms.silenceTimer = null;
3984
+ }
4394
3985
  if (ms.ws)
4395
3986
  try {
4396
3987
  ms.ws.close();
@@ -4524,12 +4115,11 @@
4524
4115
  });
4525
4116
  }
4526
4117
 
4527
- document.getElementById('share-btn').addEventListener('click', async () => {
4118
+ async function shareLink() {
4528
4119
  const urlPromise = fetch('/api/share-token')
4529
4120
  .then((r) => (r.ok ? r.json() : null))
4530
4121
  .then((data) => (data && data.url) || location.href)
4531
4122
  .catch(() => location.href);
4532
- // ClipboardItem with a promise preserves user activation across the fetch
4533
4123
  if (navigator.clipboard && typeof ClipboardItem !== 'undefined') {
4534
4124
  try {
4535
4125
  const blobPromise = urlPromise.then((u) => new Blob([u], { type: 'text/plain' }));
@@ -4538,7 +4128,6 @@
4538
4128
  return;
4539
4129
  } catch {}
4540
4130
  }
4541
- // Fallback: resolve URL first, then try legacy methods
4542
4131
  const url = await urlPromise;
4543
4132
  if (navigator.clipboard && navigator.clipboard.writeText) {
4544
4133
  try {
@@ -4552,10 +4141,9 @@
4552
4141
  } else {
4553
4142
  showShareUrlPrompt(url);
4554
4143
  }
4555
- });
4144
+ }
4556
4145
 
4557
- // ===== Refresh Button =====
4558
- document.getElementById('refresh-btn').addEventListener('click', async () => {
4146
+ async function refreshApp() {
4559
4147
  if ('caches' in window) {
4560
4148
  const keys = await caches.keys();
4561
4149
  await Promise.all(keys.map((k) => caches.delete(k)));
@@ -4565,12 +4153,282 @@
4565
4153
  if (reg) await reg.update();
4566
4154
  }
4567
4155
  location.reload();
4568
- });
4156
+ }
4157
+
4158
+ // ===== Command Palette =====
4159
+ (function setupPalette() {
4160
+ const paletteActions = [
4161
+ {
4162
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
4163
+ label: 'New tab',
4164
+ category: 'Session',
4165
+ action: () => openNewSessionModal(),
4166
+ },
4167
+ {
4168
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>',
4169
+ label: 'Close tab',
4170
+ category: 'Session',
4171
+ action: () => {
4172
+ if (!activeId) return;
4173
+ const ms = managed.get(activeId);
4174
+ const name = (ms && ms.name) || activeId.slice(0, 8);
4175
+ if (confirm('Close session "' + name + '"?')) {
4176
+ removeSession(activeId);
4177
+ }
4178
+ },
4179
+ },
4180
+ {
4181
+ icon: '<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="M17 3a2.828 2.828 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/></svg>',
4182
+ label: 'Rename session',
4183
+ category: 'Session',
4184
+ action: () => {
4185
+ if (!activeId) return;
4186
+ const ms = managed.get(activeId);
4187
+ if (!ms) return;
4188
+ const name = prompt('Rename session:', ms.name || '');
4189
+ if (name !== null && name.trim()) {
4190
+ ms.name = name.trim();
4191
+ renderTabs();
4192
+ updateStatusBar();
4193
+ }
4194
+ },
4195
+ },
4196
+ {
4197
+ icon: '<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>',
4198
+ label: 'Split view',
4199
+ category: 'Session',
4200
+ action: () => toggleSplit(),
4201
+ },
4202
+ {
4203
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>',
4204
+ label: 'Stop session',
4205
+ category: 'Session',
4206
+ action: () => {
4207
+ if (!activeId) return;
4208
+ if (!confirm('Stop this session? The process will be killed.')) return;
4209
+ removeSession(activeId);
4210
+ },
4211
+ },
4212
+ {
4213
+ icon: '<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="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
4214
+ label: 'Find in terminal',
4215
+ category: 'Search',
4216
+ action: () => openSearchBar(),
4217
+ },
4218
+ {
4219
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
4220
+ label: 'Increase font size',
4221
+ category: 'View',
4222
+ action: () => applyZoom(fontSize + 1),
4223
+ },
4224
+ {
4225
+ icon: '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="8" y1="12" x2="16" y2="12"/></svg>',
4226
+ label: 'Decrease font size',
4227
+ category: 'View',
4228
+ action: () => applyZoom(fontSize - 1),
4229
+ },
4230
+ {
4231
+ icon: '<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="13.5" cy="6.5" r=".5" fill="currentColor"/><circle cx="17.5" cy="10.5" r=".5" fill="currentColor"/><circle cx="8.5" cy="7.5" r=".5" fill="currentColor"/><circle cx="6.5" cy="12.5" r=".5" fill="currentColor"/><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"/></svg>',
4232
+ get label() {
4233
+ const current = THEMES.find((x) => x.id === getTheme()) || THEMES[0];
4234
+ return 'Theme (' + current.name + ')';
4235
+ },
4236
+ category: 'View',
4237
+ action: () => {
4238
+ openThemeSubpanel();
4239
+ },
4240
+ },
4241
+ {
4242
+ icon: '<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="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
4243
+ label: 'Preview port',
4244
+ category: 'View',
4245
+ action: () => openPreviewModal(),
4246
+ },
4247
+ {
4248
+ icon: '<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="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
4249
+ label: 'Copy link',
4250
+ category: 'Share',
4251
+ action: shareLink,
4252
+ },
4253
+ {
4254
+ icon: '<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="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>',
4255
+ get label() {
4256
+ return notificationsEnabled ? 'Notifications (on)' : 'Notifications (off)';
4257
+ },
4258
+ category: 'Notifications',
4259
+ keepOpen: true,
4260
+ action: () => {
4261
+ notificationsEnabled = !notificationsEnabled;
4262
+ localStorage.setItem('termbeam-notifications', notificationsEnabled);
4263
+ if (
4264
+ notificationsEnabled &&
4265
+ 'Notification' in window &&
4266
+ Notification.permission === 'default'
4267
+ ) {
4268
+ Notification.requestPermission();
4269
+ }
4270
+ showToast(notificationsEnabled ? 'Notifications on' : 'Notifications off');
4271
+ renderPalette();
4272
+ },
4273
+ },
4274
+ {
4275
+ icon: '<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>',
4276
+ label: 'Refresh',
4277
+ category: 'System',
4278
+ action: () => refreshApp(),
4279
+ },
4280
+ {
4281
+ icon: '<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 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><line x1="18" y1="9" x2="12" y2="15"/><line x1="12" y1="9" x2="18" y2="15"/></svg>',
4282
+ label: 'Clear terminal',
4283
+ category: 'System',
4284
+ action: () => {
4285
+ if (!activeId) return;
4286
+ const ms = managed.get(activeId);
4287
+ if (ms) ms.term.clear();
4288
+ },
4289
+ },
4290
+ {
4291
+ icon: '<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="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>',
4292
+ label: 'About',
4293
+ category: 'System',
4294
+ action: () => {
4295
+ const ver = window._termbeamVersion || 'TermBeam';
4296
+ const overlay = document.createElement('div');
4297
+ overlay.style.cssText =
4298
+ 'position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:300;display:flex;align-items:center;justify-content:center;';
4299
+ const box = document.createElement('div');
4300
+ box.style.cssText =
4301
+ 'background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:90vw;width:320px;text-align:center;';
4302
+ box.innerHTML =
4303
+ '<div style="font-size:24px;margin-bottom:8px;">⚡</div>' +
4304
+ '<div style="font-size:16px;font-weight:600;color:var(--text);margin-bottom:4px;">TermBeam</div>' +
4305
+ '<div style="font-size:13px;color:var(--text-secondary);margin-bottom:16px;">' +
4306
+ esc(ver) +
4307
+ '</div>' +
4308
+ '<div style="font-size:12px;color:var(--text-secondary);margin-bottom:16px;">Terminal in your browser, optimized for mobile.</div>' +
4309
+ '<div style="display:flex;gap:16px;justify-content:center;margin-bottom:16px;">' +
4310
+ '<a href="https://github.com/dorlugasigal/TermBeam" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">GitHub</a>' +
4311
+ '<a href="https://dorlugasigal.github.io/TermBeam/" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">Docs</a>' +
4312
+ '<a href="https://termbeam.pages.dev" target="_blank" rel="noopener" style="color:var(--accent);font-size:12px;text-decoration:none;">Website</a>' +
4313
+ '</div>';
4314
+ const btn = document.createElement('button');
4315
+ btn.textContent = 'Close';
4316
+ btn.style.cssText =
4317
+ 'padding:6px 20px;border-radius:6px;border:none;background:var(--accent);color:#fff;font-size:13px;font-weight:600;cursor:pointer;';
4318
+ btn.onclick = () => overlay.remove();
4319
+ box.appendChild(btn);
4320
+ overlay.appendChild(box);
4321
+ overlay.addEventListener('click', (e) => {
4322
+ if (e.target === overlay) overlay.remove();
4323
+ });
4324
+ document.body.appendChild(overlay);
4325
+ },
4326
+ },
4327
+ ];
4328
+
4329
+ const backdrop = document.getElementById('palette-backdrop');
4330
+ const panel = document.getElementById('palette-panel');
4331
+ const body = document.getElementById('palette-body');
4332
+
4333
+ function renderPalette() {
4334
+ const grouped = {};
4335
+ paletteActions.forEach((a) => {
4336
+ if (!grouped[a.category]) grouped[a.category] = [];
4337
+ grouped[a.category].push(a);
4338
+ });
4339
+ body.innerHTML = '';
4340
+ Object.keys(grouped).forEach((cat) => {
4341
+ const header = document.createElement('div');
4342
+ header.className = 'palette-category';
4343
+ header.textContent = cat;
4344
+ body.appendChild(header);
4345
+ grouped[cat].forEach((a) => {
4346
+ const btn = document.createElement('button');
4347
+ btn.className = 'palette-action';
4348
+ btn.innerHTML =
4349
+ '<span class="palette-action-icon">' + a.icon + '</span>' + esc(a.label);
4350
+ btn.addEventListener('click', () => {
4351
+ if (!a.keepOpen) closePalette();
4352
+ a.action();
4353
+ });
4354
+ body.appendChild(btn);
4355
+ });
4356
+ });
4357
+ }
4358
+
4359
+ function openPalette() {
4360
+ backdrop.classList.add('open');
4361
+ panel.classList.add('open');
4362
+ }
4363
+
4364
+ function closePalette() {
4365
+ backdrop.classList.remove('open');
4366
+ panel.classList.remove('open');
4367
+ }
4368
+
4369
+ function togglePalette() {
4370
+ if (panel.classList.contains('open')) closePalette();
4371
+ else openPalette();
4372
+ }
4373
+
4374
+ backdrop.addEventListener('click', closePalette);
4375
+ document.getElementById('palette-close').addEventListener('click', closePalette);
4376
+ document.getElementById('palette-trigger').addEventListener('click', togglePalette);
4377
+
4378
+ document.addEventListener('keydown', (e) => {
4379
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
4380
+ e.preventDefault();
4381
+ togglePalette();
4382
+ }
4383
+ if (e.key === 'Escape' && panel.classList.contains('open')) {
4384
+ closePalette();
4385
+ }
4386
+ });
4387
+
4388
+ renderPalette();
4389
+ })();
4390
+
4391
+ // ===== Theme Sub-Panel =====
4392
+ (function setupThemeSubpanel() {
4393
+ const subpanel = document.getElementById('theme-subpanel');
4394
+ const list = document.getElementById('theme-subpanel-list');
4395
+ document.getElementById('theme-subpanel-close').addEventListener('click', () => {
4396
+ subpanel.classList.remove('open');
4397
+ });
4398
+ function renderThemeList() {
4399
+ const cur = getTheme();
4400
+ list.innerHTML = THEMES.map(
4401
+ (t) =>
4402
+ '<button class="theme-subpanel-item' +
4403
+ (t.id === cur ? ' active' : '') +
4404
+ '" data-tid="' +
4405
+ t.id +
4406
+ '"><span class="theme-subpanel-swatch" style="background:' +
4407
+ t.bg +
4408
+ '"></span>' +
4409
+ esc(t.name) +
4410
+ '</button>',
4411
+ ).join('');
4412
+ list.querySelectorAll('.theme-subpanel-item').forEach((btn) => {
4413
+ btn.addEventListener('click', () => {
4414
+ applyTheme(btn.dataset.tid);
4415
+ renderThemeList();
4416
+ });
4417
+ });
4418
+ }
4419
+ window.openThemeSubpanel = function () {
4420
+ renderThemeList();
4421
+ subpanel.classList.add('open');
4422
+ };
4423
+ document.addEventListener('keydown', (e) => {
4424
+ if (e.key === 'Escape' && subpanel.classList.contains('open')) {
4425
+ subpanel.classList.remove('open');
4426
+ }
4427
+ });
4428
+ })();
4569
4429
 
4570
4430
  // ===== Service Worker =====
4571
- if ('serviceWorker' in navigator) {
4572
- navigator.serviceWorker.register('/sw.js').catch(() => {});
4573
- }
4431
+ registerServiceWorker();
4574
4432
  </script>
4575
4433
  </body>
4576
4434
  </html>