@bakapiano/ccsm 0.4.0 → 0.6.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/config.js +1 -0
- package/lib/favorites.js +73 -0
- package/lib/focus.js +90 -14
- package/lib/labels.js +49 -0
- package/lib/sessions.js +48 -8
- package/lib/workspace.js +8 -4
- package/package.json +1 -1
- package/public/app.js +1052 -302
- package/public/favicon.svg +18 -0
- package/public/index.html +481 -159
- package/public/styles.css +1628 -125
- package/server.js +87 -10
package/server.js
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
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');
|
|
9
|
+
const { loadLabels, setLabel, removeLabel } = require('./lib/labels');
|
|
8
10
|
const { loadConfig, saveConfig, DATA_DIR } = require('./lib/config');
|
|
9
11
|
const {
|
|
10
12
|
saveSnapshot,
|
|
@@ -63,11 +65,68 @@ app.get('/api/sessions', asyncH(async (_req, res) => {
|
|
|
63
65
|
}));
|
|
64
66
|
|
|
65
67
|
app.get('/api/sessions/recent', asyncH(async (req, res) => {
|
|
66
|
-
const limit = Math.min(200, Number(req.query.limit) ||
|
|
68
|
+
const limit = Math.min(200, Math.max(1, Number(req.query.limit) || 15));
|
|
69
|
+
const offset = Math.max(0, Number(req.query.offset) || 0);
|
|
67
70
|
const live = await listSessions();
|
|
68
71
|
const excludeIds = new Set(live.map((s) => s.sessionId));
|
|
69
|
-
const recent = await listRecentSessions({ limit, excludeIds });
|
|
70
|
-
res.json({ recent, takenAt: Date.now() });
|
|
72
|
+
const { recent, total } = await listRecentSessions({ limit, offset, excludeIds });
|
|
73
|
+
res.json({ recent, total, limit, offset, takenAt: Date.now() });
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
// ---- favorites ----
|
|
77
|
+
// Sessions the user has starred. Stored at $DATA_DIR/favorites.json.
|
|
78
|
+
// Frontend usually GETs once at boot and updates optimistically.
|
|
79
|
+
app.get('/api/favorites', asyncH(async (_req, res) => {
|
|
80
|
+
const favorites = await listFavorites();
|
|
81
|
+
res.json({ favorites });
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
app.post('/api/favorites/:sessionId', asyncH(async (req, res) => {
|
|
85
|
+
const sessionId = req.params.sessionId;
|
|
86
|
+
let info = req.body && typeof req.body === 'object' ? req.body : {};
|
|
87
|
+
// If client didn't supply title/cwd, try to look them up from the live
|
|
88
|
+
// session list or from the jsonl files on disk. This way star-from-empty
|
|
89
|
+
// (e.g. via API) still produces a usable favorite.
|
|
90
|
+
if (!info.cwd || !info.title) {
|
|
91
|
+
const live = await listSessions();
|
|
92
|
+
const livehit = live.find((s) => s.sessionId === sessionId);
|
|
93
|
+
if (livehit) {
|
|
94
|
+
info = { cwd: livehit.cwd, title: livehit.title, ...info };
|
|
95
|
+
} else {
|
|
96
|
+
const meta = await findSessionMetadata(sessionId);
|
|
97
|
+
if (meta) info = { cwd: meta.cwd, title: meta.title, gitBranch: meta.gitBranch, ...info };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const fav = await addFavorite(sessionId, info);
|
|
101
|
+
res.json({ favorite: fav });
|
|
102
|
+
}));
|
|
103
|
+
|
|
104
|
+
app.delete('/api/favorites/:sessionId', asyncH(async (req, res) => {
|
|
105
|
+
const removed = await removeFavorite(req.params.sessionId);
|
|
106
|
+
res.json({ removed });
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
// ---- labels (rename overrides) ----
|
|
110
|
+
// Custom display titles keyed by sessionId. Empty body / empty label is
|
|
111
|
+
// treated as a delete.
|
|
112
|
+
app.get('/api/labels', asyncH(async (_req, res) => {
|
|
113
|
+
const labels = await loadLabels();
|
|
114
|
+
res.json({ labels });
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
app.put('/api/labels/:sessionId', asyncH(async (req, res) => {
|
|
118
|
+
const label = req.body && req.body.label;
|
|
119
|
+
if (!label || !String(label).trim()) {
|
|
120
|
+
const removed = await removeLabel(req.params.sessionId);
|
|
121
|
+
return res.json({ removed });
|
|
122
|
+
}
|
|
123
|
+
const saved = await setLabel(req.params.sessionId, label);
|
|
124
|
+
res.json({ label: saved });
|
|
125
|
+
}));
|
|
126
|
+
|
|
127
|
+
app.delete('/api/labels/:sessionId', asyncH(async (req, res) => {
|
|
128
|
+
const removed = await removeLabel(req.params.sessionId);
|
|
129
|
+
res.json({ removed });
|
|
71
130
|
}));
|
|
72
131
|
|
|
73
132
|
// ---- config ----
|
|
@@ -305,11 +364,13 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
|
|
|
305
364
|
const sessions = await listSessions();
|
|
306
365
|
const s = sessions.find((x) => x.sessionId === sessionId);
|
|
307
366
|
if (!s) return res.status(404).json({ error: `session ${sessionId} not live` });
|
|
367
|
+
const cfg = await loadConfig();
|
|
308
368
|
const result = await focusBySession({
|
|
309
369
|
pid: s.pid,
|
|
310
370
|
sessionId: s.sessionId,
|
|
311
371
|
title: s.title,
|
|
312
372
|
cwd: s.cwd,
|
|
373
|
+
moveToCenter: !!cfg.focusMovesToCenter,
|
|
313
374
|
});
|
|
314
375
|
res.json({ session: { pid: s.pid, sessionId: s.sessionId, cwd: s.cwd, title: s.title }, ...result });
|
|
315
376
|
}));
|
|
@@ -318,7 +379,8 @@ app.post('/api/sessions/:sessionId/focus', asyncH(async (req, res) => {
|
|
|
318
379
|
app.get('/api/terminals', (_req, res) => res.json({ terminals: listTerminalKinds() }));
|
|
319
380
|
|
|
320
381
|
// ---- health ----
|
|
321
|
-
|
|
382
|
+
const pkg = require('./package.json');
|
|
383
|
+
app.get('/api/health', (_req, res) => res.json({ ok: true, pid: process.pid, version: pkg.version, name: pkg.name }));
|
|
322
384
|
|
|
323
385
|
// ---- auto-snapshot scheduler ----
|
|
324
386
|
let snapshotTimer = null;
|
|
@@ -373,7 +435,7 @@ function findAppModeBrowser() {
|
|
|
373
435
|
}
|
|
374
436
|
|
|
375
437
|
function openInBrowser(url, mode) {
|
|
376
|
-
if (process.platform !== 'win32'
|
|
438
|
+
if (mode === 'none' || process.platform !== 'win32') return { kind: 'none', child: null };
|
|
377
439
|
const { spawn } = require('node:child_process');
|
|
378
440
|
const fs = require('node:fs');
|
|
379
441
|
|
|
@@ -390,25 +452,28 @@ function openInBrowser(url, mode) {
|
|
|
390
452
|
[
|
|
391
453
|
`--app=${url}`,
|
|
392
454
|
`--user-data-dir=${profileDir}`,
|
|
393
|
-
'--window-size=
|
|
455
|
+
'--window-size=1500,1100',
|
|
394
456
|
'--no-first-run',
|
|
395
457
|
'--no-default-browser-check',
|
|
396
458
|
],
|
|
397
459
|
{ detached: true, stdio: 'ignore' }
|
|
398
460
|
);
|
|
399
461
|
child.unref();
|
|
400
|
-
return;
|
|
462
|
+
return { kind: 'app', child };
|
|
401
463
|
}
|
|
402
464
|
console.log('[ccsm] no Edge/Chrome found for app mode, falling back to default browser');
|
|
403
465
|
}
|
|
404
466
|
|
|
405
|
-
// mode === 'tab' (or app-mode fallback)
|
|
467
|
+
// mode === 'tab' (or app-mode fallback). cmd's `start` builtin exits
|
|
468
|
+
// immediately after launching the default browser — the child handle
|
|
469
|
+
// isn't usable for lifecycle tracking.
|
|
406
470
|
const child = spawn('cmd.exe', ['/c', 'start', '', url], {
|
|
407
471
|
detached: true,
|
|
408
472
|
stdio: 'ignore',
|
|
409
473
|
windowsHide: true,
|
|
410
474
|
});
|
|
411
475
|
child.unref();
|
|
476
|
+
return { kind: 'tab', child: null };
|
|
412
477
|
}
|
|
413
478
|
|
|
414
479
|
(async () => {
|
|
@@ -420,7 +485,19 @@ function openInBrowser(url, mode) {
|
|
|
420
485
|
console.log(`work dir: ${cfg.workDir}`);
|
|
421
486
|
console.log(`terminal: ${cfg.terminal} · ${cfg.claudeCommand}${cfg.terminal === 'wt' ? ` (via ${cfg.commandShell})` : ''}`);
|
|
422
487
|
const mode = cfg.browserMode || (cfg.autoOpenBrowser === false ? 'none' : 'app');
|
|
423
|
-
openInBrowser(url, mode);
|
|
488
|
+
const opened = openInBrowser(url, mode);
|
|
489
|
+
|
|
490
|
+
// Interactive npx-style launch: tie server lifetime to the chromeless
|
|
491
|
+
// browser window. When the user closes the window, msedge.exe (with its
|
|
492
|
+
// own --user-data-dir process group) exits — we hear that and shut down
|
|
493
|
+
// so the terminal returns. Headless / nohup launches stay running.
|
|
494
|
+
if (opened.kind === 'app' && opened.child && process.stdout.isTTY) {
|
|
495
|
+
opened.child.on('exit', () => {
|
|
496
|
+
console.log('[ccsm] browser window closed · shutting down');
|
|
497
|
+
process.exit(0);
|
|
498
|
+
});
|
|
499
|
+
console.log('[ccsm] tied to browser window — close it to stop ccsm');
|
|
500
|
+
}
|
|
424
501
|
startSnapshotLoop();
|
|
425
502
|
})().catch((err) => {
|
|
426
503
|
console.error('startup failed:', err);
|