@bakapiano/ccsm 0.22.5 → 0.22.6
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.22.
|
|
3
|
+
"version": "0.22.6",
|
|
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/css/layout.css
CHANGED
|
@@ -152,25 +152,26 @@ body.is-resizing-sidebar .app {
|
|
|
152
152
|
gap: var(--s-2);
|
|
153
153
|
flex-shrink: 0;
|
|
154
154
|
}
|
|
155
|
-
.session-title-text {
|
|
156
|
-
font-weight: 500;
|
|
157
|
-
font-size: 13px;
|
|
158
|
-
letter-spacing:
|
|
159
|
-
color: var(--ink);
|
|
160
|
-
flex
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
155
|
+
.session-title-text {
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
font-size: 13px;
|
|
158
|
+
letter-spacing: 0;
|
|
159
|
+
color: var(--ink);
|
|
160
|
+
flex: 0 1 auto;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
max-width: min(32ch, 36vw);
|
|
163
|
+
overflow: hidden;
|
|
164
|
+
text-overflow: ellipsis;
|
|
165
|
+
white-space: nowrap;
|
|
166
|
+
}
|
|
167
|
+
.session-title-cwd {
|
|
168
|
+
color: var(--ink-muted);
|
|
169
|
+
font-size: 11.5px;
|
|
170
|
+
font-family: var(--mono);
|
|
171
|
+
flex: 1 1 auto;
|
|
172
|
+
min-width: 0;
|
|
173
|
+
max-width: min(72ch, 52vw);
|
|
174
|
+
overflow: hidden;
|
|
175
|
+
text-overflow: ellipsis;
|
|
176
|
+
white-space: nowrap;
|
|
177
|
+
}
|
package/public/css/terminals.css
CHANGED
|
@@ -383,17 +383,11 @@
|
|
|
383
383
|
height: 18px;
|
|
384
384
|
margin-right: 5px;
|
|
385
385
|
border-radius: 3px;
|
|
386
|
-
opacity:
|
|
386
|
+
opacity: 1;
|
|
387
387
|
flex: 0 0 auto;
|
|
388
388
|
cursor: pointer;
|
|
389
389
|
}
|
|
390
|
-
.session-tab:hover .session-tab-close,
|
|
391
|
-
.session-tab.is-active .session-tab-close,
|
|
392
|
-
.session-tab:focus-within .session-tab-close {
|
|
393
|
-
opacity: .72;
|
|
394
|
-
}
|
|
395
390
|
.session-tab-close:hover {
|
|
396
|
-
opacity: 1;
|
|
397
391
|
background: rgba(255, 255, 255, 0.12);
|
|
398
392
|
}
|
|
399
393
|
.session-tab-close svg {
|
|
@@ -205,15 +205,6 @@ export class TerminalInstance {
|
|
|
205
205
|
ro.observe(host);
|
|
206
206
|
this.disposables.push(() => ro.disconnect());
|
|
207
207
|
|
|
208
|
-
const vv = window.visualViewport;
|
|
209
|
-
const onVisualResize = () => this.scheduleLayout({ retries: true });
|
|
210
|
-
vv?.addEventListener?.('resize', onVisualResize);
|
|
211
|
-
vv?.addEventListener?.('scroll', onVisualResize);
|
|
212
|
-
this.disposables.push(() => {
|
|
213
|
-
vv?.removeEventListener?.('resize', onVisualResize);
|
|
214
|
-
vv?.removeEventListener?.('scroll', onVisualResize);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
208
|
const onHostClick = () => this.xterm.focus();
|
|
218
209
|
if (this.xterm.isMobile) {
|
|
219
210
|
host.addEventListener('click', onHostClick);
|
|
@@ -61,6 +61,8 @@ export class XtermTerminal {
|
|
|
61
61
|
this.webglAddon = null;
|
|
62
62
|
this.webglContextLossDisposable = null;
|
|
63
63
|
this.refreshDimensionListeners = new Set();
|
|
64
|
+
this.resizeScrollState = null;
|
|
65
|
+
this.resizeScrollStateTimer = null;
|
|
64
66
|
this.host = null;
|
|
65
67
|
|
|
66
68
|
this.raw = new Terminal({
|
|
@@ -151,7 +153,7 @@ export class XtermTerminal {
|
|
|
151
153
|
if (!proposed) return null;
|
|
152
154
|
|
|
153
155
|
if (proposed.cols !== this.raw.cols || proposed.rows !== this.raw.rows) {
|
|
154
|
-
|
|
156
|
+
this._resizeRaw(proposed.cols, proposed.rows);
|
|
155
157
|
}
|
|
156
158
|
lastKnownGridDimensions = proposed;
|
|
157
159
|
return proposed;
|
|
@@ -163,7 +165,7 @@ export class XtermTerminal {
|
|
|
163
165
|
|
|
164
166
|
resize(cols, rows) {
|
|
165
167
|
if (!(cols > 0 && rows > 0)) return;
|
|
166
|
-
|
|
168
|
+
this._resizeRaw(cols, rows);
|
|
167
169
|
lastKnownGridDimensions = { cols: this.raw.cols, rows: this.raw.rows };
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -217,6 +219,9 @@ export class XtermTerminal {
|
|
|
217
219
|
}
|
|
218
220
|
|
|
219
221
|
dispose() {
|
|
222
|
+
if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
|
|
223
|
+
this.resizeScrollState = null;
|
|
224
|
+
this.resizeScrollStateTimer = null;
|
|
220
225
|
if (this.host?.xterm === this.raw) {
|
|
221
226
|
try { delete this.host.xterm; } catch { this.host.xterm = undefined; }
|
|
222
227
|
}
|
|
@@ -268,6 +273,61 @@ export class XtermTerminal {
|
|
|
268
273
|
}
|
|
269
274
|
}
|
|
270
275
|
|
|
276
|
+
_resizeRaw(cols, rows) {
|
|
277
|
+
const scrollState = this._scrollStateForResize();
|
|
278
|
+
try { this.raw.resize(cols, rows); } catch {}
|
|
279
|
+
this._restoreScrollStateIfNeeded(scrollState);
|
|
280
|
+
requestAnimationFrame(() => this._restoreScrollStateIfNeeded(scrollState));
|
|
281
|
+
setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 150);
|
|
282
|
+
setTimeout(() => this._restoreScrollStateIfNeeded(scrollState), 350);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
_scrollStateForResize() {
|
|
286
|
+
const state = this._captureScrollState();
|
|
287
|
+
if (state?.viewportY > 0) {
|
|
288
|
+
this._rememberResizeScrollState(state);
|
|
289
|
+
return state;
|
|
290
|
+
}
|
|
291
|
+
return this.resizeScrollState;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
_rememberResizeScrollState(state) {
|
|
295
|
+
this.resizeScrollState = state;
|
|
296
|
+
if (this.resizeScrollStateTimer) clearTimeout(this.resizeScrollStateTimer);
|
|
297
|
+
this.resizeScrollStateTimer = setTimeout(() => {
|
|
298
|
+
this.resizeScrollState = null;
|
|
299
|
+
this.resizeScrollStateTimer = null;
|
|
300
|
+
}, 500);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_captureScrollState() {
|
|
304
|
+
const buffer = this.raw?.buffer?.active;
|
|
305
|
+
if (!buffer) return null;
|
|
306
|
+
return {
|
|
307
|
+
viewportY: buffer.viewportY,
|
|
308
|
+
baseY: buffer.baseY,
|
|
309
|
+
atBottom: buffer.viewportY >= buffer.baseY,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
_restoreScrollStateIfNeeded(state) {
|
|
314
|
+
if (!state || !(state.viewportY > 0)) return;
|
|
315
|
+
const buffer = this.raw?.buffer?.active;
|
|
316
|
+
if (!buffer) return;
|
|
317
|
+
|
|
318
|
+
if (state.atBottom) {
|
|
319
|
+
if (buffer.viewportY < buffer.baseY) {
|
|
320
|
+
try { this.raw.scrollToBottom(); } catch {}
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const target = Math.min(state.viewportY, buffer.baseY);
|
|
326
|
+
if (target > 0 && buffer.viewportY !== target) {
|
|
327
|
+
try { this.raw.scrollToLine(target); } catch {}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
271
331
|
_installSelectionCopyGuard() {
|
|
272
332
|
this.raw.attachCustomKeyEventHandler((ev) => {
|
|
273
333
|
if (ev.type === 'keydown'
|
|
@@ -146,7 +146,6 @@ export function SessionsPage() {
|
|
|
146
146
|
const id = activeSessionId.value;
|
|
147
147
|
const list = sessions.value;
|
|
148
148
|
const session = id ? list.find((s) => s.id === id) : null;
|
|
149
|
-
const runningSessions = list.filter((s) => s.status === 'running');
|
|
150
149
|
const [resumeError, setResumeError] = useState(null);
|
|
151
150
|
const [actionBusy, setActionBusy] = useState(false);
|
|
152
151
|
const [openTerminalIds, setOpenTerminalIds] = useState(() => new Set());
|
|
@@ -175,24 +174,24 @@ export function SessionsPage() {
|
|
|
175
174
|
}, [session?.id, session?.status, session?.cliId, session?.manualStopped, retryNonce]);
|
|
176
175
|
|
|
177
176
|
useEffect(() => {
|
|
178
|
-
const
|
|
177
|
+
const existingIds = new Set(list.map((s) => s.id));
|
|
179
178
|
setOpenTerminalIds((prev) => {
|
|
180
179
|
const next = new Set();
|
|
181
180
|
let changed = false;
|
|
182
181
|
for (const sid of prev) {
|
|
183
|
-
if (
|
|
182
|
+
if (existingIds.has(sid)) {
|
|
184
183
|
next.add(sid);
|
|
185
184
|
} else {
|
|
186
185
|
changed = true;
|
|
187
186
|
}
|
|
188
187
|
}
|
|
189
|
-
if (session?.
|
|
188
|
+
if (session?.id && existingIds.has(session.id) && !next.has(session.id)) {
|
|
190
189
|
next.add(session.id);
|
|
191
190
|
changed = true;
|
|
192
191
|
}
|
|
193
192
|
return changed || next.size !== prev.size ? next : prev;
|
|
194
193
|
});
|
|
195
|
-
}, [list, session?.id
|
|
194
|
+
}, [list, session?.id]);
|
|
196
195
|
|
|
197
196
|
if (!session) return null;
|
|
198
197
|
|
|
@@ -202,12 +201,13 @@ export function SessionsPage() {
|
|
|
202
201
|
? (config.value?.clis || []).filter((c) => c.id !== cli.id && c.type === cli.type)
|
|
203
202
|
: [];
|
|
204
203
|
const running = session.status === 'running';
|
|
205
|
-
const
|
|
204
|
+
const openSessions = Array.from(openTerminalIds)
|
|
206
205
|
.map((sid) => list.find((s) => s.id === sid))
|
|
207
|
-
.filter(
|
|
208
|
-
const
|
|
209
|
-
? [...
|
|
210
|
-
:
|
|
206
|
+
.filter(Boolean);
|
|
207
|
+
const tabSessions = session && !openSessions.some((s) => s.id === session.id)
|
|
208
|
+
? [...openSessions, session]
|
|
209
|
+
: openSessions;
|
|
210
|
+
const terminalSessions = tabSessions.filter((s) => s.status === 'running');
|
|
211
211
|
const title = session.title || session.workspace || session.id.slice(0, 12);
|
|
212
212
|
|
|
213
213
|
const onCloseTab = (sid) => {
|
|
@@ -219,8 +219,8 @@ export function SessionsPage() {
|
|
|
219
219
|
});
|
|
220
220
|
|
|
221
221
|
if (sid !== session.id) return;
|
|
222
|
-
const currentIndex =
|
|
223
|
-
const remaining =
|
|
222
|
+
const currentIndex = tabSessions.findIndex((s) => s.id === sid);
|
|
223
|
+
const remaining = tabSessions.filter((s) => s.id !== sid);
|
|
224
224
|
const replacement = currentIndex >= 0
|
|
225
225
|
? remaining[Math.min(currentIndex, remaining.length - 1)] || remaining[remaining.length - 1]
|
|
226
226
|
: remaining[0];
|
|
@@ -233,14 +233,14 @@ export function SessionsPage() {
|
|
|
233
233
|
};
|
|
234
234
|
|
|
235
235
|
const onReorderTabs = (orderedIds) => {
|
|
236
|
-
const
|
|
236
|
+
const existingIds = new Set(list.map((s) => s.id));
|
|
237
237
|
setOpenTerminalIds((prev) => {
|
|
238
238
|
const nextIds = [];
|
|
239
239
|
for (const sid of orderedIds) {
|
|
240
|
-
if (
|
|
240
|
+
if (existingIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
|
|
241
241
|
}
|
|
242
242
|
for (const sid of prev) {
|
|
243
|
-
if (
|
|
243
|
+
if (existingIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
|
|
244
244
|
}
|
|
245
245
|
return new Set(nextIds);
|
|
246
246
|
});
|
|
@@ -317,21 +317,14 @@ export function SessionsPage() {
|
|
|
317
317
|
} catch (e) { setToast(e.message, 'error'); }
|
|
318
318
|
};
|
|
319
319
|
|
|
320
|
-
return html`
|
|
321
|
-
<${PageTitleBar} title=${html`
|
|
322
|
-
<span class="session-title-text">${title}</span>
|
|
323
|
-
<span class="session-title-
|
|
324
|
-
<span class="mono">${session.cwd}</span>
|
|
325
|
-
<span>·</span>
|
|
326
|
-
<span>${cli ? cli.name : session.cliId}</span>
|
|
327
|
-
${session.repos.length ? html`<span>·</span><span>${session.repos.join(', ')}</span>` : null}
|
|
328
|
-
<span>·</span>
|
|
329
|
-
<span>${running ? 'running' : (resumeError ? 'resume failed' : (session.manualStopped ? 'stopped' : 'resuming…'))}</span>
|
|
330
|
-
</span>
|
|
320
|
+
return html`
|
|
321
|
+
<${PageTitleBar} title=${html`
|
|
322
|
+
<span class="session-title-text" title=${title}>${title}</span>
|
|
323
|
+
<span class="session-title-cwd" title=${session.cwd}>${session.cwd}</span>
|
|
331
324
|
`} />
|
|
332
325
|
<${SessionTabs}
|
|
333
326
|
activeId=${session.id}
|
|
334
|
-
openSessions=${
|
|
327
|
+
openSessions=${tabSessions}
|
|
335
328
|
onActivate=${(sid) => selectSession(sid)}
|
|
336
329
|
onClose=${onCloseTab}
|
|
337
330
|
onReorder=${onReorderTabs}
|