@bakapiano/ccsm 0.15.1 → 0.15.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/CLAUDE.md +60 -0
- package/lib/cliActivity.js +24 -3
- package/package.json +1 -1
- package/public/css/sidebar.css +42 -1
- package/server.js +4 -1
package/CLAUDE.md
CHANGED
|
@@ -443,6 +443,66 @@ fresh server. So `npm i -g @bakapiano/ccsm@latest && ccsm` is one
|
|
|
443
443
|
seamless step. From the frontend, the About page's Upgrade button
|
|
444
444
|
achieves the same thing without leaving the browser.
|
|
445
445
|
|
|
446
|
+
### Release process
|
|
447
|
+
|
|
448
|
+
Three artifacts ship per release: a git tag, a GitHub Release, and an
|
|
449
|
+
npm publish. The whole thing is CI-driven — you never `npm publish`
|
|
450
|
+
locally — but it requires you to drive three steps in order:
|
|
451
|
+
|
|
452
|
+
1. **Commit + bump + push (local).** Stage everything, write a release
|
|
453
|
+
commit, then bump + tag + push:
|
|
454
|
+
|
|
455
|
+
```powershell
|
|
456
|
+
git add -A
|
|
457
|
+
git commit -m "vX.Y.Z: <one-line summary>
|
|
458
|
+
|
|
459
|
+
<body>
|
|
460
|
+
|
|
461
|
+
Co-Authored-By: Claude ..."
|
|
462
|
+
npm --prefix . version <patch|minor|major> -m "v%s"
|
|
463
|
+
git push origin main
|
|
464
|
+
git push origin vX.Y.Z
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`npm version` writes the new version into `package.json` +
|
|
468
|
+
`package-lock.json`, creates its OWN commit, and tags it. The
|
|
469
|
+
`--prefix .` is needed on Windows where bare `npm version` errors on
|
|
470
|
+
the global `%APPDATA%\npm\package.json`. Push BOTH `main` and the
|
|
471
|
+
tag — pushing only main skips the tag-triggered draft-release
|
|
472
|
+
workflow.
|
|
473
|
+
|
|
474
|
+
2. **Tag-push fires two workflows automatically:**
|
|
475
|
+
- `Deploy frontend to GitHub Pages` → publishes `pages-root/` → `/`
|
|
476
|
+
and `public/` → `/<X.Y.Z>/` on `gh-pages`. Old `/<X.Y.Z>/`
|
|
477
|
+
subdirs stay forever (`keep_files: true`).
|
|
478
|
+
- `Draft GitHub Release on tag push` → creates a **draft** release
|
|
479
|
+
for `vX.Y.Z`.
|
|
480
|
+
|
|
481
|
+
3. **Publish the draft (manual one-liner):**
|
|
482
|
+
|
|
483
|
+
```powershell
|
|
484
|
+
gh release edit vX.Y.Z --draft=false
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
This flips the draft to "published", which fires the third workflow
|
|
488
|
+
— `Publish to npm` — using the `NPM_TOKEN` repo secret with
|
|
489
|
+
provenance. The runner needs ~30s; verify with
|
|
490
|
+
`gh run watch <run-id> --exit-status` or just refresh npmjs.com.
|
|
491
|
+
|
|
492
|
+
The reason for the draft step instead of auto-publishing on tag push:
|
|
493
|
+
gives you a chance to abort a half-baked tag (delete the draft +
|
|
494
|
+
`git push --delete origin vX.Y.Z`) before it lands on the public
|
|
495
|
+
registry.
|
|
496
|
+
|
|
497
|
+
### Why we don't publish from the local box
|
|
498
|
+
|
|
499
|
+
`npm publish` from a dev machine works in principle but skips
|
|
500
|
+
provenance attestation (the sigstore + GitHub OIDC binding that npm
|
|
501
|
+
displays as a "Provenance" badge on the package page). CI has the OIDC
|
|
502
|
+
token; you don't. Local publish also wouldn't have the consistent
|
|
503
|
+
runner state, so reproducible-build claims fall apart. The pipeline
|
|
504
|
+
exists; use it.
|
|
505
|
+
|
|
446
506
|
## Cross-platform
|
|
447
507
|
|
|
448
508
|
Today: Windows-first.
|
package/lib/cliActivity.js
CHANGED
|
@@ -90,9 +90,20 @@ async function resolveTranscript(record, cliCfg) {
|
|
|
90
90
|
async function probeActivity(record, cliCfg) {
|
|
91
91
|
let s = state.get(record.id);
|
|
92
92
|
if (!s) {
|
|
93
|
-
s = { resolvedPath: null, lastMtimeMs: 0, lastChangedAt: 0 };
|
|
93
|
+
s = { resolvedPath: null, lastMtimeMs: 0, lastChangedAt: 0, lastOutputAt: 0 };
|
|
94
94
|
state.set(record.id, s);
|
|
95
95
|
}
|
|
96
|
+
// PTY output (CLI is streaming text — thinking spinners, token output,
|
|
97
|
+
// status lines) is the strongest signal that the CLI is working. It's
|
|
98
|
+
// ALSO the only signal we have when the transcript file isn't being
|
|
99
|
+
// updated — claude/codex buffer reasoning + tool results for tens of
|
|
100
|
+
// seconds before flushing a turn, so mtime alone reports "idle"
|
|
101
|
+
// through long thinking phases. Check PTY first; short-circuit if the
|
|
102
|
+
// CLI is clearly active, skipping the fs.stat below.
|
|
103
|
+
const now = Date.now();
|
|
104
|
+
if (s.lastOutputAt && (now - s.lastOutputAt) < WORKING_WINDOW_MS) {
|
|
105
|
+
return 'working';
|
|
106
|
+
}
|
|
96
107
|
if (!s.resolvedPath) {
|
|
97
108
|
s.resolvedPath = await resolveTranscript(record, cliCfg);
|
|
98
109
|
if (!s.resolvedPath) return 'unknown';
|
|
@@ -105,7 +116,6 @@ async function probeActivity(record, cliCfg) {
|
|
|
105
116
|
s.resolvedPath = null;
|
|
106
117
|
return 'unknown';
|
|
107
118
|
}
|
|
108
|
-
const now = Date.now();
|
|
109
119
|
if (mtimeMs !== s.lastMtimeMs) {
|
|
110
120
|
s.lastMtimeMs = mtimeMs;
|
|
111
121
|
s.lastChangedAt = now;
|
|
@@ -113,6 +123,17 @@ async function probeActivity(record, cliCfg) {
|
|
|
113
123
|
return (now - s.lastChangedAt) < WORKING_WINDOW_MS ? 'working' : 'idle';
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
// Called from server.js's spawnCliSession onData hook. Cheap (timestamp
|
|
127
|
+
// write); bound by how often the PTY emits, which is fine.
|
|
128
|
+
function noteOutput(sessionId) {
|
|
129
|
+
let s = state.get(sessionId);
|
|
130
|
+
if (!s) {
|
|
131
|
+
s = { resolvedPath: null, lastMtimeMs: 0, lastChangedAt: 0, lastOutputAt: 0 };
|
|
132
|
+
state.set(sessionId, s);
|
|
133
|
+
}
|
|
134
|
+
s.lastOutputAt = Date.now();
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
function releaseSession(sessionId) { state.delete(sessionId); }
|
|
117
138
|
|
|
118
|
-
module.exports = { probeActivity, releaseSession };
|
|
139
|
+
module.exports = { probeActivity, noteOutput, releaseSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.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/css/sidebar.css
CHANGED
|
@@ -568,11 +568,52 @@ body.is-resizing-sidebar * {
|
|
|
568
568
|
}
|
|
569
569
|
.tree-session.is-running .tree-dot::after {
|
|
570
570
|
background: var(--green);
|
|
571
|
+
/* Soft halo so the dot reads as "alive" even from across the sidebar.
|
|
572
|
+
Box-shadow uses currentColor isn't ideal here (the dot itself uses
|
|
573
|
+
background, not color), so we hardcode each state's halo color
|
|
574
|
+
below. */
|
|
575
|
+
box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.55);
|
|
576
|
+
animation: tree-dot-breathe-idle 2.8s ease-in-out infinite;
|
|
571
577
|
}
|
|
572
578
|
/* Working = CLI is actively writing to its transcript (i.e. thinking
|
|
573
|
-
or printing tokens). Idle stays green; working flips
|
|
579
|
+
or printing tokens). Idle stays green + slow breathe; working flips
|
|
580
|
+
to blue + faster, more obvious breathe. */
|
|
574
581
|
.tree-session.is-running.is-working .tree-dot::after {
|
|
575
582
|
background: var(--blue, #4a73a5);
|
|
583
|
+
box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
|
|
584
|
+
animation: tree-dot-breathe-working 1.4s ease-in-out infinite;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
@keyframes tree-dot-breathe-idle {
|
|
588
|
+
0%, 100% {
|
|
589
|
+
box-shadow: 0 0 0 0 rgba(74, 138, 74, 0.45);
|
|
590
|
+
opacity: 0.85;
|
|
591
|
+
}
|
|
592
|
+
50% {
|
|
593
|
+
box-shadow: 0 0 0 4px rgba(74, 138, 74, 0);
|
|
594
|
+
opacity: 1;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
@keyframes tree-dot-breathe-working {
|
|
598
|
+
0%, 100% {
|
|
599
|
+
box-shadow: 0 0 0 0 rgba(74, 115, 165, 0.65);
|
|
600
|
+
opacity: 0.9;
|
|
601
|
+
transform: scale(1);
|
|
602
|
+
}
|
|
603
|
+
50% {
|
|
604
|
+
box-shadow: 0 0 0 5px rgba(74, 115, 165, 0);
|
|
605
|
+
opacity: 1;
|
|
606
|
+
transform: scale(1.15);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
/* Respect users who've asked for less motion — keep the color signal
|
|
610
|
+
but drop the pulse. */
|
|
611
|
+
@media (prefers-reduced-motion: reduce) {
|
|
612
|
+
.tree-session.is-running .tree-dot::after,
|
|
613
|
+
.tree-session.is-running.is-working .tree-dot::after {
|
|
614
|
+
animation: none;
|
|
615
|
+
box-shadow: none;
|
|
616
|
+
}
|
|
576
617
|
}
|
|
577
618
|
.tree-label {
|
|
578
619
|
flex: 1;
|
package/server.js
CHANGED
|
@@ -221,7 +221,10 @@ function spawnCliSession({ cli, cwd, sessionId, meta, extraArgs = [] }) {
|
|
|
221
221
|
cwd,
|
|
222
222
|
env,
|
|
223
223
|
meta: { ...meta, cliId: cli.id, cliName: cli.name },
|
|
224
|
-
onData: () => {
|
|
224
|
+
onData: () => {
|
|
225
|
+
persistedSessions.touch(sessionId).catch(() => {});
|
|
226
|
+
try { require('./lib/cliActivity').noteOutput(sessionId); } catch {}
|
|
227
|
+
},
|
|
225
228
|
onExit: ({ exitCode }) => {
|
|
226
229
|
persistedSessions.markExited(sessionId, exitCode).catch(() => {});
|
|
227
230
|
},
|