@bakapiano/ccsm 0.21.3 → 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 +1 -1
- package/public/js/api.js +4 -1
- package/public/js/backend.js +39 -0
- package/public/js/components/TerminalView.js +14 -0
- package/public/js/streaming.js +5 -2
- package/server.js +15 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.21.
|
|
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;
|
package/public/js/backend.js
CHANGED
|
@@ -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.
|
package/public/js/streaming.js
CHANGED
|
@@ -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 } });
|