@bakapiano/ccsm 0.15.0 → 0.15.1

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/lib/config.js CHANGED
@@ -130,26 +130,20 @@ function mergeWithDefaults(partial) {
130
130
  if (existing) {
131
131
  existing.builtin = true;
132
132
  // Backfill defaults from the built-in template for any field the
133
- // user's saved copy is missing keeps old configs aligned with new
134
- // schema additions (resumeIdArgs, type, etc.) without clobbering
135
- // the user's customisations.
136
- if (existing.resumeIdArgs == null) existing.resumeIdArgs = def.resumeIdArgs;
137
- if (existing.newSessionIdArgs == null) existing.newSessionIdArgs = def.newSessionIdArgs;
138
- // Drop the v0.x `resumeArgs` fallback all builtins now have
139
- // pre-assigned UUIDs (claude/copilot via flag, codex via seed) so
140
- // resumeIdArgs always applies on resume.
133
+ // user's saved copy is missing OR has as an empty array. Empty
134
+ // arrays matter because users upgrading from a pre-0.15 config
135
+ // never wrote `newSessionIdArgs` (didn't exist), AND a partial
136
+ // 0.14→0.15 dev iteration shipped codex with `[]`. Treat both
137
+ // the same: a builtin with no template means "use the canonical
138
+ // one ccsm now knows about", since these fields are the
139
+ // integration contract with the upstream CLI not user knobs.
140
+ const needsBackfill = (v) => v == null || (Array.isArray(v) && v.length === 0);
141
+ if (needsBackfill(existing.resumeIdArgs)) existing.resumeIdArgs = def.resumeIdArgs;
142
+ if (needsBackfill(existing.newSessionIdArgs)) existing.newSessionIdArgs = def.newSessionIdArgs;
143
+ // Drop the v0.x `resumeArgs` fallback — every builtin now has a
144
+ // pre-assigned UUID (claude/copilot via flag, codex via seed) so
145
+ // resumeIdArgs always applies on resume; the field is dead weight.
141
146
  delete existing.resumeArgs;
142
- // Special-case codex: an unreleased earlier iteration of this
143
- // schema shipped `newSessionIdArgs: []` for codex. The seeded-file
144
- // trick (lib/codexSeed) now lets us pre-assign, so backfill the
145
- // ['resume','<id>'] template over an empty array too.
146
- if (existing.id === 'codex'
147
- && Array.isArray(existing.newSessionIdArgs)
148
- && existing.newSessionIdArgs.length === 0
149
- && Array.isArray(def.newSessionIdArgs)
150
- && def.newSessionIdArgs.length > 0) {
151
- existing.newSessionIdArgs = def.newSessionIdArgs;
152
- }
153
147
  if (!existing.type) existing.type = def.type;
154
148
  } else {
155
149
  out.clis.unshift({ ...def });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
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",
@@ -65,8 +65,16 @@ function UpgradeCard() {
65
65
  if (!info?.updateAvailable) return;
66
66
  setUpgrading(true);
67
67
  try {
68
- await api('POST', '/api/upgrade', { target: 'latest' });
68
+ const r = await api('POST', '/api/upgrade', { target: 'latest' });
69
69
  setToast(`upgrading to v${info.latest} · backend will restart`);
70
+ if (r?.closeFrontend) {
71
+ // Backend will respawn with a fresh browser window — close this
72
+ // one so the user isn't stuck on the OfflineBanner during the
73
+ // upgrade window. window.close() only works when the window was
74
+ // script-opened (Edge --app=, our spawned browser); regular tabs
75
+ // ignore it silently, which is fine (OfflineBanner takes over).
76
+ setTimeout(() => { try { window.close(); } catch {} }, 400);
77
+ }
70
78
  } catch (e) {
71
79
  setUpgrading(false);
72
80
  setToast(e.message, 'error');
@@ -431,8 +431,16 @@ function RestartButton() {
431
431
  if (!ok) return;
432
432
  setBusy(true);
433
433
  try {
434
- await restartBackend();
434
+ const r = await restartBackend();
435
435
  setToast('restarting backend…');
436
+ if (r?.closeFrontend) {
437
+ // Backend respawn will pop a fresh browser window — close this
438
+ // one so the user isn't stuck on the OfflineBanner during the
439
+ // ~3s downtime. window.close() only fires in script-opened
440
+ // windows (Edge --app=); regular tabs ignore it and stay open,
441
+ // which is the right behavior for them.
442
+ setTimeout(() => { try { window.close(); } catch {} }, 400);
443
+ }
436
444
  } catch (e) {
437
445
  setBusy(false);
438
446
  setToast(e.message, 'error');
@@ -62,7 +62,12 @@ function pidAlive(pid) {
62
62
 
63
63
  const isWin = process.platform === 'win32';
64
64
  const ccsmCmd = isWin ? 'ccsm.cmd' : 'ccsm';
65
- const childEnv = { ...process.env, CCSM_NO_BROWSER: '1' };
65
+ // Inherit env but DROP CCSM_NO_BROWSER so the respawned server pops a
66
+ // fresh browser window — the frontend that triggered the restart
67
+ // called window.close() in parallel, and the new window takes its
68
+ // place without the OfflineBanner gap.
69
+ const childEnv = { ...process.env };
70
+ delete childEnv.CCSM_NO_BROWSER;
66
71
  let exe, exeArgs;
67
72
  if (isWin) {
68
73
  exe = process.env.ComSpec || 'cmd.exe';
@@ -126,7 +126,12 @@ function pidAlive(pid) {
126
126
  const ccsmCmd = installPrefix
127
127
  ? (isWin ? path.join(installPrefix, 'ccsm.cmd') : path.join(installPrefix, 'bin', 'ccsm'))
128
128
  : (isWin ? 'ccsm.cmd' : 'ccsm');
129
- const childEnv = { ...process.env, CCSM_NO_BROWSER: '1' };
129
+ // Drop CCSM_NO_BROWSER so the post-upgrade server pops a fresh window
130
+ // — the frontend that triggered the upgrade window.close()'d in
131
+ // parallel, and the new window takes its place. Skips the
132
+ // OfflineBanner gap that used to bridge the upgrade.
133
+ const childEnv = { ...process.env };
134
+ delete childEnv.CCSM_NO_BROWSER;
130
135
  let exe, exeArgs;
131
136
  if (isWin) {
132
137
  exe = process.env.ComSpec || 'cmd.exe';
package/server.js CHANGED
@@ -926,7 +926,7 @@ app.post('/api/restart', asyncH(async (_req, res) => {
926
926
  restartInFlight = true;
927
927
 
928
928
  if (process.env.CCSM_DEV === '1') {
929
- res.json({ ok: true, started: true, mode: 'dev' });
929
+ res.json({ ok: true, started: true, mode: 'dev', closeFrontend: false });
930
930
  setImmediate(() => gracefulShutdown('restart (dev)'));
931
931
  return;
932
932
  }
@@ -941,7 +941,11 @@ app.post('/api/restart', asyncH(async (_req, res) => {
941
941
  return res.status(500).json({ error: `helper copy failed: ${e.message}` });
942
942
  }
943
943
  const args = [helperTmp, String(currentPort), String(process.pid)];
944
- res.json({ ok: true, started: true, helper: helperTmp });
944
+ // closeFrontend asks the calling tab to window.close() itself — the
945
+ // helper will respawn ccsm WITHOUT CCSM_NO_BROWSER, so a fresh window
946
+ // pops up once the new backend is listening. Net effect: the user
947
+ // never sees the OfflineBanner during a restart.
948
+ res.json({ ok: true, started: true, helper: helperTmp, closeFrontend: true });
945
949
 
946
950
  setImmediate(() => {
947
951
  const { spawn } = require('node:child_process');
@@ -1081,7 +1085,7 @@ app.post('/api/upgrade', asyncH(async (req, res) => {
1081
1085
  }
1082
1086
  const args = [helperTmp, target, String(currentPort), String(process.pid), installPrefix, respawn];
1083
1087
 
1084
- res.json({ ok: true, started: true, target, helper: helperTmp });
1088
+ res.json({ ok: true, started: true, target, helper: helperTmp, closeFrontend: true });
1085
1089
 
1086
1090
  // Flush response, then spawn helper detached and gracefulShutdown so
1087
1091
  // the helper's npm install isn't fighting our open file handles.