@bakapiano/ccsm 0.22.5 → 0.22.7

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 (59) 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 +279 -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 +177 -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 +547 -553
  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 +28 -9
  42. package/public/js/components/XtermTerminal.js +62 -2
  43. package/public/js/components/useDragSort.js +67 -67
  44. package/public/js/dialog.js +67 -67
  45. package/public/js/icons.js +212 -212
  46. package/public/js/main.js +296 -296
  47. package/public/js/pages/AboutPage.js +90 -90
  48. package/public/js/pages/ConfigurePage.js +728 -713
  49. package/public/js/pages/LaunchPage.js +421 -421
  50. package/public/js/pages/RemotePage.js +743 -743
  51. package/public/js/pages/SessionsPage.js +73 -80
  52. package/public/js/state.js +335 -335
  53. package/scripts/dev.js +149 -149
  54. package/scripts/install.js +153 -153
  55. package/scripts/restart-helper.js +96 -96
  56. package/scripts/upgrade-helper.js +687 -687
  57. package/server.js +1820 -1807
  58. package/public/manifest.webmanifest +0 -25
  59. package/public/setup/index.html +0 -567
@@ -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';
@@ -29,7 +29,7 @@ function SessionTabs({ activeId, openSessions, onActivate, onClose, onReorder, o
29
29
  <div class="session-tabs" role="tablist">
30
30
  <div class="session-tabs-list">
31
31
  ${open.map((s) => {
32
- const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
32
+ const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
33
33
  const Icon = IconForCliType(cli?.type) || IconTerminal;
34
34
  const t = s.title || s.workspace || s.id.slice(0, 12);
35
35
  const isActive = s.id === activeId;
@@ -73,28 +73,28 @@ function SessionTabs({ activeId, openSessions, onActivate, onClose, onReorder, o
73
73
  </button>
74
74
  </div>`;
75
75
  })}
76
- ${/* <button class="session-tab session-tab-add" onClick=${onNew} title="New session">
77
- <${IconPlus} />
78
- </button> */ null}
79
- </div>
80
- ${kebab ? html`<div class="session-tabs-right">${kebab}</div>` : null}
81
- </div>`;
82
- }
83
-
76
+ ${/* <button class="session-tab session-tab-add" onClick=${onNew} title="New session">
77
+ <${IconPlus} />
78
+ </button> */ null}
79
+ </div>
80
+ ${kebab ? html`<div class="session-tabs-right">${kebab}</div>` : null}
81
+ </div>`;
82
+ }
83
+
84
84
  function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor, onSwitchCli }) {
85
85
  const [open, setOpen] = useState(false);
86
86
  const anchor = useRef(null);
87
87
  return html`
88
88
  <button class="session-menu-btn" ref=${anchor}
89
89
  aria-label="Session actions" title="Session actions"
90
- onClick=${() => setOpen((v) => !v)}>
91
- <${IconMoreVert} />
92
- </button>
93
- ${open ? html`
94
- <${Popover} anchor=${anchor} align="right" width=${200}
95
- onClose=${() => setOpen(false)}>
96
- <div class="session-menu">
97
- <button class="session-menu-item" onClick=${() => { setOpen(false); onOpenEditor(); }}>
90
+ onClick=${() => setOpen((v) => !v)}>
91
+ <${IconMoreVert} />
92
+ </button>
93
+ ${open ? html`
94
+ <${Popover} anchor=${anchor} align="right" width=${200}
95
+ onClose=${() => setOpen(false)}>
96
+ <div class="session-menu">
97
+ <button class="session-menu-item" onClick=${() => { setOpen(false); onOpenEditor(); }}>
98
98
  <${IconExternal} /> Open in editor
99
99
  </button>
100
100
  ${switchableClis.length ? html`
@@ -114,8 +114,8 @@ function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor
114
114
  </button>
115
115
  <button class="session-menu-item danger" onClick=${() => { setOpen(false); onDelete(); }}>
116
116
  <${IconClose} /> Delete
117
- </button>
118
- </div>
117
+ </button>
118
+ </div>
119
119
  </${Popover}>` : null}`;
120
120
  }
121
121
 
@@ -146,24 +146,23 @@ export function SessionsPage() {
146
146
  const id = activeSessionId.value;
147
147
  const list = sessions.value;
148
148
  const session = id ? list.find((s) => s.id === id) : null;
149
- const runningSessions = list.filter((s) => s.status === 'running');
150
149
  const [resumeError, setResumeError] = useState(null);
151
150
  const [actionBusy, setActionBusy] = useState(false);
152
151
  const [openTerminalIds, setOpenTerminalIds] = useState(() => new Set());
153
152
  // Bumps to force the auto-resume effect to re-run on Retry without
154
153
  // mutating any signal. Primitive in the dep array → identity changes.
155
154
  const [retryNonce, setRetryNonce] = useState(0);
156
-
157
- // No session selected → bounce to the Launch page. Done in an effect so
158
- // we don't mutate signals during render. Returning null while the bounce
159
- // is in flight avoids a flash of empty content.
160
- useEffect(() => {
161
- if (!session) selectTab('launch');
162
- }, [session]);
163
-
164
- // Auto-resume when the active session is exited. resumeSession() in
165
- // api.js dedups in-flight calls per session id, so simultaneous fires
166
- // from here and from Sidebar.onClick collapse into one request.
155
+
156
+ // No session selected → bounce to the Launch page. Done in an effect so
157
+ // we don't mutate signals during render. Returning null while the bounce
158
+ // is in flight avoids a flash of empty content.
159
+ useEffect(() => {
160
+ if (!session) selectTab('launch');
161
+ }, [session]);
162
+
163
+ // Auto-resume when the active session is exited. resumeSession() in
164
+ // api.js dedups in-flight calls per session id, so simultaneous fires
165
+ // from here and from Sidebar.onClick collapse into one request.
167
166
  useEffect(() => {
168
167
  if (!session) return;
169
168
  if (session.status === 'running') { setResumeError(null); return; }
@@ -175,39 +174,40 @@ export function SessionsPage() {
175
174
  }, [session?.id, session?.status, session?.cliId, session?.manualStopped, retryNonce]);
176
175
 
177
176
  useEffect(() => {
178
- const runningIds = new Set(runningSessions.map((s) => s.id));
177
+ const existingIds = new Set(list.map((s) => s.id));
179
178
  setOpenTerminalIds((prev) => {
180
179
  const next = new Set();
181
180
  let changed = false;
182
181
  for (const sid of prev) {
183
- if (runningIds.has(sid)) {
182
+ if (existingIds.has(sid)) {
184
183
  next.add(sid);
185
184
  } else {
186
185
  changed = true;
187
186
  }
188
187
  }
189
- if (session?.status === 'running' && !next.has(session.id)) {
188
+ if (session?.id && existingIds.has(session.id) && !next.has(session.id)) {
190
189
  next.add(session.id);
191
190
  changed = true;
192
191
  }
193
192
  return changed || next.size !== prev.size ? next : prev;
194
193
  });
195
- }, [list, session?.id, session?.status]);
194
+ }, [list, session?.id]);
196
195
 
197
196
  if (!session) return null;
198
-
197
+
199
198
  const cli = (config.value?.clis || []).find((c) => c.id === session.cliId);
200
199
  const cliForSession = (s) => (config.value?.clis || []).find((c) => c.id === s.cliId);
201
200
  const switchableClis = cli
202
201
  ? (config.value?.clis || []).filter((c) => c.id !== cli.id && c.type === cli.type)
203
202
  : [];
204
203
  const running = session.status === 'running';
205
- const retainedSessions = Array.from(openTerminalIds)
204
+ const openSessions = Array.from(openTerminalIds)
206
205
  .map((sid) => list.find((s) => s.id === sid))
207
- .filter((s) => s && s.status === 'running');
208
- const terminalSessions = running && !retainedSessions.some((s) => s.id === session.id)
209
- ? [...retainedSessions, session]
210
- : retainedSessions;
206
+ .filter(Boolean);
207
+ const tabSessions = session && !openSessions.some((s) => s.id === session.id)
208
+ ? [...openSessions, session]
209
+ : openSessions;
210
+ const terminalSessions = tabSessions.filter((s) => s.status === 'running');
211
211
  const title = session.title || session.workspace || session.id.slice(0, 12);
212
212
 
213
213
  const onCloseTab = (sid) => {
@@ -219,8 +219,8 @@ export function SessionsPage() {
219
219
  });
220
220
 
221
221
  if (sid !== session.id) return;
222
- const currentIndex = terminalSessions.findIndex((s) => s.id === sid);
223
- const remaining = terminalSessions.filter((s) => s.id !== sid);
222
+ const currentIndex = tabSessions.findIndex((s) => s.id === sid);
223
+ const remaining = tabSessions.filter((s) => s.id !== sid);
224
224
  const replacement = currentIndex >= 0
225
225
  ? remaining[Math.min(currentIndex, remaining.length - 1)] || remaining[remaining.length - 1]
226
226
  : remaining[0];
@@ -233,14 +233,14 @@ export function SessionsPage() {
233
233
  };
234
234
 
235
235
  const onReorderTabs = (orderedIds) => {
236
- const runningIds = new Set(runningSessions.map((s) => s.id));
236
+ const existingIds = new Set(list.map((s) => s.id));
237
237
  setOpenTerminalIds((prev) => {
238
238
  const nextIds = [];
239
239
  for (const sid of orderedIds) {
240
- if (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
240
+ if (existingIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
241
241
  }
242
242
  for (const sid of prev) {
243
- if (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
243
+ if (existingIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
244
244
  }
245
245
  return new Set(nextIds);
246
246
  });
@@ -275,21 +275,21 @@ export function SessionsPage() {
275
275
  setActionBusy(false);
276
276
  }
277
277
  };
278
- const onRename = async () => {
279
- const next = await ccsmPrompt('Rename session', title, { okLabel: 'Save' });
280
- if (next === null) return;
281
- try { await setSessionTitle(session.id, next.trim()); }
282
- catch (e) { setToast(e.message, 'error'); }
283
- };
284
- const onDelete = async () => {
285
- const ok = await ccsmConfirm(`Delete session ${title}? PTY will be killed if alive.`, {
286
- title: 'Delete session', okLabel: 'Delete', danger: true });
287
- if (!ok) return;
288
- try {
289
- await deleteSession(session.id);
290
- activeSessionId.value = null;
291
- } catch (e) { setToast(e.message, 'error'); }
292
- };
278
+ const onRename = async () => {
279
+ const next = await ccsmPrompt('Rename session', title, { okLabel: 'Save' });
280
+ if (next === null) return;
281
+ try { await setSessionTitle(session.id, next.trim()); }
282
+ catch (e) { setToast(e.message, 'error'); }
283
+ };
284
+ const onDelete = async () => {
285
+ const ok = await ccsmConfirm(`Delete session ${title}? PTY will be killed if alive.`, {
286
+ title: 'Delete session', okLabel: 'Delete', danger: true });
287
+ if (!ok) return;
288
+ try {
289
+ await deleteSession(session.id);
290
+ activeSessionId.value = null;
291
+ } catch (e) { setToast(e.message, 'error'); }
292
+ };
293
293
  const onOpenEditor = async () => {
294
294
  try {
295
295
  const r = await openSessionInEditor(session.id);
@@ -316,22 +316,15 @@ export function SessionsPage() {
316
316
  }
317
317
  } catch (e) { setToast(e.message, 'error'); }
318
318
  };
319
-
320
- return html`
321
- <${PageTitleBar} title=${html`
322
- <span class="session-title-text">${title}</span>
323
- <span class="session-title-meta">
324
- <span class="mono">${session.cwd}</span>
325
- <span>·</span>
326
- <span>${cli ? cli.name : session.cliId}</span>
327
- ${session.repos.length ? html`<span>·</span><span>${session.repos.join(', ')}</span>` : null}
328
- <span>·</span>
329
- <span>${running ? 'running' : (resumeError ? 'resume failed' : (session.manualStopped ? 'stopped' : 'resuming…'))}</span>
330
- </span>
319
+
320
+ return html`
321
+ <${PageTitleBar} title=${html`
322
+ <span class="session-title-text" title=${title}>${title}</span>
323
+ <span class="session-title-cwd" title=${session.cwd}>${session.cwd}</span>
331
324
  `} />
332
325
  <${SessionTabs}
333
326
  activeId=${session.id}
334
- openSessions=${terminalSessions}
327
+ openSessions=${tabSessions}
335
328
  onActivate=${(sid) => selectSession(sid)}
336
329
  onClose=${onCloseTab}
337
330
  onReorder=${onReorderTabs}