@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.
- package/CLAUDE.md +538 -538
- package/README.md +189 -189
- package/bin/ccsm.js +235 -235
- package/lib/cliActivity.js +139 -139
- package/lib/codexSeed.js +183 -183
- package/lib/config.js +274 -274
- package/lib/devices.js +229 -229
- package/lib/folders.js +124 -124
- package/lib/localCliSessions.js +519 -519
- package/lib/persistedSessions.js +129 -129
- package/lib/tunnel.js +621 -621
- package/lib/webTerminal.js +225 -225
- package/lib/workspace.js +233 -233
- package/package.json +57 -57
- package/public/css/base.css +99 -99
- package/public/css/cards.css +183 -183
- package/public/css/feedback.css +504 -504
- package/public/css/forms.css +453 -453
- package/public/css/layout.css +176 -176
- package/public/css/modal.css +190 -190
- package/public/css/responsive.css +176 -176
- package/public/css/sidebar.css +707 -707
- package/public/css/terminals.css +592 -592
- package/public/css/tokens.css +81 -81
- package/public/css/wco.css +196 -196
- package/public/css/widgets.css +2725 -2725
- package/public/index.html +152 -152
- package/public/js/api.js +371 -371
- package/public/js/backend.js +149 -149
- package/public/js/components/App.js +73 -73
- package/public/js/components/DirectoryPicker.js +203 -203
- package/public/js/components/EntityFormModal.js +153 -153
- package/public/js/components/Modal.js +57 -57
- package/public/js/components/OfflineBanner.js +67 -67
- package/public/js/components/PageTitleBar.js +13 -13
- package/public/js/components/PendingApprovalOverlay.js +128 -128
- package/public/js/components/Picker.js +179 -179
- package/public/js/components/Popover.js +55 -55
- package/public/js/components/RestartOverlay.js +36 -36
- package/public/js/components/Sidebar.js +380 -380
- package/public/js/components/TerminalInstance.js +148 -22
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/XtermTerminal.js +62 -15
- package/public/js/components/useDragSort.js +67 -67
- package/public/js/dialog.js +67 -67
- package/public/js/icons.js +212 -212
- package/public/js/main.js +296 -296
- package/public/js/pages/AboutPage.js +90 -90
- package/public/js/pages/ConfigurePage.js +713 -713
- package/public/js/pages/LaunchPage.js +421 -421
- package/public/js/pages/RemotePage.js +743 -743
- package/public/js/pages/SessionsPage.js +100 -100
- package/public/js/state.js +335 -335
- package/public/manifest.webmanifest +25 -0
- package/public/setup/index.html +567 -0
- package/scripts/dev.js +149 -149
- package/scripts/install.js +153 -153
- package/scripts/restart-helper.js +96 -96
- package/scripts/upgrade-helper.js +687 -687
- 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
|
+
}
|