@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/public/js/main.js CHANGED
@@ -4,13 +4,23 @@
4
4
 
5
5
  import { render } from 'preact';
6
6
  import { html } from './html.js';
7
- import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa } from './state.js';
7
+ import { loadPersisted, clockTick, lastRefreshAt, installPrompt, isInstalledPwa, sidebarForcedCollapsed } from './state.js';
8
8
  import { httpBase } from './backend.js';
9
- import { loadConfig, refreshAll, loadSessions, loadRecent, loadSnapshot, loadWorkspaces, loadWebTerminals, pollHealth } from './api.js';
9
+ import { loadConfig, refreshAll, loadSessions, loadFolders, loadWorkspaces, pollHealth } from './api.js';
10
10
  import { setToast } from './toast.js';
11
11
  import { App } from './components/App.js';
12
12
 
13
13
  loadPersisted();
14
+ // Pin the document title to "CCSM" — some Chromium builds will inject the
15
+ // current URL or path into the standalone window title bar if the page
16
+ // title is empty / changes; locking it here keeps the OS title bar text
17
+ // stable across navigation, tab switches, and PWA-install refresh.
18
+ const lockTitle = () => { if (document.title !== 'CCSM') document.title = 'CCSM'; };
19
+ lockTitle();
20
+ new MutationObserver(lockTitle).observe(
21
+ document.querySelector('title') || document.head,
22
+ { childList: true, subtree: true, characterData: true }
23
+ );
14
24
  render(html`<${App} />`, document.getElementById('app'));
15
25
 
16
26
  // PWA install affordance — Chromium fires `beforeinstallprompt` when the
@@ -43,7 +53,22 @@ function applyIsAppClass() {
43
53
  applyIsAppClass();
44
54
  matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass);
45
55
 
