@bakapiano/ccsm 0.21.2 → 0.21.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.21.2",
3
+ "version": "0.21.4",
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",
package/public/js/api.js CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { signal } from '@preact/signals';
5
5
  import * as S from './state.js';
6
- import { httpBase, getToken, getDeviceId, getDeviceCode, isRemoteAccess } from './backend.js';
6
+ import { httpBase, getToken, getDeviceId, getDeviceCode, isRemoteAccess, estimateTermSize } from './backend.js';
7
7
 
8
8
  // Global pending-approval signal. Flipped to true whenever any /api
9
9
  // call returns 403 {pending:true}; PendingApprovalOverlay watches this
@@ -286,6 +286,9 @@ export function resumeSession(sessionId) {
286
286
  // Resolved terminal theme → backend sets a matching COLORFGBG so the
287
287
  // CLI's light/dark auto-detection follows the ccsm terminal.
288
288
  theme: document.documentElement.dataset.theme,
289
+ // Seed the PTY at the pane's real size so alt-screen CLIs (claude)
290
+ // don't lay out at node-pty's 30-row default and get stranded short.
291
+ ...(estimateTermSize() || {}),
289
292
  });
290
293
  await loadSessions();
291
294
  return r.launched;
@@ -83,6 +83,45 @@ export function getDeviceId() {
83
83
  }
84
84
  }
85
85
 
86
+ // ── Initial terminal geometry ─────────────────────────────────────
87
+ // Estimate how many cols/rows the live session pane can hold, so a
88
+ // resumed / newly-launched PTY can spawn at roughly the right size
89
+ // instead of node-pty's 120×30 default. Why it matters: an alt-screen
90
+ // TUI like claude lays its entire UI out the instant it starts, using
91
+ // whatever size the PTY had then. xterm only sends the real size once
92
+ // its WebSocket opens — a beat later — and claude, having already
93
+ // painted at 30 rows, doesn't re-expand to fill a tall window until
94
+ // something forces a redraw (e.g. the user resizing it). On a big
95
+ // display that strands the terminal at ~1/4 height. Seeding the spawn
96
+ // with the pane's real dimensions sidesteps the race; xterm's own fit
97
+ // still corrects any few-row estimate error when it attaches.
98
+ // Returns null when nothing measurable is mounted, so the caller omits
99
+ // the hint and the backend keeps its default.
100
+ export function estimateTermSize() {
101
+ let w, h;
102
+ const pane = document.querySelector('.terminal-host')
103
+ || document.querySelector('.session-pane-body');
104
+ if (pane) {
105
+ const r = pane.getBoundingClientRect();
106
+ w = r.width; h = r.height;
107
+ } else {
108
+ // Launching from the Launch page — no pane yet. Approximate from the
109
+ // window minus the sidebar column and the ~70px of top chrome.
110
+ const sb = document.querySelector('.sidebar');
111
+ w = window.innerWidth - (sb ? sb.getBoundingClientRect().width : 232) - 32;
112
+ h = window.innerHeight - 70;
113
+ }
114
+ if (!(w > 40) || !(h > 40)) return null;
115
+ // Mirror TerminalView's font sizing (13px desktop / 11px mobile,
116
+ // lineHeight 1.2); cell advance ≈ 0.6em for the mono stack.
117
+ const isMobile = window.matchMedia('(max-width: 640px)').matches;
118
+ const fontSize = isMobile ? 11 : 13;
119
+ return {
120
+ cols: Math.max(20, Math.min(400, Math.floor(w / (fontSize * 0.6)))),
121
+ rows: Math.max(8, Math.min(200, Math.floor(h / (fontSize * 1.2)))),
122
+ };
123
+ }
124
+
86
125
  // Per-device 4-digit human-verification code. Sent alongside the
87
126
  // device id so the operator approving on the host can match what
88
127
  // they see in the Remote page against what the requesting user
