@bakapiano/ccsm 0.22.3 → 0.22.4

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 (60) hide show
  1. package/CLAUDE.md +538 -538
  2. package/README.md +189 -189
  3. package/bin/ccsm.js +235 -235
  4. package/lib/cliActivity.js +139 -139
  5. package/lib/codexSeed.js +183 -183
  6. package/lib/config.js +274 -274
  7. package/lib/devices.js +229 -229
  8. package/lib/folders.js +124 -124
  9. package/lib/localCliSessions.js +519 -519
  10. package/lib/persistedSessions.js +129 -129
  11. package/lib/tunnel.js +621 -621
  12. package/lib/webTerminal.js +225 -225
  13. package/lib/workspace.js +233 -233
  14. package/package.json +57 -57
  15. package/public/css/base.css +99 -99
  16. package/public/css/cards.css +183 -183
  17. package/public/css/feedback.css +504 -504
  18. package/public/css/forms.css +453 -453
  19. package/public/css/layout.css +176 -176
  20. package/public/css/modal.css +190 -190
  21. package/public/css/responsive.css +176 -176
  22. package/public/css/sidebar.css +707 -707
  23. package/public/css/terminals.css +592 -592
  24. package/public/css/tokens.css +81 -81
  25. package/public/css/wco.css +196 -196
  26. package/public/css/widgets.css +2725 -2725
  27. package/public/index.html +152 -152
  28. package/public/js/api.js +371 -371
  29. package/public/js/backend.js +149 -149
  30. package/public/js/components/App.js +73 -73
  31. package/public/js/components/DirectoryPicker.js +203 -203
  32. package/public/js/components/EntityFormModal.js +153 -153
  33. package/public/js/components/Modal.js +57 -57
  34. package/public/js/components/OfflineBanner.js +67 -67
  35. package/public/js/components/PageTitleBar.js +13 -13
  36. package/public/js/components/PendingApprovalOverlay.js +128 -128
  37. package/public/js/components/Picker.js +179 -179
  38. package/public/js/components/Popover.js +55 -55
  39. package/public/js/components/RestartOverlay.js +36 -36
  40. package/public/js/components/Sidebar.js +380 -380
  41. package/public/js/components/TerminalInstance.js +148 -22
  42. package/public/js/components/TerminalResizeDebouncer.js +126 -0
  43. package/public/js/components/XtermTerminal.js +62 -15
  44. package/public/js/components/useDragSort.js +67 -67
  45. package/public/js/dialog.js +67 -67
  46. package/public/js/icons.js +212 -212
  47. package/public/js/main.js +296 -296
  48. package/public/js/pages/AboutPage.js +90 -90
  49. package/public/js/pages/ConfigurePage.js +713 -713
  50. package/public/js/pages/LaunchPage.js +421 -421
  51. package/public/js/pages/RemotePage.js +743 -743
  52. package/public/js/pages/SessionsPage.js +100 -100
  53. package/public/js/state.js +335 -335
  54. package/public/manifest.webmanifest +25 -0
  55. package/public/setup/index.html +567 -0
  56. package/scripts/dev.js +149 -149
  57. package/scripts/install.js +153 -153
  58. package/scripts/restart-helper.js +96 -96
  59. package/scripts/upgrade-helper.js +687 -687
  60. package/server.js +1807 -1807
@@ -1,9 +1,9 @@
1
- // Sessions page · the main pane. Shows the terminal for the currently
2
- // selected session (activeSessionId), with a thin header providing
3
- // session metadata + a session-tabs strip (future multi-tab support)
4
- // and a kebab menu top-right for per-session actions. When a session is
5
- // selected but not running we auto-resume it — no manual button.
6
-
1
+ // Sessions page · the main pane. Shows the terminal for the currently
2
+ // selected session (activeSessionId), with a thin header providing
3
+ // session metadata + a session-tabs strip (future multi-tab support)
4
+ // and a kebab menu top-right for per-session actions. When a session is
5
+ // selected but not running we auto-resume it — no manual button.
6
+
7
7
  import { html } from '../html.js';
