@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 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.
@@ -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.1",
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",
@@ -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 to blue. */
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: () => { persistedSessions.touch(sessionId).catch(() => {}); },
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
  },