56
+ // Force-collapse the sidebar on narrow viewports. Mirrors the responsive
57
+ // CSS so JS state (toggle visibility, tree-render gating) agrees with the
58
+ // rendered layout.
59
+ const narrowMq = matchMedia('(max-width: 900px)');
60
+ function applyNarrow() { sidebarForcedCollapsed.value = narrowMq.matches; }
61
+ applyNarrow();
62
+ narrowMq.addEventListener('change', applyNarrow);
63
+
46
64
  (async () => {
65
+ // Version-mismatch guard runs FIRST. If the user's backend has been
66
+ // upgraded since this per-version frontend was loaded, bounce back to
67
+ // the router immediately — no point loading config from a server that
68
+ // speaks a different API revision. Runs in dev too (it no-ops without
69
+ // the build-time <meta>).
70
+ await bootVersionGuard();
71
+
47
72
  try {
48
73
  await loadConfig();
49
74
  await refreshAll();
@@ -61,7 +86,7 @@ matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass
61
86
  // move in/out of a workspace silently and the grid stays stale.
62
87
  setInterval(async () => {
63
88
  try {
64
- await Promise.all([loadSessions(), loadRecent(), loadSnapshot(), loadWorkspaces(), loadWebTerminals()]);
89
+ await Promise.all([loadSessions(), loadFolders(), loadWorkspaces()]);
65
90
  lastRefreshAt.value = Date.now();
66
91
  } catch { /* swallow — next tick retries */ }
67
92
  pollHealth();
@@ -79,3 +104,29 @@ matchMedia('(display-mode: browser)').addEventListener('change', applyIsAppClass
79
104
  setInterval(ping, 10_000);
80
105
  document.addEventListener('visibilitychange', () => { if (!document.hidden) ping(); });
81
106
  })();
107
+
108
+ // ─── version routing guard ───────────────────────────────────────────
109
+ // Each deployed frontend is pinned to one backend version. The GH-Pages
110
+ // workflow bakes the version into <meta name="ccsm-frontend-version">
111
+ // so we can detect "backend has been upgraded since this frontend was
112
+ // loaded" and bounce back through the router at /ccsm/ for a fresh
113
+ // match. In dev (no meta tag, same-origin served-by-backend), the check
114
+ // no-ops — we're always running the frontend that ships with this
115
+ // backend by definition.
116
+ async function bootVersionGuard() {
117
+ const meta = document.querySelector('meta[name="ccsm-frontend-version"]');
118
+ if (!meta) return; // dev mode
119
+ const myVer = meta.getAttribute('content');
120
+ if (!myVer) return;
121
+ let backendVer = null;
122
+ try {
123
+ const r = await fetch(httpBase() + '/api/health', { cache: 'no-store' });
124
+ if (!r.ok) return;
125
+ backendVer = (await r.json()).version;
126
+ } catch { return; } // offline → OfflineBanner takes over
127
+ if (!backendVer || backendVer === myVer) return;
128
+ // Mismatch. Bounce up one level to the router. The router will
129
+ // probe /api/health again and redirect to ./<backendVer>/.
130
+ console.warn(`[ccsm] frontend ${myVer} ≠ backend ${backendVer} — re-routing`);
131
+ location.replace('../');
132
+ }
@@ -1,7 +1,10 @@
1
1
  import { html } from '../html.js';
2
+ import { useEffect, useState } from 'preact/hooks';
2
3
  import { serverHealth, installPrompt, isInstalledPwa } from '../state.js';
3
4
  import { setToast } from '../toast.js';
5
+ import { api } from '../api.js';
4
6
  import { Card } from '../components/Card.js';
7
+ import { PageTitleBar } from '../components/PageTitleBar.js';
5
8
  import { BrandMark, IconGithub, IconExternal } from '../icons.js';
6
9
 
7
10
  const REPO_URL = 'https://github.com/bakapiano/ccsm';
@@ -40,10 +43,87 @@ function InstallCard() {
40
43
  </${Card}>`;
41
44
  }
42
45
 
46
+ function UpgradeCard() {
47
+ const [info, setInfo] = useState(null); // { current, latest, updateAvailable, fetchedAt, error? }
48
+ const [checking, setChecking] = useState(true);
49
+ const [upgrading, setUpgrading] = useState(false);
50
+
51
+ const refresh = async (force = false) => {
52
+ setChecking(true);
53
+ try {
54
+ const r = await api('GET', '/api/version' + (force ? '?refresh=1' : ''));
55
+ setInfo(r);
56
+ } catch (e) {
57
+ setInfo({ error: e.message });
58
+ } finally {
59
+ setChecking(false);
60
+ }
61
+ };
62
+ useEffect(() => { refresh(false); }, []);
63
+
64
+ const onUpgrade = async () => {
65
+ if (!info?.updateAvailable) return;
66
+ setUpgrading(true);
67
+ try {
68
+ await api('POST', '/api/upgrade', { target: 'latest' });
69
+ setToast(`upgrading to v${info.latest} · backend will restart`);
70
+ } catch (e) {
71
+ setUpgrading(false);
72
+ setToast(e.message, 'error');
73
+ }
74
+ // No "finally" reset — the server is about to shut down, and the
75
+ // OfflineBanner takes over UI. When the router reroutes us to the new
76
+ // version's frontend, this component re-mounts fresh.
77
+ };
78
+
79
+ const current = info?.current || serverHealth.value.version || '';
80
+ const latest = info?.latest;
81
+ const updateAvailable = !!info?.updateAvailable;
82
+
83
+ return html`
84
+ <${Card} title="Version">
85
+ <div class="about-version-row">
86
+ <div>
87
+ <div class="about-version-line">
88
+ Installed · <span class="mono">v${current || '?'}</span>
89
+ </div>
90
+ ${latest && !updateAvailable ? html`
91
+ <div class="muted-text" style="margin-top:4px">You're on the latest release.</div>
92
+ ` : null}
93
+ ${updateAvailable ? html`
94
+ <div class="about-update-line">
95
+ Update available · <span class="mono">v${latest}</span>
96
+ </div>
97
+ ` : null}
98
+ ${info?.error ? html`
99
+ <div class="muted-text" style="margin-top:4px">Couldn't reach npm registry.</div>
100
+ ` : null}
101
+ </div>
102
+ <div class="about-version-actions">
103
+ <button class="action subtle" onClick=${() => refresh(true)} disabled=${checking || upgrading}>
104
+ ${checking ? 'Checking…' : 'Check'}
105
+ </button>
106
+ ${updateAvailable ? html`
107
+ <button class="action primary" onClick=${onUpgrade} disabled=${upgrading}>
108
+ ${upgrading ? 'Upgrading…' : `Upgrade to v${latest}`}
109
+ </button>
110
+ ` : null}
111
+ </div>
112
+ </div>
113
+ ${upgrading ? html`
114
+ <p class="muted-text" style="margin-top:var(--s-3)">
115
+ Running <code>npm i -g @bakapiano/ccsm@latest</code>. The backend will restart automatically — you'll see the "Backend not running" screen briefly.
116
+ </p>
117
+ ` : null}
118
+ </${Card}>`;
119
+ }
120
+
43
121
  export function AboutPage() {
44
122
  const version = serverHealth.value.version;
45
123
 
46
124
  return html`
125
+ <${PageTitleBar} title="About" />
126
+ <${UpgradeCard} />
47
127
  <${InstallCard} />
48
128
  <${Card} title="ccsm">
49
129
  <div class="about-block">