@bakapiano/ccsm 0.17.8 → 0.17.10
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/package.json +1 -1
- package/public/css/layout.css +5 -1
- package/public/css/terminals.css +43 -0
- package/public/css/wco.css +67 -23
- package/public/js/main.js +50 -7
- package/public/js/pages/SessionsPage.js +18 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.10",
|
|
4
4
|
"description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
package/public/css/layout.css
CHANGED
|
@@ -105,7 +105,11 @@ body.is-resizing-sidebar .app {
|
|
|
105
105
|
align-items: center;
|
|
106
106
|
justify-content: flex-start;
|
|
107
107
|
gap: var(--s-3);
|
|
108
|
-
height: 40px;
|
|
108
|
+
height: calc(40px * var(--anti-zoom, 1));
|
|
109
|
+
min-height: calc(40px * var(--anti-zoom, 1));
|
|
110
|
+
max-height: calc(40px * var(--anti-zoom, 1));
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
box-sizing: border-box;
|
|
109
113
|
margin: 0 calc(-1 * var(--s-4)) 0;
|
|
110
114
|
padding: 0 var(--s-5);
|
|
111
115
|
/* Replace the hard inset shadow rule with a soft gradient fade — gives
|
package/public/css/terminals.css
CHANGED
|
@@ -232,6 +232,49 @@
|
|
|
232
232
|
background: var(--bg-elev);
|
|
233
233
|
overflow: hidden;
|
|
234
234
|
}
|
|
235
|
+
.session-actions {
|
|
236
|
+
display: flex;
|
|
237
|
+
align-items: stretch;
|
|
238
|
+
justify-content: flex-start;
|
|
239
|
+
gap: 0;
|
|
240
|
+
height: calc(24px * var(--anti-zoom, 1));
|
|
241
|
+
min-height: calc(24px * var(--anti-zoom, 1));
|
|
242
|
+
max-height: calc(24px * var(--anti-zoom, 1));
|
|
243
|
+
flex-shrink: 0;
|
|
244
|
+
box-sizing: border-box;
|
|
245
|
+
padding: 0;
|
|
246
|
+
background: var(--accent);
|
|
247
|
+
border-top: 0;
|
|
248
|
+
}
|
|
249
|
+
.session-actions .action {
|
|
250
|
+
display: inline-flex;
|
|
251
|
+
align-items: center;
|
|
252
|
+
gap: 5px;
|
|
253
|
+
height: 100%;
|
|
254
|
+
padding: 0 10px;
|
|
255
|
+
font-size: 11px;
|
|
256
|
+
font-weight: 500;
|
|
257
|
+
line-height: 1;
|
|
258
|
+
color: rgba(255, 255, 255, 0.85);
|
|
259
|
+
background: transparent;
|
|
260
|
+
border: 0;
|
|
261
|
+
border-radius: 0;
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
transition: background-color .12s, color .12s;
|
|
264
|
+
}
|
|
265
|
+
.session-actions .action:hover {
|
|
266
|
+
color: #fff;
|
|
267
|
+
background: rgba(255, 255, 255, 0.14);
|
|
268
|
+
}
|
|
269
|
+
.session-actions .action.danger:hover {
|
|
270
|
+
color: #fff;
|
|
271
|
+
background: rgba(0, 0, 0, 0.22);
|
|
272
|
+
}
|
|
273
|
+
.session-actions .action svg {
|
|
274
|
+
width: 13px;
|
|
275
|
+
height: 13px;
|
|
276
|
+
stroke-width: 1.75;
|
|
277
|
+
}
|
|
235
278
|
.session-pane-head {
|
|
236
279
|
display: flex;
|
|
237
280
|
align-items: center;
|
package/public/css/wco.css
CHANGED
|
@@ -55,15 +55,6 @@ body.is-app .page-subtitle {
|
|
|
55
55
|
body.is-app .sidebar { padding-top: 0; }
|
|
56
56
|
body.is-app .main { padding-top: 0; }
|
|
57
57
|
|
|
58
|
-
/* Reserve room on the right of page-title-bar so its action buttons don't
|
|
59
|
-
slide under the floating OS controls. 150px covers Windows controls
|
|
60
|
-
(~46px each × 3) with breathing room; macOS traffic lights are on the
|
|
61
|
-
left so this padding is harmless there. WCO override below uses the
|
|
62
|
-
precise env() value when available. */
|
|
63
|
-
body.is-app .page-title-bar {
|
|
64
|
-
padding-right: calc(var(--s-4) + 150px);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
body.is-app .page-title,
|
|
68
59
|
body.is-app .page-subtitle {
|
|
69
60
|
display: none;
|
|
@@ -75,24 +66,77 @@ body.is-app .page-head {
|
|
|
75
66
|
min-height: 28px;
|
|
76
67
|
}
|
|
77
68
|
|
|
78
|
-
/* PWA
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
69
|
+
/* Standalone PWA (browser still paints its own title bar above our
|
|
70
|
+
document): suppress our duplicate chrome — the brand mark + collapse
|
|
71
|
+
row at top-left of the sidebar AND the page-title-bar header — since
|
|
72
|
+
the OS title bar already shows the app name and serves as the visual
|
|
73
|
+
header. With sidebar-top hidden, the nav tabs (Sessions/Launch/…)
|
|
74
|
+
float up to start at the very top of the sidebar column. A 1px
|
|
75
|
+
border-top across the whole app grid gives the OS title bar something
|
|
76
|
+
to rest against instead of bleeding straight into our content. */
|
|
77
|
+
body.is-app:not(.is-wco) .sidebar-top {
|
|
78
|
+
display: none;
|
|
86
79
|
}
|
|
87
|
-
body.is-app .
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
body.is-app:not(.is-wco) .page-title-bar {
|
|
81
|
+
display: none;
|
|
82
|
+
}
|
|
83
|
+
body.is-app:not(.is-wco) .app {
|
|
84
|
+
/* Hairline separator under the OS title bar. Painted as an inset
|
|
85
|
+
box-shadow rather than a border so it doesn't change .app's box
|
|
86
|
+
model — a real border-top added 1px to .app's height, and the
|
|
87
|
+
grid's implicit row auto-sized off content (.main reporting 720)
|
|
88
|
+
rather than the (now 719px) container, so .main overflowed the
|
|
89
|
+
viewport by exactly 1px and put a permanent vertical scrollbar
|
|
90
|
+
down the right of the terminal page. Inset shadow draws the same
|
|
91
|
+
line with zero layout cost. */
|
|
92
|
+
box-shadow: inset 0 1px 0 var(--border);
|
|
93
|
+
}
|
|
94
|
+
/* With page-title-bar hidden, session-pane's `margin-top: calc(-1 *
|
|
95
|
+
var(--s-4))` — designed to flush it against the (now invisible)
|
|
96
|
+
title-bar — would otherwise yank the terminal up into the OS title-bar
|
|
97
|
+
border. Zero the margin AND drop session-pane's `height: 100%` so flex
|
|
98
|
+
does all the height math; otherwise the height: 100% double-counts and
|
|
99
|
+
creates dead space at the bottom. */
|
|
100
|
+
body.is-app:not(.is-wco) .session-pane {
|
|
101
|
+
margin-top: 0;
|
|
102
|
+
height: auto;
|
|
103
|
+
}
|
|
104
|
+
/* Settings page · with the title-bar hidden in standalone its content
|
|
105
|
+
would otherwise butt straight against the OS title-bar border. Give
|
|
106
|
+
it back a little breathing room (Sessions / Launch / About don't
|
|
107
|
+
need this — they have their own top chrome or flush-to-edge intent). */
|
|
108
|
+
body.is-app:not(.is-wco) [data-panel="configure"] {
|
|
109
|
+
padding-top: var(--s-4);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* WCO mode only: the browser has hidden its own title bar and floats OS
|
|
113
|
+
controls (min/max/close) over our content top-right. Our 34px top band
|
|
114
|
+
IS the title bar in that case — it gets shrunk to match the OS control
|
|
115
|
+
strip height, and reserves a right-side padding so action buttons don't
|
|
116
|
+
slide under those floating controls. 150px covers Windows controls
|
|
117
|
+
(~46px × 3) with breathing room; the env() override below uses the
|
|
118
|
+
precise titlebar-area-width when the browser exposes it.
|
|
119
|
+
In plain standalone PWA (browser still paints its own title bar above
|
|
120
|
+
our document), none of this applies — our page-title-bar behaves like
|
|
121
|
+
a normal 40px page header from layout.css. */
|
|
122
|
+
body.is-wco .page-title-bar {
|
|
123
|
+
padding-right: calc(var(--s-4) + 150px);
|
|
124
|
+
}
|
|
125
|
+
body.is-wco .page-title-bar,
|
|
126
|
+
body.is-wco .sidebar-top {
|
|
127
|
+
height: calc(34px * var(--anti-zoom, 1));
|
|
128
|
+
min-height: calc(34px * var(--anti-zoom, 1));
|
|
129
|
+
max-height: calc(34px * var(--anti-zoom, 1));
|
|
130
|
+
}
|
|
131
|
+
body.is-wco .sidebar-brand,
|
|
132
|
+
body.is-wco .sidebar-brand-button,
|
|
133
|
+
body.is-wco .collapse-toggle {
|
|
134
|
+
height: 34px;
|
|
135
|
+
min-height: 34px;
|
|
92
136
|
}
|
|
93
137
|
|
|
94
138
|
@media (display-mode: window-controls-overlay) {
|
|
95
|
-
body.is-
|
|
139
|
+
body.is-wco .page-title-bar {
|
|
96
140
|
padding-right: calc(var(--s-4) + 100vw - env(titlebar-area-width, 100vw));
|
|
97
141
|
}
|
|
98
142
|
}
|
package/public/js/main.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// the mount root.
|
|
4
4
|
|
|
5
5
|
import { render } from 'preact';
|
|
6
|
+
import { effect } from '@preact/signals';
|
|
6
7
|
import { html } from './html.js';
|
|
7
|
-
import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa, sidebarForcedCollapsed } from './state.js';
|
|
8
|
+
import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa, sidebarForcedCollapsed, activeTab, activeSessionId, sessions, TAB_HEADINGS } from './state.js';
|
|
8
9
|
import { httpBase } from './backend.js';
|
|
9
10
|
import { loadConfig, refreshAll, loadSessions, loadFolders, loadWorkspaces, pollHealth } from './api.js';
|
|
10
11
|
import { setToast } from './toast.js';
|
|
@@ -13,12 +14,29 @@ import { installGlobalKeybindings } from './keybindings.js';
|
|
|
13
14
|
|
|
14
15
|
loadPersisted();
|
|
15
16
|
installGlobalKeybindings();
|
|
16
|
-
// Window/tab title
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
// Window/tab title — reactive. In standalone PWA mode we hide our own
|
|
18
|
+
// .page-title-bar and the browser-drawn OS title bar takes its place,
|
|
19
|
+
// so document.title is what the user actually sees as the header. It
|
|
20
|
+
// mirrors what would have been in our hidden header: session title +
|
|
21
|
+
// cwd on the Sessions tab, the page heading elsewhere.
|
|
22
|
+
// MutationObserver guards against Chromium standalone builds that
|
|
23
|
+
// occasionally try to inject the URL into the title bar.
|
|
24
|
+
let desiredTitle = 'CCSM';
|
|
25
|
+
function lockTitle() { if (document.title !== desiredTitle) document.title = desiredTitle; }
|
|
26
|
+
function computeTitle() {
|
|
27
|
+
const tab = activeTab.value;
|
|
28
|
+
if (tab === 'sessions') {
|
|
29
|
+
const id = activeSessionId.value;
|
|
30
|
+
const s = id ? sessions.value.find((x) => x.id === id) : null;
|
|
31
|
+
if (s) {
|
|
32
|
+
const name = s.title || s.workspace || s.id.slice(0, 12);
|
|
33
|
+
return `${name} · ${s.cwd} · CCSM`;
|
|
34
|
+
}
|
|
35
|
+
return 'Sessions · CCSM';
|
|
36
|
+
}
|
|
37
|
+
return `${TAB_HEADINGS[tab]?.title || 'CCSM'} · CCSM`;
|
|
38
|
+
}
|
|
39
|
+
effect(() => { desiredTitle = computeTitle(); lockTitle(); });
|
|
22
40
|
new MutationObserver(lockTitle).observe(
|
|
23
41
|
document.querySelector('title') || document.head,
|
|
24
42
|
{ childList: true, subtree: true, characterData: true }
|
|
@@ -48,12 +66,23 @@ mq.addEventListener('change', () => { isInstalledPwa.value = mq.matches; });
|
|
|
48
66
|
// (display-mode: browser) gets it. Used by wco.css to gate user-select
|
|
49
67
|
// on drag regions so chromeless --app= windows can be dragged by
|
|
50
68
|
// clicking the page title, while normal tabs still allow text select.
|
|
69
|
+
//
|
|
70
|
+
// "is-wco" is the stricter case: window-controls-overlay mode where the
|
|
71
|
+
// browser hides its title bar entirely and only floats OS controls in
|
|
72
|
+
// the top-right. In that mode our .page-title-bar IS the title bar and
|
|
73
|
+
// needs the 34px height + padding-right reservation. In plain standalone
|
|
74
|
+
// PWA (browser still paints its own title bar above our content), we
|
|
75
|
+
// don't need any of that — page-title-bar can behave like a regular tab.
|
|
51
76
|
function applyIsAppClass() {
|
|
52
77
|
const isApp = !matchMedia('(display-mode: browser)').matches;
|
|
78
|
+
const isWco = matchMedia('(display-mode: window-controls-overlay)').matches;
|
|
53
79
|
document.body.classList.toggle('is-app', isApp);
|
|
80
|
+
document.body.classList.toggle('is-wco', isWco);
|
|
54
81
|
}
|
|
55
82
|
applyIsAppClass();
|
|
56
83
|
matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass);
|
|
84
|
+
matchMedia('(display-mode: window-controls-overlay)').addEventListener('change', applyIsAppClass);
|
|
85
|
+
matchMedia('(display-mode: standalone)').addEventListener('change', applyIsAppClass);
|
|
57
86
|
|
|
58
87
|
// Force-collapse the sidebar on narrow viewports. Mirrors the responsive
|
|
59
88
|
// CSS so JS state (toggle visibility, tree-render gating) agrees with the
|
|
@@ -63,6 +92,20 @@ function applyNarrow() { sidebarForcedCollapsed.value = narrowMq.matches; }
|
|
|
63
92
|
applyNarrow();
|
|
64
93
|
narrowMq.addEventListener('change', applyNarrow);
|
|
65
94
|
|
|
95
|
+
// Counter-zoom for chrome bars (page-title-bar, session-actions). Browser
|
|
96
|
+
// page zoom (Ctrl+wheel) scales every CSS px including our header heights;
|
|
97
|
+
// without this, the header gets visually taller at 150%+ which the user
|
|
98
|
+
// usually doesn't want. We detect zoom via outerWidth/innerWidth and write
|
|
99
|
+
// 1/zoom into --anti-zoom so the CSS can `calc(40px * var(--anti-zoom))`
|
|
100
|
+
// each bar back to a constant on-screen height.
|
|
101
|
+
function syncAntiZoom() {
|
|
102
|
+
const z = window.outerWidth / window.innerWidth || 1;
|
|
103
|
+
const inv = Math.max(0.4, Math.min(1, 1 / z)); // clamp: never grow > 100%
|
|
104
|
+
document.documentElement.style.setProperty('--anti-zoom', String(inv));
|
|
105
|
+
}
|
|
106
|
+
syncAntiZoom();
|
|
107
|
+
window.addEventListener('resize', syncAntiZoom);
|
|
108
|
+
|
|
66
109
|
(async () => {
|
|
67
110
|
// Version-mismatch guard runs FIRST. If the user's backend has been
|
|
68
111
|
// upgraded since this per-version frontend was loaded, bounce back to
|
|
@@ -11,7 +11,7 @@ import { setToast } from '../toast.js';
|
|
|
11
11
|
import { ccsmConfirm, ccsmPrompt } from '../dialog.js';
|
|
12
12
|
import { TerminalView } from '../components/TerminalView.js';
|
|
13
13
|
import { PageTitleBar } from '../components/PageTitleBar.js';
|
|
14
|
-
import { IconPencil, IconClose } from '../icons.js';
|
|
14
|
+
import { IconPencil, IconClose, IconBranch } from '../icons.js';
|
|
15
15
|
import { fmtAgo } from '../util.js';
|
|
16
16
|
|
|
17
17
|
export function SessionsPage() {
|
|
@@ -69,6 +69,7 @@ export function SessionsPage() {
|
|
|
69
69
|
setResumeError(null);
|
|
70
70
|
setRetryNonce((n) => n + 1);
|
|
71
71
|
};
|
|
72
|
+
const onFork = () => { setToast('Fork is not wired up yet'); };
|
|
72
73
|
|
|
73
74
|
return html`
|
|
74
75
|
<${PageTitleBar} title=${html`
|
|
@@ -97,5 +98,21 @@ export function SessionsPage() {
|
|
|
97
98
|
`}
|
|
98
99
|
</div>`}
|
|
99
100
|
</div>
|
|
101
|
+
<div class="session-actions">
|
|
102
|
+
${/* Fork button — wired but disabled; turn back on once the
|
|
103
|
+
--fork-session / codex fork / copilot fs-copy integrations
|
|
104
|
+
are in place. See discussion 2026-05-27. */ ''}
|
|
105
|
+
${false ? html`
|
|
106
|
+
<button class="action subtle" onClick=${onFork} title="Fork session">
|
|
107
|
+
<${IconBranch} /> Fork
|
|
108
|
+
</button>
|
|
109
|
+
` : null}
|
|
110
|
+
<button class="action subtle" onClick=${onRename} title="Rename session">
|
|
111
|
+
<${IconPencil} /> Rename
|
|
112
|
+
</button>
|
|
113
|
+
<button class="action subtle danger" onClick=${onDelete} title="Delete session">
|
|
114
|
+
<${IconClose} /> Delete
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
100
117
|
</div>`;
|
|
101
118
|
}
|