@@ -229,6 +229,20 @@ export function TerminalView({ terminalId, cliType }) {
229
229
  scheduleFit();
230
230
  termRef.current = term;
231
231
 
232
+ // Web fonts settle AFTER the first fit. The terminal's mono stack
233
+ // (`Geist Mono` / `JetBrains Mono`) loads async from Google Fonts with
234
+ // display:swap, so on a machine without a local `Cascadia Mono` the very
235
+ // first term.open()+fit() measures cell metrics against the fallback font.
236
+ // When the real font swaps in its cell height changes, making the row
237
+ // count fit computed wrong — and because the host's box size never changed,
238
+ // the ResizeObserver never fires to correct it. The terminal ends up a row
239
+ // or two short of (or past) the host, "sometimes" — depending purely on
240
+ // whether the font was already cached. Re-fit once fonts settle; it's a
241
+ // no-op when the metrics didn't actually change (e.g. local font hit).
242
+ try {
243
+ document.fonts?.ready?.then(() => { if (termRef.current === term) scheduleFit(); });
244
+ } catch {}
245
+
232
246
  // Browser WS API can't set Authorization headers — token + device
233
247
  // ride as query string when we have them (Remote-mode access).
234
248
  // Server's upgrade handler reads both when Host is non-loopback.
@@ -380,7 +394,11 @@ export function TerminalView({ terminalId, cliType }) {
380
394
  };
381
395
  const doPaste = (text) => {
382
396
  if (!text) return;
383
- if (ws.readyState !== 1) return;
397
+ // Read the live socket — `ws` is scoped to connect() and reassigned on
398
+ // every reconnect, so referencing it here would be a ReferenceError
399
+ // (which silently killed Ctrl+V via onKey's .catch).
400
+ const ws = wsRef.current;
401
+ if (!ws || ws.readyState !== 1) return;
384
402
  // Normalize line endings to \r (CR / Enter). This mirrors VSCode's
385
403
  // terminal sendText path (terminalInstance.ts ~L1385):
386
404
  // text = text.replace(/\r?\n/g, '\r');
@@ -468,7 +486,8 @@ export function TerminalView({ terminalId, cliType }) {
468
486
  ev.preventDefault();
469
487
  ev.stopPropagation();
470
488
  ev.stopImmediatePropagation();
471
- if (ws.readyState === 1) {
489
+ const ws = wsRef.current;
490
+ if (ws && ws.readyState === 1) {
472
491
  ws.send(JSON.stringify({ type: 'input', data }));
473
492
  }
474
493
  };
@@ -2,7 +2,7 @@
2
2
  // Items live in a signal keyed by repo so progress rows are reactive.
3
3
 
4
4
  import { signal } from '@preact/signals';
5
- import { httpBase } from './backend.js';
5
+ import { httpBase, estimateTermSize } from './backend.js';
6
6
 
7
7
  // progressByContext[rootId] = { repoName: { phase, percent, detail, state, indeterminate, name } }
8
8
  export const progressByContext = signal({});
@@ -63,10 +63,13 @@ export async function streamNewSession(body, { progressRootId = 'newSessionProgr
63
63
  // Pass the resolved terminal theme so the backend can hand CLIs a matching
64
64
  // COLORFGBG (light/dark detection). dataset.theme is set by applyTheme().
65
65
  const theme = document.documentElement.dataset.theme;
66
+ // Seed the PTY at the pane's real size (estimated from the window here,
67
+ // since the terminal isn't mounted yet) so alt-screen CLIs don't start
68
+ // at node-pty's 30-row default and get stranded short of a tall window.
66
69
  const res = await fetch(httpBase() + '/api/sessions/new', {
67
70
  method: 'POST',
68
71
  headers: { 'Content-Type': 'application/json' },
69
- body: JSON.stringify({ ...body, theme }),
72
+ body: JSON.stringify({ ...body, theme, ...(estimateTermSize() || {}) }),
70
73
  });
71
74
  if (!res.ok && res.headers.get('content-type')?.startsWith('application/json')) {
72
75
  const j = await res.json();
package/server.js CHANGED
@@ -281,7 +281,7 @@ function quoteForCmd(s) {
281
281
  return s;
282
282
  }
283
283
 
284
- function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme }) {
284
+ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme, cols, rows }) {
285
285
  if (!webTerminal.available) {
286
286
  const e = new Error('node-pty unavailable · cannot spawn web terminal');
287
287
  e.code = 'PTY_UNAVAILABLE';
@@ -331,12 +331,22 @@ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme }) {
331
331
  if (theme === 'light' || theme === 'dark') {
332
332
  env.COLORFGBG = theme === 'light' ? '0;15' : '15;0';
333
333
  }
334
+ // Spawn the PTY at the size the frontend measured for its terminal pane
335
+ // (clamped against junk), so alt-screen CLIs lay out at the right height
336
+ // from the first frame instead of node-pty's 120×30 default. Omitted ⇒
337
+ // webTerminal.spawn keeps its default; xterm's first resize corrects any
338
+ // small estimate error on attach regardless.
339
+ const sized = (Number(cols) > 0 && Number(rows) > 0)
340
+ ? { cols: Math.min(400, Math.max(20, Math.floor(Number(cols)))),
341
+ rows: Math.min(200, Math.max(8, Math.floor(Number(rows)))) }
342
+ : {};
334
343
  const trySpawn = (executable) => webTerminal.spawn({
335
344
  id: sessionId,
336
345
  command: executable,
337
346
  args,
338
347
  cwd,
339
348
  env,
349
+ ...sized,
340
350
  meta: { ...meta, cliId: cli.id, cliName: cli.name },
341
351
  onData: () => {
342
352
  persistedSessions.touch(sessionId).catch(() => {});
@@ -867,6 +877,8 @@ app.post('/api/sessions/new', async (req, res) => {
867
877
  meta: { title: workspace.name, workspace: workspace.name, cwd: workspace.path },
868
878
  extraArgs: [...themeArgs, ...newSessionArgs],
869
879
  theme: req.body && req.body.theme,
880
+ cols: req.body && req.body.cols,
881
+ rows: req.body && req.body.rows,
870
882
  });
871
883
  await persistedSessions.markRunning(record.id, entry.meta.pid);
872
884
  launched = { id: record.id, pid: entry.meta.pid, cliId: cli.id };
@@ -1004,6 +1016,8 @@ app.post('/api/sessions/:id/resume', asyncH(async (req, res) => {
1004
1016
  meta: { title: record.title || record.workspace, workspace: record.workspace, cwd: record.cwd },
1005
1017
  extraArgs: [...themeArgs, ...extraArgs],
1006
1018
  theme: req.body && req.body.theme,
1019
+ cols: req.body && req.body.cols,
1020
+ rows: req.body && req.body.rows,
1007
1021
  });
1008
1022
  await persistedSessions.markRunning(record.id, entry.meta.pid);
1009
1023
  res.json({ launched: { id: record.id, pid: entry.meta.pid, cliId: cli.id } });