@bakapiano/ccsm 0.21.1 → 0.21.2

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/codexSeed.js CHANGED
@@ -158,5 +158,26 @@ async function seedCodexSession({ id, cwd, cli }) {
158
158
  return file;
159
159
  }
160
160
 
161
- module.exports = { seedCodexSession, probeCodexHome, parseCodexHomeFromDoctor };
161
+ // Copy ccsm's bundled light codex syntax theme into the codex home's themes/
162
+ // dir so `-c tui.theme=ccsm-light` resolves. This theme carries light
163
+ // markup.inserted/deleted backgrounds, which at true-color level override
164
+ // codex's diff palette — the only way to get a LIGHT diff on Windows, where
165
+ // codex's own background detection (default_bg()) is compiled out and always
166
+ // falls back to a dark diff. Idempotent (writes only when missing/changed).
167
+ async function ensureCodexLightTheme(home) {
168
+ if (!home) return false;
169
+ const src = path.join(__dirname, 'codexThemes', 'ccsm-light.tmTheme');
170
+ const dstDir = path.join(home, 'themes');
171
+ const dst = path.join(dstDir, 'ccsm-light.tmTheme');
172
+ try {
173
+ const content = await fs.readFile(src, 'utf8');
174
+ await fs.mkdir(dstDir, { recursive: true });
175
+ let existing = null;
176
+ try { existing = await fs.readFile(dst, 'utf8'); } catch {}
177
+ if (existing !== content) await fs.writeFile(dst, content, 'utf8');
178
+ return true;
179
+ } catch { return false; }
180
+ }
181
+
182
+ module.exports = { seedCodexSession, probeCodexHome, parseCodexHomeFromDoctor, ensureCodexLightTheme };
162
183
 
