@bakapiano/ccsm 0.22.2 → 0.22.3
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/lib/webTerminal.js
CHANGED
|
@@ -122,13 +122,15 @@ function spawn({ command, args = [], cwd, env, cols = 120, rows = 30, meta = {},
|
|
|
122
122
|
// Strip ANSI sequences from history that would cause spurious
|
|
123
123
|
// terminal-to-host responses if a fresh xterm.js re-parses the replay.
|
|
124
124
|
// Specifically: device-attribute / device-status queries (CSI c, CSI 0c,
|
|
125
|
-
// CSI >0c, CSI 5n, CSI 6n, …)
|
|
126
|
-
// them, but on attach we replay everything;
|
|
127
|
-
// xterm answers them too, the reply goes through
|
|
128
|
-
// the CLI sees garbage bytes in its stdin, and echoes
|
|
129
|
-
// visible junk like `[?12;2c
|
|
125
|
+
// CSI >0c, CSI 5n, CSI 6n, …) and OSC 10/11 default color queries. The
|
|
126
|
+
// original xterm already answered them, but on attach we replay everything;
|
|
127
|
+
// without scrubbing, the new xterm answers them too, the reply goes through
|
|
128
|
+
// our onData→PTY pipe, the CLI sees garbage bytes in its stdin, and echoes
|
|
129
|
+
// them back as visible junk like `[?12;2c` or `]11;rgb:…`.
|
|
130
130
|
function scrubReplayResponses(history) {
|
|
131
131
|
return history
|
|
132
|
+
// OSC 10/11 ; ? ST/BEL (default foreground/background color queries)
|
|
133
|
+
.replace(/\x1b\](?:10|11);\?(?:\x07|\x1b\\)/g, '')
|
|
132
134
|
// CSI [ ? Ps c (primary DA query)
|
|
133
135
|
.replace(/\x1b\[[?>0-9]*c/g, '')
|
|
134
136
|
// CSI [ Ps n (device status / cursor position queries)
|
|
@@ -157,7 +159,7 @@ function attach(id, ws) {
|
|
|
157
159
|
entry.sockets.clear();
|
|
158
160
|
entry.sockets.add(ws);
|
|
159
161
|
if (entry.history) {
|
|
160
|
-
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history) })); } catch {}
|
|
162
|
+
try { ws.send(JSON.stringify({ type: 'output', data: scrubReplayResponses(entry.history), replay: true })); } catch {}
|
|
161
163
|
}
|
|
162
164
|
if (entry.exitedAt) {
|
|
163
165
|
try { ws.send(JSON.stringify({ type: 'exit', code: entry.exitCode })); } catch {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.3",
|
|
4
4
|
"description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -17,6 +17,8 @@ export class TerminalInstance {
|
|
|
17
17
|
this.reconnectTimer = null;
|
|
18
18
|
this.attempts = 0;
|
|
19
19
|
this.everOpened = false;
|
|
20
|
+
this.inReplay = false;
|
|
21
|
+
this.lastSentDimensions = null;
|
|
20
22
|
this.disposables = [];
|
|
21
23
|
this.helperTextarea = null;
|
|
22
24
|
}
|
|
@@ -43,6 +45,16 @@ export class TerminalInstance {
|
|
|
43
45
|
this.xterm.applyResolvedTheme();
|
|
44
46
|
}
|
|
45
47
|
|
|
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);
|
|
54
|
+
}
|
|
55
|
+
return dimensions;
|
|
56
|
+
}
|
|
57
|
+
|
|
46
58
|
dispose() {
|
|
47
59
|
this.closedByUs = true;
|
|
48
60
|
if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
|
|
@@ -66,14 +78,15 @@ export class TerminalInstance {
|
|
|
66
78
|
}
|
|
67
79
|
this.everOpened = true;
|
|
68
80
|
this.attempts = 0;
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
81
|
+
this.layout();
|
|
82
|
+
this.xterm.scheduleLayout();
|
|
83
|
+
this._sendResize(this.xterm.cols, this.xterm.rows, true);
|
|
71
84
|
};
|
|
72
85
|
ws.onmessage = (ev) => {
|
|
73
86
|
let frame;
|
|
74
87
|
try { frame = JSON.parse(ev.data); } catch { return; }
|
|
75
88
|
if (frame.type === 'output') {
|
|
76
|
-
this.
|
|
89
|
+
this._writeProcessData(frame.data, !!frame.replay);
|
|
77
90
|
} else if (frame.type === 'exit') {
|
|
78
91
|
this.xterm.write(`\r\n\x1b[2m[process exited · code ${frame.code}]\x1b[0m\r\n`);
|
|
79
92
|
}
|
|
@@ -101,10 +114,11 @@ export class TerminalInstance {
|
|
|
101
114
|
|
|
102
115
|
_wireXtermEvents() {
|
|
103
116
|
const dataDisposable = this.xterm.onData((data) => {
|
|
117
|
+
if (this.inReplay) return;
|
|
104
118
|
this._sendFrame({ type: 'input', data });
|
|
105
119
|
});
|
|
106
120
|
const resizeDisposable = this.xterm.onResize(({ cols, rows }) => {
|
|
107
|
-
this.
|
|
121
|
+
this._sendResize(cols, rows);
|
|
108
122
|
});
|
|
109
123
|
this.disposables.push(
|
|
110
124
|
() => dataDisposable.dispose(),
|
|
@@ -114,12 +128,19 @@ export class TerminalInstance {
|
|
|
114
128
|
|
|
115
129
|
_wireDomLifecycle() {
|
|
116
130
|
const host = this.host;
|
|
117
|
-
const ro = new ResizeObserver(() =>
|
|
131
|
+
const ro = new ResizeObserver((entries) => {
|
|
132
|
+
const box = entries[0]?.contentRect;
|
|
133
|
+
if (box) {
|
|
134
|
+
this.layout(box.width, box.height);
|
|
135
|
+
} else {
|
|
136
|
+
this.layout();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
118
139
|
ro.observe(host);
|
|
119
140
|
this.disposables.push(() => ro.disconnect());
|
|
120
141
|
|
|
121
142
|
const vv = window.visualViewport;
|
|
122
|
-
const onVisualResize = () => this.xterm.
|
|
143
|
+
const onVisualResize = () => this.xterm.scheduleLayout();
|
|
123
144
|
vv?.addEventListener?.('resize', onVisualResize);
|
|
124
145
|
vv?.addEventListener?.('scroll', onVisualResize);
|
|
125
146
|
this.disposables.push(() => {
|
|
@@ -146,7 +167,8 @@ export class TerminalInstance {
|
|
|
146
167
|
if (panel.hasAttribute('data-active')) {
|
|
147
168
|
requestAnimationFrame(() => {
|
|
148
169
|
this.xterm.clearTextureAtlas();
|
|
149
|
-
this.xterm.
|
|
170
|
+
this.xterm.scheduleLayout();
|
|
171
|
+
this.layout();
|
|
150
172
|
this.xterm.refresh();
|
|
151
173
|
});
|
|
152
174
|
}
|
|
@@ -235,6 +257,7 @@ export class TerminalInstance {
|
|
|
235
257
|
_registerColorOscHandlers() {
|
|
236
258
|
const answerColorOsc = (code, getHex) => (data) => {
|
|
237
259
|
if (data !== '?') return false;
|
|
260
|
+
if (this.inReplay) return true;
|
|
238
261
|
const hex = getHex();
|
|
239
262
|
const ch = (i) => parseInt(hex.slice(i, i + 2), 16);
|
|
240
263
|
const w = (v) => (v * 257).toString(16).padStart(4, '0');
|
|
@@ -254,6 +277,29 @@ export class TerminalInstance {
|
|
|
254
277
|
}
|
|
255
278
|
}
|
|
256
279
|
|
|
280
|
+
_sendResize(cols, rows, force = false) {
|
|
281
|
+
if (!(cols > 0 && rows > 0)) return;
|
|
282
|
+
if (!force
|
|
283
|
+
&& this.lastSentDimensions
|
|
284
|
+
&& this.lastSentDimensions.cols === cols
|
|
285
|
+
&& this.lastSentDimensions.rows === rows) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.lastSentDimensions = { cols, rows };
|
|
289
|
+
this._sendFrame({ type: 'resize', cols, rows });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
_writeProcessData(data, replay) {
|
|
293
|
+
if (!replay) {
|
|
294
|
+
this.xterm.write(data);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.inReplay = true;
|
|
298
|
+
this.xterm.write(data, () => {
|
|
299
|
+
this.inReplay = false;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
257
303
|
_wsUrl() {
|
|
258
304
|
const tok = getToken();
|
|
259
305
|
const dev = getDeviceId();
|
|
@@ -9,6 +9,10 @@ import { ClipboardAddon } from '@xterm/addon-clipboard';
|
|
|
9
9
|
import { WebglAddon } from '@xterm/addon-webgl';
|
|
10
10
|
import { isDarkTheme } from '../state.js';
|
|
11
11
|
|
|
12
|
+
const DEFAULT_COLS = 80;
|
|
13
|
+
const DEFAULT_ROWS = 24;
|
|
14
|
+
const SCROLLBAR_WIDTH_FALLBACK = 14;
|
|
15
|
+
|
|
12
16
|
// Dark xterm theme - VSCode's Dark+ terminal palette, verbatim (see
|
|
13
17
|
// microsoft/vscode src/.../terminal/common/terminalColorRegistry.ts).
|
|
14
18
|
const THEME_DARK = {
|
|
@@ -47,6 +51,8 @@ const THEME_LIGHT = {
|
|
|
47
51
|
|
|
48
52
|
export const themeFor = (dark) => (dark ? THEME_DARK : THEME_LIGHT);
|
|
49
53
|
|
|
54
|
+
let lastKnownGridDimensions = { cols: DEFAULT_COLS, rows: DEFAULT_ROWS };
|
|
55
|
+
|
|
50
56
|
export class XtermTerminal {
|
|
51
57
|
constructor() {
|
|
52
58
|
this.isMobile = window.matchMedia('(max-width: 640px)').matches;
|
|
@@ -59,6 +65,8 @@ export class XtermTerminal {
|
|
|
59
65
|
fontFamily: '"Cascadia Mono", "Geist Mono", "JetBrains Mono", Consolas, monospace',
|
|
60
66
|
fontSize: this.isMobile ? 11 : 13,
|
|
61
67
|
lineHeight: 1.2,
|
|
68
|
+
cols: lastKnownGridDimensions.cols,
|
|
69
|
+
rows: lastKnownGridDimensions.rows,
|
|
62
70
|
cursorBlink: true,
|
|
63
71
|
cursorStyle: 'bar',
|
|
64
72
|
scrollback: 5000,
|
|
@@ -89,10 +97,10 @@ export class XtermTerminal {
|
|
|
89
97
|
attachToElement(host) {
|
|
90
98
|
this.host = host;
|
|
91
99
|
this.raw.open(host);
|
|
92
|
-
this.
|
|
100
|
+
this.scheduleLayout();
|
|
93
101
|
try {
|
|
94
102
|
document.fonts?.ready?.then(() => {
|
|
95
|
-
if (this.host === host) this.
|
|
103
|
+
if (this.host === host) this.scheduleLayout();
|
|
96
104
|
});
|
|
97
105
|
} catch {}
|
|
98
106
|
}
|
|
@@ -120,15 +128,34 @@ export class XtermTerminal {
|
|
|
120
128
|
try { this.raw.write('\x1b[?25l'); } catch {}
|
|
121
129
|
}
|
|
122
130
|
|
|
123
|
-
|
|
124
|
-
this.
|
|
131
|
+
scheduleLayout() {
|
|
132
|
+
this.layoutFromElement();
|
|
125
133
|
requestAnimationFrame(() => {
|
|
126
|
-
this.
|
|
127
|
-
setTimeout(() => this.
|
|
128
|
-
setTimeout(() => this.
|
|
134
|
+
this.layoutFromElement();
|
|
135
|
+
setTimeout(() => this.layoutFromElement(), 60);
|
|
136
|
+
setTimeout(() => this.layoutFromElement(), 200);
|
|
129
137
|
});
|
|
130
138
|
}
|
|
131
139
|
|
|
140
|
+
layoutFromElement() {
|
|
141
|
+
if (!this.host) return null;
|
|
142
|
+
const rect = this.host.getBoundingClientRect();
|
|
143
|
+
return this.layout(rect.width, rect.height);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
layout(width, height) {
|
|
147
|
+
if (!(width > 0 && height > 0)) return null;
|
|
148
|
+
|
|
149
|
+
const proposed = this._proposeDimensions(width, height);
|
|
150
|
+
if (!proposed) return null;
|
|
151
|
+
|
|
152
|
+
if (proposed.cols !== this.raw.cols || proposed.rows !== this.raw.rows) {
|
|
153
|
+
try { this.raw.resize(proposed.cols, proposed.rows); } catch {}
|
|
154
|
+
}
|
|
155
|
+
lastKnownGridDimensions = proposed;
|
|
156
|
+
return proposed;
|
|
157
|
+
}
|
|
158
|
+
|
|
132
159
|
fit() {
|
|
133
160
|
try { this.fitAddon.fit(); } catch {}
|
|
134
161
|
}
|
|
@@ -142,7 +169,7 @@ export class XtermTerminal {
|
|
|
142
169
|
}
|
|
143
170
|
|
|
144
171
|
write(data, callback) {
|
|
145
|
-
try { this.raw.write(data, callback); } catch {}
|
|
172
|
+
try { this.raw.write(data, callback); } catch { callback?.(); }
|
|
146
173
|
}
|
|
147
174
|
|
|
148
175
|
reset() {
|
|
@@ -195,4 +222,64 @@ export class XtermTerminal {
|
|
|
195
222
|
return true;
|
|
196
223
|
});
|
|
197
224
|
}
|
|
225
|
+
|
|
226
|
+
_proposeDimensions(width, height) {
|
|
227
|
+
const cell = this._cellDimensions();
|
|
228
|
+
if (!cell) return null;
|
|
229
|
+
|
|
230
|
+
const elementStyle = this.raw.element
|
|
231
|
+
? window.getComputedStyle(this.raw.element)
|
|
232
|
+
: null;
|
|
233
|
+
const px = (v) => Number.parseFloat(v || '0') || 0;
|
|
234
|
+
const horizontalPadding = elementStyle
|
|
235
|
+
? px(elementStyle.paddingLeft) + px(elementStyle.paddingRight)
|
|
236
|
+
: 0;
|
|
237
|
+
const verticalPadding = elementStyle
|
|
238
|
+
? px(elementStyle.paddingTop) + px(elementStyle.paddingBottom)
|
|
239
|
+
: 0;
|
|
240
|
+
const scrollbarWidth = this._scrollbarWidth();
|
|
241
|
+
|
|
242
|
+
const availableWidth = Math.max(0, width - horizontalPadding - scrollbarWidth);
|
|
243
|
+
const availableHeight = Math.max(0, height - verticalPadding);
|
|
244
|
+
if (!(availableWidth > 0 && availableHeight > 0)) return null;
|
|
245
|
+
|
|
246
|
+
const dpr = window.devicePixelRatio || 1;
|
|
247
|
+
const scaledWidth = availableWidth * dpr;
|
|
248
|
+
const scaledCellWidth = cell.width * dpr;
|
|
249
|
+
const scaledHeight = availableHeight * dpr;
|
|
250
|
+
const scaledCellHeight = Math.ceil(cell.height * dpr);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
cols: Math.max(1, Math.floor(scaledWidth / scaledCellWidth)),
|
|
254
|
+
rows: Math.max(1, Math.floor(scaledHeight / scaledCellHeight)),
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
_cellDimensions() {
|
|
259
|
+
const cell = this.raw?._core?._renderService?.dimensions?.css?.cell;
|
|
260
|
+
if (cell?.width > 0 && cell?.height > 0) {
|
|
261
|
+
return { width: cell.width, height: cell.height };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const proposed = (() => {
|
|
265
|
+
try { return this.fitAddon.proposeDimensions?.(); } catch { return null; }
|
|
266
|
+
})();
|
|
267
|
+
if (proposed?.cols > 0 && proposed?.rows > 0 && this.host) {
|
|
268
|
+
const rect = this.host.getBoundingClientRect();
|
|
269
|
+
return {
|
|
270
|
+
width: rect.width / proposed.cols,
|
|
271
|
+
height: rect.height / proposed.rows,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_scrollbarWidth() {
|
|
278
|
+
const core = this.raw?._core;
|
|
279
|
+
const width =
|
|
280
|
+
core?._viewport?.scrollBarWidth ??
|
|
281
|
+
core?.viewport?.scrollBarWidth ??
|
|
282
|
+
0;
|
|
283
|
+
return width > 0 ? width : SCROLLBAR_WIDTH_FALLBACK;
|
|
284
|
+
}
|
|
198
285
|
}
|