@bakapiano/ccsm 0.9.0 → 0.10.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 +222 -195
- package/README.md +77 -79
- package/lib/cliSessionWatcher.js +249 -0
- package/lib/config.js +101 -24
- package/lib/folders.js +96 -0
- package/lib/localCliSessions.js +177 -0
- package/lib/persistedSessions.js +134 -0
- package/lib/webTerminal.js +31 -18
- package/lib/workspace.js +26 -4
- package/package.json +1 -1
- package/public/assets/claude-color.svg +1 -0
- package/public/assets/codex-color.svg +1 -0
- package/public/assets/copilot-color.svg +1 -0
- package/public/css/base.css +22 -5
- package/public/css/cards.css +37 -3
- package/public/css/feedback.css +127 -43
- package/public/css/forms.css +97 -25
- package/public/css/layout.css +74 -26
- package/public/css/modal.css +40 -26
- package/public/css/responsive.css +2 -2
- package/public/css/sidebar.css +424 -25
- package/public/css/terminals.css +138 -0
- package/public/css/tokens.css +28 -12
- package/public/css/wco.css +38 -39
- package/public/css/widgets.css +1177 -6
- package/public/index.html +35 -2
- package/public/js/api.js +194 -37
- package/public/js/components/AdoptModal.js +171 -0
- package/public/js/components/App.js +1 -11
- package/public/js/components/DirectoryPicker.js +203 -0
- package/public/js/components/EntityFormModal.js +105 -0
- package/public/js/components/Modal.js +51 -0
- package/public/js/components/OfflineBanner.js +29 -23
- package/public/js/components/PageTitleBar.js +13 -0
- package/public/js/components/Picker.js +179 -0
- package/public/js/components/Popover.js +55 -0
- package/public/js/components/Sidebar.js +219 -32
- package/public/js/components/TerminalView.js +27 -3
- package/public/js/components/useDragSort.js +67 -0
- package/public/js/dialog.js +10 -2
- package/public/js/icons.js +66 -3
- package/public/js/main.js +54 -3
- package/public/js/pages/AboutPage.js +80 -0
- package/public/js/pages/ConfigurePage.js +429 -207
- package/public/js/pages/LaunchPage.js +326 -86
- package/public/js/pages/SessionsPage.js +91 -41
- package/public/js/state.js +102 -73
- package/public/manifest.webmanifest +2 -2
- package/server.js +755 -441
- package/lib/favorites.js +0 -51
- package/lib/focus.js +0 -369
- package/lib/labels.js +0 -29
- package/lib/launcher.js +0 -219
- package/lib/sessions.js +0 -272
- package/lib/snapshot.js +0 -141
- package/public/js/actions.js +0 -107
- package/public/js/components/Fab.js +0 -11
- package/public/js/components/FavoritesTable.js +0 -81
- package/public/js/components/Footer.js +0 -12
- package/public/js/components/NewSessionModal.js +0 -153
- package/public/js/components/PageHead.js +0 -33
- package/public/js/components/Pagination.js +0 -27
- package/public/js/components/RecentTable.js +0 -68
- package/public/js/components/SessionsTable.js +0 -71
- package/public/js/components/SnapshotPanel.js +0 -77
- package/public/js/components/TitleCell.js +0 -40
- package/public/js/components/WorkspacesGrid.js +0 -41
- package/public/js/pages/TerminalsPage.js +0 -74
package/CLAUDE.md
CHANGED
|
@@ -1,27 +1,29 @@
|
|
|
1
1
|
# ccsm — Claude Code Session Manager
|
|
2
2
|
|
|
3
|
-
A small Node/Express + Preact web tool that
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A small Node/Express + Preact web tool that runs every Claude/Codex/
|
|
4
|
+
Copilot CLI session inside a single web app. PTYs live in-process
|
|
5
|
+
(node-pty), sessions persist across restarts, and `--resume <uuid>`
|
|
6
|
+
reattaches to the exact upstream conversation.
|
|
7
7
|
|
|
8
8
|
## Why this exists
|
|
9
9
|
|
|
10
10
|
When you're running 8–10 concurrent `claude` sessions across ad-hoc
|
|
11
11
|
clones (`D:\proj`, `D:\proj2`, `…`, plus GUID worktree dirs), it's easy
|
|
12
12
|
to lose track of which terminal is which session. ccsm gives an
|
|
13
|
-
at-a-glance
|
|
13
|
+
at-a-glance sidebar, organises sessions into folders, and `--resume`s
|
|
14
|
+
each one in the same xterm.js panel.
|
|
14
15
|
|
|
15
16
|
## Architecture: hosted frontend + local backend
|
|
16
17
|
|
|
17
|
-
The
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
via the workflow at `.github/workflows/deploy-pages.yml`.
|
|
18
|
+
The frontend is **not** bundled in the npm package — it's hosted on
|
|
19
|
+
GitHub Pages and matched to your installed backend version through a
|
|
20
|
+
version router.
|
|
21
21
|
|
|
22
22
|
```
|
|
23
23
|
┌── browser ────────────────────────────┐
|
|
24
|
-
│ https://bakapiano.github.io/ccsm/
|
|
24
|
+
│ https://bakapiano.github.io/ccsm/ ← version router (tiny)
|
|
25
|
+
│ ↓
|
|
26
|
+
│ https://bakapiano.github.io/ccsm/X.Y.Z/ ← per-version frontend
|
|
25
27
|
└────────────┬──────────────────────────┘
|
|
26
28
|
│ fetch /api/* (CORS, allow-list)
|
|
27
29
|
│ ws://localhost:7777/ws/*
|
|
@@ -29,23 +31,37 @@ via the workflow at `.github/workflows/deploy-pages.yml`.
|
|
|
29
31
|
┌── local backend ──────────────────────┐
|
|
30
32
|
│ npm i -g @bakapiano/ccsm │
|
|
31
33
|
│ ccsm │
|
|
32
|
-
│ ├── /api/sessions /api/
|
|
33
|
-
│ ├── /api/sessions/
|
|
34
|
-
│ ├── /
|
|
35
|
-
│ ├── /
|
|
36
|
-
│
|
|
34
|
+
│ ├── /api/sessions /api/sessions/new │
|
|
35
|
+
│ ├── /api/sessions/:id/resume │
|
|
36
|
+
│ ├── /api/sessions/adopt │
|
|
37
|
+
│ ├── /ws/terminal/:id (PTY) │
|
|
38
|
+
│ ├── /api/version /api/upgrade │
|
|
39
|
+
│ ├── /api/heartbeat /api/health │
|
|
40
|
+
│ └── /api/shutdown │
|
|
37
41
|
└───────────────────────────────────────┘
|
|
38
42
|
```
|
|
39
43
|
|
|
40
|
-
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
**Version routing.** GH Pages root (`/ccsm/`) hosts a tiny static
|
|
45
|
+
router (`pages-root/index.html`) that probes `localhost:7777/api/health`
|
|
46
|
+
and redirects to `./<backend.version>/`. Each release publishes a fresh
|
|
47
|
+
`/ccsm/<X.Y.Z>/` subdir; old ones stay forever via the workflow's
|
|
48
|
+
`keep_files: true`. Result: a 1:1 frontend↔backend version pin, no
|
|
49
|
+
semver-compat logic, and old backends keep working indefinitely.
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
Each per-version frontend has its version baked into a `<meta
|
|
52
|
+
name="ccsm-frontend-version">` at deploy time (injected by the GH Pages
|
|
53
|
+
workflow). On boot it re-fetches `/api/health` and bounces back through
|
|
54
|
+
the router via `location.replace('../')` if the backend has since been
|
|
55
|
+
upgraded.
|
|
56
|
+
|
|
57
|
+
When the backend is offline the router itself shows a "Start ccsm" UI
|
|
58
|
+
with a `ccsm://start` link (same protocol-handler trick we already
|
|
59
|
+
register at install time). No need to redirect to a stale version.
|
|
60
|
+
|
|
61
|
+
**Dev mode.** When running from a checkout (`__dirname` not under
|
|
47
62
|
`node_modules`), the backend ALSO serves `public/` so contributors can
|
|
48
|
-
iterate at `localhost:7777/` without pushing.
|
|
63
|
+
iterate at `localhost:7777/` without pushing. In dev there's no
|
|
64
|
+
`<meta>` tag → the version guard no-ops.
|
|
49
65
|
|
|
50
66
|
## Run
|
|
51
67
|
|
|
@@ -57,26 +73,24 @@ npm install -g @bakapiano/ccsm
|
|
|
57
73
|
ccsm
|
|
58
74
|
```
|
|
59
75
|
|
|
60
|
-
`ccsm` opens the
|
|
76
|
+
`ccsm` opens the version router in a chromeless Edge `--app=` window.
|
|
61
77
|
Terminal returns immediately (the server is spawned detached). Close
|
|
62
|
-
the window → server saves a final snapshot and exits within
|
|
78
|
+
the window → server saves a final snapshot of state and exits within
|
|
79
|
+
~12s.
|
|
63
80
|
|
|
64
81
|
If you don't want the auto-opened window (e.g. you live in the PWA),
|
|
65
|
-
just visit `https://bakapiano.github.io/ccsm
|
|
66
|
-
down you see
|
|
82
|
+
just visit `https://bakapiano.github.io/ccsm/` — when backend is
|
|
83
|
+
down you see the inline OfflineBanner with a **Start ccsm** button.
|
|
67
84
|
|
|
68
85
|
Default port `7777`, default workDir `~/ccsm-workspaces`. Config +
|
|
69
|
-
|
|
70
|
-
settings editable through the Configure
|
|
86
|
+
state live at `~/.ccsm/` (override with `CCSM_HOME=<path>`). All
|
|
87
|
+
settings editable through the Configure page
|
|
71
88
|
(`~/.ccsm/config.json` on disk). Notable knobs:
|
|
72
89
|
|
|
73
|
-
- `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port.
|
|
90
|
+
- `port` (default `7777`) — preferred listen port. If taken, ccsm tries `+1..+9` then asks the OS for any free port.
|
|
74
91
|
- `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
|
-
- `
|
|
76
|
-
- `
|
|
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.
|
|
92
|
+
- `clis` — array of CLI definitions. Built-ins for `claude`, `codex`, `copilot`; users can add `other` CLIs with custom `command`, `args`, `resumeArgs`, `resumeIdArgs`, `shell` (direct/pwsh/cmd).
|
|
93
|
+
- `defaultCliId` — which CLI the Launch page pre-selects.
|
|
80
94
|
|
|
81
95
|
## ccsm:// protocol · "wake on click"
|
|
82
96
|
|
|
@@ -93,39 +107,49 @@ spawned `ccsm.cmd` runs **completely hidden**. No console flash. The
|
|
|
93
107
|
`.cmd` goes through `bin/ccsm.js`, which detects `ccsm://start` in argv,
|
|
94
108
|
spawns `server.js` detached with `CCSM_NO_BROWSER=1`, and exits.
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
110
|
+
The router's "Start ccsm" button (and OfflineBanner inside each
|
|
111
|
+
per-version frontend) is just `<a href="ccsm://start">`. First click
|
|
112
|
+
triggers a one-time Windows confirmation dialog ("Open ccsm.cmd?");
|
|
113
|
+
ticking "Always allow" makes it silent thereafter.
|
|
114
|
+
|
|
115
|
+
postinstall (`scripts/install.js`) registers the protocol
|
|
116
|
+
unconditionally on Windows — including npx-cache installs. The path
|
|
117
|
+
stored in the registry points at whatever `ccsm.cmd` location npm gave
|
|
118
|
+
us (`<prefix>/ccsm.cmd` from `npm config get prefix`).
|
|
119
|
+
|
|
120
|
+
## In-app upgrade
|
|
99
121
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
122
|
+
About page surfaces the installed version, polls
|
|
123
|
+
`registry.npmjs.org/@bakapiano%2Fccsm/latest` (cached 30 min) for the
|
|
124
|
+
latest published version, and offers an **Upgrade** button when newer.
|
|
125
|
+
`POST /api/upgrade` spawns `npm i -g @bakapiano/ccsm@latest` detached,
|
|
126
|
+
then on success spawns a fresh `ccsm` (also detached) and
|
|
127
|
+
gracefulShutdowns. The OfflineBanner appears briefly; the router then
|
|
128
|
+
picks up the new version on its next probe.
|
|
129
|
+
|
|
130
|
+
The `target` field is regex-validated (`/^[a-z0-9.+\-^~]+$/i`) before
|
|
131
|
+
the spawn — npm install doesn't shell out, but defends against argv
|
|
132
|
+
weirdness regardless. Concurrent calls return `409`.
|
|
104
133
|
|
|
105
134
|
## Lifecycle
|
|
106
135
|
|
|
107
136
|
Single `gracefulShutdown(reason)` function in `server.js` is the only
|
|
108
|
-
exit path. It
|
|
109
|
-
|
|
137
|
+
exit path. It kills any PTY children, then `process.exit(0)`. Every
|
|
138
|
+
trigger funnels here:
|
|
110
139
|
|
|
111
140
|
| trigger | path |
|
|
112
141
|
|---|---|
|
|
113
142
|
| auto-spawned browser window closes | `child.on('exit')` — see smart-kill below |
|
|
114
143
|
| `POST /api/shutdown` | from npm uninstall, from launcher's auto-upgrade |
|
|
144
|
+
| `POST /api/upgrade` after install completes | self-restart |
|
|
115
145
|
| SIGINT / SIGTERM | OS signals |
|
|
116
146
|
| heartbeat watchdog timeout | 90s with no heartbeat, only when launched via `bin/ccsm.js` |
|
|
117
147
|
|
|
118
148
|
**Smart browser-exit**: when the spawned browser child dies, we don't
|
|
119
149
|
kill immediately. Two filters:
|
|
120
150
|
|
|
121
|
-
1. **Fast-exit (<5s)** — Edge `--app=` often hands the URL off to an
|
|
122
|
-
|
|
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.
|
|
151
|
+
1. **Fast-exit (<5s)** — Edge `--app=` often hands the URL off to an existing Edge profile process group and the spawned child dies milliseconds after creation. We ignore any exit inside the first 5s.
|
|
152
|
+
2. **Deferred multi-client check (12s)** — after a real close, wait 12s and check if any heartbeat arrived AFTER the close timestamp. If yes, a hosted-frontend tab (or another window) is keeping us busy, stay alive. If no, gracefulShutdown.
|
|
129
153
|
|
|
130
154
|
Frontend heartbeat cadence is 10s (in `main.js`), so one full cycle
|
|
131
155
|
fits inside the 12s decision window.
|
|
@@ -133,9 +157,52 @@ fits inside the 12s decision window.
|
|
|
133
157
|
Environment overrides:
|
|
134
158
|
- `CCSM_KEEP_ALIVE=1` → disable both browser-exit hook and heartbeat watchdog. For automation hosts.
|
|
135
159
|
- `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.
|
|
160
|
+
- `CCSM_NO_BROWSER=1` → set by the launcher when handling a `ccsm://` click or by `/api/upgrade` self-respawn; suppresses the server's auto-open browser.
|
|
137
161
|
- `CCSM_NO_DEV=1` → suppress dev-mode features (static serving, hot-reload SSE) even when running from a checkout.
|
|
138
162
|
|
|
163
|
+
## Sessions: persisted, adopted, resumed
|
|
164
|
+
|
|
165
|
+
There's **one source of truth**: `~/.ccsm/sessions.json`, managed by
|
|
166
|
+
`lib/persistedSessions.js`. Every session ccsm starts goes in there
|
|
167
|
+
with `{ id, cliId, cwd, workspace, title, folderId, repos,
|
|
168
|
+
cliSessionId, status, … }`. We don't enumerate `~/.claude/` or walk
|
|
169
|
+
process trees anymore.
|
|
170
|
+
|
|
171
|
+
**`cliSessionId` capture.** For `claude` / `codex` / `copilot`, on
|
|
172
|
+
spawn we kick off `lib/cliSessionWatcher.js`. It polls the CLI's
|
|
173
|
+
transcript directory (`~/.claude/projects/<slug>/`,
|
|
174
|
+
`~/.codex/sessions/`, `~/.copilot/session-state/`) for a new file whose
|
|
175
|
+
recorded cwd matches our spawn cwd, then stores its UUID into the
|
|
176
|
+
record. Later, **resume** rewrites the args using the CLI's
|
|
177
|
+
`resumeIdArgs` template (`['--resume', '<id>']`) so the upstream
|
|
178
|
+
conversation reattaches precisely. Fallback when no UUID is captured:
|
|
179
|
+
`cli.resumeArgs` (`--continue` / `resume --last`).
|
|
180
|
+
|
|
181
|
+
Watcher lifecycle: 5-minute timeout. If no transcript ever appears,
|
|
182
|
+
the ccsm record is removed (we assume the user closed the CLI before
|
|
183
|
+
it persisted anything). The cleanup fn returned by `captureSessionId`
|
|
184
|
+
is tracked in a module-scope `Map` keyed by ccsm session id, and torn
|
|
185
|
+
down on PTY exit and on session delete — otherwise a still-running
|
|
186
|
+
watcher could match a future session's transcript and stamp the wrong
|
|
187
|
+
UUID onto a dead record.
|
|
188
|
+
|
|
189
|
+
If the record already has a `cliSessionId` (typical for resume and
|
|
190
|
+
**always** for `adopt`-imported sessions), we skip the watcher
|
|
191
|
+
entirely — there's nothing left to capture, and the timeout would
|
|
192
|
+
wipe the record after 5 min.
|
|
193
|
+
|
|
194
|
+
**Adopt.** "Import existing session" on the Launch page lists
|
|
195
|
+
sessions found on disk (`/api/cli-sessions/:type`) and lets the user
|
|
196
|
+
add one to ccsm with `/api/sessions/adopt`. The created record is
|
|
197
|
+
born `status: 'exited'` with `cliSessionId` pre-set. Clicking it in
|
|
198
|
+
the sidebar runs the normal resume flow — which uses the captured id.
|
|
199
|
+
|
|
200
|
+
**Auto-resume.** SessionsPage doesn't show a "Resume" button. On
|
|
201
|
+
mount, if the active session's status isn't `running`, it calls
|
|
202
|
+
`resumeSession()`. `resumeSession()` in `api.js` keeps a per-id
|
|
203
|
+
in-flight Map so the same call from Sidebar.onClick and the
|
|
204
|
+
SessionsPage effect collapse into a single backend hit.
|
|
205
|
+
|
|
139
206
|
## Layout
|
|
140
207
|
|
|
141
208
|
```
|
|
@@ -147,52 +214,49 @@ ccsm/
|
|
|
147
214
|
│ ├── install.js # postinstall · ccsm:// + launcher.vbs
|
|
148
215
|
│ └── uninstall.js # preuninstall · cleanup + /api/shutdown
|
|
149
216
|
├── lib/
|
|
150
|
-
│ ├──
|
|
151
|
-
│ ├──
|
|
152
|
-
│ ├──
|
|
153
|
-
│ ├──
|
|
154
|
-
│ ├──
|
|
155
|
-
│ ├── webTerminal.js # in-process PTY pool · node-pty + WebSocket
|
|
156
|
-
│ ├── favorites.js · labels.js # pinned sessions / rename overrides
|
|
217
|
+
│ ├── persistedSessions.js # ~/.ccsm/sessions.json — source of truth
|
|
218
|
+
│ ├── folders.js # ~/.ccsm/folders.json — sidebar tree
|
|
219
|
+
│ ├── localCliSessions.js # scan ~/.claude · ~/.codex · ~/.copilot
|
|
220
|
+
│ ├── cliSessionWatcher.js # capture upstream session UUID after spawn
|
|
221
|
+
│ ├── workspace.js # ws-N allocation under workDir, repo clones
|
|
222
|
+
│ ├── webTerminal.js # in-process PTY pool · node-pty + WebSocket
|
|
157
223
|
│ ├── jsonStore.js # shared keyed-JSON store factory
|
|
158
|
-
│ └── config.js # loadConfig / saveConfig
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
224
|
+
│ └── config.js # loadConfig / saveConfig + DATA_DIR
|
|
225
|
+
├── pages-root/ # pushed to GH Pages /
|
|
226
|
+
│ ├── index.html # version router · probe localhost, redirect
|
|
227
|
+
│ ├── manifest.webmanifest # PWA · stable id, start_url: ./
|
|
228
|
+
│ └── favicon.svg
|
|
229
|
+
└── public/ # pushed to GH Pages /<version>/
|
|
230
|
+
├── index.html # workflow injects <meta ccsm-frontend-version>
|
|
231
|
+
├── manifest.webmanifest # per-version (links back to root scope)
|
|
162
232
|
├── favicon.svg
|
|
163
233
|
├── js/
|
|
164
234
|
│ ├── backend.js # httpBase() / wsBase() — same-origin local, cross-origin hosted
|
|
165
|
-
│ ├── main.js # boot ·
|
|
235
|
+
│ ├── main.js # boot · version guard · clock · heartbeat
|
|
166
236
|
│ ├── state.js # signals
|
|
167
|
-
│ ├── api.js # fetch wrapper + loaders
|
|
237
|
+
│ ├── api.js # fetch wrapper + loaders + dedup-aware resumeSession
|
|
168
238
|
│ ├── streaming.js # NDJSON clone-progress stream
|
|
169
|
-
│ ├── actions.js # focus / resume / favorite / rename
|
|
170
239
|
│ ├── dialog.js · toast.js # ccsmConfirm / ccsmPrompt / setToast
|
|
171
240
|
│ ├── html.js · icons.js · util.js
|
|
172
241
|
│ ├── components/
|
|
173
|
-
│ │ ├── App.js · Sidebar.js ·
|
|
174
|
-
│ │ ├── ServerStatus.js · Toast.js ·
|
|
175
|
-
│ │ ├── Card.js ·
|
|
176
|
-
│ │ ├──
|
|
177
|
-
│ │ ├──
|
|
178
|
-
│ │ ├── WorkspacesGrid.js · SnapshotPanel.js · ProgressList.js
|
|
179
|
-
│ │ ├── TerminalView.js · NewSessionModal.js
|
|
180
|
-
│ │ └── DialogHost.js
|
|
242
|
+
│ │ ├── App.js · Sidebar.js · PageTitleBar.js
|
|
243
|
+
│ │ ├── ServerStatus.js · Toast.js · OfflineBanner.js · DialogHost.js
|
|
244
|
+
│ │ ├── Card.js · Modal.js · Popover.js · Picker.js · EntityFormModal.js
|
|
245
|
+
│ │ ├── DirectoryPicker.js · AdoptModal.js
|
|
246
|
+
│ │ ├── ProgressList.js · TerminalView.js · useDragSort.js
|
|
181
247
|
│ └── pages/
|
|
182
248
|
│ ├── SessionsPage.js · LaunchPage.js
|
|
183
|
-
│ ├── TerminalsPage.js
|
|
184
249
|
│ ├── ConfigurePage.js · AboutPage.js
|
|
185
|
-
└── css/ #
|
|
250
|
+
└── css/ # 12 focused stylesheets
|
|
186
251
|
├── tokens.css · base.css · layout.css
|
|
187
|
-
├── sidebar.css · cards.css ·
|
|
252
|
+
├── sidebar.css · cards.css · forms.css
|
|
188
253
|
├── widgets.css · feedback.css · modal.css
|
|
189
254
|
├── terminals.css · wco.css · responsive.css
|
|
190
255
|
|
|
191
256
|
~/.ccsm/ # or $CCSM_HOME
|
|
192
257
|
├── config.json # source of truth
|
|
193
|
-
├──
|
|
194
|
-
├──
|
|
195
|
-
├── favorites.json · labels.json # pinned / renamed sessions
|
|
258
|
+
├── sessions.json # persisted sessions (id, cliSessionId, …)
|
|
259
|
+
├── folders.json # folder tree
|
|
196
260
|
├── server.log # detached-server stdout/stderr
|
|
197
261
|
├── .first-run-shown # marker so launcher only prints PWA hint once
|
|
198
262
|
└── browser-profile/ # Edge/Chrome --user-data-dir when browserMode=app
|
|
@@ -209,18 +273,15 @@ idempotent).
|
|
|
209
273
|
|
|
210
274
|
## Locked-in design decisions
|
|
211
275
|
|
|
212
|
-
**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
276
|
+
**Single in-app terminal, no `wt`.** PTYs run in-process via node-pty
|
|
277
|
+
and stream to xterm.js over `/ws/terminal/:id`. We dropped the
|
|
278
|
+
`wt`-per-session, focus-by-HWND, snapshot-of-live-claudes layer
|
|
279
|
+
entirely — too platform-specific and the web terminal handles
|
|
280
|
+
everything the old path did.
|
|
216
281
|
|
|
217
|
-
**
|
|
218
|
-
|
|
219
|
-
|
|
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()`).
|
|
282
|
+
**Workspace = folder holding multiple repo clones.** Each `ws-N` under
|
|
283
|
+
`workDir` contains a subdirectory per cloned repo. CLIs launch at the
|
|
284
|
+
workspace root so all selected repos are sibling folders.
|
|
224
285
|
|
|
225
286
|
**Workspace naming.** Auto-allocated names are `ws-1`, `ws-2`, …
|
|
226
287
|
(lowest free integer). Hand-named folders under `workDir` are still
|
|
@@ -228,9 +289,9 @@ picked up.
|
|
|
228
289
|
|
|
229
290
|
**Frontend trusts the backend's capability advertisement.**
|
|
230
291
|
`/api/capabilities` returns `{ webTerminal: true|false, ... }`. The
|
|
231
|
-
frontend uses ONLY features the backend says it has.
|
|
232
|
-
|
|
233
|
-
|
|
292
|
+
frontend uses ONLY features the backend says it has. Breaking changes
|
|
293
|
+
ship a new `/ccsm/<X.Y.Z>/` frontend; the router pins users to the
|
|
294
|
+
matching version.
|
|
234
295
|
|
|
235
296
|
**One source of truth for cross-origin.** `public/js/backend.js`
|
|
236
297
|
exports `httpBase()` and `wsBase()`. Localhost → same-origin (empty
|
|
@@ -241,26 +302,23 @@ allows `https://bakapiano.github.io` only — never `*`.
|
|
|
241
302
|
|
|
242
303
|
| Method | Path | Purpose |
|
|
243
304
|
|---|---|---|
|
|
244
|
-
| GET | `/api/sessions` | live sessions sorted by `updatedAt` desc |
|
|
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
305
|
| GET / PUT | `/api/config` | read / replace config |
|
|
253
|
-
| GET
|
|
254
|
-
|
|
|
255
|
-
|
|
|
306
|
+
| GET | `/api/sessions` | list persisted sessions |
|
|
307
|
+
| PUT | `/api/sessions/:id` | rename / move to folder |
|
|
308
|
+
| DELETE | `/api/sessions/:id` | kill PTY + drop record + stop any watcher |
|
|
309
|
+
| POST | `/api/sessions/new` | body `{cliId, cwd?, repos?, folderId?, title?}` — NDJSON stream (workspace · clone-progress · launched) |
|
|
310
|
+
| POST | `/api/sessions/:id/resume` | re-spawn at `cwd` with `cli.resumeIdArgs <id>` (fallback `resumeArgs`) |
|
|
311
|
+
| GET | `/api/cli-sessions/:type` | scan disk for unimported `claude`/`codex`/`copilot` sessions |
|
|
312
|
+
| POST | `/api/sessions/adopt` | body `{cliId, cliSessionId, cwd, title?, folderId?}` — create a `status:exited` record with `cliSessionId` pre-set |
|
|
313
|
+
| GET | `/api/folders` · POST `/api/folders` · PUT/DELETE `/api/folders/:id` · POST `/api/folders/reorder` | folder CRUD |
|
|
256
314
|
| GET | `/api/workspaces` | workspaces under workDir with repo clone status + in-use flag |
|
|
257
|
-
| GET | `/api/
|
|
258
|
-
| GET | `/api/
|
|
259
|
-
|
|
|
315
|
+
| GET | `/api/browse` | directory browser for the Launch page workdir picker |
|
|
316
|
+
| GET | `/api/version` | `{ current, latest, updateAvailable, fetchedAt, cached, error? }` (npm registry cached 30 min, `?refresh=1` to bust) |
|
|
317
|
+
| POST | `/api/upgrade` | body `{target?}` — `npm i -g @bakapiano/ccsm@<target>` then self-restart |
|
|
260
318
|
| GET | `/api/capabilities` | `{ webTerminal: bool, ... }` for frontend feature gating |
|
|
261
|
-
| GET | `/api/health` |
|
|
319
|
+
| GET | `/api/health` | `{ ok, pid, version, name }` — used by router probe + heartbeat |
|
|
262
320
|
| POST | `/api/heartbeat` | called every 10s by the frontend; feeds lifecycle decisions |
|
|
263
|
-
| POST | `/api/spawn-browser` | open another
|
|
321
|
+
| POST | `/api/spawn-browser` | open another browser window into the running server (used by `bin/ccsm.js` for auto-upgrade-restart) |
|
|
264
322
|
| POST | `/api/shutdown` | gracefulShutdown — used by uninstall + auto-upgrade |
|
|
265
323
|
| WS | `/ws/terminal/:id` | xterm.js bridge to a PTY in the webTerminal pool |
|
|
266
324
|
| GET (dev) | `/api/dev/ping` · `/api/dev/reload` | hot-reload SSE (only when running from a checkout) |
|
|
@@ -278,56 +336,36 @@ Browsers always send Origin on WS upgrades.
|
|
|
278
336
|
|
|
279
337
|
## Non-obvious gotchas
|
|
280
338
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
same
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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`.
|
|
339
|
+
**`firstJsonField` close-event race.** `lib/cliSessionWatcher.js` reads
|
|
340
|
+
the first few lines of the upstream CLI's transcript to extract its
|
|
341
|
+
`cwd` field. `readline.close()` synchronously emits `'close'` on
|
|
342
|
+
Windows, which would re-enter `done(null)` and clobber a freshly
|
|
343
|
+
resolved value. Guarded by a `settled` flag.
|
|
344
|
+
|
|
345
|
+
**Watcher rejected entries are NOT memoised by mtime.** Earlier
|
|
346
|
+
implementations cached "this file was old at first poll, skip forever".
|
|
347
|
+
Wrong — `claude` *appends* to an existing transcript on `--resume`, so
|
|
348
|
+
the file's mtime later crosses `spawnAt` and the watcher must
|
|
349
|
+
re-evaluate. Only `cwd`-mismatch rejections are memoised (those are
|
|
350
|
+
stable per file).
|
|
351
|
+
|
|
352
|
+
**Adopt is atomic.** `persistedSessions.create()` accepts `status` +
|
|
353
|
+
`cliSessionId` so the adopt endpoint writes the record in a single
|
|
354
|
+
file write rather than `create({running})` + `update({exited,
|
|
355
|
+
cliSessionId})`. The two-write form had a window where a concurrent
|
|
356
|
+
GET /api/sessions could see `running` with no live PTY, fooling the
|
|
357
|
+
sidebar's "skip resume if running" guard.
|
|
358
|
+
|
|
359
|
+
**Auto-resume dedup is module-level in api.js.** Sidebar.onClick and
|
|
360
|
+
SessionsPage's effect can both fire for the same exited session in the
|
|
361
|
+
same tick. `resumeSession()` keeps a per-id in-flight `Map` so the
|
|
362
|
+
second caller awaits the first one's promise instead of issuing a
|
|
363
|
+
second `/resume`.
|
|
364
|
+
|
|
365
|
+
**Heartbeat watchdog only when launched.** Set via `CCSM_LAUNCHER=1` by
|
|
366
|
+
`bin/ccsm.js`. If you start `server.js` directly (e.g. dev), the
|
|
367
|
+
90-second timeout doesn't apply — convenient when stepping through
|
|
368
|
+
code, but you have to ctrl-c yourself when done.
|
|
331
369
|
|
|
332
370
|
**ccsm:// silent dispatch.** Direct registration of `ccsm.cmd` as the
|
|
333
371
|
protocol handler causes a brief console window flash (cmd hosts the
|
|
@@ -360,7 +398,6 @@ indicators, progress bars, page-actions banner) is ink/gray.
|
|
|
360
398
|
- `--ink-mid` / `--ink-muted` / `--ink-faint`
|
|
361
399
|
- `--accent` `#b3614a` desaturated terracotta — brand only
|
|
362
400
|
- 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)
|
|
364
401
|
|
|
365
402
|
**Type**:
|
|
366
403
|
- Body / headings: **Geist** (Google Fonts, 300–700).
|
|
@@ -371,50 +408,41 @@ indicators, progress bars, page-actions banner) is ink/gray.
|
|
|
371
408
|
- `.action` (default) — white bg, ink-mid border, ink text.
|
|
372
409
|
- `.action.primary` — black ink bg, white text. The "do this" CTA.
|
|
373
410
|
- `.action.subtle` — transparent bg, light border.
|
|
374
|
-
- `.action.danger` — filled red bg + white text
|
|
411
|
+
- `.action.danger` — filled red bg + white text.
|
|
375
412
|
|
|
376
413
|
**Layout**:
|
|
377
414
|
- Sidebar (collapsible, ~232px ↔ ~60px, state in `localStorage["ccsm.sidebar-collapsed"]`)
|
|
378
415
|
- brand mark + `CCSM.` wordmark
|
|
379
|
-
-
|
|
380
|
-
-
|
|
381
|
-
- Page-
|
|
416
|
+
- tabs: Sessions / Launch / Configure / About
|
|
417
|
+
- folder tree of persisted sessions (drag-sortable)
|
|
418
|
+
- Page-title bar: title on the left, server-status pill + Refresh button on the right
|
|
382
419
|
- Top-right control group uses fixed `min-height: 28px` and `border-radius: 999px` so server-status + Refresh align as a coherent control row
|
|
383
420
|
|
|
384
|
-
**
|
|
385
|
-
|
|
386
|
-
- Panel switch: 0.35s `panel-in` fade-up.
|
|
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`.
|
|
421
|
+
**No emoji in the UI** unless the user typed it. Use inline SVG icons
|
|
422
|
+
everywhere (line stroke, 1.5–2px) so they take `currentColor`.
|
|
392
423
|
|
|
393
424
|
**PWA + WCO**:
|
|
394
|
-
- `display_override: ["window-controls-overlay", "standalone"]` in
|
|
395
|
-
|
|
396
|
-
|
|
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.
|
|
425
|
+
- `display_override: ["window-controls-overlay", "standalone"]` in the manifest. When installed and launched as PWA, the title bar's middle is reclaimed; only OS controls float top-right.
|
|
426
|
+
- `public/css/wco.css` provides drag regions (`-webkit-app-region: drag`) on `.sidebar-brand`, `.page-head`, etc., unconditional. Interactive elements opt out via the no-drag block.
|
|
427
|
+
- The root PWA manifest at `pages-root/manifest.webmanifest` has stable `id: /ccsm/` so installs survive across version-router redirects to new `/ccsm/<X.Y.Z>/` subdirs.
|
|
404
428
|
|
|
405
429
|
## Versioning
|
|
406
430
|
|
|
407
|
-
The hosted frontend lives at
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
-
|
|
411
|
-
|
|
431
|
+
The hosted frontend lives at `https://bakapiano.github.io/ccsm/`. The
|
|
432
|
+
deploy workflow publishes two things to gh-pages on every push to main:
|
|
433
|
+
|
|
434
|
+
1. `pages-root/` → `/` (the router, plus root PWA manifest)
|
|
435
|
+
2. `public/` → `/<pkg.version>/` (the per-version frontend; workflow injects `<meta name="ccsm-frontend-version">` at build time)
|
|
436
|
+
|
|
437
|
+
Old version dirs stay forever (`keep_files: true`), so a user on an
|
|
438
|
+
older backend keeps loading the matching frontend until they upgrade.
|
|
412
439
|
|
|
413
440
|
`bin/ccsm.js` does auto-upgrade-restart: when the user runs `ccsm` and
|
|
414
441
|
the installed package version differs from a running backend, it POSTs
|
|
415
442
|
`/api/shutdown` to the old, waits for the port to free, then spawns a
|
|
416
443
|
fresh server. So `npm i -g @bakapiano/ccsm@latest && ccsm` is one
|
|
417
|
-
seamless step.
|
|
444
|
+
seamless step. From the frontend, the About page's Upgrade button
|
|
445
|
+
achieves the same thing without leaving the browser.
|
|
418
446
|
|
|
419
447
|
## Cross-platform
|
|
420
448
|
|
|
@@ -422,16 +450,15 @@ Today: Windows-first.
|
|
|
422
450
|
|
|
423
451
|
Cross-platform-clean already:
|
|
424
452
|
- Frontend (pure web)
|
|
453
|
+
- Router page (pure HTML/JS)
|
|
425
454
|
- `bin/ccsm.js` (pure node)
|
|
426
455
|
- `lib/webTerminal.js` (node-pty handles platform)
|
|
427
|
-
- `lib/
|
|
456
|
+
- `lib/persistedSessions.js`, `lib/folders.js`, `lib/config.js`, `lib/jsonStore.js`, `lib/cliSessionWatcher.js`, `lib/localCliSessions.js`, `lib/workspace.js` (fs only)
|
|
428
457
|
- `server.js` Express + ws
|
|
429
458
|
|
|
430
459
|
Windows-specific (need ports for Mac/Linux):
|
|
431
460
|
- `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
|
-
- `
|
|
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`.
|
|
461
|
+
- The `--app=` browser detection and PATH-merge in `server.js` are Windows-shaped (Edge first, registry HKCU\Environment for PATH).
|
|
435
462
|
|
|
436
463
|
Pattern for adding a platform: `switch (process.platform)` at each
|
|
437
464
|
entry point in those files. Each platform branch is roughly 50-100
|
|
@@ -443,6 +470,6 @@ When adding features, the natural extension points:
|
|
|
443
470
|
- **New REST routes**: `server.js` (keep under `/api/*`, use the `asyncH` wrapper, decide if it needs CORS by being in the allow-list).
|
|
444
471
|
- **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
472
|
- **Persistent user data**: drop a JSON file under `~/.ccsm/` and use `lib/jsonStore.js`'s factory.
|
|
446
|
-
- **
|
|
447
|
-
- **Different launch modes**: `lib/launcher.js` — but check first whether the "one window per session" decision still holds.
|
|
473
|
+
- **Different CLIs**: add a built-in to `DEFAULT_CLIS` in `lib/config.js`, an icon to `public/js/icons.js`, and (if the CLI persists transcripts on disk) a `profile` to `lib/cliSessionWatcher.js` and a list helper to `lib/localCliSessions.js`.
|
|
448
474
|
- **A capability**: advertise via `/api/capabilities`. Frontend gates UI on `caps.<feature>`.
|
|
475
|
+
- **Bumping the frontend**: just `npm version <patch|minor|major>` + push. The GH Pages workflow publishes to `/<new-version>/` and the router redirects users to it.
|