@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.
- package/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +225 -225
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +645 -543
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +159 -22
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/TerminalView.js +15 -2
- package/public/js/components/XtermTerminal.js +74 -15
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +199 -80
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- 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.
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
369
|
+
this._beginReplay();
|
|
298
370
|
this.xterm.write(data, () => {
|
|
299
|
-
this.
|
|
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
|
-
|
|
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.
|
|
102
|
+
host.xterm = this.raw;
|
|
103
|
+
this._enableWebglRenderer();
|
|
101
104
|
try {
|
|
102
105
|
document.fonts?.ready?.then(() => {
|
|
103
|
-
if (this.host === host) this.
|
|
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
|
-
|
|
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.
|
|
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(() => {
|
|
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;
|