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.
package/README.md CHANGED
@@ -15,7 +15,7 @@
15
15
 
16
16
  TermBeam lets you access your terminal from a phone, tablet, or any browser — no SSH, no port forwarding, no config files. Run one command and scan the QR code.
17
17
 
18
- I built this because I kept needing to run quick commands on my dev machine while away from my desk, and SSH on a phone is painful. TermBeam gives you a real terminal with a touch-friendly UI that actually works on small screens.
18
+ I built this because I kept needing to run quick commands on my dev machine while away from my desk, and SSH on a phone is painful. TermBeam gives you a real terminal with a touch-optimized UI — key bar, swipe scroll, pinch zoom — that actually works on small screens. You get multi-session tabs with split view, terminal search, a command palette, 12 themes, and secure remote access out of the box.
19
19
 
20
20
  [Full documentation](https://dorlugasigal.github.io/TermBeam/) · [Website](https://termbeam.pages.dev)
21
21
 
@@ -26,8 +26,8 @@ https://github.com/user-attachments/assets/9dd4f3d7-f017-4314-9b3a-f6a5688e3671
26
26
  <table align="center">
27
27
  <tr>
28
28
  <td align="center"><img src="docs/assets/screenshots/mobile-session-hub.jpeg" alt="Session hub on mobile" width="250" /></td>
29
- <td align="center"><img src="docs/assets/screenshots/mobile-session-preview.jpeg" alt="Session preview on mobile" width="250" /></td>
30
29
  <td align="center"><img src="docs/assets/screenshots/mobile-terminal.jpeg" alt="Terminal on mobile" width="250" /></td>
30
+ <td align="center"><img src="docs/assets/screenshots/mobile-session-preview.jpeg" alt="Session preview on mobile" width="250" /></td>
31
31
  </tr>
32
32
  </table>
33
33
 
@@ -57,33 +57,6 @@ termbeam --no-tunnel # LAN-only (no tunnel)
57
57
  termbeam --no-password # disable password protection
58
58
  ```
59
59
 
60
- ## Features
61
-
62
- - **Mobile-first UI** with on-screen touch bar (arrow keys, Tab, Enter, Ctrl shortcuts, Esc) and touch-optimized controls
63
- - **Copy/paste support** — Copy button opens text overlay for finger-selectable terminal content; Paste button with clipboard API + fallback modal
64
- - **Image paste** — paste images from clipboard, uploaded to server
65
- - **Tabbed multi-session terminal** — open, switch, and manage multiple sessions from a single tab bar with drag-to-reorder
66
- - **Split view** — view two sessions side-by-side (horizontal on desktop, vertical on mobile)
67
- - **Session colors** — assign a color to each session for quick identification
68
- - **Activity indicators** — see how recently each session had output (e.g. "3s ago", "5m ago")
69
- - **Tab previews** — hover (desktop) or long-press (mobile) a tab to preview the last few lines of output
70
- - **Side panel** (mobile) — slide-out session list with output previews for quick switching
71
- - **Create sessions anywhere** — new session modal available from both the hub page and the terminal page
72
- - **Touch scrolling** — swipe to scroll through terminal history
73
- - **Share button** — share the TermBeam URL via Web Share API, clipboard, or legacy copy fallback (works over HTTP); each share gets a fresh auto-login link with a 5-minute share token
74
- - **QR code auto-login** — scan the QR code to log in automatically without typing the password (share token, 5-minute expiry)
75
- - **Refresh button** — clear PWA/service worker cache and reload to get the latest version
76
- - **iPhone PWA safe area** — full support for `viewport-fit=cover` and safe area insets on notched devices
77
- - **Password auth** with token-based cookies and rate-limited login
78
- - **Folder browser** to pick working directories without typing paths
79
- - **Initial command** — optionally launch a session straight into `htop`, `vim`, or any command
80
- - **Shell detection** — auto-detects your shell on all platforms (PowerShell, cmd, bash, zsh, Git Bash, WSL)
81
- - **QR code on startup** for instant phone connection
82
- - **Light/dark theme** with persistent preference
83
- - **Adjustable font size** via status bar controls, saved across sessions
84
- - **Port preview** — reverse-proxy a single local web server port and preview it in the browser (HTTP only; no WebSocket/HMR; best with server-rendered apps)
85
- - **Remote access via [DevTunnel](#remote-access)** — ephemeral or persisted public URLs
86
-
87
60
  ## Remote Access
88
61
 
89
62
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "termbeam",
3
- "version": "1.4.0",
3
+ "version": "1.7.0",
4
4
  "description": "Beam your terminal to any device — mobile-optimized web terminal with multi-session support",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -0,0 +1,217 @@
1
+ /* TermBeam shared theme variables — imported by index.html and terminal.html */
2
+
3
+ :root {
4
+ --bg: #1e1e1e;
5
+ --surface: #252526;
6
+ --border: #3c3c3c;
7
+ --border-subtle: #474747;
8
+ --text: #d4d4d4;
9
+ --text-secondary: #858585;
10
+ --text-dim: #6e6e6e;
11
+ --text-muted: #555555;
12
+ --accent: #0078d4;
13
+ --accent-hover: #1a8ae8;
14
+ --accent-active: #005a9e;
15
+ --danger: #f14c4c;
16
+ --danger-hover: #d73a3a;
17
+ --success: #89d185;
18
+ --overlay-bg: rgba(0, 0, 0, 0.85);
19
+ }
20
+
21
+ [data-theme='light'] {
22
+ --bg: #ffffff;
23
+ --surface: #f3f3f3;
24
+ --border: #e0e0e0;
25
+ --border-subtle: #d0d0d0;
26
+ --text: #1e1e1e;
27
+ --text-secondary: #616161;
28
+ --text-dim: #767676;
29
+ --text-muted: #a0a0a0;
30
+ --accent: #0078d4;
31
+ --accent-hover: #106ebe;
32
+ --accent-active: #005a9e;
33
+ --danger: #e51400;
34
+ --danger-hover: #c20000;
35
+ --success: #16825d;
36
+ --overlay-bg: rgba(0, 0, 0, 0.5);
37
+ }
38
+
39
+ [data-theme='monokai'] {
40
+ --bg: #272822;
41
+ --surface: #1e1f1c;
42
+ --border: #49483e;
43
+ --border-subtle: #5c5c4f;
44
+ --text: #f8f8f2;
45
+ --text-secondary: #a59f85;
46
+ --text-dim: #75715e;
47
+ --text-muted: #5a5854;
48
+ --accent: #a6e22e;
49
+ --accent-hover: #b8f53c;
50
+ --accent-active: #8acc16;
51
+ --danger: #f92672;
52
+ --danger-hover: #e0155d;
53
+ --success: #a6e22e;
54
+ --overlay-bg: rgba(0, 0, 0, 0.75);
55
+ }
56
+
57
+ [data-theme='solarized-dark'] {
58
+ --bg: #002b36;
59
+ --surface: #073642;
60
+ --border: #586e75;
61
+ --border-subtle: #657b83;
62
+ --text: #839496;
63
+ --text-secondary: #657b83;
64
+ --text-dim: #586e75;
65
+ --text-muted: #4a5a62;
66
+ --accent: #268bd2;
67
+ --accent-hover: #379ce3;
68
+ --accent-active: #1a7abf;
69
+ --danger: #dc322f;
70
+ --danger-hover: #c8221f;
71
+ --success: #859900;
72
+ --overlay-bg: rgba(0, 0, 0, 0.75);
73
+ }
74
+
75
+ [data-theme='solarized-light'] {
76
+ --bg: #fdf6e3;
77
+ --surface: #eee8d5;
78
+ --border: #93a1a1;
79
+ --border-subtle: #839496;
80
+ --text: #657b83;
81
+ --text-secondary: #93a1a1;
82
+ --text-dim: #a0a0a0;
83
+ --text-muted: #b0b0b0;
84
+ --accent: #268bd2;
85
+ --accent-hover: #379ce3;
86
+ --accent-active: #1a7abf;
87
+ --danger: #dc322f;
88
+ --danger-hover: #c8221f;
89
+ --success: #859900;
90
+ --overlay-bg: rgba(0, 0, 0, 0.4);
91
+ }
92
+
93
+ [data-theme='nord'] {
94
+ --bg: #2e3440;
95
+ --surface: #3b4252;
96
+ --border: #434c5e;
97
+ --border-subtle: #4c566a;
98
+ --text: #d8dee9;
99
+ --text-secondary: #b0bac9;
100
+ --text-dim: #7b88a1;
101
+ --text-muted: #5c6a85;
102
+ --accent: #88c0d0;
103
+ --accent-hover: #9fd4e4;
104
+ --accent-active: #6aafbf;
105
+ --danger: #bf616a;
106
+ --danger-hover: #a84d57;
107
+ --success: #a3be8c;
108
+ --overlay-bg: rgba(0, 0, 0, 0.75);
109
+ }
110
+
111
+ [data-theme='dracula'] {
112
+ --bg: #282a36;
113
+ --surface: #343746;
114
+ --border: #44475a;
115
+ --border-subtle: #525568;
116
+ --text: #f8f8f2;
117
+ --text-secondary: #c1c4d2;
118
+ --text-dim: #8e92a4;
119
+ --text-muted: #6272a4;
120
+ --accent: #bd93f9;
121
+ --accent-hover: #d0b0ff;
122
+ --accent-active: #a77de7;
123
+ --danger: #ff5555;
124
+ --danger-hover: #e03d3d;
125
+ --success: #50fa7b;
126
+ --overlay-bg: rgba(0, 0, 0, 0.75);
127
+ }
128
+
129
+ [data-theme='github-dark'] {
130
+ --bg: #0d1117;
131
+ --surface: #161b22;
132
+ --border: #30363d;
133
+ --border-subtle: #3d444d;
134
+ --text: #c9d1d9;
135
+ --text-secondary: #8b949e;
136
+ --text-dim: #6e7681;
137
+ --text-muted: #484f58;
138
+ --accent: #58a6ff;
139
+ --accent-hover: #79b8ff;
140
+ --accent-active: #388bfd;
141
+ --danger: #f85149;
142
+ --danger-hover: #da3633;
143
+ --success: #3fb950;
144
+ --overlay-bg: rgba(0, 0, 0, 0.75);
145
+ }
146
+
147
+ [data-theme='one-dark'] {
148
+ --bg: #282c34;
149
+ --surface: #21252b;
150
+ --border: #3e4452;
151
+ --border-subtle: #4b5263;
152
+ --text: #abb2bf;
153
+ --text-secondary: #7f848e;
154
+ --text-dim: #5c6370;
155
+ --text-muted: #4b5263;
156
+ --accent: #61afef;
157
+ --accent-hover: #7dc0ff;
158
+ --accent-active: #4d9ede;
159
+ --danger: #e06c75;
160
+ --danger-hover: #c95c67;
161
+ --success: #98c379;
162
+ --overlay-bg: rgba(0, 0, 0, 0.75);
163
+ }
164
+
165
+ [data-theme='catppuccin'] {
166
+ --bg: #1e1e2e;
167
+ --surface: #313244;
168
+ --border: #45475a;
169
+ --border-subtle: #585b70;
170
+ --text: #cdd6f4;
171
+ --text-secondary: #a6adc8;
172
+ --text-dim: #7f849c;
173
+ --text-muted: #585b70;
174
+ --accent: #89b4fa;
175
+ --accent-hover: #b4d0ff;
176
+ --accent-active: #5c9de3;
177
+ --danger: #f38ba8;
178
+ --danger-hover: #eb7c9d;
179
+ --success: #a6e3a1;
180
+ --overlay-bg: rgba(0, 0, 0, 0.75);
181
+ }
182
+
183
+ [data-theme='gruvbox'] {
184
+ --bg: #282828;
185
+ --surface: #3c3836;
186
+ --border: #504945;
187
+ --border-subtle: #665c54;
188
+ --text: #ebdbb2;
189
+ --text-secondary: #d5c4a1;
190
+ --text-dim: #a89984;
191
+ --text-muted: #7c6f64;
192
+ --accent: #83a598;
193
+ --accent-hover: #9dbfb4;
194
+ --accent-active: #6a8f8a;
195
+ --danger: #fb4934;
196
+ --danger-hover: #e33826;
197
+ --success: #b8bb26;
198
+ --overlay-bg: rgba(0, 0, 0, 0.75);
199
+ }
200
+
201
+ [data-theme='night-owl'] {
202
+ --bg: #011627;
203
+ --surface: #0d2a45;
204
+ --border: #1d3b53;
205
+ --border-subtle: #264863;
206
+ --text: #d6deeb;
207
+ --text-secondary: #8badc1;
208
+ --text-dim: #5f7e97;
209
+ --text-muted: #3f5f7d;
210
+ --accent: #7fdbca;
211
+ --accent-hover: #9ff0e0;
212
+ --accent-active: #62c5b5;
213
+ --danger: #ef5350;
214
+ --danger-hover: #d83130;
215
+ --success: #addb67;
216
+ --overlay-bg: rgba(0, 0, 0, 0.75);
217
+ }
package/public/index.html CHANGED
@@ -15,235 +15,56 @@
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 — Beam Your Terminal to Any Device</title>
19
20
  <style>
20
21
  :root {
21
- --bg: #1e1e1e;
22
- --surface: #252526;
23
- --border: #3c3c3c;
24
- --border-subtle: #474747;
25
- --text: #d4d4d4;
26
- --text-secondary: #858585;
27
- --text-dim: #6e6e6e;
28
- --text-muted: #5a5a5a;
29
- --accent: #0078d4;
30
- --accent-hover: #1a8ae8;
31
- --accent-active: #005a9e;
32
- --danger: #f14c4c;
33
- --danger-hover: #d73a3a;
34
- --success: #89d185;
35
22
  --info: #b0b0b0;
36
23
  --shadow: rgba(0, 0, 0, 0.15);
37
- --overlay-bg: rgba(0, 0, 0, 0.7);
38
24
  }
39
25
  [data-theme='light'] {
40
- --bg: #ffffff;
41
- --surface: #f3f3f3;
42
- --border: #e0e0e0;
43
- --border-subtle: #d0d0d0;
44
- --text: #1e1e1e;
45
- --text-secondary: #616161;
46
- --text-dim: #767676;
47
- --text-muted: #a0a0a0;
48
- --accent: #0078d4;
49
- --accent-hover: #106ebe;
50
- --accent-active: #005a9e;
51
- --danger: #e51400;
52
- --danger-hover: #c20000;
53
- --success: #16825d;
54
26
  --info: #616161;
55
27
  --shadow: rgba(0, 0, 0, 0.06);
56
- --overlay-bg: rgba(0, 0, 0, 0.4);
57
28
  }
58
29
  [data-theme='monokai'] {
59
- --bg: #272822;
60
- --surface: #1e1f1c;
61
- --border: #49483e;
62
- --border-subtle: #5c5c4f;
63
- --text: #f8f8f2;
64
- --text-secondary: #a59f85;
65
- --text-dim: #75715e;
66
- --text-muted: #5a5854;
67
- --accent: #a6e22e;
68
- --accent-hover: #b8f53c;
69
- --accent-active: #8acc16;
70
- --danger: #f92672;
71
- --danger-hover: #e0155d;
72
- --success: #a6e22e;
73
30
  --info: #a59f85;
74
31
  --shadow: rgba(0, 0, 0, 0.3);
75
- --overlay-bg: rgba(0, 0, 0, 0.75);
76
32
  }
77
33
  [data-theme='solarized-dark'] {
78
- --bg: #002b36;
79
- --surface: #073642;
80
- --border: #586e75;
81
- --border-subtle: #657b83;
82
- --text: #839496;
83
- --text-secondary: #657b83;
84
- --text-dim: #586e75;
85
- --text-muted: #4a5a62;
86
- --accent: #268bd2;
87
- --accent-hover: #379ce3;
88
- --accent-active: #1a7abf;
89
- --danger: #dc322f;
90
- --danger-hover: #c8221f;
91
- --success: #859900;
92
34
  --info: #657b83;
93
35
  --shadow: rgba(0, 0, 0, 0.25);
94
- --overlay-bg: rgba(0, 0, 0, 0.75);
95
36
  }
96
37
  [data-theme='solarized-light'] {
97
- --bg: #fdf6e3;
98
- --surface: #eee8d5;
99
- --border: #93a1a1;
100
- --border-subtle: #839496;
101
- --text: #657b83;
102
- --text-secondary: #93a1a1;
103
- --text-dim: #a0a0a0;
104
- --text-muted: #b0b0b0;
105
- --accent: #268bd2;
106
- --accent-hover: #379ce3;
107
- --accent-active: #1a7abf;
108
- --danger: #dc322f;
109
- --danger-hover: #c8221f;
110
- --success: #859900;
111
38
  --info: #93a1a1;
112
39
  --shadow: rgba(0, 0, 0, 0.08);
113
- --overlay-bg: rgba(0, 0, 0, 0.4);
114
40
  }
115
41
  [data-theme='nord'] {
116
- --bg: #2e3440;
117
- --surface: #3b4252;
118
- --border: #434c5e;
119
- --border-subtle: #4c566a;
120
- --text: #d8dee9;
121
- --text-secondary: #b0bac9;
122
- --text-dim: #7b88a1;
123
- --text-muted: #5c6a85;
124
- --accent: #88c0d0;
125
- --accent-hover: #9fd4e4;
126
- --accent-active: #6aafbf;
127
- --danger: #bf616a;
128
- --danger-hover: #a84d57;
129
- --success: #a3be8c;
130
42
  --info: #b0bac9;
131
43
  --shadow: rgba(0, 0, 0, 0.2);
132
- --overlay-bg: rgba(0, 0, 0, 0.75);
133
44
  }
134
45
  [data-theme='dracula'] {
135
- --bg: #282a36;
136
- --surface: #343746;
137
- --border: #44475a;
138
- --border-subtle: #525568;
139
- --text: #f8f8f2;
140
- --text-secondary: #c1c4d2;
141
- --text-dim: #8e92a4;
142
- --text-muted: #6272a4;
143
- --accent: #bd93f9;
144
- --accent-hover: #d0b0ff;
145
- --accent-active: #a77de7;
146
- --danger: #ff5555;
147
- --danger-hover: #e03d3d;
148
- --success: #50fa7b;
149
46
  --info: #c1c4d2;
150
47
  --shadow: rgba(0, 0, 0, 0.25);
151
- --overlay-bg: rgba(0, 0, 0, 0.75);
152
48
  }
153
49
  [data-theme='github-dark'] {
154
- --bg: #0d1117;
155
- --surface: #161b22;
156
- --border: #30363d;
157
- --border-subtle: #3d444d;
158
- --text: #c9d1d9;
159
- --text-secondary: #8b949e;
160
- --text-dim: #6e7681;
161
- --text-muted: #484f58;
162
- --accent: #58a6ff;
163
- --accent-hover: #79b8ff;
164
- --accent-active: #388bfd;
165
- --danger: #f85149;
166
- --danger-hover: #da3633;
167
- --success: #3fb950;
168
50
  --info: #8b949e;
169
51
  --shadow: rgba(0, 0, 0, 0.3);
170
- --overlay-bg: rgba(0, 0, 0, 0.75);
171
52
  }
172
53
  [data-theme='one-dark'] {
173
- --bg: #282c34;
174
- --surface: #21252b;
175
- --border: #3e4452;
176
- --border-subtle: #4b5263;
177
- --text: #abb2bf;
178
- --text-secondary: #7f848e;
179
- --text-dim: #5c6370;
180
- --text-muted: #4b5263;
181
- --accent: #61afef;
182
- --accent-hover: #7dc0ff;
183
- --accent-active: #4d9ede;
184
- --danger: #e06c75;
185
- --danger-hover: #c95c67;
186
- --success: #98c379;
187
54
  --info: #7f848e;
188
55
  --shadow: rgba(0, 0, 0, 0.25);
189
- --overlay-bg: rgba(0, 0, 0, 0.75);
190
56
  }
191
57
  [data-theme='catppuccin'] {
192
- --bg: #1e1e2e;
193
- --surface: #313244;
194
- --border: #45475a;
195
- --border-subtle: #585b70;
196
- --text: #cdd6f4;
197
- --text-secondary: #a6adc8;
198
- --text-dim: #7f849c;
199
- --text-muted: #585b70;
200
- --accent: #89b4fa;
201
- --accent-hover: #b4d0ff;
202
- --accent-active: #5c9de3;
203
- --danger: #f38ba8;
204
- --danger-hover: #eb7c9d;
205
- --success: #a6e3a1;
206
58
  --info: #a6adc8;
207
59
  --shadow: rgba(0, 0, 0, 0.2);
208
- --overlay-bg: rgba(0, 0, 0, 0.75);
209
60
  }
210
61
  [data-theme='gruvbox'] {
211
- --bg: #282828;
212
- --surface: #3c3836;
213
- --border: #504945;
214
- --border-subtle: #665c54;
215
- --text: #ebdbb2;
216
- --text-secondary: #d5c4a1;
217
- --text-dim: #a89984;
218
- --text-muted: #7c6f64;
219
- --accent: #83a598;
220
- --accent-hover: #9dbfb4;
221
- --accent-active: #6a8f8a;
222
- --danger: #fb4934;
223
- --danger-hover: #e33826;
224
- --success: #b8bb26;
225
62
  --info: #d5c4a1;
226
63
  --shadow: rgba(0, 0, 0, 0.25);
227
- --overlay-bg: rgba(0, 0, 0, 0.75);
228
64
  }
229
65
  [data-theme='night-owl'] {
230
- --bg: #011627;
231
- --surface: #0d2a45;
232
- --border: #1d3b53;
233
- --border-subtle: #264863;
234
- --text: #d6deeb;
235
- --text-secondary: #8badc1;
236
- --text-dim: #5f7e97;
237
- --text-muted: #3f5f7d;
238
- --accent: #7fdbca;
239
- --accent-hover: #9ff0e0;
240
- --accent-active: #62c5b5;
241
- --danger: #ef5350;
242
- --danger-hover: #d83130;
243
- --success: #addb67;
244
66
  --info: #8badc1;
245
67
  --shadow: rgba(0, 0, 0, 0.3);
246
- --overlay-bg: rgba(0, 0, 0, 0.75);
247
68
  }
248
69
  * {
249
70
  margin: 0;
@@ -507,6 +328,32 @@
507
328
  transform: scale(0.95);
508
329
  }
509
330
 
331
+ .session-card .git-info {
332
+ display: flex;
333
+ flex-wrap: wrap;
334
+ gap: 4px 10px;
335
+ font-size: 12px;
336
+ color: var(--text-secondary);
337
+ align-items: center;
338
+ }
339
+ .session-card .git-info .git-badge {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ gap: 4px;
343
+ background: var(--bg);
344
+ padding: 2px 8px;
345
+ border-radius: 4px;
346
+ transition:
347
+ background 0.3s,
348
+ color 0.3s;
349
+ }
350
+ .session-card .git-info .git-status-clean {
351
+ color: var(--success);
352
+ }
353
+ .session-card .git-info .git-status-dirty {
354
+ color: var(--warning, #fbbf24);
355
+ }
356
+
510
357
  .new-session {
511
358
  position: fixed;
512
359
  bottom: calc(16px + env(safe-area-inset-bottom, 0px));
@@ -1121,46 +968,12 @@
1121
968
  </div>
1122
969
  </div>
1123
970
 
971
+ <script src="/js/shared.js"></script>
972
+ <script src="/js/themes.js"></script>
1124
973
  <script>
1125
- // Theme
1126
- const THEMES = [
1127
- { id: 'dark', name: 'Dark', bg: '#1e1e1e' },
1128
- { id: 'light', name: 'Light', bg: '#f3f3f3' },
1129
- { id: 'monokai', name: 'Monokai', bg: '#272822' },
1130
- { id: 'solarized-dark', name: 'Solarized Dark', bg: '#002b36' },
1131
- { id: 'solarized-light', name: 'Solarized Light', bg: '#fdf6e3' },
1132
- { id: 'nord', name: 'Nord', bg: '#2e3440' },
1133
- { id: 'dracula', name: 'Dracula', bg: '#282a36' },
1134
- { id: 'github-dark', name: 'GitHub Dark', bg: '#0d1117' },
1135
- { id: 'one-dark', name: 'One Dark', bg: '#282c34' },
1136
- { id: 'catppuccin', name: 'Catppuccin', bg: '#1e1e2e' },
1137
- { id: 'gruvbox', name: 'Gruvbox', bg: '#282828' },
1138
- { id: 'night-owl', name: 'Night Owl', bg: '#011627' },
1139
- ];
1140
- function getTheme() {
1141
- return localStorage.getItem('termbeam-theme') || 'dark';
1142
- }
1143
- function applyTheme(theme) {
1144
- document.documentElement.setAttribute('data-theme', theme);
1145
- const t = THEMES.find((x) => x.id === theme) || THEMES[0];
1146
- document.querySelector('meta[name="theme-color"]').content = t.bg;
1147
- localStorage.setItem('termbeam-theme', theme);
1148
- document.querySelectorAll('.theme-option').forEach((el) => {
1149
- el.classList.toggle('active', el.dataset.themeOption === theme);
1150
- });
1151
- }
1152
- applyTheme(getTheme());
1153
- document.getElementById('theme-toggle').addEventListener('click', (e) => {
1154
- e.stopPropagation();
1155
- document.getElementById('theme-picker').classList.toggle('open');
1156
- });
1157
- document.addEventListener('click', () => {
1158
- document.getElementById('theme-picker').classList.remove('open');
1159
- });
974
+ // Close theme picker when option is selected (index-specific behavior)
1160
975
  document.querySelectorAll('.theme-option').forEach((el) => {
1161
- el.addEventListener('click', (e) => {
1162
- e.stopPropagation();
1163
- applyTheme(el.dataset.themeOption);
976
+ el.addEventListener('click', () => {
1164
977
  document.getElementById('theme-picker').classList.remove('open');
1165
978
  });
1166
979
  });
@@ -1204,6 +1017,16 @@
1204
1017
  <span><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> ${s.clients} connected</span>
1205
1018
  <span title="Last activity">${getActivityLabel(s.lastActivity)}</span>
1206
1019
  </div>
1020
+ ${
1021
+ s.git
1022
+ ? `<div class="git-info">
1023
+ <span class="git-badge"><svg width="12" height="12" 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> ${esc(s.git.branch || 'detached')}</span>
1024
+ ${s.git.provider ? `<span class="git-badge">${esc(s.git.provider)}</span>` : ''}
1025
+ ${s.git.repoName ? `<span class="git-badge">${esc(s.git.repoName)}</span>` : ''}
1026
+ ${s.git.status ? `<span class="git-badge ${s.git.status.clean ? 'git-status-clean' : 'git-status-dirty'}">${s.git.status.clean ? '✓ clean' : esc(s.git.status.summary)}</span>` : ''}
1027
+ </div>`
1028
+ : ''
1029
+ }
1207
1030
  <button class="connect-btn">Connect →</button>
1208
1031
  </div>
1209
1032
  </div>
@@ -1226,12 +1049,6 @@
1226
1049
  });
1227
1050
  }
1228
1051
 
1229
- function esc(str) {
1230
- const d = document.createElement('div');
1231
- d.textContent = str;
1232
- return d.innerHTML;
1233
- }
1234
-
1235
1052
  document.getElementById('new-session-btn').addEventListener('click', () => {
1236
1053
  loadShells();
1237
1054
  modal.classList.add('visible');
@@ -1504,21 +1321,6 @@
1504
1321
  setInterval(loadSessions, 3000);
1505
1322
 
1506
1323
  // Share button
1507
- function copyToClipboardFallback(text) {
1508
- const ta = document.createElement('textarea');
1509
- ta.value = text;
1510
- ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px';
1511
- document.body.appendChild(ta);
1512
- ta.focus();
1513
- ta.select();
1514
- let ok = false;
1515
- try {
1516
- ok = document.execCommand('copy');
1517
- } catch {}
1518
- document.body.removeChild(ta);
1519
- return ok;
1520
- }
1521
-
1522
1324
  function showShareToast(msg, duration) {
1523
1325
  const toast = document.createElement('div');
1524
1326
  toast.textContent = msg;
@@ -1602,9 +1404,7 @@
1602
1404
  location.reload();
1603
1405
  });
1604
1406
 
1605
- if ('serviceWorker' in navigator) {
1606
- navigator.serviceWorker.register('/sw.js').catch(() => {});
1607
- }
1407
+ registerServiceWorker();
1608
1408
  </script>
1609
1409
  </body>
1610
1410
  </html>