@bakapiano/ccsm 0.22.3 → 0.22.5
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 +645 -543
- 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 +159 -22
- package/public/js/components/TerminalResizeDebouncer.js +126 -0
- package/public/js/components/TerminalView.js +15 -2
- package/public/js/components/XtermTerminal.js +74 -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 +199 -80
- 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';
|
|
@@ -13,58 +13,88 @@ import { ccsmConfirm, ccsmPrompt } from '../dialog.js';
|
|
|
13
13
|
import { TerminalView } from '../components/TerminalView.js';
|
|
14
14
|
import { PageTitleBar } from '../components/PageTitleBar.js';
|
|
15
15
|
import { Popover } from '../components/Popover.js';
|
|
16
|
+
import { useDragSort } from '../components/useDragSort.js';
|
|
16
17
|
import { IconMoreVert, IconPencil, IconClose, IconPlus, IconForCliType, IconTerminal, IconExternal, IconPlay, IconStop } from '../icons.js';
|
|
17
18
|
import { fmtAgo } from '../util.js';
|
|
18
19
|
|
|
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.
|
|
20
|
+
function SessionTabs({ activeId, openSessions, onActivate, onClose, onReorder, onNew, kebab }) {
|
|
23
21
|
const active = activeId ? sessions.value.find((s) => s.id === activeId) : null;
|
|
24
|
-
|
|
25
|
-
const open =
|
|
22
|
+
const base = Array.isArray(openSessions) ? openSessions : [];
|
|
23
|
+
const open = active && !base.some((s) => s.id === active.id)
|
|
24
|
+
? [...base, active]
|
|
25
|
+
: base;
|
|
26
|
+
const dnd = useDragSort(open.map((s) => s.id), onReorder);
|
|
27
|
+
if (!open.length) return null;
|
|
26
28
|
return html`
|
|
27
29
|
<div class="session-tabs" role="tablist">
|
|
28
30
|
<div class="session-tabs-list">
|
|
29
31
|
${open.map((s) => {
|
|
30
|
-
const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
|
|
32
|
+
const cli = (config.value?.clis || []).find((c) => c.id === s.cliId);
|
|
31
33
|
const Icon = IconForCliType(cli?.type) || IconTerminal;
|
|
32
34
|
const t = s.title || s.workspace || s.id.slice(0, 12);
|
|
33
35
|
const isActive = s.id === activeId;
|
|
36
|
+
const running = s.status === 'running';
|
|
37
|
+
const working = running && s.activity === 'working';
|
|
38
|
+
const statusText = running ? (working ? 'running, working' : 'running') : 'stopped';
|
|
39
|
+
const statusClass = `${running ? ' is-running' : ' is-stopped'}${working ? ' is-working' : ''}`;
|
|
40
|
+
const onKeyDown = (ev) => {
|
|
41
|
+
if (ev.key !== 'Enter' && ev.key !== ' ') return;
|
|
42
|
+
ev.preventDefault();
|
|
43
|
+
onActivate(s.id);
|
|
44
|
+
};
|
|
34
45
|
return html`
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
<div key=${s.id}
|
|
47
|
+
role="tab"
|
|
48
|
+
aria-selected=${isActive}
|
|
49
|
+
aria-label=${`${t}, ${statusText}`}
|
|
50
|
+
tabIndex=${0}
|
|
51
|
+
class=${`session-tab${isActive ? ' is-active' : ''}${statusClass}`}
|
|
52
|
+
data-session-id=${s.id}
|
|
53
|
+
title=${`${t} · ${statusText} · ${s.cwd}`}
|
|
54
|
+
onKeyDown=${onKeyDown}
|
|
55
|
+
...${dnd.rowProps(s.id)}>
|
|
56
|
+
<div class="session-tab-main"
|
|
57
|
+
onClick=${() => onActivate(s.id)}
|
|
58
|
+
...${dnd.handleProps(s.id)}>
|
|
59
|
+
<span class="session-tab-icon"><${Icon} /></span>
|
|
60
|
+
<span class="session-tab-label">${t}</span>
|
|
61
|
+
</div>
|
|
62
|
+
<button class="session-tab-close"
|
|
63
|
+
type="button"
|
|
64
|
+
title="Close tab"
|
|
65
|
+
aria-label=${`Close ${t}`}
|
|
66
|
+
onPointerDown=${(ev) => ev.stopPropagation()}
|
|
67
|
+
onClick=${(ev) => {
|
|
68
|
+
ev.preventDefault();
|
|
69
|
+
ev.stopPropagation();
|
|
70
|
+
onClose(s.id);
|
|
71
|
+
}}>
|
|
72
|
+
<${IconClose} />
|
|
73
|
+
</button>
|
|
74
|
+
</div>`;
|
|
45
75
|
})}
|
|
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
|
-
|
|
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
|
+
|
|
54
84
|
function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor, onSwitchCli }) {
|
|
55
85
|
const [open, setOpen] = useState(false);
|
|
56
86
|
const anchor = useRef(null);
|
|
57
87
|
return html`
|
|
58
88
|
<button class="session-menu-btn" ref=${anchor}
|
|
59
89
|
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(); }}>
|
|
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(); }}>
|
|
68
98
|
<${IconExternal} /> Open in editor
|
|
69
99
|
</button>
|
|
70
100
|
${switchableClis.length ? html`
|
|
@@ -84,8 +114,8 @@ function SessionMenu({ session, switchableClis, onRename, onDelete, onOpenEditor
|
|
|
84
114
|
</button>
|
|
85
115
|
<button class="session-menu-item danger" onClick=${() => { setOpen(false); onDelete(); }}>
|
|
86
116
|
<${IconClose} /> Delete
|
|
87
|
-
</button>
|
|
88
|
-
</div>
|
|
117
|
+
</button>
|
|
118
|
+
</div>
|
|
89
119
|
</${Popover}>` : null}`;
|
|
90
120
|
}
|
|
91
121
|
|
|
@@ -116,22 +146,24 @@ export function SessionsPage() {
|
|
|
116
146
|
const id = activeSessionId.value;
|
|
117
147
|
const list = sessions.value;
|
|
118
148
|
const session = id ? list.find((s) => s.id === id) : null;
|
|
149
|
+
const runningSessions = list.filter((s) => s.status === 'running');
|
|
119
150
|
const [resumeError, setResumeError] = useState(null);
|
|
120
151
|
const [actionBusy, setActionBusy] = useState(false);
|
|
152
|
+
const [openTerminalIds, setOpenTerminalIds] = useState(() => new Set());
|
|
121
153
|
// Bumps to force the auto-resume effect to re-run on Retry without
|
|
122
154
|
// mutating any signal. Primitive in the dep array → identity changes.
|
|
123
155
|
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.
|
|
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.
|
|
135
167
|
useEffect(() => {
|
|
136
168
|
if (!session) return;
|
|
137
169
|
if (session.status === 'running') { setResumeError(null); return; }
|
|
@@ -142,15 +174,78 @@ export function SessionsPage() {
|
|
|
142
174
|
.catch((e) => { setResumeError(e.message); setToast(e.message, 'error'); });
|
|
143
175
|
}, [session?.id, session?.status, session?.cliId, session?.manualStopped, retryNonce]);
|
|
144
176
|
|
|
145
|
-
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const runningIds = new Set(runningSessions.map((s) => s.id));
|
|
179
|
+
setOpenTerminalIds((prev) => {
|
|
180
|
+
const next = new Set();
|
|
181
|
+
let changed = false;
|
|
182
|
+
for (const sid of prev) {
|
|
183
|
+
if (runningIds.has(sid)) {
|
|
184
|
+
next.add(sid);
|
|
185
|
+
} else {
|
|
186
|
+
changed = true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (session?.status === 'running' && !next.has(session.id)) {
|
|
190
|
+
next.add(session.id);
|
|
191
|
+
changed = true;
|
|
192
|
+
}
|
|
193
|
+
return changed || next.size !== prev.size ? next : prev;
|
|
194
|
+
});
|
|
195
|
+
}, [list, session?.id, session?.status]);
|
|
146
196
|
|
|
197
|
+
if (!session) return null;
|
|
198
|
+
|
|
147
199
|
const cli = (config.value?.clis || []).find((c) => c.id === session.cliId);
|
|
200
|
+
const cliForSession = (s) => (config.value?.clis || []).find((c) => c.id === s.cliId);
|
|
148
201
|
const switchableClis = cli
|
|
149
202
|
? (config.value?.clis || []).filter((c) => c.id !== cli.id && c.type === cli.type)
|
|
150
203
|
: [];
|
|
151
204
|
const running = session.status === 'running';
|
|
205
|
+
const retainedSessions = Array.from(openTerminalIds)
|
|
206
|
+
.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;
|
|
152
211
|
const title = session.title || session.workspace || session.id.slice(0, 12);
|
|
153
212
|
|
|
213
|
+
const onCloseTab = (sid) => {
|
|
214
|
+
setOpenTerminalIds((prev) => {
|
|
215
|
+
if (!prev.has(sid)) return prev;
|
|
216
|
+
const next = new Set(prev);
|
|
217
|
+
next.delete(sid);
|
|
218
|
+
return next;
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
if (sid !== session.id) return;
|
|
222
|
+
const currentIndex = terminalSessions.findIndex((s) => s.id === sid);
|
|
223
|
+
const remaining = terminalSessions.filter((s) => s.id !== sid);
|
|
224
|
+
const replacement = currentIndex >= 0
|
|
225
|
+
? remaining[Math.min(currentIndex, remaining.length - 1)] || remaining[remaining.length - 1]
|
|
226
|
+
: remaining[0];
|
|
227
|
+
if (replacement) {
|
|
228
|
+
selectSession(replacement.id);
|
|
229
|
+
} else {
|
|
230
|
+
activeSessionId.value = null;
|
|
231
|
+
selectTab('launch');
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const onReorderTabs = (orderedIds) => {
|
|
236
|
+
const runningIds = new Set(runningSessions.map((s) => s.id));
|
|
237
|
+
setOpenTerminalIds((prev) => {
|
|
238
|
+
const nextIds = [];
|
|
239
|
+
for (const sid of orderedIds) {
|
|
240
|
+
if (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
|
|
241
|
+
}
|
|
242
|
+
for (const sid of prev) {
|
|
243
|
+
if (runningIds.has(sid) && !nextIds.includes(sid)) nextIds.push(sid);
|
|
244
|
+
}
|
|
245
|
+
return new Set(nextIds);
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
154
249
|
const onResume = async () => {
|
|
155
250
|
clearResumeFailure(session.id);
|
|
156
251
|
setResumeError(null);
|
|
@@ -180,21 +275,21 @@ export function SessionsPage() {
|
|
|
180
275
|
setActionBusy(false);
|
|
181
276
|
}
|
|
182
277
|
};
|
|
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
|
-
};
|
|
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
|
+
};
|
|
198
293
|
const onOpenEditor = async () => {
|
|
199
294
|
try {
|
|
200
295
|
const r = await openSessionInEditor(session.id);
|
|
@@ -221,14 +316,14 @@ export function SessionsPage() {
|
|
|
221
316
|
}
|
|
222
317
|
} catch (e) { setToast(e.message, 'error'); }
|
|
223
318
|
};
|
|
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>
|
|
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>
|
|
232
327
|
${session.repos.length ? html`<span>·</span><span>${session.repos.join(', ')}</span>` : null}
|
|
233
328
|
<span>·</span>
|
|
234
329
|
<span>${running ? 'running' : (resumeError ? 'resume failed' : (session.manualStopped ? 'stopped' : 'resuming…'))}</span>
|
|
@@ -236,7 +331,10 @@ export function SessionsPage() {
|
|
|
236
331
|
`} />
|
|
237
332
|
<${SessionTabs}
|
|
238
333
|
activeId=${session.id}
|
|
334
|
+
openSessions=${terminalSessions}
|
|
239
335
|
onActivate=${(sid) => selectSession(sid)}
|
|
336
|
+
onClose=${onCloseTab}
|
|
337
|
+
onReorder=${onReorderTabs}
|
|
240
338
|
onNew=${() => selectTab('launch')}
|
|
241
339
|
kebab=${html`
|
|
242
340
|
<${SessionControls} running=${running}
|
|
@@ -251,9 +349,29 @@ export function SessionsPage() {
|
|
|
251
349
|
onSwitchCli=${onSwitchCli} />`} />
|
|
252
350
|
<div class="session-pane">
|
|
253
351
|
<div class="session-pane-body">
|
|
254
|
-
${
|
|
255
|
-
|
|
256
|
-
|
|
352
|
+
${terminalSessions.length ? html`
|
|
353
|
+
<div class="terminal-stack">
|
|
354
|
+
${terminalSessions.map((s) => {
|
|
355
|
+
const sCli = cliForSession(s);
|
|
356
|
+
const active = running && s.id === session.id;
|
|
357
|
+
return html`
|
|
358
|
+
<div key=${s.id}
|
|
359
|
+
class=${`terminal-layer${active ? ' is-active' : ''}`}
|
|
360
|
+
data-terminal-id=${s.id}
|
|
361
|
+
data-active=${active || null}
|
|
362
|
+
aria-hidden=${!active}>
|
|
363
|
+
<${TerminalView}
|
|
364
|
+
key=${s.id}
|
|
365
|
+
terminalId=${s.id}
|
|
366
|
+
cliType=${sCli?.type}
|
|
367
|
+
visible=${active}
|
|
368
|
+
/>
|
|
369
|
+
</div>`;
|
|
370
|
+
})}
|
|
371
|
+
</div>
|
|
372
|
+
` : null}
|
|
373
|
+
${!running
|
|
374
|
+
? html`
|
|
257
375
|
<div class="terminal-empty">
|
|
258
376
|
${resumeError ? html`
|
|
259
377
|
<div>Failed to resume: <span class="mono">${resumeError}</span></div>
|
|
@@ -266,7 +384,8 @@ export function SessionsPage() {
|
|
|
266
384
|
` : html`
|
|
267
385
|
<div>Resuming session…</div>
|
|
268
386
|
`}
|
|
269
|
-
</div>`
|
|
387
|
+
</div>`
|
|
388
|
+
: null}
|
|
270
389
|
</div>
|
|
271
390
|
</div>`;
|
|
272
391
|
}
|