@@ -0,0 +1,77 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <!-- ccsm-light: a GitHub-Light-flavoured codex syntax theme whose
4
+ markup.inserted / markup.deleted scopes carry LIGHT diff backgrounds.
5
+ ccsm injects this (via `-c tui.theme=ccsm-light`) when the ccsm terminal
6
+ is in light mode, because codex's own diff theme detection (default_bg())
7
+ is compiled out on Windows and always falls back to a DARK diff palette —
8
+ which buries readability on a white terminal. At true-color level codex
9
+ lets a syntax theme's markup scope backgrounds override that default, so
10
+ these two scopes are what flip the diff to light. -->
11
+ <plist version="1.0">
12
+ <dict>
13
+ <key>name</key>
14
+ <string>ccsm-light</string>
15
+ <key>settings</key>
16
+ <array>
17
+ <dict>
18
+ <key>settings</key>
19
+ <dict>
20
+ <key>background</key><string>#ffffff</string>
21
+ <key>foreground</key><string>#1f2328</string>
22
+ <key>caret</key><string>#1f2328</string>
23
+ <key>selection</key><string>#b6d6fd</string>
24
+ <key>lineHighlight</key><string>#eaeef2</string>
25
+ </dict>
26
+ </dict>
27
+ <dict>
28
+ <key>scope</key><string>comment, punctuation.definition.comment</string>
29
+ <key>settings</key><dict><key>foreground</key><string>#6e7781</string></dict>
30
+ </dict>
31
+ <dict>
32
+ <key>scope</key><string>string, string.quoted, punctuation.definition.string</string>
33
+ <key>settings</key><dict><key>foreground</key><string>#0a3069</string></dict>
34
+ </dict>
35
+ <dict>
36
+ <key>scope</key><string>constant.numeric, constant.language, constant.character, constant.other</string>
37
+ <key>settings</key><dict><key>foreground</key><string>#0550ae</string></dict>
38
+ </dict>
39
+ <dict>
40
+ <key>scope</key><string>keyword, keyword.control, keyword.operator, storage, storage.type, storage.modifier</string>
41
+ <key>settings</key><dict><key>foreground</key><string>#cf222e</string></dict>
42
+ </dict>
43
+ <dict>
44
+ <key>scope</key><string>entity.name.function, support.function, meta.function-call</string>
45
+ <key>settings</key><dict><key>foreground</key><string>#8250df</string></dict>
46
+ </dict>
47
+ <dict>
48
+ <key>scope</key><string>entity.name.type, entity.name.class, entity.name.namespace, support.type, support.class</string>
49
+ <key>settings</key><dict><key>foreground</key><string>#953800</string></dict>
50
+ </dict>
51
+ <dict>
52
+ <key>scope</key><string>variable, variable.other, variable.parameter</string>
53
+ <key>settings</key><dict><key>foreground</key><string>#1f2328</string></dict>
54
+ </dict>
55
+ <dict>
56
+ <key>scope</key><string>entity.name.tag, support.type.property-name</string>
57
+ <key>settings</key><dict><key>foreground</key><string>#116329</string></dict>
58
+ </dict>
59
+ <dict>
60
+ <key>scope</key><string>markup.inserted, markup.inserted.diff, meta.diff.header.to-file</string>
61
+ <key>settings</key>
62
+ <dict>
63
+ <key>background</key><string>#e6ffec</string>
64
+ <key>foreground</key><string>#1a7f37</string>
65
+ </dict>
66
+ </dict>
67
+ <dict>
68
+ <key>scope</key><string>markup.deleted, markup.deleted.diff, meta.diff.header.from-file</string>
69
+ <key>settings</key>
70
+ <dict>
71
+ <key>background</key><string>#ffebe9</string>
72
+ <key>foreground</key><string>#cf222e</string>
73
+ </dict>
74
+ </dict>
75
+ </array>
76
+ </dict>
77
+ </plist>
package/lib/config.js CHANGED
@@ -56,8 +56,13 @@ const DEFAULT_CLIS = [
56
56
  name: 'GitHub Copilot',
57
57
  command: 'copilot',
58
58
  args: [],
59
+ // copilot has no `--resume <id>` (space) flag — that parses as `--resume`
60
+ // (valueless → session picker) plus a stray positional arg, which hangs on
61
+ // a blank screen. `--session-id <id>` both creates AND resumes a session by
62
+ // id (`copilot --help`: "Resume an existing session or task by ID"), so we
63
+ // use it for both new and resume.
59
64
  newSessionIdArgs: ['--session-id', '<id>'],
60
- resumeIdArgs: ['--resume', '<id>'],
65
+ resumeIdArgs: ['--session-id', '<id>'],
61
66
  shell: 'direct',
62
67
  type: 'copilot',
63
68
  builtin: true,
@@ -149,6 +154,16 @@ function mergeWithDefaults(partial) {
149
154
  const needsBackfill = (v) => v == null || (Array.isArray(v) && v.length === 0);
150
155
  if (needsBackfill(existing.resumeIdArgs)) existing.resumeIdArgs = def.resumeIdArgs;
151
156
  if (needsBackfill(existing.newSessionIdArgs)) existing.newSessionIdArgs = def.newSessionIdArgs;
157
+ // Heal the old broken copilot resume flag: `--resume <id>` (space form)
158
+ // isn't valid copilot syntax — it parses as a valueless `--resume`
159
+ // (session picker) plus a stray positional, hanging on a blank screen.
160
+ // The builtin now uses `--session-id <id>`. Migrate configs still
161
+ // carrying the old default; genuine user customisations are left alone.
162
+ if (existing.id === 'copilot'
163
+ && Array.isArray(existing.resumeIdArgs)
164
+ && existing.resumeIdArgs.join(' ') === '--resume <id>') {
165
+ existing.resumeIdArgs = def.resumeIdArgs;
166
+ }
152
167
  // Drop the v0.x `resumeArgs` fallback — every builtin now has a
153
168
  // pre-assigned UUID (claude/copilot via flag, codex via seed) so
154
169
  // resumeIdArgs always applies on resume; the field is dead weight.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
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
@@ -282,7 +282,11 @@ export function resumeSession(sessionId) {
282
282
  const cached = resumeInFlight.get(sessionId);
283
283
  if (cached) return cached;
284
284
  const p = (async () => {
285
- const r = await api('POST', `/api/sessions/${sessionId}/resume`);
285
+ const r = await api('POST', `/api/sessions/${sessionId}/resume`, {
286
+ // Resolved terminal theme → backend sets a matching COLORFGBG so the
287
+ // CLI's light/dark auto-detection follows the ccsm terminal.
288
+ theme: document.documentElement.dataset.theme,
289
+ });
286
290
  await loadSessions();
287
291
  return r.launched;
288
292
  })();
@@ -47,7 +47,7 @@ function tokenizeCliArgs(v) {
47
47
  const CLI_TYPE_DEFAULTS = {
48
48
  claude: { command: 'claude', resumeIdArgs: '--resume <id>', newSessionIdArgs: '--session-id <id>' },
49
49
  codex: { command: 'codex', resumeIdArgs: 'resume <id>', newSessionIdArgs: 'resume <id>' },
50
- copilot: { command: 'copilot', resumeIdArgs: '--resume <id>', newSessionIdArgs: '--session-id <id>' },
50
+ copilot: { command: 'copilot', resumeIdArgs: '--session-id <id>', newSessionIdArgs: '--session-id <id>' },
51
51
  other: { resumeIdArgs: '', newSessionIdArgs: '' },
52
52
  };
53
53
 
@@ -171,7 +171,7 @@ function LaunchHero() {
171
171
  onChange: (v, next) => {
172
172
  const presets = { claude: { command: 'claude', resumeArgs: '--continue', resumeIdArgs: '--resume <id>', name: 'Claude Code' },
173
173
  codex: { command: 'codex', resumeArgs: 'resume --last', resumeIdArgs: 'resume <id>', name: 'OpenAI Codex' },
174
- copilot: { command: 'copilot', resumeArgs: '--continue', resumeIdArgs: '--resume <id>', name: 'GitHub Copilot' },
174
+ copilot: { command: 'copilot', resumeArgs: '--continue', resumeIdArgs: '--session-id <id>', name: 'GitHub Copilot' },
175
175
  other: {} }[v] || {};
176
176
  const patch = {};
177
177
  if (presets.resumeArgs != null) patch.resumeArgs = presets.resumeArgs;
@@ -60,10 +60,13 @@ function applyEvent(ev, rootId) {
60
60
  // onMeta(event) is called for workspace/launched/done events so the caller can
61
61
  // surface them in their own result text area.
62
62
  export async function streamNewSession(body, { progressRootId = 'newSessionProgress', onMeta } = {}) {
63
+ // Pass the resolved terminal theme so the backend can hand CLIs a matching
64
+ // COLORFGBG (light/dark detection). dataset.theme is set by applyTheme().
65
+ const theme = document.documentElement.dataset.theme;
63
66
  const res = await fetch(httpBase() + '/api/sessions/new', {
64
67
  method: 'POST',
65
68
  headers: { 'Content-Type': 'application/json' },
66
- body: JSON.stringify(body),
69
+ body: JSON.stringify({ ...body, theme }),
67
70
  });
68
71
  if (!res.ok && res.headers.get('content-type')?.startsWith('application/json')) {
69
72
  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 = [] }) {
284
+ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [], theme }) {
285
285
  if (!webTerminal.available) {
286
286
  const e = new Error('node-pty unavailable · cannot spawn web terminal');
287
287
  e.code = 'PTY_UNAVAILABLE';
@@ -321,6 +321,16 @@ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [] }) {
321
321
  // spawnEnv() also strips duplicate path-case keys so our override
322
322
  // doesn't get shadowed by the inherited `Path` from process.env.
323
323
  const env = spawnEnv(cli.env);
324
+ // Tell background-aware CLIs which way the ccsm terminal is painted, so
325
+ // their light/dark auto-detection matches it. COLORFGBG (fg;bg ANSI indices)
326
+ // is the de-facto signal that codex (its DiffTheme probes it), copilot, and
327
+ // claude all read — bg 15 = light, 0 = dark. claude additionally gets OSC
328
+ // 10/11 answers + --settings auto; this covers codex/copilot, which detect
329
+ // via COLORFGBG, not OSC. The frontend passes its resolved theme on spawn;
330
+ // a theme switch is picked up on the next resume.
331
+ if (theme === 'light' || theme === 'dark') {
332
+ env.COLORFGBG = theme === 'light' ? '0;15' : '15;0';
333
+ }
324
334
  const trySpawn = (executable) => webTerminal.spawn({
325
335
  id: sessionId,
326
336
  command: executable,
@@ -849,12 +859,14 @@ app.post('/api/sessions/new', async (req, res) => {
849
859
  cliSessionId: preAssignedId || undefined,
850
860
  });
851
861
  try {
862
+ const themeArgs = await codexThemeArgs(cli, req.body && req.body.theme);
852
863
  const entry = spawnCliSession({
853
864
  cli,
854
865
  cwd: workspace.path,
855
866
  sessionId: record.id,
856
867
  meta: { title: workspace.name, workspace: workspace.name, cwd: workspace.path },
857
- extraArgs: newSessionArgs,
868
+ extraArgs: [...themeArgs, ...newSessionArgs],
869
+ theme: req.body && req.body.theme,
858
870
  });
859
871
  await persistedSessions.markRunning(record.id, entry.meta.pid);
860
872
  launched = { id: record.id, pid: entry.meta.pid, cliId: cli.id };
@@ -983,13 +995,15 @@ app.post('/api/sessions/:id/resume', asyncH(async (req, res) => {
983
995
  // pre-assignment refactor every ccsm-launched session has one (via
984
996
  // newSessionIdArgs flag or the codex seed trick), and adopted
985
997
  // sessions inherit theirs from the disk scan.
998
+ const themeArgs = await codexThemeArgs(cli, req.body && req.body.theme);
986
999
  const extraArgs = buildResumeArgs(cli, record);
987
1000
  const entry = spawnCliSession({
988
1001
  cli,
989
1002
  cwd: record.cwd,
990
1003
  sessionId: record.id,
991
1004
  meta: { title: record.title || record.workspace, workspace: record.workspace, cwd: record.cwd },
992
- extraArgs,
1005
+ extraArgs: [...themeArgs, ...extraArgs],
1006
+ theme: req.body && req.body.theme,
993
1007
  });
994
1008
  await persistedSessions.markRunning(record.id, entry.meta.pid);
995
1009
  res.json({ launched: { id: record.id, pid: entry.meta.pid, cliId: cli.id } });
@@ -998,6 +1012,31 @@ app.post('/api/sessions/:id/resume', asyncH(async (req, res) => {
998
1012
  }
999
1013
  }));
1000
1014
 
1015
+ // codex-only: when the ccsm terminal is in LIGHT mode, inject a session-scoped
1016
+ // `-c tui.theme=ccsm-light`. codex's diff theme detection (default_bg()) is
1017
+ // compiled out on Windows and always falls back to a DARK diff palette, which
1018
+ // reads poorly on a white terminal — and it ignores COLORFGBG/OSC. The only
1019
+ // lever is a syntax theme whose markup.inserted/deleted scopes carry light
1020
+ // backgrounds (they override the diff palette at true-color level). We ship
1021
+ // that theme (ccsm-light.tmTheme), copy it into the codex home, and point
1022
+ // tui.theme at it. Returns the args to prepend (before `resume <id>` so the
1023
+ // global -c lands before the subcommand), or [] when not applicable. Skipped
1024
+ // in dark mode (codex's dark default is already correct on a dark terminal)
1025
+ // and when the user configured their own tui.theme in cli.args.
1026
+ async function codexThemeArgs(cli, theme) {
1027
+ if (!cli || cli.type !== 'codex' || theme !== 'light') return [];
1028
+ const args = cli.args || [];
1029
+ const userSet = args.some((a, i) =>
1030
+ String(a).includes('tui.theme') || (a === '-c' && String(args[i + 1] || '').includes('tui.theme')));
1031
+ if (userSet) return [];
1032
+ try {
1033
+ const { probeCodexHome, ensureCodexLightTheme } = require('./lib/codexSeed');
1034
+ const home = await probeCodexHome({ command: cli.command, shell: cli.shell });
1035
+ if (!(await ensureCodexLightTheme(home))) return [];
1036
+ return ['-c', 'tui.theme="ccsm-light"'];
1037
+ } catch { return []; }
1038
+ }
1039
+
1001
1040
  // Build the args appended on resume: substitute the captured upstream
1002
1041
  // session UUID into cli.resumeIdArgs (e.g. ['--resume', '<id>'] →
1003
1042
  // ['--resume', '7c28...']). Throws if either piece is missing — by