@bakapiano/ccsm 0.6.0 → 0.8.4
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 +377 -123
- package/README.md +172 -38
- package/bin/ccsm.js +194 -0
- package/lib/favorites.js +23 -45
- package/lib/jsonStore.js +60 -0
- package/lib/labels.js +21 -41
- package/lib/webTerminal.js +173 -0
- package/package.json +11 -3
- package/public/css/base.css +82 -0
- package/public/css/cards.css +149 -0
- package/public/css/feedback.css +219 -0
- package/public/css/forms.css +282 -0
- package/public/css/layout.css +107 -0
- package/public/css/modal.css +169 -0
- package/public/css/responsive.css +10 -0
- package/public/css/sidebar.css +165 -0
- package/public/css/tables.css +266 -0
- package/public/css/terminals.css +112 -0
- package/public/css/tokens.css +63 -0
- package/public/css/wco.css +70 -0
- package/public/css/widgets.css +204 -0
- package/public/favicon.svg +1 -1
- package/public/index.html +52 -490
- package/public/js/actions.js +87 -0
- package/public/js/api.js +103 -0
- package/public/js/backend.js +28 -0
- package/public/js/components/App.js +45 -0
- package/public/js/components/Card.js +24 -0
- package/public/js/components/DialogHost.js +45 -0
- package/public/js/components/Fab.js +11 -0
- package/public/js/components/FavoritesTable.js +81 -0
- package/public/js/components/Footer.js +12 -0
- package/public/js/components/NewSessionModal.js +142 -0
- package/public/js/components/OfflineBanner.js +52 -0
- package/public/js/components/PageHead.js +33 -0
- package/public/js/components/Pagination.js +27 -0
- package/public/js/components/ProgressList.js +32 -0
- package/public/js/components/RecentTable.js +68 -0
- package/public/js/components/RepoPicker.js +40 -0
- package/public/js/components/ReposEditor.js +74 -0
- package/public/js/components/ServerStatus.js +18 -0
- package/public/js/components/SessionsTable.js +71 -0
- package/public/js/components/Sidebar.js +52 -0
- package/public/js/components/SnapshotPanel.js +77 -0
- package/public/js/components/TerminalView.js +108 -0
- package/public/js/components/TitleCell.js +40 -0
- package/public/js/components/Toast.js +8 -0
- package/public/js/components/WorkspacePicker.js +19 -0
- package/public/js/components/WorkspacesGrid.js +41 -0
- package/public/js/dialog.js +59 -0
- package/public/js/html.js +6 -0
- package/public/js/icons.js +114 -0
- package/public/js/main.js +81 -0
- package/public/js/pages/AboutPage.js +85 -0
- package/public/js/pages/ConfigurePage.js +194 -0
- package/public/js/pages/LaunchPage.js +117 -0
- package/public/js/pages/SessionsPage.js +47 -0
- package/public/js/pages/TerminalsPage.js +74 -0
- package/public/js/state.js +87 -0
- package/public/js/streaming.js +96 -0
- package/public/js/toast.js +14 -0
- package/public/js/util.js +24 -0
- package/public/manifest.webmanifest +14 -0
- package/scripts/install.js +132 -0
- package/scripts/uninstall.js +56 -0
- package/server.js +286 -30
- package/public/app.js +0 -1353
- package/public/styles.css +0 -1639
package/CLAUDE.md
CHANGED
|
@@ -1,110 +1,284 @@
|
|
|
1
1
|
# ccsm — Claude Code Session Manager
|
|
2
2
|
|
|
3
|
-
A small Node/Express +
|
|
3
|
+
A small Node/Express + Preact web tool that gives a single pane over all
|
|
4
|
+
live Claude Code sessions on this machine, snapshots them, restores them
|
|
5
|
+
through Windows Terminal, and launches new sessions inside isolated
|
|
6
|
+
workspaces.
|
|
4
7
|
|
|
5
8
|
## Why this exists
|
|
6
9
|
|
|
7
|
-
When you're running 8–10 concurrent `claude` sessions across ad-hoc
|
|
10
|
+
When you're running 8–10 concurrent `claude` sessions across ad-hoc
|
|
11
|
+
clones (`D:\proj`, `D:\proj2`, `…`, plus GUID worktree dirs), it's easy
|
|
12
|
+
to lose track of which terminal is which session. ccsm gives an
|
|
13
|
+
at-a-glance list and a snapshot/restore safety net.
|
|
14
|
+
|
|
15
|
+
## Architecture: hosted frontend + local backend
|
|
16
|
+
|
|
17
|
+
The single most important fact about ccsm v0.8+ is that **the frontend
|
|
18
|
+
is no longer bundled into the npm package** in production. It lives at
|
|
19
|
+
`https://bakapiano.github.io/cssm/v1/`, served by GitHub Pages, deployed
|
|
20
|
+
via the workflow at `.github/workflows/deploy-pages.yml`.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
┌── browser ────────────────────────────┐
|
|
24
|
+
│ https://bakapiano.github.io/cssm/v1/ ← static frontend
|
|
25
|
+
└────────────┬──────────────────────────┘
|
|
26
|
+
│ fetch /api/* (CORS, allow-list)
|
|
27
|
+
│ ws://localhost:7777/ws/*
|
|
28
|
+
▼
|
|
29
|
+
┌── local backend ──────────────────────┐
|
|
30
|
+
│ npm i -g @bakapiano/ccsm │
|
|
31
|
+
│ ccsm │
|
|
32
|
+
│ ├── /api/sessions /api/snapshot │
|
|
33
|
+
│ ├── /api/sessions/new (NDJSON) │
|
|
34
|
+
│ ├── /ws/terminal/:id (PTY) │
|
|
35
|
+
│ ├── /api/heartbeat /api/spawn-browser│
|
|
36
|
+
│ └── /api/health /api/shutdown │
|
|
37
|
+
└───────────────────────────────────────┘
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Why this split:
|
|
41
|
+
- Frontend can be updated independently — push to `main`, CI rebuilds GH Pages, every user hot-refreshes.
|
|
42
|
+
- No service worker complexity needed for "PWA loads even when backend is dead". The page itself is on GH Pages; the backend going down only loses `/api/*`, and the page handles that gracefully with an OfflineBanner.
|
|
43
|
+
- Cross-platform path forward — backend can be ported per-OS, but the frontend never has to be.
|
|
44
|
+
- Versioned at `/v1/` so future breaking changes ship a fresh `/v2/` while `/v1/` keeps serving the older clients.
|
|
45
|
+
|
|
46
|
+
In **dev mode** (running from a checkout — `__dirname` not under
|
|
47
|
+
`node_modules`), the backend ALSO serves `public/` so contributors can
|
|
48
|
+
iterate at `localhost:7777/` without pushing.
|
|
8
49
|
|
|
9
50
|
## Run
|
|
10
51
|
|
|
11
52
|
```powershell
|
|
12
|
-
#
|
|
13
|
-
|
|
53
|
+
# install once
|
|
54
|
+
npm install -g @bakapiano/ccsm
|
|
14
55
|
|
|
15
|
-
#
|
|
16
|
-
|
|
56
|
+
# then anywhere
|
|
57
|
+
ccsm
|
|
17
58
|
```
|
|
18
|
-
Then open http://localhost:7777.
|
|
19
59
|
|
|
20
|
-
|
|
60
|
+
`ccsm` opens the hosted frontend in a chromeless Edge `--app=` window.
|
|
61
|
+
Terminal returns immediately (the server is spawned detached). Close
|
|
62
|
+
the window → server saves a final snapshot and exits within ~12s.
|
|
21
63
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
- `terminal` — `wt` | `powershell` | `pwsh` | `cmd`. wt opens a fresh window per launch (`wt -w new` is set to defeat the "fold into existing window" setting some users have). The other three each spawn via `cmd /c start ... <shell>`.
|
|
26
|
-
- `commandShell` (default `pwsh`) — only consulted when `terminal=wt`. Values `pwsh` / `powershell` wrap `claudeCommand` inside `<shell> -NoExit -NoLogo -Command "Set-Location ...; & '<cmd>' '<args>'..."` so PowerShell aliases / functions / profile-defined names (like `ccp` from `$PROFILE`) resolve. `none` runs the command directly via wt (raw `CreateProcess`) — fine if `claudeCommand` is an actual exe on PATH, broken for aliases. `pwsh` / `powershell` kinds already wrap natively so this knob doesn't affect them; `cmd` kind has no shell concept for aliases.
|
|
27
|
-
- `autoFocusOnLaunch` (default true) — after every launch (new session, finder, resume, restore) the server takes an HWND snapshot of terminal windows, polls for a new HWND, and `SetForegroundWindow`s it. See gotcha below — wt is multi-window single-process, so we diff HWNDs not PIDs.
|
|
64
|
+
If you don't want the auto-opened window (e.g. you live in the PWA),
|
|
65
|
+
just visit `https://bakapiano.github.io/cssm/v1/` — when backend is
|
|
66
|
+
down you see an OfflineBanner with a **Start ccsm** button.
|
|
28
67
|
|
|
29
|
-
|
|
68
|
+
Default port `7777`, default workDir `~/ccsm-workspaces`. Config +
|
|
69
|
+
snapshots live at `~/.ccsm/` (override with `CCSM_HOME=<path>`). All
|
|
70
|
+
settings editable through the Configure panel
|
|
71
|
+
(`~/.ccsm/config.json` on disk). Notable knobs:
|
|
72
|
+
|
|
73
|
+
- `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port. The startup log prints the actual URL.
|
|
74
|
+
- `browserMode` (default `app`) — `app` finds Edge or Chrome and spawns it with `--app=<url> --user-data-dir=<DATA_DIR>/browser-profile`. `tab` opens the default browser. `none` skips opening.
|
|
75
|
+
- `claudeCommand` (default `"claude"`) — what gets `--resume`'d or freshly invoked. Can be an exe, alias, function, or wrapper script.
|
|
76
|
+
- `terminal` — `wt` | `powershell` | `pwsh` | `cmd`. wt opens a fresh window per launch (`wt -w new`).
|
|
77
|
+
- `commandShell` (default `pwsh`) — only consulted when `terminal=wt`. Wraps `claudeCommand` in `pwsh -NoExit -NoLogo -Command ...` so PowerShell aliases resolve.
|
|
78
|
+
- `autoFocusOnLaunch` (default true) — after every launch (new session, finder, resume, restore) the server takes an HWND snapshot of terminal windows, polls for a new HWND, and `SetForegroundWindow`s it.
|
|
79
|
+
- `finderPrompt` — initial message passed to the "Ask Claude to find a session" finder session.
|
|
80
|
+
|
|
81
|
+
## ccsm:// protocol · "wake on click"
|
|
82
|
+
|
|
83
|
+
The hosted frontend can't spawn processes (sandboxed). For "click to
|
|
84
|
+
wake backend" we register a per-user URL protocol handler on Windows:
|
|
30
85
|
|
|
31
86
|
```
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
├── lib\
|
|
35
|
-
│ ├── sessions.js # reads ~/.claude/sessions/*.json + cross-checks live PIDs (tasklist)
|
|
36
|
-
│ │ # + pulls last ai-title from ~/.claude/projects/<cwd-slug>/<sessionId>.jsonl
|
|
37
|
-
│ │ # + listRecentSessions(limit, offset) returns paged {recent, total}
|
|
38
|
-
│ ├── snapshot.js # save/load/rotate/restore — restore = launch one wt window per session
|
|
39
|
-
│ ├── workspace.js # workspace = subfolder under workDir holding repo clones;
|
|
40
|
-
│ │ # "in use" = any live session's cwd is at/under the workspace path
|
|
41
|
-
│ ├── launcher.js # dispatches across terminal kinds (wt/powershell/pwsh/cmd);
|
|
42
|
-
│ │ # path.resolve()s cwd; throws if cwd doesn't exist
|
|
43
|
-
│ ├── focus.js # PowerShell + Win32 — listWindowsOf, focusByHwnd, focusByPid,
|
|
44
|
-
│ │ # focusNewlyOpenedHwnd (HWND-diff for auto-focus on launch)
|
|
45
|
-
│ ├── favorites.js # user-pinned sessions, ~/.ccsm/favorites.json keyed by sessionId
|
|
46
|
-
│ └── config.js # loadConfig/saveConfig with defaults
|
|
47
|
-
├── public\
|
|
48
|
-
│ ├── index.html, app.js, styles.css # vanilla, auto-refresh every 5s
|
|
49
|
-
└── package.json # bin entry → `ccsm` (for npx github:bakapiano/cssm)
|
|
50
|
-
|
|
51
|
-
~/.ccsm/ # or $CCSM_HOME
|
|
52
|
-
├── config.json # source of truth
|
|
53
|
-
├── snapshot.json # latest snapshot, rewritten every 60s
|
|
54
|
-
├── snapshots/ # rotating history (default keep=30)
|
|
55
|
-
├── favorites.json # { [sessionId]: { sessionId, cwd, title, gitBranch, addedAt } }
|
|
56
|
-
└── browser-profile/ # Edge/Chrome --user-data-dir when browserMode=app
|
|
87
|
+
HKCU\Software\Classes\ccsm\shell\open\command
|
|
88
|
+
→ wscript.exe "<LOCALAPPDATA>\ccsm\launcher.vbs" "%1"
|
|
57
89
|
```
|
|
58
90
|
|
|
59
|
-
|
|
91
|
+
`launcher.vbs` uses `Shell.Run(..., 0, False)` — windowstyle 0 means the
|
|
92
|
+
spawned `ccsm.cmd` runs **completely hidden**. No console flash. The
|
|
93
|
+
`.cmd` goes through `bin/ccsm.js`, which detects `ccsm://start` in argv,
|
|
94
|
+
spawns `server.js` detached with `CCSM_NO_BROWSER=1`, and exits.
|
|
60
95
|
|
|
61
|
-
|
|
96
|
+
OfflineBanner's "Start ccsm" button is just an `<a href="ccsm://start">`.
|
|
97
|
+
First click triggers a one-time Windows confirmation dialog ("Open
|
|
98
|
+
ccsm.cmd?"); ticking "Always allow" makes it silent thereafter.
|
|
99
|
+
|
|
100
|
+
postinstall (`scripts/install.js`) registers the protocol unconditionally
|
|
101
|
+
on Windows — including npx-cache installs. The path stored in the
|
|
102
|
+
registry points at whatever `ccsm.cmd` location npm gave us
|
|
103
|
+
(`<prefix>/ccsm.cmd` from `npm config get prefix`).
|
|
104
|
+
|
|
105
|
+
## Lifecycle
|
|
106
|
+
|
|
107
|
+
Single `gracefulShutdown(reason)` function in `server.js` is the only
|
|
108
|
+
exit path. It races `saveSnapshot()` against a 2s timeout, kills any
|
|
109
|
+
PTY children, then `process.exit(0)`. Every trigger funnels here:
|
|
110
|
+
|
|
111
|
+
| trigger | path |
|
|
112
|
+
|---|---|
|
|
113
|
+
| auto-spawned browser window closes | `child.on('exit')` — see smart-kill below |
|
|
114
|
+
| `POST /api/shutdown` | from npm uninstall, from launcher's auto-upgrade |
|
|
115
|
+
| SIGINT / SIGTERM | OS signals |
|
|
116
|
+
| heartbeat watchdog timeout | 90s with no heartbeat, only when launched via `bin/ccsm.js` |
|
|
117
|
+
|
|
118
|
+
**Smart browser-exit**: when the spawned browser child dies, we don't
|
|
119
|
+
kill immediately. Two filters:
|
|
120
|
+
|
|
121
|
+
1. **Fast-exit (<5s)** — Edge `--app=` often hands the URL off to an
|
|
122
|
+
existing Edge profile process group and the spawned child dies
|
|
123
|
+
milliseconds after creation. We ignore any exit inside the first 5s.
|
|
124
|
+
|
|
125
|
+
2. **Deferred multi-client check (12s)** — after a real close, wait 12s
|
|
126
|
+
and check if any heartbeat arrived AFTER the close timestamp. If
|
|
127
|
+
yes, a hosted-frontend tab (or another window) is keeping us busy,
|
|
128
|
+
stay alive. If no, gracefulShutdown.
|
|
62
129
|
|
|
63
|
-
|
|
130
|
+
Frontend heartbeat cadence is 10s (in `main.js`), so one full cycle
|
|
131
|
+
fits inside the 12s decision window.
|
|
132
|
+
|
|
133
|
+
Environment overrides:
|
|
134
|
+
- `CCSM_KEEP_ALIVE=1` → disable both browser-exit hook and heartbeat watchdog. For automation hosts.
|
|
135
|
+
- `CCSM_LAUNCHER=1` → set by `bin/ccsm.js` when it spawns the server; enables the heartbeat watchdog.
|
|
136
|
+
- `CCSM_NO_BROWSER=1` → set by the launcher when handling a `ccsm://` click; suppresses the server's auto-open browser.
|
|
137
|
+
- `CCSM_NO_DEV=1` → suppress dev-mode features (static serving, hot-reload SSE) even when running from a checkout.
|
|
138
|
+
|
|
139
|
+
## Layout
|
|
64
140
|
|
|
65
141
|
```
|
|
66
|
-
|
|
67
|
-
├──
|
|
68
|
-
|
|
69
|
-
│
|
|
70
|
-
├──
|
|
71
|
-
│
|
|
142
|
+
ccsm/
|
|
143
|
+
├── server.js # Express + WebSocket; API-only in prod
|
|
144
|
+
├── bin/ccsm.js # launcher · detach, wake-on-protocol,
|
|
145
|
+
│ # auto-upgrade-restart, first-run hint
|
|
146
|
+
├── scripts/
|
|
147
|
+
│ ├── install.js # postinstall · ccsm:// + launcher.vbs
|
|
148
|
+
│ └── uninstall.js # preuninstall · cleanup + /api/shutdown
|
|
149
|
+
├── lib/
|
|
150
|
+
│ ├── sessions.js # ~/.claude/sessions/*.json + tasklist PID check
|
|
151
|
+
│ ├── snapshot.js # save / load / rotate / restore
|
|
152
|
+
│ ├── workspace.js # workspace = folder under workDir
|
|
153
|
+
│ ├── launcher.js # spawn wt / pwsh / cmd
|
|
154
|
+
│ ├── focus.js # PowerShell + Win32 EnumWindows / SetForegroundWindow
|
|
155
|
+
│ ├── webTerminal.js # in-process PTY pool · node-pty + WebSocket bridge
|
|
156
|
+
│ ├── favorites.js · labels.js # pinned sessions / rename overrides
|
|
157
|
+
│ ├── jsonStore.js # shared keyed-JSON store factory
|
|
158
|
+
│ └── config.js # loadConfig / saveConfig
|
|
159
|
+
└── public/ # Preact + HTM + signals (no build step) — also pushed to GH Pages /v1/
|
|
160
|
+
├── index.html # relative paths so it works at any host path
|
|
161
|
+
├── manifest.webmanifest # PWA · relative start_url, WCO display override
|
|
162
|
+
├── favicon.svg
|
|
163
|
+
├── js/
|
|
164
|
+
│ ├── backend.js # httpBase() / wsBase() — same-origin local, cross-origin hosted
|
|
165
|
+
│ ├── main.js # boot · clock tick · heartbeat · is-app body class
|
|
166
|
+
│ ├── state.js # signals
|
|
167
|
+
│ ├── api.js # fetch wrapper + loaders
|
|
168
|
+
│ ├── streaming.js # NDJSON clone-progress stream
|
|
169
|
+
│ ├── actions.js # focus / resume / favorite / rename
|
|
170
|
+
│ ├── dialog.js · toast.js # ccsmConfirm / ccsmPrompt / setToast
|
|
171
|
+
│ ├── html.js · icons.js · util.js
|
|
172
|
+
│ ├── components/
|
|
173
|
+
│ │ ├── App.js · Sidebar.js · PageHead.js · Footer.js
|
|
174
|
+
│ │ ├── ServerStatus.js · Toast.js · Fab.js · OfflineBanner.js
|
|
175
|
+
│ │ ├── Card.js · Pagination.js · TitleCell.js
|
|
176
|
+
│ │ ├── SessionsTable.js · RecentTable.js · FavoritesTable.js
|
|
177
|
+
│ │ ├── RepoPicker.js · ReposEditor.js · WorkspacePicker.js
|
|
178
|
+
│ │ ├── WorkspacesGrid.js · SnapshotPanel.js · ProgressList.js
|
|
179
|
+
│ │ ├── TerminalView.js · NewSessionModal.js
|
|
180
|
+
│ │ └── DialogHost.js
|
|
181
|
+
│ └── pages/
|
|
182
|
+
│ ├── SessionsPage.js · LaunchPage.js
|
|
183
|
+
│ ├── TerminalsPage.js
|
|
184
|
+
│ ├── ConfigurePage.js · AboutPage.js
|
|
185
|
+
└── css/ # 13 focused stylesheets
|
|
186
|
+
├── tokens.css · base.css · layout.css
|
|
187
|
+
├── sidebar.css · cards.css · tables.css · forms.css
|
|
188
|
+
├── widgets.css · feedback.css · modal.css
|
|
189
|
+
├── terminals.css · wco.css · responsive.css
|
|
190
|
+
|
|
191
|
+
~/.ccsm/ # or $CCSM_HOME
|
|
192
|
+
├── config.json # source of truth
|
|
193
|
+
├── snapshot.json # latest auto-snapshot
|
|
194
|
+
├── snapshots/ # rotating history (default keep=30)
|
|
195
|
+
├── favorites.json · labels.json # pinned / renamed sessions
|
|
196
|
+
├── server.log # detached-server stdout/stderr
|
|
197
|
+
├── .first-run-shown # marker so launcher only prints PWA hint once
|
|
198
|
+
└── browser-profile/ # Edge/Chrome --user-data-dir when browserMode=app
|
|
199
|
+
|
|
200
|
+
%LOCALAPPDATA%/ccsm/
|
|
201
|
+
└── launcher.vbs # silent ccsm:// dispatcher (written by postinstall)
|
|
202
|
+
|
|
203
|
+
HKCU\Software\Classes\ccsm # URL protocol registration
|
|
72
204
|
```
|
|
73
205
|
|
|
74
|
-
|
|
206
|
+
On first run, if a legacy `<repo>/data/` directory exists and `~/.ccsm/`
|
|
207
|
+
is empty, `lib/config.js` copies the old data over (one-time,
|
|
208
|
+
idempotent).
|
|
209
|
+
|
|
210
|
+
## Locked-in design decisions
|
|
211
|
+
|
|
212
|
+
**Workspace = folder holding multiple repo clones.** Each `ws-N` under
|
|
213
|
+
`workDir` contains a subdirectory per cloned repo. Claude launches at
|
|
214
|
+
the workspace root so all selected repos are sibling folders. "One repo
|
|
215
|
+
per workspace" was explicitly rejected.
|
|
216
|
+
|
|
217
|
+
**wt: one window per session, not stacked tabs.** Both
|
|
218
|
+
`/api/snapshot/restore` and `/api/sessions/new` open a fresh `wt`
|
|
219
|
+
window. "Stacked tabs via `-w 0 nt`" was explicitly rejected.
|
|
220
|
+
|
|
221
|
+
**"In use" detection.** A workspace is in use iff any live Claude
|
|
222
|
+
session's cwd is at-or-inside the workspace path (case-insensitive
|
|
223
|
+
Windows compare via `path.resolve().toLowerCase()`).
|
|
75
224
|
|
|
76
|
-
**
|
|
225
|
+
**Workspace naming.** Auto-allocated names are `ws-1`, `ws-2`, …
|
|
226
|
+
(lowest free integer). Hand-named folders under `workDir` are still
|
|
227
|
+
picked up.
|
|
77
228
|
|
|
78
|
-
**
|
|
229
|
+
**Frontend trusts the backend's capability advertisement.**
|
|
230
|
+
`/api/capabilities` returns `{ webTerminal: true|false, ... }`. The
|
|
231
|
+
frontend uses ONLY features the backend says it has. Future breaking
|
|
232
|
+
changes ship a fresh `/v2/` frontend path; old `/v1/` still works
|
|
233
|
+
against backends that advertise the v1 contract.
|
|
79
234
|
|
|
80
|
-
**
|
|
235
|
+
**One source of truth for cross-origin.** `public/js/backend.js`
|
|
236
|
+
exports `httpBase()` and `wsBase()`. Localhost → same-origin (empty
|
|
237
|
+
base). Anything else → `http://localhost:7777`. CORS on the backend
|
|
238
|
+
allows `https://bakapiano.github.io` only — never `*`.
|
|
81
239
|
|
|
82
240
|
## API surface
|
|
83
241
|
|
|
84
242
|
| Method | Path | Purpose |
|
|
85
243
|
|---|---|---|
|
|
86
244
|
| GET | `/api/sessions` | live sessions sorted by `updatedAt` desc |
|
|
87
|
-
| GET
|
|
245
|
+
| GET | `/api/sessions/recent` | recently-used sessions from jsonl mtimes · `?limit=&offset=` |
|
|
246
|
+
| POST | `/api/sessions/new` | body `{repos, workspace?, terminal: 'wt'\|'web'}` — NDJSON stream |
|
|
247
|
+
| POST | `/api/sessions/finder` | opens a wt with `claude` in `~/.ccsm` and `finderPrompt` |
|
|
248
|
+
| POST | `/api/sessions/:id/resume` | body `{cwd}` — `wt -d <cwd> claude --resume <id>` |
|
|
249
|
+
| POST | `/api/sessions/:id/focus` | match a wt window by title, fall back to PID-parent walk |
|
|
250
|
+
| GET | `/api/sessions/web` | list active web-terminal PTY sessions |
|
|
251
|
+
| DELETE | `/api/sessions/web/:id` | kill a web-terminal PTY |
|
|
252
|
+
| GET / PUT | `/api/config` | read / replace config |
|
|
88
253
|
| GET / POST | `/api/snapshot` | latest snapshot / force-save now |
|
|
89
254
|
| GET | `/api/snapshot/history` | rotated history filenames |
|
|
90
|
-
| POST | `/api/snapshot/restore` | launch one wt window per session in
|
|
255
|
+
| POST | `/api/snapshot/restore` | launch one wt window per session in snapshot |
|
|
91
256
|
| GET | `/api/workspaces` | workspaces under workDir with repo clone status + in-use flag |
|
|
92
|
-
|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
| GET | `/api/
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
| POST | `/api/
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
| GET | `/api/
|
|
102
|
-
|
|
103
|
-
`/api/sessions/new` streams **NDJSON** (one JSON object per line
|
|
257
|
+
| GET | `/api/favorites` · POST/DELETE `/api/favorites/:id` | star / unstar |
|
|
258
|
+
| GET | `/api/labels` · PUT/DELETE `/api/labels/:id` | rename / clear override |
|
|
259
|
+
| GET | `/api/terminals` | enumerate built-in terminal kinds |
|
|
260
|
+
| GET | `/api/capabilities` | `{ webTerminal: bool, ... }` for frontend feature gating |
|
|
261
|
+
| GET | `/api/health` | sanity ping + version |
|
|
262
|
+
| POST | `/api/heartbeat` | called every 10s by the frontend; feeds lifecycle decisions |
|
|
263
|
+
| POST | `/api/spawn-browser` | open another Edge window into the running server |
|
|
264
|
+
| POST | `/api/shutdown` | gracefulShutdown — used by uninstall + auto-upgrade |
|
|
265
|
+
| WS | `/ws/terminal/:id` | xterm.js bridge to a PTY in the webTerminal pool |
|
|
266
|
+
| GET (dev) | `/api/dev/ping` · `/api/dev/reload` | hot-reload SSE (only when running from a checkout) |
|
|
267
|
+
|
|
268
|
+
`/api/sessions/new` streams **NDJSON** (one JSON object per line). Event
|
|
269
|
+
types: `workspace`, `clone-start`, `clone-progress` (phase/percent/
|
|
270
|
+
current/total/detail), `clone-line` (raw git stderr line when not a
|
|
271
|
+
progress line), `clone-end`, `launched`, `done`. The frontend reads it
|
|
272
|
+
with `fetch().body.getReader()` + `TextDecoder` and updates per-repo
|
|
273
|
+
progress bars live.
|
|
274
|
+
|
|
275
|
+
**WebSocket Origin check**: same allow-list as CORS. The upgrade handler
|
|
276
|
+
rejects any Origin not in `ALLOWED_ORIGINS` (plus localhost/127.0.0.1).
|
|
277
|
+
Browsers always send Origin on WS upgrades.
|
|
104
278
|
|
|
105
279
|
## Non-obvious gotchas
|
|
106
280
|
|
|
107
|
-
**wt.exe `-d` flag
|
|
281
|
+
**wt.exe `-d` flag** (marker-file probes confirming `%CD%` inside the new tab):
|
|
108
282
|
|
|
109
283
|
| variant | result |
|
|
110
284
|
|---|---|
|
|
@@ -116,79 +290,159 @@ The alternative ("one repo per workspace") was explicitly rejected — don't ref
|
|
|
116
290
|
| `wt -d "D:\ccsm" <cmd>` (literal quotes) | ✗ wt doesn't open |
|
|
117
291
|
| `wt new-tab -d D:\ccsm <cmd>` | ✗ wt doesn't open |
|
|
118
292
|
|
|
119
|
-
ccsm uses variant 1 and always `path.resolve()`s the cwd first — defends
|
|
293
|
+
ccsm uses variant 1 and always `path.resolve()`s the cwd first — defends
|
|
294
|
+
against a malformed `D:ccsm` being interpreted as "current dir on drive
|
|
295
|
+
D + ccsm".
|
|
120
296
|
|
|
121
297
|
**Don't test wt launching via `node -e "..."` inside `bash -c`.** Backslashes get eaten by shell quoting and the JS string ends up malformed. Write a `.js` file and `node file.js` instead.
|
|
122
298
|
|
|
123
|
-
**focusBySession — title-based wt window matching.** Walking up the
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
299
|
+
**focusBySession — title-based wt window matching.** Walking up the
|
|
300
|
+
claude.exe PID parent chain and taking `MainWindowHandle` of the wt
|
|
301
|
+
process *always* returns the same canonical window in modern multi-
|
|
302
|
+
window single-process wt — clicking different sessions all focused the
|
|
303
|
+
same window. `focusBySession` lists all visible wt windows
|
|
304
|
+
(`EnumWindows` filtered by process name), strips the leading wt status
|
|
305
|
+
glyph (`✳ `, `⠐ `, `⠠ ` …) and compares to the session's ai-title.
|
|
306
|
+
Falls back to title-substring, then cwd-basename, then the old PID-
|
|
307
|
+
parent walk when no unique match.
|
|
308
|
+
|
|
309
|
+
**Focus helper (`lib/focus.js`).** One `powershell.exe -EncodedCommand <base64>` invocation per call, dispatched by `CCSM_FOCUS_MODE` env var. Encoded as UTF-16-LE-base64. Three baked-in gotchas:
|
|
310
|
+
1. C# `out _` discard breaks PowerShell 5.1's C# compiler — use a named `uint dummy`.
|
|
311
|
+
2. Windows blocks background processes from `SetForegroundWindow` — synthesize an Alt-key down/up via `keybd_event 0x12` first ("Alt-key trick").
|
|
312
|
+
3. `ConvertTo-Json -AsArray` is PS 7+; on PS 5.1 build the JSON array manually.
|
|
313
|
+
|
|
314
|
+
**HWND-diff for auto-focus, not PID-diff.** Modern Windows Terminal is multi-window single-process: one `WindowsTerminal.exe` PID owns 8+ top-level HWNDs.
|
|
315
|
+
|
|
316
|
+
**wt `-w new`.** wt's `windowingBehavior` setting in some profiles folds new `wt …` invocations into the existing window as a tab. Force-prepending `-w new` makes wt always create a new window.
|
|
317
|
+
|
|
318
|
+
**Auto-snapshot loop.** `setInterval` in `server.js` calls
|
|
319
|
+
`saveSnapshot` every `snapshotIntervalMs`. The history dir grows until
|
|
320
|
+
`snapshotHistoryKeep` is exceeded, then oldest are pruned. AND
|
|
321
|
+
`gracefulShutdown` always saves one last snapshot before exit so a
|
|
322
|
+
restart can restore current state.
|
|
323
|
+
|
|
324
|
+
**Session listing.** `~/.claude/sessions/<pid>.json` is the source of
|
|
325
|
+
truth. We cross-check the `pid` field against
|
|
326
|
+
`tasklist /FI "IMAGENAME eq claude.exe"` so stale entries from crashed
|
|
327
|
+
claudes don't appear. `ai-title` is read by tailing the last 1 MB of
|
|
328
|
+
the matching `.jsonl` and finding the last `"type":"ai-title"` line.
|
|
329
|
+
|
|
330
|
+
**`projectSlugForCwd`.** `cwd.replace(/[:\\]/g, '-')`, e.g. `D:\ccsm` → `D--ccsm`.
|
|
331
|
+
|
|
332
|
+
**ccsm:// silent dispatch.** Direct registration of `ccsm.cmd` as the
|
|
333
|
+
protocol handler causes a brief console window flash (cmd hosts the
|
|
334
|
+
.cmd file). The wscript.exe + .vbs wrapper avoids it entirely — wscript
|
|
335
|
+
is a Windows-subsystem host (no console) and `Shell.Run(..., 0, False)`
|
|
336
|
+
launches the target hidden. The `.vbs` is generated at install time
|
|
337
|
+
with the correct ccsm.cmd path baked in.
|
|
338
|
+
|
|
339
|
+
**Edge --app handoff race.** When the user has an existing Edge profile
|
|
340
|
+
process running, `--app=URL --user-data-dir=DIR` against the same DIR
|
|
341
|
+
may cause the new msedge.exe to immediately exit after handing the URL
|
|
342
|
+
off to the existing process. Our child handle dies milliseconds after
|
|
343
|
+
spawn. The lifecycle hook ignores any browser-child exit inside the
|
|
344
|
+
first 5s for exactly this reason.
|
|
139
345
|
|
|
140
346
|
## Frontend design language
|
|
141
347
|
|
|
142
|
-
The UI deliberately copies **claude.ai's** calm light aesthetic — warm
|
|
348
|
+
The UI deliberately copies **claude.ai's** calm light aesthetic — warm
|
|
349
|
+
cream surfaces, generous spacing, soft borders, **no orange highlights**.
|
|
350
|
+
The brand orange `#b3614a` survives only in the brand mark / wordmark
|
|
351
|
+
dot. Every other "highlight" use (selection, focus rings, dirty
|
|
352
|
+
indicators, progress bars, page-actions banner) is ink/gray.
|
|
143
353
|
|
|
144
|
-
**Palette** (CSS vars in `public/
|
|
354
|
+
**Palette** (CSS vars in `public/css/tokens.css`):
|
|
145
355
|
- `--bg` `#faf9f5` warm cream page background
|
|
146
356
|
- `--bg-elev` `#ffffff` card surfaces
|
|
147
|
-
- `--sidebar-bg` `#
|
|
148
|
-
- `--border` `#e8e3d5`
|
|
149
|
-
- `--ink` `#1a1815` body text (warm near-black)
|
|
150
|
-
- `--ink-mid`
|
|
151
|
-
- `--
|
|
152
|
-
-
|
|
153
|
-
-
|
|
357
|
+
- `--sidebar-bg` `#faf9f5` (same as `--bg`, single continuous surface)
|
|
358
|
+
- `--border` `#e8e3d5`
|
|
359
|
+
- `--ink` `#1a1815` body text (warm near-black, also used for terminal background)
|
|
360
|
+
- `--ink-mid` / `--ink-muted` / `--ink-faint`
|
|
361
|
+
- `--accent` `#b3614a` desaturated terracotta — brand only
|
|
362
|
+
- Status: green `#4a8a4a` idle · blue `#4a73a5` busy (pulsing) · red `#b73f3f` danger
|
|
363
|
+
- Favorite star: `#e3b341` (gold, the only intentional non-grayscale accent in the data area)
|
|
154
364
|
|
|
155
365
|
**Type**:
|
|
156
|
-
- Body / headings: **Geist** (Google Fonts, 300–700).
|
|
157
|
-
- Mono: **JetBrains Mono** for paths, PIDs, sessionIds, branch tags
|
|
366
|
+
- Body / headings: **Geist** (Google Fonts, 300–700).
|
|
367
|
+
- Mono: **JetBrains Mono** for paths, PIDs, sessionIds, branch tags.
|
|
158
368
|
- Always `font-variant-numeric: tabular-nums` on numeric cells.
|
|
159
369
|
|
|
370
|
+
**Buttons**:
|
|
371
|
+
- `.action` (default) — white bg, ink-mid border, ink text.
|
|
372
|
+
- `.action.primary` — black ink bg, white text. The "do this" CTA.
|
|
373
|
+
- `.action.subtle` — transparent bg, light border.
|
|
374
|
+
- `.action.danger` — filled red bg + white text (e.g. Remove repo).
|
|
375
|
+
|
|
160
376
|
**Layout**:
|
|
161
|
-
-
|
|
162
|
-
-
|
|
163
|
-
-
|
|
164
|
-
-
|
|
165
|
-
|
|
166
|
-
-
|
|
167
|
-
- Cards: `.card` (white, 10px radius, very soft `--shadow`), `.card-head` with title+meta, `.card-body` with optional `.card-body-flush` for tables.
|
|
168
|
-
- Tables: wrapped in `.table-scroll` (`overflow-x: auto`, min-width 760px) so narrow viewports scroll horizontally instead of cramping.
|
|
377
|
+
- Sidebar (collapsible, ~232px ↔ ~60px, state in `localStorage["ccsm.sidebar-collapsed"]`)
|
|
378
|
+
- brand mark + `CCSM.` wordmark
|
|
379
|
+
- 5 nav items: Sessions / Launch / Terminals / Configure / About (Terminals only shown when `capabilities.webTerminal === true`)
|
|
380
|
+
- footer: Collapse toggle (chevron flips on collapse)
|
|
381
|
+
- Page-head: title + subtitle on the left, server-status pill + Refresh button on the right
|
|
382
|
+
- Top-right control group uses fixed `min-height: 28px` and `border-radius: 999px` so server-status + Refresh align as a coherent control row
|
|
169
383
|
|
|
170
384
|
**Animation**:
|
|
171
|
-
-
|
|
385
|
+
- Row staggered fade-in on first render. Preact's component identity prevents re-stage on auto-refresh (no `markRendered` machinery needed in v0.7+).
|
|
172
386
|
- Panel switch: 0.35s `panel-in` fade-up.
|
|
173
|
-
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
387
|
+
- Busy status mark: blue pulse via `box-shadow` keyframes.
|
|
388
|
+
|
|
389
|
+
**No emoji in the UI** unless the user typed it (e.g. wt status glyphs
|
|
390
|
+
in session titles). Use inline SVG icons everywhere (line stroke, 1.5–
|
|
391
|
+
2px) so they take `currentColor`.
|
|
392
|
+
|
|
393
|
+
**PWA + WCO**:
|
|
394
|
+
- `display_override: ["window-controls-overlay", "standalone"]` in
|
|
395
|
+
manifest. When installed and launched as PWA, the title bar's
|
|
396
|
+
middle is reclaimed; only OS controls float top-right.
|
|
397
|
+
- `public/css/wco.css` provides drag regions (`-webkit-app-region:
|
|
398
|
+
drag` on `.sidebar-brand`, `.page-head`, etc., unconditional — Chromium
|
|
399
|
+
ignores in plain tabs, honors in PWA / `--app=`). Interactive
|
|
400
|
+
elements opt out via the no-drag block.
|
|
401
|
+
- Padding gets shuffled (`body.is-app .main / .sidebar` lose top
|
|
402
|
+
padding, children compensate) so the very top of the window is
|
|
403
|
+
draggable instead of being `.main`'s dead padding zone.
|
|
404
|
+
|
|
405
|
+
## Versioning
|
|
406
|
+
|
|
407
|
+
The hosted frontend lives at `/v1/`. Backend follows semver but treats
|
|
408
|
+
its API surface as additive:
|
|
409
|
+
- Patch: bug fixes, no API change.
|
|
410
|
+
- Minor: new endpoints, new fields on existing responses. Old frontend tolerates unknown extras.
|
|
411
|
+
- Major: breaking changes. New frontend at `/v2/`; old frontend at `/v1/` keeps working against backends that advertise the v1 contract via `/api/capabilities`.
|
|
412
|
+
|
|
413
|
+
`bin/ccsm.js` does auto-upgrade-restart: when the user runs `ccsm` and
|
|
414
|
+
the installed package version differs from a running backend, it POSTs
|
|
415
|
+
`/api/shutdown` to the old, waits for the port to free, then spawns a
|
|
416
|
+
fresh server. So `npm i -g @bakapiano/ccsm@latest && ccsm` is one
|
|
417
|
+
seamless step.
|
|
418
|
+
|
|
419
|
+
## Cross-platform
|
|
420
|
+
|
|
421
|
+
Today: Windows-first.
|
|
422
|
+
|
|
423
|
+
Cross-platform-clean already:
|
|
424
|
+
- Frontend (pure web)
|
|
425
|
+
- `bin/ccsm.js` (pure node)
|
|
426
|
+
- `lib/webTerminal.js` (node-pty handles platform)
|
|
427
|
+
- `lib/snapshot.js`, `lib/config.js`, `lib/jsonStore.js` (fs only)
|
|
428
|
+
- `server.js` Express + ws
|
|
429
|
+
|
|
430
|
+
Windows-specific (need ports for Mac/Linux):
|
|
431
|
+
- `scripts/install.js` — uses `reg.exe` and `wscript.exe`. Mac: write `Info.plist` with `CFBundleURLTypes`. Linux: write `~/.local/share/applications/ccsm.desktop` with `MimeType=x-scheme-handler/ccsm`.
|
|
432
|
+
- `lib/focus.js` — PowerShell + Win32. Mac: `osascript`. Linux: `wmctrl`.
|
|
433
|
+
- `lib/launcher.js` — wt/pwsh/cmd. Mac: Terminal.app via osascript. Linux: gnome-terminal etc.
|
|
434
|
+
- `lib/sessions.js` — `tasklist`. Mac/Linux: `ps -eo pid,comm`.
|
|
435
|
+
|
|
436
|
+
Pattern for adding a platform: `switch (process.platform)` at each
|
|
437
|
+
entry point in those files. Each platform branch is roughly 50-100
|
|
438
|
+
lines.
|
|
186
439
|
|
|
187
440
|
## Extending
|
|
188
441
|
|
|
189
442
|
When adding features, the natural extension points:
|
|
190
|
-
- New REST routes
|
|
191
|
-
- Frontend
|
|
192
|
-
- Persistent user data
|
|
193
|
-
- Workspace lifecycle (delete, rename): `lib/workspace.js`.
|
|
194
|
-
- Different launch modes
|
|
443
|
+
- **New REST routes**: `server.js` (keep under `/api/*`, use the `asyncH` wrapper, decide if it needs CORS by being in the allow-list).
|
|
444
|
+
- **Frontend page**: `public/js/pages/<Name>Page.js`, route in `App.js`, sidebar nav item in `Sidebar.js`, heading in `state.js`'s `TAB_HEADINGS`.
|
|
445
|
+
- **Persistent user data**: drop a JSON file under `~/.ccsm/` and use `lib/jsonStore.js`'s factory.
|
|
446
|
+
- **Workspace lifecycle** (delete, rename): `lib/workspace.js`.
|
|
447
|
+
- **Different launch modes**: `lib/launcher.js` — but check first whether the "one window per session" decision still holds.
|
|
448
|
+
- **A capability**: advertise via `/api/capabilities`. Frontend gates UI on `caps.<feature>`.
|