@bakapiano/ccsm 0.3.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 +61 -3
- package/lib/favorites.js +73 -0
- package/lib/focus.js +58 -0
- package/lib/sessions.js +153 -0
- package/package.json +1 -1
- package/public/app.js +591 -252
- package/public/index.html +375 -143
- package/public/styles.css +1193 -125
- package/server.js +72 -8
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 } = 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,
|
|
@@ -26,6 +27,7 @@ const {
|
|
|
26
27
|
} = require('./lib/launcher');
|
|
27
28
|
const {
|
|
28
29
|
focusByPid,
|
|
30
|
+
focusBySession,
|
|
29
31
|
snapshotWindowsOf,
|
|
30
32
|
focusNewlyOpenedHwnd,
|
|
31
33
|
} = require('./lib/focus');
|
|
@@ -61,6 +63,48 @@ app.get('/api/sessions', asyncH(async (_req, res) => {
|
|
|
61
63
|
res.json({ sessions, takenAt: Date.now() });
|
|
62
64
|
}));
|
|
63
65
|
|
|
66
|
+
app.get('/api/sessions/recent', asyncH(async (req, res) => {
|
|
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);
|
|
69
|
+
const live = await listSessions();
|
|
70
|
+
const excludeIds = new Set(live.map((s) => s.sessionId));
|
|
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 });
|
|
106
|
+
}));
|
|
107
|
+
|
|
64
108
|
// ---- config ----
|
|
65
109
|
|
|
66
110
|
app.get('/api/config', asyncH(async (_req, res) => {
|
|
@@ -296,8 +340,13 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
|
|
|
296
340
|
const sessions = await listSessions();
|
|
297
341
|
const s = sessions.find((x) => x.sessionId === sessionId);
|
|
298
342
|
if (!s) return res.status(404).json({ error: `session ${sessionId} not live` });
|
|
299
|
-
const result = await
|
|
300
|
-
|
|
343
|
+
const result = await focusBySession({
|
|
344
|
+
pid: s.pid,
|
|
345
|
+
sessionId: s.sessionId,
|
|
346
|
+
title: s.title,
|
|
347
|
+
cwd: s.cwd,
|
|
348
|
+
});
|
|
349
|
+
res.json({ session: { pid: s.pid, sessionId: s.sessionId, cwd: s.cwd, title: s.title }, ...result });
|
|
301
350
|
}));
|
|
302
351
|
|
|
303
352
|
// ---- terminal kinds ----
|
|
@@ -359,7 +408,7 @@ function findAppModeBrowser() {
|
|
|
359
408
|
}
|
|
360
409
|
|
|
361
410
|
function openInBrowser(url, mode) {
|
|
362
|
-
if (process.platform !== 'win32'
|
|
411
|
+
if (mode === 'none' || process.platform !== 'win32') return { kind: 'none', child: null };
|
|
363
412
|
const { spawn } = require('node:child_process');
|
|
364
413
|
const fs = require('node:fs');
|
|
365
414
|
|
|
@@ -376,25 +425,28 @@ function openInBrowser(url, mode) {
|
|
|
376
425
|
[
|
|
377
426
|
`--app=${url}`,
|
|
378
427
|
`--user-data-dir=${profileDir}`,
|
|
379
|
-
'--window-size=
|
|
428
|
+
'--window-size=1500,1100',
|
|
380
429
|
'--no-first-run',
|
|
381
430
|
'--no-default-browser-check',
|
|
382
431
|
],
|
|
383
432
|
{ detached: true, stdio: 'ignore' }
|
|
384
433
|
);
|
|
385
434
|
child.unref();
|
|
386
|
-
return;
|
|
435
|
+
return { kind: 'app', child };
|
|
387
436
|
}
|
|
388
437
|
console.log('[ccsm] no Edge/Chrome found for app mode, falling back to default browser');
|
|
389
438
|
}
|
|
390
439
|
|
|
391
|
-
// 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.
|
|
392
443
|
const child = spawn('cmd.exe', ['/c', 'start', '', url], {
|
|
393
444
|
detached: true,
|
|
394
445
|
stdio: 'ignore',
|
|
395
446
|
windowsHide: true,
|
|
396
447
|
});
|
|
397
448
|
child.unref();
|
|
449
|
+
return { kind: 'tab', child: null };
|
|
398
450
|
}
|
|
399
451
|
|
|
400
452
|
(async () => {
|
|
@@ -406,7 +458,19 @@ function openInBrowser(url, mode) {
|
|
|
406
458
|
console.log(`work dir: ${cfg.workDir}`);
|
|
407
459
|
console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
|
|
408
460
|
const mode = cfg.browserMode || (cfg.autoOpenBrowser === false ? 'none' : 'app');
|
|
409
|
-
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
|
+
}
|
|
410
474
|
startSnapshotLoop();
|
|
411
475
|
})().catch((err) => {
|
|
412
476
|
console.error('startup failed:', err);
|