@bakapiano/ccsm 0.22.3 → 0.22.5

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.
Files changed (61) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +274 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +176 -176
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +645 -543
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +159 -22
  42. package/public/js/components/TerminalResizeDebouncer.js +126 -0
  43. package/public/js/components/TerminalView.js +15 -2
  44. package/public/js/components/XtermTerminal.js +74 -15
  45. package/public/js/components/useDragSort.js +67 -67
  46. package/public/js/dialog.js +67 -67
  47. package/public/js/icons.js +212 -212
  48. package/public/js/main.js +296 -296
  49. package/public/js/pages/AboutPage.js +90 -90
  50. package/public/js/pages/ConfigurePage.js +713 -713
  51. package/public/js/pages/LaunchPage.js +421 -421
  52. package/public/js/pages/RemotePage.js +743 -743
  53. package/public/js/pages/SessionsPage.js +199 -80
  54. package/public/js/state.js +335 -335
  55. package/public/manifest.webmanifest +25 -0
  56. package/public/setup/index.html +567 -0
  57. package/scripts/dev.js +149 -149
  58. package/scripts/install.js +153 -153
  59. package/scripts/restart-helper.js +96 -96
  60. package/scripts/upgrade-helper.js +687 -687
  61. package/server.js +1807 -1807
@@ -3,6 +3,7 @@
3
3
  // resize propagation, paste handling, and browser/mobile lifecycle hooks.
4
4
 
5
5
  import { wsBase, getToken, getDeviceId } from '../backend.js';
6
+ import { TerminalResizeDebouncer } from './TerminalResizeDebouncer.js';
6
7
  import { XtermTerminal } from './XtermTerminal.js';
7
8
 
