@bakapiano/ccsm 0.4.0 → 0.5.0
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/CLAUDE.md +58 -3
- package/lib/favorites.js +73 -0
- package/lib/sessions.js +48 -8
- package/package.json +1 -1
- package/public/app.js +567 -276
- package/public/index.html +373 -163
- package/public/styles.css +1193 -125
- package/server.js +59 -9
package/server.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const express = require('express');
|
|
6
6
|
|
|
7
|
-
const { listSessions, listRecentSessions } = require('./lib/sessions');
|
|
7
|
+
const { listSessions, listRecentSessions, findSessionMetadata } = require('./lib/sessions');
|
|
8
|
+
const { listFavorites, addFavorite, removeFavorite, loadFavorites } = require('./lib/favorites');
|
|
8
9
|
const { loadConfig, saveConfig, DATA_DIR } = require('./lib/config');
|
|
9
10
|
const {
|
|
10
11
|
saveSnapshot,
|
|
@@ -63,11 +64,45 @@ app.get('/api/sessions', asyncH(async (_req, res) => {
|
|
|
63
64
|
}));
|
|
64
65
|
|
|
65
66
|
app.get('/api/sessions/recent', asyncH(async (req, res) => {
|
|
66
|
-
const limit = Math.min(200, Number(req.query.limit) ||
|
|
67
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 15));
|
|
68
|
+
const offset = Math.max(0, Number(req.query.offset) || 0);
|
|
67
69
|
const live = await listSessions();
|
|
68
70
|
const excludeIds = new Set(live.map((s) => s.sessionId));
|
|
69
|
-
const recent = await listRecentSessions({ limit, excludeIds });
|
|
70
|
-
res.json({ recent, takenAt: Date.now() });
|
|
71
|
+
const { recent, total } = await listRecentSessions({ limit, offset, excludeIds });
|
|
72
|
+
res.json({ recent, total, limit, offset, takenAt: Date.now() });
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
// ---- favorites ----
|
|
76
|
+
// Sessions the user has starred. Stored at $DATA_DIR/favorites.json.
|
|
77
|
+
// Frontend usually GETs once at boot and updates optimistically.
|
|
78
|
+
app.get('/api/favorites', asyncH(async (_req, res) => {
|
|
79
|
+
const favorites = await listFavorites();
|
|
80
|
+
res.json({ favorites });
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
app.post('/api/favorites/:sessionId', asyncH(async (req, res) => {
|
|
84
|
+
const sessionId = req.params.sessionId;
|
|
85
|
+
let info = req.body && typeof req.body === 'object' ? req.body : {};
|
|
86
|
+
// If client didn't supply title/cwd, try to look them up from the live
|
|
87
|
+
// session list or from the jsonl files on disk. This way star-from-empty
|
|
88
|
+
// (e.g. via API) still produces a usable favorite.
|
|
89
|
+
if (!info.cwd || !info.title) {
|
|
90
|
+
const live = await listSessions();
|
|
91
|
+
const livehit = live.find((s) => s.sessionId === sessionId);
|
|
92
|
+
if (livehit) {
|
|
93
|
+
info = { cwd: livehit.cwd, title: livehit.title, ...info };
|
|
94
|
+
} else {
|
|
95
|
+
const meta = await findSessionMetadata(sessionId);
|
|
96
|
+
if (meta) info = { cwd: meta.cwd, title: meta.title, gitBranch: meta.gitBranch, ...info };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const fav = await addFavorite(sessionId, info);
|
|
100
|
+
res.json({ favorite: fav });
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
app.delete('/api/favorites/:sessionId', asyncH(async (req, res) => {
|
|
104
|
+
const removed = await removeFavorite(req.params.sessionId);
|
|
105
|
+
res.json({ removed });
|
|
71
106
|
}));
|
|
72
107
|
|
|
73
108
|
// ---- config ----
|
|
@@ -373,7 +408,7 @@ function findAppModeBrowser() {
|
|
|
373
408
|
}
|
|
374
409
|
|
|
375
410
|
function openInBrowser(url, mode) {
|
|
376
|
-
if (process.platform !== 'win32'
|
|
411
|
+
if (mode === 'none' || process.platform !== 'win32') return { kind: 'none', child: null };
|
|
377
412
|
const { spawn } = require('node:child_process');
|
|
378
413
|
const fs = require('node:fs');
|
|
379
414
|
|
|
@@ -390,25 +425,28 @@ function openInBrowser(url, mode) {
|
|
|
390
425
|
[
|
|
391
426
|
`--app=${url}`,
|
|
392
427
|
`--user-data-dir=${profileDir}`,
|
|
393
|
-
'--window-size=
|
|
428
|
+
'--window-size=1500,1100',
|
|
394
429
|
'--no-first-run',
|
|
395
430
|
'--no-default-browser-check',
|
|
396
431
|
],
|
|
397
432
|
{ detached: true, stdio: 'ignore' }
|
|
398
433
|
);
|
|
399
434
|
child.unref();
|
|
400
|
-
return;
|
|
435
|
+
return { kind: 'app', child };
|
|
401
436
|
}
|
|
402
437
|
console.log('[ccsm] no Edge/Chrome found for app mode, falling back to default browser');
|
|
403
438
|
}
|
|
404
439
|
|
|
405
|
-
// mode === 'tab' (or app-mode fallback)
|
|
440
|
+
// mode === 'tab' (or app-mode fallback). cmd's `start` builtin exits
|
|
441
|
+
// immediately after launching the default browser — the child handle
|
|
442
|
+
// isn't usable for lifecycle tracking.
|
|
406
443
|
const child = spawn('cmd.exe', ['/c', 'start', '', url], {
|
|
407
444
|
detached: true,
|
|
408
445
|
stdio: 'ignore',
|
|
409
446
|
windowsHide: true,
|
|
410
447
|
});
|
|
411
448
|
child.unref();
|
|
449
|
+
return { kind: 'tab', child: null };
|
|
412
450
|
}
|
|
413
451
|
|
|
414
452
|
(async () => {
|
|
@@ -420,7 +458,19 @@ function openInBrowser(url, mode) {
|
|
|
420
458
|
console.log(`work dir: ${cfg.workDir}`);
|
|
421
459
|
console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
|
|
422
460
|
const mode = cfg.browserMode || (cfg.autoOpenBrowser === false ? 'none' : 'app');
|
|
423
|
-
openInBrowser(url, mode);
|
|
461
|
+
const opened = openInBrowser(url, mode);
|
|
462
|
+
|
|
463
|
+
// Interactive npx-style launch: tie server lifetime to the chromeless
|
|
464
|
+
// browser window. When the user closes the window, msedge.exe (with its
|
|
465
|
+
// own --user-data-dir process group) exits — we hear that and shut down
|
|
466
|
+
// so the terminal returns. Headless / nohup launches stay running.
|
|
467
|
+
if (opened.kind === 'app' && opened.child && process.stdout.isTTY) {
|
|
468
|
+
opened.child.on('exit', () => {
|
|
469
|
+
console.log('[ccsm] browser window closed · shutting down');
|
|
470
|
+
process.exit(0);
|
|
471
|
+
});
|
|
472
|
+
console.log('[ccsm] tied to browser window — close it to stop ccsm');
|
|
473
|
+
}
|
|
424
474
|
startSnapshotLoop();
|
|
425
475
|
})().catch((err) => {
|
|
426
476
|
console.error('startup failed:', err);
|