8
8
  import { useEffect, useRef, useState } from 'preact/hooks';
9
9
  import { activeSessionId, sessions, config, selectTab, selectSession, clockTick } from '../state.js';
@@ -15,56 +15,56 @@ import { PageTitleBar } from '../components/PageTitleBar.js';
15
15
  import { Popover } from '../components/Popover.js';
16
16
  import { IconMoreVert, IconPencil, IconClose, IconPlus, IconForCliType, IconTerminal, IconExternal, IconPlay, IconStop } from '../icons.js';
17
17
  import { fmtAgo } from '../util.js';
18
-
19
- function SessionTabs({ activeId, onActivate, onNew, kebab }) {
20
- // For now we only show the currently active session as a single tab —
21
- // other open sessions are hidden, and the "+ new" affordance is parked
22
- // until multi-tab UX lands.
23
- const active = activeId ? sessions.value.find((s) => s.id === activeId) : null;
24
- if (!active) return null;
25
- const open = [active];
26
- return html`
27
- <div class="session-tabs" role="tablist">
28
- <div class="session-tabs-list">
29
- ${open.map((s) => {
30
- const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
31
- const Icon = IconForCliType(cli?.type) || IconTerminal;
32
- const t = s.title || s.workspace || s.id.slice(0, 12);
33
- const isActive = s.id === activeId;
34
- return html`
35
- <button key=${s.id}
36
- role="tab"
37
- aria-selected=${isActive}
38
- class=${`session-tab${isActive ? ' is-active' : ''}`}
39
- onClick=${() => onActivate(s.id)}
40
- title=${`${t} · ${s.cwd}`}>
41
- <span class="session-tab-icon"><${Icon} /></span>
42
- <span class="session-tab-label">${t}</span>
43
- ${s.status !== 'running' ? html`<span class="session-tab-meta">·</span>` : null}
44
- </button>`;
45
- })}
46
- ${/* <button class="session-tab session-tab-add" onClick=${onNew} title="New session">
47
- <${IconPlus} />
48
- </button> */ null}
49
- </div>
50
- ${kebab ? html`<div class="session-tabs-right">${kebab}</div>` : null}
51
- </div>`;
52
- }
53
-
18
+
19
+ function SessionTabs({ activeId, onActivate, onNew, kebab }) {
20
+ // For now we only show the currently active session as a single tab —
21
+ // other open sessions are hidden, and the "+ new" affordance is parked
22
+ // until multi-tab UX lands.
23
+ const active = activeId ? sessions.value.find((s) => s.id === activeId) : null;
24
+ if (!active) return null;
25
+ const open = [active];
26
+ return html`
27
+ <div class="session-tabs" role="tablist">
28
+ <div class="session-tabs-list">
29
+ ${open.map((s) => {
30
+ const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
31
+ const Icon = IconForCliType(cli?.type) || IconTerminal;
32
+ const t = s.title || s.workspace || s.id.slice(0, 12);
33
+ const isActive = s.id === activeId;
34
+ return html`
35
+ <button key=${s.id}
36
+ role="tab"
37
+ aria-selected=${isActive}
38
+ class=${`session-tab${isActive ? ' is-active' : ''}`}
39
+ onClick=${() => onActivate(s.id)}
40
+ title=${`${t} · ${s.cwd}`}>
41
+ <span class="session-tab-icon"><${Icon} /></span>
42
+ <span class="session-tab-label">${t}</span>
43
+ ${s.status !== 'running' ? html`<span class="session-tab-meta">·</span>` : null}
44
+ </button>`;
45
+ })}
46
+ ${/* <button class="session-tab session-tab-add" onClick=${onNew} title="New session">
47
+ <${IconPlus} />
48
+ </button> */ null}
49
+ </div>
50
+ ${kebab ? html`<div class="session-tabs-right">${kebab}</div>` : null}
51
+ </div>`;
52
+ }
53
+
54
54
  function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor, onSwitchCli }) {
55
55
  const [open, setOpen] = useState(false);
56
56
  const anchor = useRef(null);
57
57
  return html`
58
58
  <button class="session-menu-btn" ref=${anchor}
59
59
  aria-label="Session actions" title="Session actions"
60
- onClick=${() => setOpen((v) => !v)}>
61
- <${IconMoreVert} />
62
- </button>
63
- ${open ? html`
64
- <${Popover} anchor=${anchor} align="right" width=${200}
65
- onClose=${() => setOpen(false)}>
66
- <div class="session-menu">
67
- <button class="session-menu-item" onClick=${() => { setOpen(false); onOpenEditor(); }}>
60
+ onClick=${() => setOpen((v) => !v)}>
61
+ <${IconMoreVert} />
62
+ </button>
63
+ ${open ? html`
64
+ <${Popover} anchor=${anchor} align="right" width=${200}
65
+ onClose=${() => setOpen(false)}>
66
+ <div class="session-menu">
67
+ <button class="session-menu-item" onClick=${() => { setOpen(false); onOpenEditor(); }}>
68
68
  <${IconExternal} /> Open in editor
69
69
  </button>
70
70
  ${switchableClis.length ? html`
@@ -84,8 +84,8 @@ function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor
84
84
  </button>
85
85
  <button class="session-menu-item danger" onClick=${() => { setOpen(false); onDelete(); }}>
86
86
  <${IconClose} /> Delete
87
- </button>
88
- </div>
87
+ </button>
88
+ </div>
89
89
  </${Popover}>` : null}`;
90
90
  }
91
91
 
@@ -121,17 +121,17 @@ export function SessionsPage() {
121
121
  // Bumps to force the auto-resume effect to re-run on Retry without
122
122
  // mutating any signal. Primitive in the dep array → identity changes.
123
123
  const [retryNonce, setRetryNonce] = useState(0);
124
-
125
- // No session selected → bounce to the Launch page. Done in an effect so
126
- // we don't mutate signals during render. Returning null while the bounce
127
- // is in flight avoids a flash of empty content.
128
- useEffect(() => {
129
- if (!session) selectTab('launch');
130
- }, [session]);
131
-
132
- // Auto-resume when the active session is exited. resumeSession() in
133
- // api.js dedups in-flight calls per session id, so simultaneous fires
134
- // from here and from Sidebar.onClick collapse into one request.
124
+
125
+ // No session selected → bounce to the Launch page. Done in an effect so
126
+ // we don't mutate signals during render. Returning null while the bounce
127
+ // is in flight avoids a flash of empty content.
128
+ useEffect(() => {
129
+ if (!session) selectTab('launch');
130
+ }, [session]);
131
+
132
+ // Auto-resume when the active session is exited. resumeSession() in
133
+ // api.js dedups in-flight calls per session id, so simultaneous fires
134
+ // from here and from Sidebar.onClick collapse into one request.
135
135
  useEffect(() => {
136
136
  if (!session) return;
137
137
  if (session.status === 'running') { setResumeError(null); return; }
@@ -141,16 +141,16 @@ export function SessionsPage() {
141
141
  .then((launched) => { if (launched?.id) selectSession(launched.id); })
142
142
  .catch((e) => { setResumeError(e.message); setToast(e.message, 'error'); });
143
143
  }, [session?.id, session?.status, session?.cliId, session?.manualStopped, retryNonce]);
144
-
145
- if (!session) return null;
146
-
144
+
145
+ if (!session) return null;
146
+
147
147
  const cli = (config.value?.clis || []).find((c) => c.id === session.cliId);
148
148
  const switchableClis = cli
149
149
  ? (config.value?.clis || []).filter((c) => c.id !== cli.id && c.type === cli.type)
150
150
  : [];
151
151
  const running = session.status === 'running';
152
152
  const title = session.title || session.workspace || session.id.slice(0, 12);
153
-
153
+
154
154
  const onResume = async () => {
155
155
  clearResumeFailure(session.id);
156
156
  setResumeError(null);
@@ -180,21 +180,21 @@ export function SessionsPage() {
180
180
  setActionBusy(false);
181
181
  }
182
182
  };
183
- const onRename = async () => {
184
- const next = await ccsmPrompt('Rename session', title, { okLabel: 'Save' });
185
- if (next === null) return;
186
- try { await setSessionTitle(session.id, next.trim()); }
187
- catch (e) { setToast(e.message, 'error'); }
188
- };
189
- const onDelete = async () => {
190
- const ok = await ccsmConfirm(`Delete session ${title}? PTY will be killed if alive.`, {
191
- title: 'Delete session', okLabel: 'Delete', danger: true });
192
- if (!ok) return;
193
- try {
194
- await deleteSession(session.id);
195
- activeSessionId.value = null;
196
- } catch (e) { setToast(e.message, 'error'); }
197
- };
183
+ const onRename = async () => {
184
+ const next = await ccsmPrompt('Rename session', title, { okLabel: 'Save' });
185
+ if (next === null) return;
186
+ try { await setSessionTitle(session.id, next.trim()); }
187
+ catch (e) { setToast(e.message, 'error'); }
188
+ };
189
+ const onDelete = async () => {
190
+ const ok = await ccsmConfirm(`Delete session ${title}? PTY will be killed if alive.`, {
191
+ title: 'Delete session', okLabel: 'Delete', danger: true });
192
+ if (!ok) return;
193
+ try {
194
+ await deleteSession(session.id);
195
+ activeSessionId.value = null;
196
+ } catch (e) { setToast(e.message, 'error'); }
197
+ };
198
198
  const onOpenEditor = async () => {
199
199
  try {
200
200
  const r = await openSessionInEditor(session.id);
@@ -221,14 +221,14 @@ export function SessionsPage() {
221
221
  }
222
222
  } catch (e) { setToast(e.message, 'error'); }
223
223
  };
224
-
225
- return html`
226
- <${PageTitleBar} title=${html`
227
- <span class="session-title-text">${title}</span>
228
- <span class="session-title-meta">
229
- <span class="mono">${session.cwd}</span>
230
- <span>·</span>
231
- <span>${cli ? cli.name : session.cliId}</span>
224
+
225
+ return html`
226
+ <${PageTitleBar} title=${html`
227
+ <span class="session-title-text">${title}</span>
228
+ <span class="session-title-meta">
229
+ <span class="mono">${session.cwd}</span>
230
+ <span>·</span>
231
+ <span>${cli ? cli.name : session.cliId}</span>
232
232
  ${session.repos.length ? html`<span>·</span><span>${session.repos.join(', ')}</span>` : null}
233
233
  <span>·</span>
234
234
  <span>${running ? 'running' : (resumeError ? 'resume failed' : (session.manualStopped ? 'stopped' : 'resuming…'))}</span>
@@ -249,12 +249,12 @@ export function SessionsPage() {
249
249
  onDelete=${onDelete}
250
250
  onOpenEditor=${onOpenEditor}
251
251
  onSwitchCli=${onSwitchCli} />`} />
252
- <div class="session-pane">
253
- <div class="session-pane-body">
254
- ${running
255
- ? html`<${TerminalView} terminalId=${session.id} cliType=${cli?.type} />`
256
- : html`
257
- <div class="terminal-empty">
252
+ <div class="session-pane">
253
+ <div class="session-pane-body">
254
+ ${running
255
+ ? html`<${TerminalView} terminalId=${session.id} cliType=${cli?.type} />`
256
+ : html`
257
+ <div class="terminal-empty">
258
258
  ${resumeError ? html`
259
259
  <div>Failed to resume: <span class="mono">${resumeError}</span></div>
260
260
  <button class="action primary" onClick=${onRetry}>Retry</button>
@@ -266,7 +266,7 @@ export function SessionsPage() {
266
266
  ` : html`
267
267
  <div>Resuming session…</div>
268
268
  `}
269
- </div>`}
270
- </div>
271
- </div>`;
272
- }
269
+ </div>`}
270
+ </div>
271
+ </div>`;
272
+ }