@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.5",
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",
@@ -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: -0.005em;
159
- color: var(--ink);
160
- flex-shrink: 0;
161
- }
162
- .session-title-meta {
163
- display: flex;
164
- align-items: center;
165
- gap: 6px;
166
- color: var(--ink-muted);
167
- font-size: 11.5px;
168
- overflow: hidden;
169
- white-space: nowrap;
170
- }
171
- .session-title-meta .mono {
172
- font-family: var(--mono);
173
- overflow: hidden;
174
- text-overflow: ellipsis;
175
- max-width: 40vw;
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
+ }
@@ -383,17 +383,11 @@
383
383
  height: 18px;
384
384
  margin-right: 5px;
385
385
  border-radius: 3px;
386
- opacity: 0;
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
- try { this.raw.resize(proposed.cols, proposed.rows); } catch {}
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
- try { this.raw.resize(cols, rows); } catch {}
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 runningIds = new Set(runningSessions.map((s) => s.id));
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 (runningIds.has(sid)) {
182
+ if (existingIds.has(sid)) {
184
183
  next.add(sid);
185
184
  } else {
186
185
  changed = true;
187
186
  }
188
187
  }
189
- if (session?.status === 'running' && !next.has(session.id)) {
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, session?.status]);
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 retainedSessions = Array.from(openTerminalIds)
204
+ const openSessions = Array.from(openTerminalIds)
206
205
  .map((sid) => list.find((s) => s.id === sid))
207
- .filter((s) => s && s.status === 'running');
208
- const terminalSessions = running && !retainedSessions.some((s) => s.id === session.id)
209
- ? [...retainedSessions, session]
210
- : retainedSessions;
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 = terminalSessions.findIndex((s) => s.id === sid);
223
- const remaining = terminalSessions.filter((s) => s.id !== sid);
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 runningIds = new Set(runningSessions.map((s) => s.id));
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 (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
240
+ if (existingIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
241
241
  }
242
242
  for (const sid of prev) {
243
- if (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
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-meta">
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=${terminalSessions}
327
+ openSessions=${tabSessions}
335
328
  onActivate=${(sid) => selectSession(sid)}
336
329
  onClose=${onCloseTab}
337
330
  onReorder=${onReorderTabs}