@bakapiano/ccsm 0.17.9 → 0.17.11
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 +1 -1
- package/public/css/layout.css +6 -3
- package/public/css/terminals.css +122 -31
- package/public/css/wco.css +67 -11
- package/public/js/icons.js +5 -0
- package/public/js/main.js +1 -2
- package/public/js/pages/SessionsPage.js +75 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bakapiano/ccsm",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.11",
|
|
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/layout.css
CHANGED
|
@@ -98,12 +98,15 @@ body.is-resizing-sidebar .app {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/* Per-page title strip — height matches the sidebar collapse-toggle
|
|
101
|
-
(28px) so the topmost row of the window reads as one unified band.
|
|
101
|
+
(28px) so the topmost row of the window reads as one unified band.
|
|
102
|
+
Title on the left, actions slot on the right. The WCO padding-right
|
|
103
|
+
reservation (wco.css) shifts actions inward so they clear the OS
|
|
104
|
+
window controls overlay. */
|
|
102
105
|
.page-title-bar {
|
|
103
106
|
display: flex;
|
|
104
|
-
flex-direction: row
|
|
107
|
+
flex-direction: row;
|
|
105
108
|
align-items: center;
|
|
106
|
-
justify-content:
|
|
109
|
+
justify-content: space-between;
|
|
107
110
|
gap: var(--s-3);
|
|
108
111
|
height: calc(40px * var(--anti-zoom, 1));
|
|
109
112
|
min-height: calc(40px * var(--anti-zoom, 1));
|
package/public/css/terminals.css
CHANGED
|
@@ -226,55 +226,146 @@
|
|
|
226
226
|
min-height: 0;
|
|
227
227
|
height: 100%;
|
|
228
228
|
margin: 0 calc(-1 * var(--s-4));
|
|
229
|
-
/* Cancel the tab-panel gap above so the terminal sits flush under the
|
|
230
|
-
title bar with no white band. */
|
|
231
|
-
margin-top: calc(-1 * var(--s-4));
|
|
232
229
|
background: var(--bg-elev);
|
|
233
230
|
overflow: hidden;
|
|
234
231
|
}
|
|
235
|
-
|
|
232
|
+
|
|
233
|
+
/* Session tabs · sits between the page-title-bar and the session-pane.
|
|
234
|
+
One row of small tab buttons; the active one has a darker bg + accent
|
|
235
|
+
underline. Last child is a "+" button that bounces to /launch. The
|
|
236
|
+
strip is full-bleed (matches .session-pane horizontal extents). */
|
|
237
|
+
.session-tabs {
|
|
236
238
|
display: flex;
|
|
237
239
|
align-items: stretch;
|
|
238
|
-
justify-content: flex-start;
|
|
239
240
|
gap: 0;
|
|
240
|
-
height:
|
|
241
|
-
min-height: calc(24px * var(--anti-zoom, 1));
|
|
242
|
-
max-height: calc(24px * var(--anti-zoom, 1));
|
|
241
|
+
height: 30px;
|
|
243
242
|
flex-shrink: 0;
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
padding: 0 2px 0 0;
|
|
244
|
+
/* Negative bottom margin cancels the tab-panel's gap to the
|
|
245
|
+
session-pane underneath, so the strip sits flush against the
|
|
246
|
+
terminal. Negative horizontal margin cancels .main's padding so
|
|
247
|
+
the strip is full-bleed like the terminal underneath. */
|
|
248
|
+
margin: 0 calc(-1 * var(--s-4)) calc(-1 * var(--s-4));
|
|
249
|
+
background: #2d2a26;
|
|
250
|
+
border-bottom: 0;
|
|
251
|
+
}
|
|
252
|
+
.session-tabs-list {
|
|
253
|
+
display: flex;
|
|
254
|
+
align-items: stretch;
|
|
255
|
+
gap: 0;
|
|
256
|
+
flex: 1;
|
|
257
|
+
min-width: 0;
|
|
258
|
+
overflow: hidden;
|
|
248
259
|
}
|
|
249
|
-
.session-
|
|
250
|
-
display:
|
|
260
|
+
.session-tabs-right {
|
|
261
|
+
display: flex;
|
|
251
262
|
align-items: center;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
263
|
+
flex-shrink: 0;
|
|
264
|
+
padding-right: 2px;
|
|
265
|
+
}
|
|
266
|
+
/* Close the gap to the page-title-bar above — only when there IS one.
|
|
267
|
+
In standalone PWA the title-bar is display:none and .session-tabs is
|
|
268
|
+
the first tab-panel child; a negative margin-top there would push
|
|
269
|
+
the strip up over the OS title-bar border. */
|
|
270
|
+
.page-title-bar + .session-tabs {
|
|
271
|
+
margin-top: calc(-1 * var(--s-4));
|
|
272
|
+
}
|
|
273
|
+
.session-tab {
|
|
274
|
+
appearance: none;
|
|
275
|
+
background: #423d37;
|
|
260
276
|
border: 0;
|
|
261
|
-
border-
|
|
277
|
+
border-bottom: 2px solid transparent;
|
|
278
|
+
margin-bottom: -1px; /* overlap container border-bottom */
|
|
279
|
+
padding: 0 10px;
|
|
280
|
+
display: inline-flex;
|
|
281
|
+
align-items: center;
|
|
282
|
+
gap: 6px;
|
|
283
|
+
font: inherit;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
color: rgba(255, 255, 255, 0.65);
|
|
262
286
|
cursor: pointer;
|
|
287
|
+
max-width: 200px;
|
|
288
|
+
min-width: 0;
|
|
263
289
|
transition: background-color .12s, color .12s;
|
|
264
290
|
}
|
|
265
|
-
.session-
|
|
291
|
+
.session-tab:hover { background: #4f4942; color: #fff; }
|
|
292
|
+
.session-tab.is-active {
|
|
293
|
+
background: var(--ink);
|
|
266
294
|
color: #fff;
|
|
267
|
-
|
|
295
|
+
border-bottom-color: var(--ink);
|
|
268
296
|
}
|
|
269
|
-
.session-
|
|
297
|
+
.session-tab-icon { display: inline-flex; flex-shrink: 0; }
|
|
298
|
+
.session-tab-icon svg { width: 14px; height: 14px; }
|
|
299
|
+
.session-tab-icon img { width: 14px; height: 14px; }
|
|
300
|
+
.session-tab-label {
|
|
301
|
+
white-space: nowrap;
|
|
302
|
+
overflow: hidden;
|
|
303
|
+
text-overflow: ellipsis;
|
|
304
|
+
min-width: 0;
|
|
305
|
+
}
|
|
306
|
+
.session-tab-meta { color: rgba(255, 255, 255, 0.5); font-size: 11px; }
|
|
307
|
+
.session-tab.is-active .session-tab-meta { color: rgba(255, 255, 255, 0.6); }
|
|
308
|
+
.session-tab-add {
|
|
309
|
+
background: transparent;
|
|
310
|
+
max-width: none;
|
|
311
|
+
padding: 0 8px;
|
|
270
312
|
color: #fff;
|
|
271
|
-
background: rgba(0, 0, 0, 0.22);
|
|
272
313
|
}
|
|
273
|
-
.session-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
314
|
+
.session-tab-add:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
315
|
+
.session-tab-add svg { width: 14px; height: 14px; }
|
|
316
|
+
|
|
317
|
+
/* Kebab in the page-title-bar (top-right). Compact 24px square so it
|
|
318
|
+
doesn't dominate the masthead. In WCO mode the title-bar already
|
|
319
|
+
reserves padding-right for OS controls, so this slides cleanly to
|
|
320
|
+
the left of them. */
|
|
321
|
+
.session-menu-btn {
|
|
322
|
+
appearance: none;
|
|
323
|
+
background: transparent;
|
|
324
|
+
border: 0;
|
|
325
|
+
width: 26px;
|
|
326
|
+
height: 26px;
|
|
327
|
+
border-radius: 5px;
|
|
328
|
+
display: inline-flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
justify-content: center;
|
|
331
|
+
color: #fff;
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
flex-shrink: 0;
|
|
334
|
+
transition: background-color .12s, color .12s;
|
|
335
|
+
}
|
|
336
|
+
.session-menu-btn:hover { background: rgba(255, 255, 255, 0.1); color: #fff; }
|
|
337
|
+
.session-menu-btn svg { width: 16px; height: 16px; }
|
|
338
|
+
|
|
339
|
+
.session-menu {
|
|
340
|
+
background: var(--bg-elev);
|
|
341
|
+
border: 1px solid var(--border);
|
|
342
|
+
border-radius: 6px;
|
|
343
|
+
padding: 4px;
|
|
344
|
+
box-shadow: var(--shadow-md, 0 4px 16px rgba(0,0,0,0.08));
|
|
345
|
+
display: flex;
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
gap: 2px;
|
|
277
348
|
}
|
|
349
|
+
.session-menu-item {
|
|
350
|
+
appearance: none;
|
|
351
|
+
background: transparent;
|
|
352
|
+
border: 0;
|
|
353
|
+
padding: 7px 10px;
|
|
354
|
+
border-radius: 4px;
|
|
355
|
+
display: flex;
|
|
356
|
+
align-items: center;
|
|
357
|
+
gap: 8px;
|
|
358
|
+
font: inherit;
|
|
359
|
+
font-size: 13px;
|
|
360
|
+
color: var(--ink);
|
|
361
|
+
cursor: pointer;
|
|
362
|
+
text-align: left;
|
|
363
|
+
}
|
|
364
|
+
.session-menu-item:hover { background: var(--bg); }
|
|
365
|
+
.session-menu-item.danger { color: var(--danger, #b73f3f); }
|
|
366
|
+
.session-menu-item.danger:hover { background: rgba(183, 63, 63, 0.08); }
|
|
367
|
+
.session-menu-item svg { width: 14px; height: 14px; }
|
|
368
|
+
|
|
278
369
|
.session-pane-head {
|
|
279
370
|
display: flex;
|
|
280
371
|
align-items: center;
|
package/public/css/wco.css
CHANGED
|
@@ -80,8 +80,34 @@ body.is-app:not(.is-wco) .sidebar-top {
|
|
|
80
80
|
body.is-app:not(.is-wco) .page-title-bar {
|
|
81
81
|
display: none;
|
|
82
82
|
}
|
|
83
|
+
/* terminals.css uses `.page-title-bar + .session-tabs { margin-top: -s-4 }`
|
|
84
|
+
to flush the tab strip against the in-page title-bar. Adjacent-sibling
|
|
85
|
+
selectors match by DOM order regardless of display, so the rule still
|
|
86
|
+
pulls session-tabs up by 16px even when the title-bar is hidden — the
|
|
87
|
+
top half of the tabs ends up clipped above the viewport / under the OS
|
|
88
|
+
title bar. Reset to 0 in standalone PWA. */
|
|
89
|
+
body.is-app:not(.is-wco) .page-title-bar + .session-tabs {
|
|
90
|
+
margin-top: 0;
|
|
91
|
+
}
|
|
83
92
|
body.is-app:not(.is-wco) .app {
|
|
84
|
-
|
|
93
|
+
/* Hairline separator under the OS title bar. Has to sit ABOVE the
|
|
94
|
+
sidebar's own background — an inset box-shadow on .app gets covered
|
|
95
|
+
by .sidebar (which paints var(--ui-bg) across its full area inside
|
|
96
|
+
.app), so the line was invisible. An absolutely-positioned ::before
|
|
97
|
+
paints over both columns; it's outside the grid track so it doesn't
|
|
98
|
+
reintroduce the 1px overflow that a real `border-top: 1px` did. */
|
|
99
|
+
position: relative;
|
|
100
|
+
}
|
|
101
|
+
body.is-app:not(.is-wco) .app::before {
|
|
102
|
+
content: "";
|
|
103
|
+
position: absolute;
|
|
104
|
+
top: 0;
|
|
105
|
+
left: 0;
|
|
106
|
+
right: 0;
|
|
107
|
+
height: 1px;
|
|
108
|
+
background: var(--border);
|
|
109
|
+
z-index: 10;
|
|
110
|
+
pointer-events: none;
|
|
85
111
|
}
|
|
86
112
|
/* With page-title-bar hidden, session-pane's `margin-top: calc(-1 *
|
|
87
113
|
var(--s-4))` — designed to flush it against the (now invisible)
|
|
@@ -93,13 +119,20 @@ body.is-app:not(.is-wco) .session-pane {
|
|
|
93
119
|
margin-top: 0;
|
|
94
120
|
height: auto;
|
|
95
121
|
}
|
|
96
|
-
/* Settings
|
|
97
|
-
would otherwise butt straight against the OS
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
body.is-app:not(.is-wco) [data-panel="configure"]
|
|
122
|
+
/* Settings + Launch · with the in-page title-bar hidden in standalone
|
|
123
|
+
mode, their content would otherwise butt straight against the OS
|
|
124
|
+
title-bar border. Give a little breathing room. Sessions has its
|
|
125
|
+
own full-bleed terminal pane; About has its own hero header. */
|
|
126
|
+
body.is-app:not(.is-wco) [data-panel="configure"],
|
|
127
|
+
body.is-app:not(.is-wco) [data-panel="launch"] {
|
|
101
128
|
padding-top: var(--s-4);
|
|
102
129
|
}
|
|
130
|
+
/* Sidebar nav rows (New Session / Settings) also need a top gap in
|
|
131
|
+
standalone — body.is-app zeros .sidebar's padding-top so the in-page
|
|
132
|
+
title-bar can sit flush, but with that title-bar gone in standalone
|
|
133
|
+
mode the nav buttons end up jammed against the OS title-bar border.
|
|
134
|
+
Restore a small inset so they read as a real nav, not as overflow. */
|
|
135
|
+
body.is-app:not(.is-wco) .sidebar { padding-top: var(--s-3); }
|
|
103
136
|
|
|
104
137
|
/* WCO mode only: the browser has hidden its own title bar and floats OS
|
|
105
138
|
controls (min/max/close) over our content top-right. Our 34px top band
|
|
@@ -116,16 +149,39 @@ body.is-wco .page-title-bar {
|
|
|
116
149
|
}
|
|
117
150
|
body.is-wco .page-title-bar,
|
|
118
151
|
body.is-wco .sidebar-top {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
152
|
+
/* env(titlebar-area-height) is Chromium's reported safe-area height, but
|
|
153
|
+
in practice Edge on Windows often paints the OS controls overlay a few
|
|
154
|
+
px taller than that value — so sizing our top band to exactly env()
|
|
155
|
+
leaves a sliver of the strip BELOW (session-tabs) still under the
|
|
156
|
+
overlay and the kebab gets clipped. Take the max with 40px so we
|
|
157
|
+
always reserve at least the default page-title-bar height regardless
|
|
158
|
+
of what env() reports. Matches non-WCO modes too. */
|
|
159
|
+
height: calc(max(40px, env(titlebar-area-height, 40px)) * var(--anti-zoom, 1));
|
|
160
|
+
min-height: calc(max(40px, env(titlebar-area-height, 40px)) * var(--anti-zoom, 1));
|
|
161
|
+
max-height: calc(max(40px, env(titlebar-area-height, 40px)) * var(--anti-zoom, 1));
|
|
122
162
|
}
|
|
123
163
|
body.is-wco .sidebar-brand,
|
|
124
164
|
body.is-wco .sidebar-brand-button,
|
|
125
165
|
body.is-wco .collapse-toggle {
|
|
126
|
-
height:
|
|
127
|
-
min-height:
|
|
166
|
+
height: max(40px, env(titlebar-area-height, 40px));
|
|
167
|
+
min-height: max(40px, env(titlebar-area-height, 40px));
|
|
128
168
|
}
|
|
169
|
+
/* terminals.css uses the .tab-panel's gap (s-4) plus a -s-4 margin-top on
|
|
170
|
+
.session-tabs to close that gap, so the tab strip visually flushes
|
|
171
|
+
against the page-title-bar above. In WCO the negative margin pulls the
|
|
172
|
+
tabs row UP by 16px — i.e. its top edge lands ABOVE the OS overlay's
|
|
173
|
+
bottom edge, and the kebab inside gets clipped by the floating window
|
|
174
|
+
controls. Cancel both the gap and the negative margin here so the tabs
|
|
175
|
+
row sits at exactly y=titlebar-area-height (flush below the overlay)
|
|
176
|
+
without losing flushness against the title-bar. */
|
|
177
|
+
body.is-wco .tab-panel { gap: 0; }
|
|
178
|
+
body.is-wco .page-title-bar + .session-tabs { margin-top: 0; }
|
|
179
|
+
/* terminals.css also gives .session-tabs `margin-bottom: -s-4` to close
|
|
180
|
+
the gap to session-pane below — but with tab-panel gap now 0 in WCO
|
|
181
|
+
that negative margin pulls session-pane UP 16px instead, overlapping
|
|
182
|
+
the bottom of the tab labels (kebab + tab text get clipped from
|
|
183
|
+
below). Zero it out too. */
|
|
184
|
+
body.is-wco .session-tabs { margin-bottom: 0; }
|
|
129
185
|
|
|
130
186
|
@media (display-mode: window-controls-overlay) {
|
|
131
187
|
body.is-wco .page-title-bar {
|
package/public/js/icons.js
CHANGED
|
@@ -125,6 +125,11 @@ export const IconBranch = ic('0 0 24 24', html`
|
|
|
125
125
|
<circle cx="6" cy="18" r="3"/>
|
|
126
126
|
<path d="M18 9a9 9 0 0 1-9 9"/>
|
|
127
127
|
`, 18);
|
|
128
|
+
export const IconMoreVert = ic('0 0 24 24', html`
|
|
129
|
+
<circle cx="12" cy="5" r="1.6" fill="currentColor" stroke="none"/>
|
|
130
|
+
<circle cx="12" cy="12" r="1.6" fill="currentColor" stroke="none"/>
|
|
131
|
+
<circle cx="12" cy="19" r="1.6" fill="currentColor" stroke="none"/>
|
|
132
|
+
`, 16);
|
|
128
133
|
|
|
129
134
|
// Brand-colored CLI marks. These use external SVG assets (full color),
|
|
130
135
|
// rendered as <img> so the gradients / fills in the file are preserved.
|
package/public/js/main.js
CHANGED
|
@@ -92,8 +92,7 @@ function applyNarrow() { sidebarForcedCollapsed.value = narrowMq.matches; }
|
|
|
92
92
|
applyNarrow();
|
|
93
93
|
narrowMq.addEventListener('change', applyNarrow);
|
|
94
94
|
|
|
95
|
-
// Counter-zoom for
|
|
96
|
-
// page zoom (Ctrl+wheel) scales every CSS px including our header heights;
|
|
95
|
+
// Counter-zoom for the page-title-bar. Browser page zoom (Ctrl+wheel) scales every CSS px including our header heights;
|
|
97
96
|
// without this, the header gets visually taller at 150%+ which the user
|
|
98
97
|
// usually doesn't want. We detect zoom via outerWidth/innerWidth and write
|
|
99
98
|
// 1/zoom into --anti-zoom so the CSS can `calc(40px * var(--anti-zoom))`
|
|
@@ -1,19 +1,79 @@
|
|
|
1
1
|
// Sessions page · the main pane. Shows the terminal for the currently
|
|
2
2
|
// selected session (activeSessionId), with a thin header providing
|
|
3
|
-
// session metadata +
|
|
4
|
-
//
|
|
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.
|
|
5
6
|
|
|
6
7
|
import { html } from '../html.js';
|
|
7
|
-
import { useEffect, useState } from 'preact/hooks';
|
|
8
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
8
9
|
import { activeSessionId, sessions, config, selectTab, selectSession, clockTick } from '../state.js';
|
|
9
10
|
import { resumeSession, clearResumeFailure, deleteSession, setSessionTitle } from '../api.js';
|
|
10
11
|
import { setToast } from '../toast.js';
|
|
11
12
|
import { ccsmConfirm, ccsmPrompt } from '../dialog.js';
|
|
12
13
|
import { TerminalView } from '../components/TerminalView.js';
|
|
13
14
|
import { PageTitleBar } from '../components/PageTitleBar.js';
|
|
14
|
-
import {
|
|
15
|
+
import { Popover } from '../components/Popover.js';
|
|
16
|
+
import { IconMoreVert, IconPencil, IconClose, IconPlus, IconForCliType, IconTerminal } from '../icons.js';
|
|
15
17
|
import { fmtAgo } from '../util.js';
|
|
16
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
|
+
function SessionMenu({ session, onRename, onDelete }) {
|
|
55
|
+
const [open, setOpen] = useState(false);
|
|
56
|
+
const anchor = useRef(null);
|
|
57
|
+
return html`
|
|
58
|
+
<button class="session-menu-btn" ref=${anchor}
|
|
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=${180}
|
|
65
|
+
onClose=${() => setOpen(false)}>
|
|
66
|
+
<div class="session-menu">
|
|
67
|
+
<button class="session-menu-item" onClick=${() => { setOpen(false); onRename(); }}>
|
|
68
|
+
<${IconPencil} /> Rename
|
|
69
|
+
</button>
|
|
70
|
+
<button class="session-menu-item danger" onClick=${() => { setOpen(false); onDelete(); }}>
|
|
71
|
+
<${IconClose} /> Delete
|
|
72
|
+
</button>
|
|
73
|
+
</div>
|
|
74
|
+
</${Popover}>` : null}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
17
77
|
export function SessionsPage() {
|
|
18
78
|
clockTick.value; // resubscribe fmtAgo
|
|
19
79
|
const id = activeSessionId.value;
|
|
@@ -49,6 +109,11 @@ export function SessionsPage() {
|
|
|
49
109
|
const running = session.status === 'running';
|
|
50
110
|
const title = session.title || session.workspace || session.id.slice(0, 12);
|
|
51
111
|
|
|
112
|
+
const onRetry = () => {
|
|
113
|
+
clearResumeFailure(session.id);
|
|
114
|
+
setResumeError(null);
|
|
115
|
+
setRetryNonce((n) => n + 1);
|
|
116
|
+
};
|
|
52
117
|
const onRename = async () => {
|
|
53
118
|
const next = await ccsmPrompt('Rename session', title, { okLabel: 'Save' });
|
|
54
119
|
if (next === null) return;
|
|
@@ -64,12 +129,6 @@ export function SessionsPage() {
|
|
|
64
129
|
activeSessionId.value = null;
|
|
65
130
|
} catch (e) { setToast(e.message, 'error'); }
|
|
66
131
|
};
|
|
67
|
-
const onRetry = () => {
|
|
68
|
-
clearResumeFailure(session.id);
|
|
69
|
-
setResumeError(null);
|
|
70
|
-
setRetryNonce((n) => n + 1);
|
|
71
|
-
};
|
|
72
|
-
const onFork = () => { setToast('Fork is not wired up yet'); };
|
|
73
132
|
|
|
74
133
|
return html`
|
|
75
134
|
<${PageTitleBar} title=${html`
|
|
@@ -82,8 +141,12 @@ export function SessionsPage() {
|
|
|
82
141
|
<span>·</span>
|
|
83
142
|
<span>${running ? 'running' : (resumeError ? 'resume failed' : 'resuming…')}</span>
|
|
84
143
|
</span>
|
|
85
|
-
`}
|
|
86
|
-
|
|
144
|
+
`} />
|
|
145
|
+
<${SessionTabs}
|
|
146
|
+
activeId=${session.id}
|
|
147
|
+
onActivate=${(sid) => selectSession(sid)}
|
|
148
|
+
onNew=${() => selectTab('launch')}
|
|
149
|
+
kebab=${html`<${SessionMenu} session=${session} onRename=${onRename} onDelete=${onDelete} />`} />
|
|
87
150
|
<div class="session-pane">
|
|
88
151
|
<div class="session-pane-body">
|
|
89
152
|
${running
|
|
@@ -98,21 +161,5 @@ export function SessionsPage() {
|
|
|
98
161
|
`}
|
|
99
162
|
</div>`}
|
|
100
163
|
</div>
|
|
101
|
-
<div class="session-actions">
|
|
102
|
-
${/* Fork button — wired but disabled; turn back on once the
|
|
103
|
-
--fork-session / codex fork / copilot fs-copy integrations
|
|
104
|
-
are in place. See discussion 2026-05-27. */ ''}
|
|
105
|
-
${false ? html`
|
|
106
|
-
<button class="action subtle" onClick=${onFork} title="Fork session">
|
|
107
|
-
<${IconBranch} /> Fork
|
|
108
|
-
</button>
|
|
109
|
-
` : null}
|
|
110
|
-
<button class="action subtle" onClick=${onRename} title="Rename session">
|
|
111
|
-
<${IconPencil} /> Rename
|
|
112
|
-
</button>
|
|
113
|
-
<button class="action subtle danger" onClick=${onDelete} title="Delete session">
|
|
114
|
-
<${IconClose} /> Delete
|
|
115
|
-
</button>
|
|
116
|
-
</div>
|
|
117
164
|
</div>`;
|
|
118
165
|
}
|