@bakapiano/ccsm 0.9.0 → 0.10.1

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.
Files changed (69) hide show
  1. package/CLAUDE.md +222 -195
  2. package/README.md +77 -79
  3. package/lib/cliSessionWatcher.js +249 -0
  4. package/lib/config.js +101 -24
  5. package/lib/folders.js +96 -0
  6. package/lib/localCliSessions.js +177 -0
  7. package/lib/persistedSessions.js +134 -0
  8. package/lib/webTerminal.js +31 -18
  9. package/lib/workspace.js +26 -4
  10. package/package.json +1 -1
  11. package/public/assets/claude-color.svg +1 -0
  12. package/public/assets/codex-color.svg +1 -0
  13. package/public/assets/copilot-color.svg +1 -0
  14. package/public/css/base.css +22 -5
  15. package/public/css/cards.css +37 -3
  16. package/public/css/feedback.css +127 -43
  17. package/public/css/forms.css +97 -25
  18. package/public/css/layout.css +74 -26
  19. package/public/css/modal.css +40 -26
  20. package/public/css/responsive.css +2 -2
  21. package/public/css/sidebar.css +424 -25
  22. package/public/css/terminals.css +138 -0
  23. package/public/css/tokens.css +28 -12
  24. package/public/css/wco.css +38 -39
  25. package/public/css/widgets.css +1177 -6
  26. package/public/index.html +35 -2
  27. package/public/js/api.js +194 -37
  28. package/public/js/components/AdoptModal.js +171 -0
  29. package/public/js/components/App.js +1 -11
  30. package/public/js/components/DirectoryPicker.js +203 -0
  31. package/public/js/components/EntityFormModal.js +105 -0
  32. package/public/js/components/Modal.js +51 -0
  33. package/public/js/components/OfflineBanner.js +29 -23
  34. package/public/js/components/PageTitleBar.js +13 -0
  35. package/public/js/components/Picker.js +179 -0
  36. package/public/js/components/Popover.js +55 -0
  37. package/public/js/components/Sidebar.js +219 -32
  38. package/public/js/components/TerminalView.js +27 -3
  39. package/public/js/components/useDragSort.js +67 -0
  40. package/public/js/dialog.js +10 -2
  41. package/public/js/icons.js +66 -3
  42. package/public/js/main.js +54 -3
  43. package/public/js/pages/AboutPage.js +80 -0
  44. package/public/js/pages/ConfigurePage.js +429 -207
  45. package/public/js/pages/LaunchPage.js +326 -86
  46. package/public/js/pages/SessionsPage.js +91 -41
  47. package/public/js/state.js +102 -73
  48. package/public/manifest.webmanifest +2 -2
  49. package/scripts/install.js +7 -2
  50. package/server.js +755 -441
  51. package/lib/favorites.js +0 -51
  52. package/lib/focus.js +0 -369
  53. package/lib/labels.js +0 -29
  54. package/lib/launcher.js +0 -219
  55. package/lib/sessions.js +0 -272
  56. package/lib/snapshot.js +0 -141
  57. package/public/js/actions.js +0 -107
  58. package/public/js/components/Fab.js +0 -11
  59. package/public/js/components/FavoritesTable.js +0 -81
  60. package/public/js/components/Footer.js +0 -12
  61. package/public/js/components/NewSessionModal.js +0 -153
  62. package/public/js/components/PageHead.js +0 -33
  63. package/public/js/components/Pagination.js +0 -27
  64. package/public/js/components/RecentTable.js +0 -68
  65. package/public/js/components/SessionsTable.js +0 -71
  66. package/public/js/components/SnapshotPanel.js +0 -77
  67. package/public/js/components/TitleCell.js +0 -40
  68. package/public/js/components/WorkspacesGrid.js +0 -41
  69. 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 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.
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 list and a snapshot/restore safety net.
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 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/ccsm/v1/`, served by GitHub Pages, deployed
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/v1/static frontend
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/snapshot
33
- │ ├── /api/sessions/new (NDJSON)
34
- │ ├── /ws/terminal/:id (PTY)
35
- │ ├── /api/heartbeat /api/spawn-browser
36
- └── /api/health /api/shutdown
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
- 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.
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
- In **dev mode** (running from a checkout `__dirname` not under
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 hosted frontend in a chromeless Edge `--app=` window.
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 ~12s.
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/v1/` — when backend is
66
- down you see an OfflineBanner with a **Start ccsm** button.
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
- snapshots live at `~/.ccsm/` (override with `CCSM_HOME=<path>`). All
70
- settings editable through the Configure panel
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. The startup log prints the actual URL.
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
- - `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.
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
- 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.
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
- 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`).
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 races `saveSnapshot()` against a 2s timeout, kills any
109
- PTY children, then `process.exit(0)`. Every trigger funnels here:
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
- 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.
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
- │ ├── 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
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
- └── 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
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 · clock tick · heartbeat · is-app body class
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 · 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
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/ # 13 focused stylesheets
250
+ └── css/ # 12 focused stylesheets
186
251
  ├── tokens.css · base.css · layout.css
187
- ├── sidebar.css · cards.css · tables.css · forms.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
- ├── snapshot.json # latest auto-snapshot
194
- ├── snapshots/ # rotating history (default keep=30)
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
- **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.
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
- **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()`).
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. Future breaking
232
- changes ship a fresh `/v2/` frontend path; old `/v1/` still works
233
- against backends that advertise the v1 contract.
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 / POST | `/api/snapshot` | latest snapshot / force-save now |
254
- | GET | `/api/snapshot/history` | rotated history filenames |
255
- | POST | `/api/snapshot/restore` | launch one wt window per session in snapshot |
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/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 |
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` | sanity ping + version |
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 Edge window into the running server |
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
- **wt.exe `-d` flag** (marker-file probes confirming `%CD%` inside the new tab):
282
-
283
- | variant | result |
284
- |---|---|
285
- | `wt -d D:\ccsm <cmd>` | ✓ |
286
- | `wt -d D:/ccsm <cmd>` | ✓ (forward slashes fine) |
287
- | `wt --startingDirectory D:\ccsm <cmd>` | |
288
- | `wt -d D:\ccsm\ <cmd>` (trailing sep) | |
289
- | spawn `{ cwd: ... }` with no `-d` | wt ignores parent cwd |
290
- | `wt -d "D:\ccsm" <cmd>` (literal quotes) | wt doesn't open |
291
- | `wt new-tab -d D:\ccsm <cmd>` | wt doesn't open |
292
-
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".
296
-
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.
298
-
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`.
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 (e.g. Remove repo).
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
- - 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
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
- **Animation**:
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+).
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
- 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.
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 `/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`.
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/snapshot.js`, `lib/config.js`, `lib/jsonStore.js` (fs only)
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
- - `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`.
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
- - **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.
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.