@bakapiano/ccsm 0.22.4 → 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.5",
|
|
4
4
|
"description": "Claude Code Session Manager — Windows web UI to manage many concurrent claude sessions: live list, snapshot/restore, focus existing window, new session in an isolated workspace with repo clones",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
package/public/css/terminals.css
CHANGED
|
@@ -292,45 +292,118 @@
|
|
|
292
292
|
.page-title-bar + .session-tabs {
|
|
293
293
|
margin-top: calc(-1 * var(--s-4));
|
|
294
294
|
}
|
|
295
|
-
.session-tab {
|
|
296
|
-
appearance: none;
|
|
297
|
-
background: var(--term-tab);
|
|
298
|
-
border: 0;
|
|
299
|
-
border-bottom: 2px solid transparent;
|
|
300
|
-
margin-bottom: -1px; /* overlap container border-bottom */
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
font:
|
|
306
|
-
font-size: 12px;
|
|
295
|
+
.session-tab {
|
|
296
|
+
appearance: none;
|
|
297
|
+
background: var(--term-tab);
|
|
298
|
+
border: 0;
|
|
299
|
+
border-bottom: 2px solid transparent;
|
|
300
|
+
margin-bottom: -1px; /* overlap container border-bottom */
|
|
301
|
+
display: inline-flex;
|
|
302
|
+
align-items: center;
|
|
303
|
+
gap: 6px;
|
|
304
|
+
font: inherit;
|
|
305
|
+
font-size: 12px;
|
|
307
306
|
color: var(--term-tab-text);
|
|
308
|
-
cursor: pointer;
|
|
309
|
-
max-width: 200px;
|
|
310
|
-
min-width: 0;
|
|
311
|
-
transition: background-color .12s, color .12s;
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.session-tab-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
328
|
-
.session-tab
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
307
|
+
cursor: pointer;
|
|
308
|
+
max-width: 200px;
|
|
309
|
+
min-width: 0;
|
|
310
|
+
transition: background-color .12s, color .12s;
|
|
311
|
+
user-select: none;
|
|
312
|
+
position: relative;
|
|
313
|
+
}
|
|
314
|
+
.session-tab:hover { background: var(--term-tab-hover); color: var(--term-on); }
|
|
315
|
+
.session-tab.is-active {
|
|
316
|
+
background: var(--term-surface);
|
|
317
|
+
color: var(--term-on);
|
|
318
|
+
border-bottom-color: var(--term-surface);
|
|
319
|
+
}
|
|
320
|
+
.session-tab:focus-visible {
|
|
321
|
+
outline: 1px solid var(--accent);
|
|
322
|
+
outline-offset: -2px;
|
|
323
|
+
}
|
|
324
|
+
.session-tab[data-dnd-over="true"] {
|
|
325
|
+
box-shadow: inset 2px 0 0 var(--accent);
|
|
326
|
+
}
|
|
327
|
+
.session-tab::before {
|
|
328
|
+
content: "";
|
|
329
|
+
position: absolute;
|
|
330
|
+
left: 3px;
|
|
331
|
+
top: 0;
|
|
332
|
+
bottom: 0;
|
|
333
|
+
width: 3px;
|
|
334
|
+
border-radius: 0;
|
|
335
|
+
background: var(--ink-faint);
|
|
336
|
+
opacity: .55;
|
|
337
|
+
}
|
|
338
|
+
.session-tab.is-running::before {
|
|
339
|
+
background: var(--green);
|
|
340
|
+
opacity: .9;
|
|
341
|
+
}
|
|
342
|
+
.session-tab.is-working::before {
|
|
343
|
+
background: var(--blue, #4a73a5);
|
|
344
|
+
opacity: .95;
|
|
345
|
+
}
|
|
346
|
+
.session-tab.is-stopped::before {
|
|
347
|
+
background: var(--ink-faint);
|
|
348
|
+
opacity: .45;
|
|
349
|
+
}
|
|
350
|
+
.session-tab-main {
|
|
351
|
+
display: inline-flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
gap: 6px;
|
|
354
|
+
min-width: 0;
|
|
355
|
+
flex: 1 1 auto;
|
|
356
|
+
height: 100%;
|
|
357
|
+
padding: 0 0 0 14px;
|
|
358
|
+
}
|
|
359
|
+
.session-tab-main[draggable="true"] {
|
|
360
|
+
cursor: grab;
|
|
361
|
+
}
|
|
362
|
+
.session-tab-main[draggable="true"]:active {
|
|
363
|
+
cursor: grabbing;
|
|
364
|
+
}
|
|
365
|
+
.session-tab-icon { display: inline-flex; flex-shrink: 0; }
|
|
366
|
+
.session-tab-icon svg { width: 14px; height: 14px; }
|
|
367
|
+
.session-tab-icon img { width: 14px; height: 14px; }
|
|
368
|
+
.session-tab-label {
|
|
369
|
+
white-space: nowrap;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
text-overflow: ellipsis;
|
|
372
|
+
min-width: 0;
|
|
373
|
+
}
|
|
374
|
+
.session-tab-close {
|
|
375
|
+
appearance: none;
|
|
376
|
+
border: 0;
|
|
377
|
+
background: transparent;
|
|
378
|
+
color: currentColor;
|
|
379
|
+
display: inline-flex;
|
|
380
|
+
align-items: center;
|
|
381
|
+
justify-content: center;
|
|
382
|
+
width: 18px;
|
|
383
|
+
height: 18px;
|
|
384
|
+
margin-right: 5px;
|
|
385
|
+
border-radius: 3px;
|
|
386
|
+
opacity: 0;
|
|
387
|
+
flex: 0 0 auto;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
.session-tab:hover .session-tab-close,
|
|
391
|
+
.session-tab.is-active .session-tab-close,
|
|
392
|
+
.session-tab:focus-within .session-tab-close {
|
|
393
|
+
opacity: .72;
|
|
394
|
+
}
|
|
395
|
+
.session-tab-close:hover {
|
|
396
|
+
opacity: 1;
|
|
397
|
+
background: rgba(255, 255, 255, 0.12);
|
|
398
|
+
}
|
|
399
|
+
.session-tab-close svg {
|
|
400
|
+
width: 12px;
|
|
401
|
+
height: 12px;
|
|
402
|
+
}
|
|
403
|
+
.session-tab-add {
|
|
404
|
+
background: transparent;
|
|
405
|
+
max-width: none;
|
|
406
|
+
padding: 0 8px;
|
|
334
407
|
color: #fff;
|
|
335
408
|
}
|
|
336
409
|
.session-tab-add:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
@@ -472,14 +545,43 @@
|
|
|
472
545
|
gap: var(--s-2);
|
|
473
546
|
flex-shrink: 0;
|
|
474
547
|
}
|
|
475
|
-
.session-pane-body {
|
|
476
|
-
flex: 1;
|
|
477
|
-
min-height: 0;
|
|
478
|
-
background: var(--term-surface);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
548
|
+
.session-pane-body {
|
|
549
|
+
flex: 1;
|
|
550
|
+
min-height: 0;
|
|
551
|
+
background: var(--term-surface);
|
|
552
|
+
position: relative;
|
|
553
|
+
overflow: hidden;
|
|
554
|
+
}
|
|
555
|
+
.terminal-stack {
|
|
556
|
+
position: absolute;
|
|
557
|
+
inset: 0;
|
|
558
|
+
min-width: 0;
|
|
559
|
+
min-height: 0;
|
|
560
|
+
}
|
|
561
|
+
.terminal-layer {
|
|
562
|
+
position: absolute;
|
|
563
|
+
inset: 0;
|
|
564
|
+
min-width: 0;
|
|
565
|
+
min-height: 0;
|
|
566
|
+
display: flex;
|
|
567
|
+
flex-direction: column;
|
|
568
|
+
visibility: hidden;
|
|
569
|
+
pointer-events: none;
|
|
570
|
+
z-index: 0;
|
|
571
|
+
}
|
|
572
|
+
.terminal-layer.is-active {
|
|
573
|
+
visibility: visible;
|
|
574
|
+
pointer-events: auto;
|
|
575
|
+
z-index: 1;
|
|
576
|
+
}
|
|
577
|
+
.session-pane-body .terminal-host {
|
|
578
|
+
height: 100%;
|
|
579
|
+
}
|
|
580
|
+
.terminal-layer .terminal-host {
|
|
581
|
+
flex: 1 1 auto;
|
|
582
|
+
min-height: 0;
|
|
583
|
+
height: auto;
|
|
584
|
+
}
|
|
483
585
|
.session-pane-body .terminal-empty {
|
|
484
586
|
background: var(--term-surface);
|
|
485
587
|
color: var(--term-on);
|
|
@@ -63,6 +63,14 @@ export class TerminalInstance {
|
|
|
63
63
|
this.xterm.applyResolvedTheme();
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
focus() {
|
|
67
|
+
this.xterm.focus();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
blur() {
|
|
71
|
+
this.xterm.blur();
|
|
72
|
+
}
|
|
73
|
+
|
|
66
74
|
layout(width, height, immediate = false) {
|
|
67
75
|
const layoutDimensions = this._resolveLayoutDimensions(width, height);
|
|
68
76
|
if (!layoutDimensions) return null;
|
|
@@ -361,6 +369,9 @@ export class TerminalInstance {
|
|
|
361
369
|
this._beginReplay();
|
|
362
370
|
this.xterm.write(data, () => {
|
|
363
371
|
this._endReplay();
|
|
372
|
+
if (this.isVisible) {
|
|
373
|
+
this.scheduleLayout({ immediate: true, retries: true, forceRedraw: true });
|
|
374
|
+
}
|
|
364
375
|
});
|
|
365
376
|
}
|
|
366
377
|
|
|
@@ -8,7 +8,7 @@ import { themeMode } from '../state.js';
|
|
|
8
8
|
import { TerminalKeyBar } from './TerminalKeyBar.js';
|
|
9
9
|
import { TerminalInstance } from './TerminalInstance.js';
|
|
10
10
|
|
|
11
|
-
export function TerminalView({ terminalId, cliType }) {
|
|
11
|
+
export function TerminalView({ terminalId, cliType, visible = true }) {
|
|
12
12
|
const hostRef = useRef(null);
|
|
13
13
|
const instanceRef = useRef(null);
|
|
14
14
|
const [displaced, setDisplaced] = useState(false);
|
|
@@ -38,6 +38,8 @@ export function TerminalView({ terminalId, cliType }) {
|
|
|
38
38
|
});
|
|
39
39
|
instanceRef.current = instance;
|
|
40
40
|
instance.attachToElement(host);
|
|
41
|
+
instance.setVisible(visible);
|
|
42
|
+
if (visible) instance.focus();
|
|
41
43
|
|
|
42
44
|
return () => {
|
|
43
45
|
if (instanceRef.current === instance) instanceRef.current = null;
|
|
@@ -49,6 +51,17 @@ export function TerminalView({ terminalId, cliType }) {
|
|
|
49
51
|
instanceRef.current?.setCliType(cliType);
|
|
50
52
|
}, [cliType, terminalId, reattachNonce]);
|
|
51
53
|
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
const instance = instanceRef.current;
|
|
56
|
+
if (!instance) return;
|
|
57
|
+
instance.setVisible(visible);
|
|
58
|
+
if (visible) {
|
|
59
|
+
instance.focus();
|
|
60
|
+
} else {
|
|
61
|
+
instance.blur();
|
|
62
|
+
}
|
|
63
|
+
}, [visible, terminalId, reattachNonce]);
|
|
64
|
+
|
|
52
65
|
if (!terminalId) {
|
|
53
66
|
return html`<div class="terminal-empty">Select a terminal on the left, or launch a new one.</div>`;
|
|
54
67
|
}
|
|
@@ -80,6 +93,6 @@ export function TerminalView({ terminalId, cliType }) {
|
|
|
80
93
|
return html`
|
|
81
94
|
<${Fragment}>
|
|
82
95
|
<div key="host" ref=${hostRef} class="terminal-host"></div>
|
|
83
|
-
|
|
96
|
+
${visible ? html`<${TerminalKeyBar} send=${sendInput} cliType=${cliType} />` : null}
|
|
84
97
|
</${Fragment}>`;
|
|
85
98
|
}
|
|
@@ -99,6 +99,7 @@ export class XtermTerminal {
|
|
|
99
99
|
attachToElement(host) {
|
|
100
100
|
this.host = host;
|
|
101
101
|
this.raw.open(host);
|
|
102
|
+
host.xterm = this.raw;
|
|
102
103
|
this._enableWebglRenderer();
|
|
103
104
|
try {
|
|
104
105
|
document.fonts?.ready?.then(() => {
|
|
@@ -195,6 +196,14 @@ export class XtermTerminal {
|
|
|
195
196
|
try { this.raw.focus(); } catch {}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
blur() {
|
|
200
|
+
try {
|
|
201
|
+
if (this.helperTextarea && document.activeElement === this.helperTextarea) {
|
|
202
|
+
this.helperTextarea.blur();
|
|
203
|
+
}
|
|
204
|
+
} catch {}
|
|
205
|
+
}
|
|
206
|
+
|
|
198
207
|
onData(listener) {
|
|
199
208
|
return this.raw.onData(listener);
|
|
200
209
|
}
|
|
@@ -208,6 +217,9 @@ export class XtermTerminal {
|
|
|
208
217
|
}
|
|
209
218
|
|
|
210
219
|
dispose() {
|
|
220
|
+
if (this.host?.xterm === this.raw) {
|
|
221
|
+
try { delete this.host.xterm; } catch { this.host.xterm = undefined; }
|
|
222
|
+
}
|
|
211
223
|
this.host = null;
|
|
212
224
|
this._disposeWebglRenderer(false);
|
|
213
225
|
this.refreshDimensionListeners.clear();
|
|
@@ -13,36 +13,66 @@ 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
|
-
function SessionTabs({ activeId, onActivate, onNew, kebab }) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
return
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
|
|
20
|
+
function SessionTabs({ activeId, openSessions, onActivate, onClose, onReorder, onNew, kebab }) {
|
|
21
|
+
const active = activeId ? sessions.value.find((s) => s.id === activeId) : null;
|
|
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;
|
|
28
|
+
return html`
|
|
29
|
+
<div class="session-tabs" role="tablist">
|
|
30
|
+
<div class="session-tabs-list">
|
|
31
|
+
${open.map((s) => {
|
|
30
32
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
const Icon = IconForCliType(cli?.type) || IconTerminal;
|
|
34
|
+
const t = s.title || s.workspace || s.id.slice(0, 12);
|
|
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
|
+
};
|
|
45
|
+
return html`
|
|
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>`;
|
|
75
|
+
})}
|
|
46
76
|
${/* <button class="session-tab session-tab-add" onClick=${onNew} title="New session">
|
|
47
77
|
<${IconPlus} />
|
|
48
78
|
</button> */ null}
|
|
@@ -116,8 +146,10 @@ 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);
|
|
@@ -141,16 +173,79 @@ export function SessionsPage() {
|
|
|
141
173
|
.then((launched) => { if (launched?.id) selectSession(launched.id); })
|
|
142
174
|
.catch((e) => { setResumeError(e.message); setToast(e.message, 'error'); });
|
|
143
175
|
}, [session?.id, session?.status, session?.cliId, session?.manualStopped, retryNonce]);
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
|
|
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]);
|
|
196
|
+
|
|
197
|
+
if (!session) return null;
|
|
146
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);
|
|
@@ -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}
|
|
@@ -249,12 +347,32 @@ export function SessionsPage() {
|
|
|
249
347
|
onDelete=${onDelete}
|
|
250
348
|
onOpenEditor=${onOpenEditor}
|
|
251
349
|
onSwitchCli=${onSwitchCli} />`} />
|
|
252
|
-
<div class="session-pane">
|
|
253
|
-
<div class="session-pane-body">
|
|
254
|
-
${
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
350
|
+
<div class="session-pane">
|
|
351
|
+
<div class="session-pane-body">
|
|
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`
|
|
375
|
+
<div class="terminal-empty">
|
|
258
376
|
${resumeError ? html`
|
|
259
377
|
<div>Failed to resume: <span class="mono">${resumeError}</span></div>
|
|
260
378
|
<button class="action primary" onClick=${onRetry}>Retry</button>
|
|
@@ -266,7 +384,8 @@ export function SessionsPage() {
|
|
|
266
384
|
` : html`
|
|
267
385
|
<div>Resuming session…</div>
|
|
268
386
|
`}
|
|
269
|
-
</div>`
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
387
|
+
</div>`
|
|
388
|
+
: null}
|
|
389
|
+
</div>
|
|
390
|
+
</div>`;
|
|
391
|
+
}
|