@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.
@@ -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, …) the original xterm already answered
126
- // them, but on attach we replay everything; without scrubbing, the new
127
- // xterm answers them too, the reply goes through our onData→PTY pipe,
128
- // the CLI sees garbage bytes in its stdin, and echoes them back as
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.2",
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.xterm.scheduleFit();
70
- this._sendFrame({ type: 'resize', cols: this.xterm.cols, rows: this.xterm.rows });
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.xterm.write(frame.data);
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._sendFrame({ type: 'resize', cols, rows });
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(() => this.xterm.fit());
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.scheduleFit();
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.scheduleFit();
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.scheduleFit();
100
+ this.scheduleLayout();
93
101
  try {
94
102
  document.fonts?.ready?.then(() => {
95
- if (this.host === host) this.scheduleFit();
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
- scheduleFit() {
124
- this.fit();
131
+ scheduleLayout() {
132
+ this.layoutFromElement();
125
133
  requestAnimationFrame(() => {
126
- this.fit();
127
- setTimeout(() => this.fit(), 60);
128
- setTimeout(() => this.fit(), 200);
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
  }