@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakapiano/ccsm",
3
- "version": "0.17.8",
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",
@@ -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
@@ -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;
@@ -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 / WCO / --app= mode: shrink the top band to 32px so its mid-line
79
- matches the OS-floated close/max/min icons (which sit in a fixed ~32px
80
- bar at y=0 and can't be moved). Outside app mode we keep the roomier
81
- 40px band see sidebar.css / layout.css. */
82
- body.is-app .page-title-bar,
83
- body.is-app .sidebar-top {
84
- height: 32px;
85
- min-height: 32px;
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 .sidebar-brand,
88
- body.is-app .sidebar-brand-button,
89
- body.is-app .collapse-toggle {
90
- height: 32px;
91
- min-height: 32px;
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-app .page-title-bar {
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 pinned to "CCSM". A MutationObserver guards against
17
- // Chromium standalone builds that occasionally try to inject the URL
18
- // into the title bar.
19
- const expected = 'CCSM';
20
- function lockTitle() { if (document.title !== expected) document.title = expected; }
21
- lockTitle();
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
  }