8
9
  export class TerminalInstance {
@@ -18,19 +19,36 @@ export class TerminalInstance {
18
19
  this.attempts = 0;
19
20
  this.everOpened = false;
20
21
  this.inReplay = false;
22
+ this.replayDepth = 0;
23
+ this.isVisible = false;
24
+ this.lastLayoutDimensions = null;
21
25
  this.lastSentDimensions = null;
26
+ this.pendingLayoutFrame = null;
27
+ this.layoutRetryTimers = new Set();
22
28
  this.disposables = [];
23
29
  this.helperTextarea = null;
30
+ this.resizeDebouncer = new TerminalResizeDebouncer({
31
+ isVisible: () => this.isVisible,
32
+ getXterm: () => this.xterm,
33
+ resizeBoth: (cols, rows) => this._applyResize(cols, rows),
34
+ resizeX: (cols) => this._applyResize(cols, this.xterm.rows),
35
+ resizeY: (rows) => this._applyResize(this.xterm.cols, rows),
36
+ });
37
+ const refreshDisposable = this.xterm.onDidRequestRefreshDimensions(() => {
38
+ this.scheduleLayout({ immediate: this.isVisible, retries: true });
39
+ });
40
+ this.disposables.push(() => refreshDisposable.dispose());
24
41
  }
25
42
 
26
43
  attachToElement(host) {
27
44
  this.host = host;
28
45
  this.xterm.attachToElement(host);
29
46
  this._registerColorOscHandlers();
30
- this._connect();
31
47
  this._wireXtermEvents();
32
48
  this._wireDomLifecycle();
33
- this.xterm.focus();
49
+ this.setVisible(this._isHostVisible());
50
+ this._connect();
51
+ if (this.isVisible) this.xterm.focus();
34
52
  }
35
53
 
36
54
  sendInput(data) {
@@ -45,19 +63,68 @@ export class TerminalInstance {
45
63
  this.xterm.applyResolvedTheme();
46
64
  }
47
65
 
48
- layout(width, height) {
49
- const dimensions = (width > 0 && height > 0)
50
- ? this.xterm.layout(width, height)
51
- : this.xterm.layoutFromElement();
52
- if (dimensions) {
53
- this._sendResize(dimensions.cols, dimensions.rows);
66
+ focus() {
67
+ this.xterm.focus();
68
+ }
69
+
70
+ blur() {
71
+ this.xterm.blur();
72
+ }
73
+
74
+ layout(width, height, immediate = false) {
75
+ const layoutDimensions = this._resolveLayoutDimensions(width, height);
76
+ if (!layoutDimensions) return null;
77
+
78
+ this.lastLayoutDimensions = layoutDimensions;
79
+ const proposed = this.xterm.proposeDimensions(layoutDimensions.width, layoutDimensions.height);
80
+ if (!proposed) return null;
81
+
82
+ this.resizeDebouncer.resize(proposed.cols, proposed.rows, immediate);
83
+ return proposed;
84
+ }
85
+
86
+ scheduleLayout(options = {}) {
87
+ const { immediate = false, retries = false, forceRedraw = false } =
88
+ typeof options === 'boolean' ? { immediate: options } : options;
89
+ if (this.closedByUs) return null;
90
+
91
+ if (immediate) {
92
+ this._cancelScheduledLayout();
93
+ const result = this.layout(undefined, undefined, true);
94
+ if (forceRedraw) this.xterm.forceRedraw();
95
+ if (retries) this._scheduleLayoutRetries(forceRedraw);
96
+ return result;
54
97
  }
55
- return dimensions;
98
+
99
+ if (this.pendingLayoutFrame === null) {
100
+ this.pendingLayoutFrame = requestAnimationFrame(() => {
101
+ this.pendingLayoutFrame = null;
102
+ this.layout();
103
+ if (forceRedraw) this.xterm.forceRedraw();
104
+ });
105
+ }
106
+ if (retries) this._scheduleLayoutRetries(forceRedraw);
107
+ return null;
108
+ }
109
+
110
+ setVisible(visible) {
111
+ const nextVisible = !!visible;
112
+ const didChange = this.isVisible !== nextVisible;
113
+ this.isVisible = nextVisible;
114
+ this.host?.classList.toggle('active', nextVisible);
115
+
116
+ if (nextVisible) {
117
+ this.resizeDebouncer.flush();
118
+ this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
119
+ }
120
+ return didChange;
56
121
  }
57
122
 
58
123
  dispose() {
59
124
  this.closedByUs = true;
60
125
  if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
126
+ this._cancelScheduledLayout();
127
+ this.resizeDebouncer.dispose();
61
128
  for (const dispose of this.disposables.splice(0)) {
62
129
  try { dispose(); } catch {}
63
130
  }
@@ -78,8 +145,7 @@ export class TerminalInstance {
78
145
  }
79
146
  this.everOpened = true;
80
147
  this.attempts = 0;
81
- this.layout();
82
- this.xterm.scheduleLayout();
148
+ this.scheduleLayout({ immediate: true, retries: true });
83
149
  this._sendResize(this.xterm.cols, this.xterm.rows, true);
84
150
  };
85
151
  ws.onmessage = (ev) => {
@@ -140,7 +206,7 @@ export class TerminalInstance {
140
206
  this.disposables.push(() => ro.disconnect());
141
207
 
142
208
  const vv = window.visualViewport;
143
- const onVisualResize = () => this.xterm.scheduleLayout();
209
+ const onVisualResize = () => this.scheduleLayout({ retries: true });
144
210
  vv?.addEventListener?.('resize', onVisualResize);
145
211
  vv?.addEventListener?.('scroll', onVisualResize);
146
212
  this.disposables.push(() => {
@@ -155,6 +221,7 @@ export class TerminalInstance {
155
221
  }
156
222
 
157
223
  this._wireTabVisibilityRefresh(host);
224
+ this._wireDocumentVisibilityRefresh();
158
225
  this._wirePasteHandlers(host);
159
226
  this._wireModifiedEnterHandler(host);
160
227
  this._wireCompositionHandlers();
@@ -164,19 +231,24 @@ export class TerminalInstance {
164
231
  const panel = host.closest('.tab-panel');
165
232
  if (!panel) return;
166
233
  const panelMo = new MutationObserver(() => {
167
- if (panel.hasAttribute('data-active')) {
168
- requestAnimationFrame(() => {
169
- this.xterm.clearTextureAtlas();
170
- this.xterm.scheduleLayout();
171
- this.layout();
172
- this.xterm.refresh();
173
- });
174
- }
234
+ this.setVisible(this._isHostVisible());
175
235
  });
176
236
  panelMo.observe(panel, { attributes: true, attributeFilter: ['data-active'] });
177
237
  this.disposables.push(() => panelMo.disconnect());
178
238
  }
179
239
 
240
+ _wireDocumentVisibilityRefresh() {
241
+ const onVisibilityChange = () => {
242
+ this.setVisible(!document.hidden && this._isHostVisible());
243
+ };
244
+ document.addEventListener('visibilitychange', onVisibilityChange);
245
+ window.addEventListener('focus', onVisibilityChange);
246
+ this.disposables.push(
247
+ () => document.removeEventListener('visibilitychange', onVisibilityChange),
248
+ () => window.removeEventListener('focus', onVisibilityChange),
249
+ );
250
+ }
251
+
180
252
  _wirePasteHandlers(host) {
181
253
  const isOurs = () => {
182
254
  const ae = document.activeElement;
@@ -294,12 +366,77 @@ export class TerminalInstance {
294
366
  this.xterm.write(data);
295
367
  return;
296
368
  }
297
- this.inReplay = true;
369
+ this._beginReplay();
298
370
  this.xterm.write(data, () => {
299
- this.inReplay = false;
371
+ this._endReplay();
372
+ if (this.isVisible) {
373
+ this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
374
+ }
300
375
  });
301
376
  }
302
377
 
378
+ _beginReplay() {
379
+ this.replayDepth++;
380
+ this.inReplay = true;
381
+ }
382
+
383
+ _endReplay() {
384
+ this.replayDepth = Math.max(0, this.replayDepth - 1);
385
+ this.inReplay = this.replayDepth > 0;
386
+ }
387
+
388
+ _applyResize(cols, rows) {
389
+ if (this.closedByUs) return;
390
+ if (!(cols > 0 && rows > 0)) return;
391
+ this.xterm.resize(cols, rows);
392
+ this._sendResize(this.xterm.cols, this.xterm.rows);
393
+ }
394
+
395
+ _resolveLayoutDimensions(width, height) {
396
+ if (width > 0 && height > 0) {
397
+ return { width, height };
398
+ }
399
+ if (!this.host) return null;
400
+ const rect = this.host.getBoundingClientRect();
401
+ const resolvedWidth = rect.width || this.host.clientWidth;
402
+ const resolvedHeight = rect.height || this.host.clientHeight;
403
+ if (!(resolvedWidth > 0 && resolvedHeight > 0)) return null;
404
+ return { width: resolvedWidth, height: resolvedHeight };
405
+ }
406
+
407
+ _scheduleLayoutRetries(forceRedraw = false) {
408
+ this._clearLayoutRetryTimers();
409
+ for (const delay of [60, 200]) {
410
+ const timer = setTimeout(() => {
411
+ this.layoutRetryTimers.delete(timer);
412
+ this.layout(undefined, undefined, true);
413
+ if (forceRedraw) this.xterm.forceRedraw();
414
+ }, delay);
415
+ this.layoutRetryTimers.add(timer);
416
+ }
417
+ }
418
+
419
+ _cancelScheduledLayout() {
420
+ if (this.pendingLayoutFrame !== null) {
421
+ cancelAnimationFrame(this.pendingLayoutFrame);
422
+ this.pendingLayoutFrame = null;
423
+ }
424
+ this._clearLayoutRetryTimers();
425
+ }
426
+
427
+ _clearLayoutRetryTimers() {
428
+ for (const timer of this.layoutRetryTimers) clearTimeout(timer);
429
+ this.layoutRetryTimers.clear();
430
+ }
431
+
432
+ _isHostVisible() {
433
+ if (!this.host || !this.host.isConnected || document.hidden) return false;
434
+ const panel = this.host.closest('.tab-panel');
435
+ if (panel && !panel.hasAttribute('data-active')) return false;
436
+ const style = window.getComputedStyle(this.host);
437
+ return style.display !== 'none' && style.visibility !== 'hidden';
438
+ }
439
+
303
440
  _wsUrl() {
304
441
  const tok = getToken();
305
442
  const dev = getDeviceId();
@@ -0,0 +1,126 @@
1
+ const START_DEBOUNCING_THRESHOLD = 200;
2
+ const DEBOUNCE_RESIZE_X_DELAY = 100;
3
+
4
+ export class TerminalResizeDebouncer {
5
+ constructor({ isVisible, getXterm, resizeBoth, resizeX, resizeY }) {
6
+ this.isVisible = isVisible;
7
+ this.getXterm = getXterm;
8
+ this.resizeBoth = resizeBoth;
9
+ this.resizeX = resizeX;
10
+ this.resizeY = resizeY;
11
+ this.latestX = 0;
12
+ this.latestY = 0;
13
+ this.resizeXTimer = null;
14
+ this.resizeXIdle = null;
15
+ this.resizeYIdle = null;
16
+ this.disposed = false;
17
+ }
18
+
19
+ resize(cols, rows, immediate = false) {
20
+ if (this.disposed) return;
21
+ this.latestX = cols;
22
+ this.latestY = rows;
23
+
24
+ const xterm = this.getXterm();
25
+ const normalBufferLength = xterm?.normalBufferLength ?? 0;
26
+ if (immediate || normalBufferLength < START_DEBOUNCING_THRESHOLD) {
27
+ this._clearPending();
28
+ this.resizeBoth(cols, rows);
29
+ return;
30
+ }
31
+
32
+ if (!this.isVisible()) {
33
+ this._scheduleIdleResizeX();
34
+ this._scheduleIdleResizeY();
35
+ return;
36
+ }
37
+
38
+ this._cancelIdleResizeY();
39
+ this.resizeY(rows);
40
+ this._scheduleDebouncedResizeX();
41
+ }
42
+
43
+ flush() {
44
+ if (this.disposed) return;
45
+ if (!this._hasPending()) return;
46
+ this._clearPending();
47
+ this.resizeBoth(this.latestX, this.latestY);
48
+ }
49
+
50
+ dispose() {
51
+ this.disposed = true;
52
+ this._clearPending();
53
+ }
54
+
55
+ _hasPending() {
56
+ return this.resizeXTimer !== null || this.resizeXIdle !== null || this.resizeYIdle !== null;
57
+ }
58
+
59
+ _clearPending() {
60
+ this._cancelDebouncedResizeX();
61
+ this._cancelIdleResizeX();
62
+ this._cancelIdleResizeY();
63
+ }
64
+
65
+ _scheduleDebouncedResizeX() {
66
+ this._cancelIdleResizeX();
67
+ if (this.resizeXTimer !== null) clearTimeout(this.resizeXTimer);
68
+ this.resizeXTimer = setTimeout(() => {
69
+ this.resizeXTimer = null;
70
+ if (!this.disposed) this.resizeX(this.latestX);
71
+ }, DEBOUNCE_RESIZE_X_DELAY);
72
+ }
73
+
74
+ _cancelDebouncedResizeX() {
75
+ if (this.resizeXTimer !== null) {
76
+ clearTimeout(this.resizeXTimer);
77
+ this.resizeXTimer = null;
78
+ }
79
+ }
80
+
81
+ _scheduleIdleResizeX() {
82
+ this._cancelDebouncedResizeX();
83
+ if (this.resizeXIdle !== null) return;
84
+ this.resizeXIdle = this._requestIdle(() => {
85
+ this.resizeXIdle = null;
86
+ if (!this.disposed) this.resizeX(this.latestX);
87
+ });
88
+ }
89
+
90
+ _scheduleIdleResizeY() {
91
+ if (this.resizeYIdle !== null) return;
92
+ this.resizeYIdle = this._requestIdle(() => {
93
+ this.resizeYIdle = null;
94
+ if (!this.disposed) this.resizeY(this.latestY);
95
+ });
96
+ }
97
+
98
+ _cancelIdleResizeX() {
99
+ if (this.resizeXIdle !== null) {
100
+ this._cancelIdle(this.resizeXIdle);
101
+ this.resizeXIdle = null;
102
+ }
103
+ }
104
+
105
+ _cancelIdleResizeY() {
106
+ if (this.resizeYIdle !== null) {
107
+ this._cancelIdle(this.resizeYIdle);
108
+ this.resizeYIdle = null;
109
+ }
110
+ }
111
+
112
+ _requestIdle(callback) {
113
+ if (window.requestIdleCallback) {
114
+ return { kind: 'idle', id: window.requestIdleCallback(callback) };
115
+ }
116
+ return { kind: 'timeout', id: setTimeout(callback, 50) };
117
+ }
118
+
119
+ _cancelIdle(handle) {
120
+ if (handle.kind === 'idle') {
121
+ window.cancelIdleCallback?.(handle.id);
122
+ } else {
123
+ clearTimeout(handle.id);
124
+ }
125
+ }
126
+ }
@@ -8,7 +8,7 @@ import { themeMode } from '../state.js';
8
8
  import { TerminalKeyBar } from './TerminalKeyBar.js';
9
9
  import { TerminalInstance } from './TerminalInstance.js';
10
10
 
11
- export function TerminalView({ terminalId, cliType }) {
11
+ export function TerminalView({ terminalId, cliType, visible = true }) {
12
12
  const hostRef = useRef(null);
13
13
  const instanceRef = useRef(null);
14
14
  const [displaced, setDisplaced] = useState(false);
@@ -38,6 +38,8 @@ export function TerminalView({ terminalId, cliType }) {
38
38
  });
39
39
  instanceRef.current = instance;
40
40
  instance.attachToElement(host);
41
+ instance.setVisible(visible);
42
+ if (visible) instance.focus();
41
43
 
42
44
  return () => {
43
45
  if (instanceRef.current === instance) instanceRef.current = null;
@@ -49,6 +51,17 @@ export function TerminalView({ terminalId, cliType }) {
49
51
  instanceRef.current?.setCliType(cliType);
50
52
  }, [cliType, terminalId, reattachNonce]);
51
53
 
54
+ useEffect(() => {
55
+ const instance = instanceRef.current;
56
+ if (!instance) return;
57
+ instance.setVisible(visible);
58
+ if (visible) {
59
+ instance.focus();
60
+ } else {
61
+ instance.blur();
62
+ }
63
+ }, [visible, terminalId, reattachNonce]);
64
+
52
65
  if (!terminalId) {
53
66
  return html`<div class="terminal-empty">Select a terminal on the left, or launch a new one.</div>`;
54
67
  }
@@ -80,6 +93,6 @@ export function TerminalView({ terminalId, cliType }) {
80
93
  return html`
81
94
  <${Fragment}>
82
95
  <div key="host" ref=${hostRef} class="terminal-host"></div>
83
- <${TerminalKeyBar} send=${sendInput} cliType=${cliType} />
96
+ ${visible ? html`<${TerminalKeyBar} send=${sendInput} cliType=${cliType} />` : null}
84
97
  </${Fragment}>`;
85
98
  }
@@ -59,6 +59,8 @@ export class XtermTerminal {
59
59
  this.currentTheme = themeFor(isDarkTheme());
60
60
  this.fitAddon = new FitAddon();
61
61
  this.webglAddon = null;
62
+ this.webglContextLossDisposable = null;
63
+ this.refreshDimensionListeners = new Set();
62
64
  this.host = null;
63
65
 
64
66
  this.raw = new Terminal({
@@ -82,12 +84,12 @@ export class XtermTerminal {
82
84
  this.raw.loadAddon(this.fitAddon);
83
85
  this.raw.loadAddon(new WebLinksAddon());
84
86
  this.raw.loadAddon(new ClipboardAddon());
85
- this._loadRendererAddon();
86
87
  this._installSelectionCopyGuard();
87
88
  }
88
89
 
89
90
  get cols() { return this.raw.cols; }
90
91
  get rows() { return this.raw.rows; }
92
+ get normalBufferLength() { return this.raw.buffer?.normal?.length ?? 0; }
91
93
  get theme() { return this.currentTheme; }
92
94
  get parser() { return this.raw.parser; }
93
95
  get helperTextarea() {
@@ -97,14 +99,22 @@ export class XtermTerminal {
97
99
  attachToElement(host) {
98
100
  this.host = host;
99
101
  this.raw.open(host);
100
- this.scheduleLayout();
102
+ host.xterm = this.raw;
103
+ this._enableWebglRenderer();
101
104
  try {
102
105
  document.fonts?.ready?.then(() => {
103
- if (this.host === host) this.scheduleLayout();
106
+ if (this.host === host) this._fireRequestRefreshDimensions();
104
107
  });
105
108
  } catch {}
106
109
  }
107
110
 
111
+ onDidRequestRefreshDimensions(listener) {
112
+ this.refreshDimensionListeners.add(listener);
113
+ return {
114
+ dispose: () => this.refreshDimensionListeners.delete(listener),
115
+ };
116
+ }
117
+
108
118
  applyResolvedTheme() {
109
119
  const theme = themeFor(isDarkTheme());
110
120
  this.currentTheme = theme;
@@ -128,15 +138,6 @@ export class XtermTerminal {
128
138
  try { this.raw.write('\x1b[?25l'); } catch {}
129
139
  }
130
140
 
131
- scheduleLayout() {
132
- this.layoutFromElement();
133
- requestAnimationFrame(() => {
134
- this.layoutFromElement();
135
- setTimeout(() => this.layoutFromElement(), 60);
136
- setTimeout(() => this.layoutFromElement(), 200);
137
- });
138
- }
139
-
140
141
  layoutFromElement() {
141
142
  if (!this.host) return null;
142
143
  const rect = this.host.getBoundingClientRect();
@@ -156,6 +157,16 @@ export class XtermTerminal {
156
157
  return proposed;
157
158
  }
158
159
 
160
+ proposeDimensions(width, height) {
161
+ return this._proposeDimensions(width, height);
162
+ }
163
+
164
+ resize(cols, rows) {
165
+ if (!(cols > 0 && rows > 0)) return;
166
+ try { this.raw.resize(cols, rows); } catch {}
167
+ lastKnownGridDimensions = { cols: this.raw.cols, rows: this.raw.rows };
168
+ }
169
+
159
170
  fit() {
160
171
  try { this.fitAddon.fit(); } catch {}
161
172
  }
@@ -168,6 +179,11 @@ export class XtermTerminal {
168
179
  try { this.raw.clearTextureAtlas?.(); } catch {}
169
180
  }
170
181
 
182
+ forceRedraw() {
183
+ this.clearTextureAtlas();
184
+ this.refresh();
185
+ }
186
+
171
187
  write(data, callback) {
172
188
  try { this.raw.write(data, callback); } catch { callback?.(); }
173
189
  }
@@ -180,6 +196,14 @@ export class XtermTerminal {
180
196
  try { this.raw.focus(); } catch {}
181
197
  }
182
198
 
199
+ blur() {
200
+ try {
201
+ if (this.helperTextarea && document.activeElement === this.helperTextarea) {
202
+ this.helperTextarea.blur();
203
+ }
204
+ } catch {}
205
+ }
206
+
183
207
  onData(listener) {
184
208
  return this.raw.onData(listener);
185
209
  }
@@ -193,21 +217,54 @@ export class XtermTerminal {
193
217
  }
194
218
 
195
219
  dispose() {
220
+ if (this.host?.xterm === this.raw) {
221
+ try { delete this.host.xterm; } catch { this.host.xterm = undefined; }
222
+ }
196
223
  this.host = null;
224
+ this._disposeWebglRenderer(false);
225
+ this.refreshDimensionListeners.clear();
197
226
  try { this.raw.dispose(); } catch {}
198
227
  }
199
228
 
200
- _loadRendererAddon() {
229
+ _shouldLoadWebgl() {
230
+ return !this.isMobile && XtermTerminal._suggestedRendererType !== 'dom';
231
+ }
232
+
233
+ _enableWebglRenderer() {
201
234
  // Keep the current mobile guard: @xterm/addon-webgl@0.18 can mis-measure
202
235
  // glyph atlases on fractional mobile DPRs.
203
- if (this.isMobile) return;
236
+ if (!this.raw.element || !this._shouldLoadWebgl()) return;
237
+ this._disposeWebglRenderer(false);
204
238
  try {
205
239
  const webgl = new WebglAddon();
206
240
  this.webglAddon = webgl;
207
- webgl.onContextLoss(() => { try { webgl.dispose(); } catch {} });
241
+ this.webglContextLossDisposable = webgl.onContextLoss(() => {
242
+ console.warn('[ccsm] WebGL context lost, using DOM renderer');
243
+ this._disposeWebglRenderer();
244
+ });
208
245
  this.raw.loadAddon(webgl);
246
+ this._fireRequestRefreshDimensions();
209
247
  } catch (e) {
248
+ XtermTerminal._suggestedRendererType = 'dom';
249
+ this._disposeWebglRenderer(false);
210
250
  console.warn('[ccsm] WebGL addon failed, using DOM renderer:', e);
251
+ this._fireRequestRefreshDimensions();
252
+ }
253
+ }
254
+
255
+ _disposeWebglRenderer(requestRefresh = true) {
256
+ try { this.webglContextLossDisposable?.dispose(); } catch {}
257
+ this.webglContextLossDisposable = null;
258
+ if (this.webglAddon) {
259
+ try { this.webglAddon.dispose(); } catch {}
260
+ this.webglAddon = null;
261
+ }
262
+ if (requestRefresh) this._fireRequestRefreshDimensions();
263
+ }
264
+
265
+ _fireRequestRefreshDimensions() {
266
+ for (const listener of this.refreshDimensionListeners) {
267
+ try { listener(); } catch {}
211
268
  }
212
269
  }
213
270
 
@@ -283,3 +340,5 @@ export class XtermTerminal {
283
340
  return width > 0 ? width : SCROLLBAR_WIDTH_FALLBACK;
284
341
  }
285
342
  }
343
+
344
+ XtermTerminal._suggestedRendererType = undefined;