termbeam 1.11.1 → 1.12.1

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/src/websocket.js CHANGED
@@ -6,8 +6,26 @@ const ACTIVE_THRESHOLD = 60000; // 60 seconds
6
6
  // on replay: color queries trigger xterm.js to generate responses that echo
7
7
  // through the PTY as visible text, accumulating on each refresh.
8
8
  const OSC_COLOR_RE = /\x1b\](?:4;\d+|10|11|12);[^\x07\x1b]*(?:\x07|\x1b\\)/g;
9
+
10
+ // Alternate screen buffer sequences (DECSET/DECRST 1049, 1047, 47) cause
11
+ // screen wipes on replay: entering alt screen hides the normal buffer content
12
+ // and re-entering on replay makes the terminal appear blank/wiped.
13
+ // Matched enter+exit pairs are stripped along with their content (the alt
14
+ // screen output is no longer relevant after the program exits).
15
+ // Unmatched enters/exits are stripped as bare sequences.
16
+ const ALT_SCREEN_PAIR_RE = /\x1b\[\?(1049|1047|47)h[\s\S]*?\x1b\[\?\1l/g;
17
+ const ALT_SCREEN_BARE_RE = /\x1b\[\?(?:1049|1047|47)[hl]/g;
18
+
19
+ // Clear-scrollback (ESC[3J) is destructive on replay — it would wipe
20
+ // the xterm.js scrollback that the user might want to scroll through.
21
+ const CLEAR_SCROLLBACK_RE = /\x1b\[3J/g;
22
+
9
23
  function sanitizeForReplay(buf) {
10
- return buf.replace(OSC_COLOR_RE, '');
24
+ buf = buf.replace(OSC_COLOR_RE, '');
25
+ buf = buf.replace(ALT_SCREEN_PAIR_RE, '');
26
+ buf = buf.replace(ALT_SCREEN_BARE_RE, '');
27
+ buf = buf.replace(CLEAR_SCROLLBACK_RE, '');
28
+ return buf;
11
29
  }
12
30
 
13
31
  function recalcPtySize(session) {
@@ -137,6 +155,14 @@ function setupWebSocket(wss, { auth, sessions }) {
137
155
  JSON.stringify({ type: 'output', data: sanitizeForReplay(session.scrollbackBuf) }),
138
156
  );
139
157
  }
158
+ // After replaying scrollback (which has alt-screen sequences
159
+ // stripped by sanitizeForReplay), re-enter alt-screen so xterm.js
160
+ // uses the correct buffer before SIGWINCH triggers the app to repaint.
161
+ if (session.inAltScreen) {
162
+ const mode = session.altScreenMode || '1049';
163
+ ws.send(JSON.stringify({ type: 'output', data: `\x1b[?${mode}h` }));
164
+ ws._needsRedraw = true;
165
+ }
140
166
  }
141
167
  ws.send(JSON.stringify({ type: 'attached', sessionId: msg.sessionId }));
142
168
  log.info(`Client attached to session ${msg.sessionId}`);
@@ -176,9 +202,76 @@ function setupWebSocket(wss, { auth, sessions }) {
176
202
  }),
177
203
  );
178
204
  }
205
+ // After replaying scrollback (which has alt-screen sequences
206
+ // stripped), re-enter alt-screen so xterm.js uses the correct
207
+ // buffer before SIGWINCH triggers the TUI to repaint.
208
+ if (attached.inAltScreen) {
209
+ const mode = attached.altScreenMode || '1049';
210
+ ws.send(JSON.stringify({ type: 'output', data: `\x1b[?${mode}h` }));
211
+ // Force SIGWINCH so the TUI repaints into the alt buffer
212
+ recalcPtySize(attached);
213
+ const targetCols = attached._lastCols || cols;
214
+ const targetRows = attached._lastRows || rows;
215
+ try {
216
+ const small = Math.min(500, targetCols === 1 ? 2 : targetCols - 1);
217
+ attached.pty.resize(small, targetRows);
218
+ attached._lastCols = small;
219
+ } catch {
220
+ // ignore — PTY may have exited
221
+ }
222
+ if (attached._resizeBounceTimer) {
223
+ clearTimeout(attached._resizeBounceTimer);
224
+ }
225
+ attached._resizeBounceTimer = setTimeout(() => {
226
+ attached._resizeBounceTimer = null;
227
+ try {
228
+ attached.pty.resize(targetCols, targetRows);
229
+ attached._lastCols = targetCols;
230
+ } catch {
231
+ // ignore — PTY may have exited
232
+ }
233
+ }, 50);
234
+ }
179
235
  }
180
236
  } else {
181
- recalcPtySize(attached);
237
+ if (ws._needsRedraw) {
238
+ ws._needsRedraw = false;
239
+ // Force SIGWINCH so TUI apps (vim, copilot, htop) redraw
240
+ // after client reconnect. The ioctl only fires SIGWINCH when
241
+ // the size actually changes, and standard signals coalesce —
242
+ // so we must delay the restore to ensure the app queries
243
+ // the intermediate size before we resize back.
244
+ recalcPtySize(attached);
245
+ const targetCols = attached._lastCols || cols;
246
+ const targetRows = attached._lastRows || rows;
247
+ try {
248
+ const small = Math.min(500, targetCols === 1 ? 2 : targetCols - 1);
249
+ attached.pty.resize(small, targetRows);
250
+ attached._lastCols = small;
251
+ } catch {
252
+ // ignore — PTY may have exited
253
+ }
254
+ // Cancel any prior bounce timer to avoid stale restores
255
+ if (attached._resizeBounceTimer) {
256
+ clearTimeout(attached._resizeBounceTimer);
257
+ }
258
+ attached._resizeBounceTimer = setTimeout(() => {
259
+ attached._resizeBounceTimer = null;
260
+ try {
261
+ attached.pty.resize(targetCols, targetRows);
262
+ attached._lastCols = targetCols;
263
+ } catch {
264
+ // ignore — PTY may have exited
265
+ }
266
+ }, 50);
267
+ } else {
268
+ // Cancel any pending bounce timer from a prior redraw
269
+ if (attached._resizeBounceTimer) {
270
+ clearTimeout(attached._resizeBounceTimer);
271
+ attached._resizeBounceTimer = null;
272
+ }
273
+ recalcPtySize(attached);
274
+ }
182
275
  }
183
276
  }
184
277
  }
@@ -190,6 +283,12 @@ function setupWebSocket(wss, { auth, sessions }) {
190
283
  ws.on('close', () => {
191
284
  clearInterval(pingInterval);
192
285
  if (attached) {
286
+ if (attached._resizeBounceTimer) {
287
+ clearTimeout(attached._resizeBounceTimer);
288
+ attached._resizeBounceTimer = null;
289
+ // Restore PTY to correct size since the bounce was interrupted
290
+ recalcPtySize(attached);
291
+ }
193
292
  attached.clients.delete(ws);
194
293
  recalcPtySize(attached);
195
294
  log.info('Client detached');
@@ -1,217 +0,0 @@
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
- }