@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 +22 -1
- package/lib/codexThemes/ccsm-light.tmTheme +77 -0
- package/lib/config.js +16 -1
- package/package.json +1 -1
- package/public/js/api.js +5 -1
- package/public/js/pages/ConfigurePage.js +1 -1
- package/public/js/pages/LaunchPage.js +1 -1
- package/public/js/streaming.js +4 -1
- package/server.js +42 -3
package/lib/codexSeed.js
CHANGED
|
@@ -158,5 +158,26 @@ async function seedCodexSession({ id, cwd, cli }) {
|
|
|
158
158
|
return file;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
|
|
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: ['--
|
|
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.
|
|
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: '--
|
|
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: '--
|
|
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;
|
package/public/js/streaming.js
CHANGED
|
@@ -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
|