clay-server 4.3.0-beta.6 → 4.3.0-beta.8
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/lib/public/css/mobile-nav.css +13 -10
- package/lib/public/css/sidebar.css +61 -10
- package/lib/public/modules/session-creation-cta.js +33 -0
- package/lib/public/modules/sidebar-mobile.js +41 -30
- package/lib/public/modules/sidebar-sessions.js +45 -41
- package/lib/sessions.js +40 -22
- package/package.json +1 -1
|
@@ -358,8 +358,7 @@
|
|
|
358
358
|
background: rgba(var(--overlay-rgb), 0.06);
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
/*
|
|
362
|
-
that expands .mobile-vendor-list below it. Mirrors the desktop sidebar. */
|
|
361
|
+
/* Primary creation action + a labeled provider selector. */
|
|
363
362
|
.mobile-session-new-row {
|
|
364
363
|
display: flex;
|
|
365
364
|
gap: 6px;
|
|
@@ -377,26 +376,30 @@
|
|
|
377
376
|
color: var(--text);
|
|
378
377
|
gap: 8px;
|
|
379
378
|
}
|
|
380
|
-
.mobile-session-new-row .mobile-session-new-
|
|
381
|
-
flex: 0
|
|
382
|
-
width:
|
|
379
|
+
.mobile-session-new-row .mobile-session-new-provider {
|
|
380
|
+
flex: 0 1 132px;
|
|
381
|
+
width: 132px;
|
|
382
|
+
min-width: 104px;
|
|
383
383
|
margin-bottom: 0;
|
|
384
|
-
padding:
|
|
384
|
+
padding: 6px 9px;
|
|
385
385
|
border: 1px solid var(--border-subtle);
|
|
386
386
|
border-radius: 8px;
|
|
387
387
|
background: rgba(var(--overlay-rgb), 0.03);
|
|
388
388
|
color: var(--text-muted);
|
|
389
|
-
|
|
390
|
-
gap:
|
|
389
|
+
display: grid;
|
|
390
|
+
gap: 2px;
|
|
391
391
|
}
|
|
392
|
-
.mobile-session-new-row .mobile-session-new-
|
|
392
|
+
.mobile-session-new-row .mobile-session-new-provider.expanded .session-create-provider-chevron svg {
|
|
393
393
|
transform: rotate(180deg);
|
|
394
394
|
}
|
|
395
395
|
.mobile-session-new-row .mobile-session-new-main:active,
|
|
396
|
-
.mobile-session-new-row .mobile-session-new-
|
|
396
|
+
.mobile-session-new-row .mobile-session-new-provider:active {
|
|
397
397
|
background: rgba(var(--overlay-rgb), 0.1);
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
+
.mobile-session-new-row .session-create-provider-label { font-size: 9px; }
|
|
401
|
+
.mobile-session-new-row .session-create-provider-name { font-size: 12px; }
|
|
402
|
+
|
|
400
403
|
.mobile-vendor-list {
|
|
401
404
|
display: flex;
|
|
402
405
|
flex-direction: column;
|
|
@@ -1262,26 +1262,77 @@
|
|
|
1262
1262
|
opacity: 1;
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
1265
|
-
/*
|
|
1265
|
+
/* Primary creation action + a visibly grouped provider selector. */
|
|
1266
1266
|
.session-top-action-split {
|
|
1267
1267
|
display: flex;
|
|
1268
|
+
flex-wrap: wrap;
|
|
1268
1269
|
align-items: stretch;
|
|
1269
|
-
gap:
|
|
1270
|
+
gap: 6px;
|
|
1270
1271
|
min-width: 0;
|
|
1271
1272
|
}
|
|
1272
1273
|
|
|
1273
1274
|
.session-top-action-split .split-main {
|
|
1274
|
-
flex: 1 1
|
|
1275
|
+
flex: 1 1 110px;
|
|
1275
1276
|
width: auto;
|
|
1277
|
+
min-width: 110px;
|
|
1278
|
+
border: 1px solid color-mix(in srgb, var(--accent) 34%, transparent);
|
|
1279
|
+
background: color-mix(in srgb, var(--accent) 13%, transparent);
|
|
1280
|
+
color: var(--text);
|
|
1281
|
+
font-weight: 600;
|
|
1282
|
+
white-space: nowrap;
|
|
1276
1283
|
}
|
|
1277
1284
|
|
|
1278
|
-
.session-top-action-split .split-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
+
.session-top-action-split .split-main:hover {
|
|
1286
|
+
border-color: color-mix(in srgb, var(--accent) 50%, transparent);
|
|
1287
|
+
background: color-mix(in srgb, var(--accent) 19%, transparent);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.session-top-action-split .split-provider {
|
|
1291
|
+
flex: 1 1 104px;
|
|
1292
|
+
width: auto;
|
|
1293
|
+
min-width: 104px;
|
|
1294
|
+
padding: 5px 8px;
|
|
1295
|
+
border: 1px solid var(--border-subtle);
|
|
1296
|
+
background: color-mix(in srgb, var(--sidebar-hover) 48%, transparent);
|
|
1297
|
+
border-radius: 9px;
|
|
1298
|
+
display: grid;
|
|
1299
|
+
gap: 1px;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
.session-create-provider-label {
|
|
1303
|
+
display: block;
|
|
1304
|
+
color: var(--text-dimmer);
|
|
1305
|
+
font-size: 9px;
|
|
1306
|
+
font-weight: 600;
|
|
1307
|
+
line-height: 1;
|
|
1308
|
+
letter-spacing: 0.03em;
|
|
1309
|
+
text-transform: uppercase;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
.session-create-provider-choice {
|
|
1313
|
+
display: flex;
|
|
1314
|
+
align-items: center;
|
|
1315
|
+
gap: 5px;
|
|
1316
|
+
min-width: 0;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
.session-create-provider-name {
|
|
1320
|
+
overflow: hidden;
|
|
1321
|
+
color: var(--text-secondary);
|
|
1322
|
+
font-size: 11px;
|
|
1323
|
+
font-weight: 600;
|
|
1324
|
+
line-height: 1.2;
|
|
1325
|
+
text-overflow: ellipsis;
|
|
1326
|
+
white-space: nowrap;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
.session-create-provider-chevron {
|
|
1330
|
+
display: inline-flex;
|
|
1331
|
+
margin-left: auto;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
.session-create-provider[aria-expanded="true"] .session-create-provider-chevron svg {
|
|
1335
|
+
transform: rotate(180deg);
|
|
1285
1336
|
}
|
|
1286
1337
|
|
|
1287
1338
|
.session-favorites-section {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Shared, presentation-only session creation controls for desktop and mobile.
|
|
2
|
+
|
|
3
|
+
import { iconHtml } from './icons.js';
|
|
4
|
+
|
|
5
|
+
export function renderSessionCreationCta(options) {
|
|
6
|
+
var row = document.createElement("div");
|
|
7
|
+
row.className = options.rowClass + " session-create-cta";
|
|
8
|
+
|
|
9
|
+
var createButton = document.createElement("button");
|
|
10
|
+
createButton.className = options.createClass + " session-create-primary";
|
|
11
|
+
createButton.type = "button";
|
|
12
|
+
createButton.textContent = "Create session";
|
|
13
|
+
createButton.title = options.createTitle;
|
|
14
|
+
createButton.setAttribute("aria-label", options.createTitle);
|
|
15
|
+
createButton.addEventListener("click", options.onCreate);
|
|
16
|
+
row.appendChild(createButton);
|
|
17
|
+
|
|
18
|
+
var providerButton = document.createElement("button");
|
|
19
|
+
providerButton.className = options.providerClass + " session-create-provider";
|
|
20
|
+
providerButton.type = "button";
|
|
21
|
+
providerButton.title = options.providerTitle;
|
|
22
|
+
providerButton.setAttribute("aria-label", "AI provider: " + options.vendorName + ". " + options.providerTitle);
|
|
23
|
+
providerButton.setAttribute("aria-expanded", "false");
|
|
24
|
+
providerButton.setAttribute("aria-controls", options.menuId);
|
|
25
|
+
providerButton.innerHTML = '<span class="session-create-provider-label">AI provider</span>' +
|
|
26
|
+
'<span class="session-create-provider-choice"><img src="' + options.vendorAvatar +
|
|
27
|
+
'" class="' + options.iconClass + ' session-create-provider-icon" alt=""><span class="session-create-provider-name">' +
|
|
28
|
+
options.vendorName + '</span>' + iconHtml("chevron-down", "session-create-provider-chevron") + '</span>';
|
|
29
|
+
providerButton.addEventListener("click", options.onProvider);
|
|
30
|
+
row.appendChild(providerButton);
|
|
31
|
+
|
|
32
|
+
return { element: row, createButton: createButton, providerButton: providerButton };
|
|
33
|
+
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from './sidebar-sessions.js';
|
|
17
17
|
import { VENDOR_AVATARS, VENDOR_NAMES, VENDOR_ORDER, VENDOR_HOMEPAGES, isExperimentalVendor } from './app-rendering.js';
|
|
18
18
|
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
|
|
19
|
+
import { renderSessionCreationCta } from './session-creation-cta.js';
|
|
19
20
|
import {
|
|
20
21
|
getCachedProjectList,
|
|
21
22
|
getCachedCurrentSlug,
|
|
@@ -671,42 +672,49 @@ function createMobileLoopGroup(loopId, children, groupKey) {
|
|
|
671
672
|
}
|
|
672
673
|
|
|
673
674
|
// Helper: render sorted sessions into a container with date groups (with loop session grouping)
|
|
674
|
-
function renderMobileSessionsInto(container) {
|
|
675
|
-
// Mirrors the desktop sidebar's split button: one "New session" action on
|
|
676
|
-
// the project's last-used vendor, plus a chevron that expands the full
|
|
677
|
-
// vendor list. The list expands inline rather than as a fixed-position
|
|
678
|
-
// popover because it lives inside a scrolling bottom sheet.
|
|
675
|
+
export function renderMobileSessionsInto(container) {
|
|
679
676
|
var mobileDefaultVendor = resolvePreferredVendor(store.get('installedVendors') || []) || resolveDefaultVendor();
|
|
680
677
|
var mobileInstalled = store.get('installedVendors') || [];
|
|
681
678
|
var mobileDefaultState = store.get('defaultVendorState') || {};
|
|
682
679
|
|
|
683
|
-
var newRow = document.createElement("div");
|
|
684
|
-
newRow.className = "mobile-session-new-row";
|
|
685
|
-
|
|
686
|
-
var mainBtn = document.createElement("button");
|
|
687
|
-
mainBtn.className = "mobile-session-new mobile-session-new-main";
|
|
688
|
-
mainBtn.title = "New " + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + " session";
|
|
689
|
-
if (defaultVendorUnavailable(mobileInstalled)) mainBtn.title += " (saved default is unavailable; using installed priority)";
|
|
690
|
-
mainBtn.innerHTML = '<img src="' + (VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude) +
|
|
691
|
-
'" class="mobile-session-new-icon" alt=""><span>New ' + (VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor) + ' session</span>';
|
|
692
|
-
mainBtn.addEventListener("click", function () {
|
|
693
|
-
startNewSession(mobileDefaultVendor);
|
|
694
|
-
closeMobileSheet();
|
|
695
|
-
});
|
|
696
|
-
newRow.appendChild(mainBtn);
|
|
697
|
-
|
|
698
680
|
var vendorList = document.createElement("div");
|
|
699
681
|
vendorList.className = "mobile-vendor-list hidden";
|
|
700
|
-
|
|
701
|
-
var
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
682
|
+
vendorList.id = "mobile-session-provider-menu";
|
|
683
|
+
var mobileVendorName = VENDOR_NAMES[mobileDefaultVendor] || mobileDefaultVendor;
|
|
684
|
+
var mobileCreateTitle = "Create a " + mobileVendorName + " session";
|
|
685
|
+
if (defaultVendorUnavailable(mobileInstalled)) mobileCreateTitle += " (saved default is unavailable; using installed priority)";
|
|
686
|
+
var controls;
|
|
687
|
+
controls = renderSessionCreationCta({
|
|
688
|
+
rowClass: "mobile-session-new-row",
|
|
689
|
+
createClass: "mobile-session-new mobile-session-new-main",
|
|
690
|
+
providerClass: "mobile-session-new mobile-session-new-provider",
|
|
691
|
+
iconClass: "mobile-session-new-icon",
|
|
692
|
+
vendorName: mobileVendorName,
|
|
693
|
+
vendorAvatar: VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude,
|
|
694
|
+
createTitle: mobileCreateTitle,
|
|
695
|
+
providerTitle: "Choose an AI provider",
|
|
696
|
+
menuId: vendorList.id,
|
|
697
|
+
onCreate: function () { startNewSession(mobileDefaultVendor); closeMobileSheet(); },
|
|
698
|
+
onProvider: function () {
|
|
699
|
+
var opening = vendorList.classList.contains("hidden");
|
|
700
|
+
vendorList.classList.toggle("hidden", !opening);
|
|
701
|
+
controls.providerButton.classList.toggle("expanded", opening);
|
|
702
|
+
controls.providerButton.setAttribute("aria-expanded", opening ? "true" : "false");
|
|
703
|
+
if (opening) {
|
|
704
|
+
var first = vendorList.querySelector("button:not(:disabled)");
|
|
705
|
+
if (first) first.focus();
|
|
706
|
+
}
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
var newRow = controls.element;
|
|
710
|
+
vendorList.addEventListener("keydown", function (e) {
|
|
711
|
+
if (e.key !== "Escape") return;
|
|
712
|
+
e.preventDefault();
|
|
713
|
+
vendorList.classList.add("hidden");
|
|
714
|
+
controls.providerButton.classList.remove("expanded");
|
|
715
|
+
controls.providerButton.setAttribute("aria-expanded", "false");
|
|
716
|
+
controls.providerButton.focus();
|
|
708
717
|
});
|
|
709
|
-
newRow.appendChild(chevronBtn);
|
|
710
718
|
|
|
711
719
|
if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
|
|
712
720
|
var mobileFallbackNote = document.createElement("div");
|
|
@@ -727,7 +735,7 @@ function renderMobileSessionsInto(container) {
|
|
|
727
735
|
if (!isInstalled) vBtn.classList.add("disabled");
|
|
728
736
|
if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
|
|
729
737
|
vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
|
|
730
|
-
'<span>' + name + '</span>' +
|
|
738
|
+
'<span>' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
|
|
731
739
|
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
732
740
|
(isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
|
|
733
741
|
vBtn.addEventListener("click", function () {
|
|
@@ -735,6 +743,9 @@ function renderMobileSessionsInto(container) {
|
|
|
735
743
|
window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
|
|
736
744
|
return;
|
|
737
745
|
}
|
|
746
|
+
vendorList.classList.add("hidden");
|
|
747
|
+
controls.providerButton.classList.remove("expanded");
|
|
748
|
+
controls.providerButton.setAttribute("aria-expanded", "false");
|
|
738
749
|
startNewSession(vendor);
|
|
739
750
|
closeMobileSheet();
|
|
740
751
|
});
|
|
@@ -17,6 +17,7 @@ import { openGroup, separateGroup } from './split-view.js';
|
|
|
17
17
|
import { groupedSessionIds } from './split-group-helpers.js';
|
|
18
18
|
import { defaultVendorUnavailable, resolvePreferredVendor, saveDefaultVendor } from './default-vendor.js';
|
|
19
19
|
import { openPairDialog } from './split-pair-ui.js';
|
|
20
|
+
import { renderSessionCreationCta } from './session-creation-cta.js';
|
|
20
21
|
import {
|
|
21
22
|
hierarchyItemMatches,
|
|
22
23
|
prepareSidebarHierarchy,
|
|
@@ -53,10 +54,11 @@ var armedDeleteTimer = null;
|
|
|
53
54
|
store.subscribe(function (state, previous) {
|
|
54
55
|
if (state.defaultVendorState !== previous.defaultVendorState || state.installedVendors !== previous.installedVendors) {
|
|
55
56
|
var reopenNewMenu = !!(sessionCtxMenu && sessionCtxMenuAnchor);
|
|
57
|
+
var sessionList = getSessionListEl();
|
|
56
58
|
if (reopenNewMenu) closeSessionCtxMenu();
|
|
57
|
-
if (
|
|
58
|
-
if (reopenNewMenu) {
|
|
59
|
-
var freshAnchor =
|
|
59
|
+
if (sessionList) renderSessionList();
|
|
60
|
+
if (reopenNewMenu && sessionList) {
|
|
61
|
+
var freshAnchor = sessionList.querySelector(".session-top-action.split-provider");
|
|
60
62
|
if (freshAnchor) showNewSessionMenu(freshAnchor, resolveDefaultVendor());
|
|
61
63
|
}
|
|
62
64
|
}
|
|
@@ -356,58 +358,49 @@ export function startNewSession(vendor, extra) {
|
|
|
356
358
|
});
|
|
357
359
|
}
|
|
358
360
|
|
|
359
|
-
function renderSessionTopActions() {
|
|
361
|
+
export function renderSessionTopActions() {
|
|
360
362
|
var wrap = document.createElement("div");
|
|
361
363
|
wrap.className = "session-top-actions";
|
|
362
364
|
|
|
363
|
-
// Single split button. The main half creates a session with the project's
|
|
364
|
-
// last-used vendor (for Claude the server applies the claudeOpenMode pref);
|
|
365
|
-
// the chevron opens the vendor picker plus alternate launch modes.
|
|
366
365
|
var defaultVendor = resolveDefaultVendor();
|
|
367
|
-
|
|
368
|
-
var
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
chevron.innerHTML = iconHtml("chevron-down");
|
|
389
|
-
chevron.addEventListener("click", function (e) {
|
|
390
|
-
e.preventDefault();
|
|
391
|
-
e.stopPropagation();
|
|
392
|
-
if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
|
|
393
|
-
showNewSessionMenu(chevron, defaultVendor);
|
|
366
|
+
var vendorName = VENDOR_NAMES[defaultVendor] || defaultVendor;
|
|
367
|
+
var createTitle = "Create a " + vendorName + " session";
|
|
368
|
+
if (defaultVendorUnavailable(store.get('installedVendors') || [])) createTitle += " (saved default is unavailable; using installed priority)";
|
|
369
|
+
var controls;
|
|
370
|
+
controls = renderSessionCreationCta({
|
|
371
|
+
rowClass: "session-top-action-split",
|
|
372
|
+
createClass: "session-top-action split-main",
|
|
373
|
+
providerClass: "session-top-action split-provider",
|
|
374
|
+
iconClass: "session-top-action-icon",
|
|
375
|
+
vendorName: vendorName,
|
|
376
|
+
vendorAvatar: VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude,
|
|
377
|
+
createTitle: createTitle,
|
|
378
|
+
providerTitle: "Choose an AI provider or launch mode",
|
|
379
|
+
menuId: "session-new-menu",
|
|
380
|
+
onCreate: function () { startNewSession(defaultVendor); },
|
|
381
|
+
onProvider: function (e) {
|
|
382
|
+
e.preventDefault();
|
|
383
|
+
e.stopPropagation();
|
|
384
|
+
if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
|
|
385
|
+
showNewSessionMenu(controls.providerButton, defaultVendor);
|
|
386
|
+
},
|
|
394
387
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
wrap.appendChild(cell);
|
|
388
|
+
wrap.appendChild(controls.element);
|
|
398
389
|
return wrap;
|
|
399
390
|
}
|
|
400
391
|
|
|
401
|
-
//
|
|
402
|
-
// element/var so the global document click handler closes it.
|
|
392
|
+
// Provider disclosure anchored to the grouped provider control. Reuses the
|
|
393
|
+
// session-ctx-menu element/var so the global document click handler closes it.
|
|
403
394
|
function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
404
395
|
closeSessionCtxMenu();
|
|
405
396
|
|
|
406
397
|
var menu = document.createElement("div");
|
|
407
398
|
menu.className = "session-ctx-menu session-new-menu";
|
|
399
|
+
menu.id = "session-new-menu";
|
|
408
400
|
var installed = store.get('installedVendors') || [];
|
|
409
401
|
var defaultVendorState = store.get('defaultVendorState') || {};
|
|
410
402
|
sessionCtxMenuAnchor = anchorBtn;
|
|
403
|
+
anchorBtn.setAttribute("aria-expanded", "true");
|
|
411
404
|
|
|
412
405
|
if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
|
|
413
406
|
var fallbackNote = document.createElement("div");
|
|
@@ -430,7 +423,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
430
423
|
if (vendor === defaultVendor) item.classList.add("active");
|
|
431
424
|
item.type = "button";
|
|
432
425
|
item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
|
|
433
|
-
'<span class="session-new-vendor-name">' + name + '</span>' +
|
|
426
|
+
'<span class="session-new-vendor-name">' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
|
|
434
427
|
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
435
428
|
(isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
|
|
436
429
|
item.title = isInstalled
|
|
@@ -526,6 +519,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
526
519
|
|
|
527
520
|
document.body.appendChild(menu);
|
|
528
521
|
sessionCtxMenu = menu;
|
|
522
|
+
menu.addEventListener("keydown", function (e) {
|
|
523
|
+
if (e.key !== "Escape") return;
|
|
524
|
+
e.preventDefault();
|
|
525
|
+
e.stopPropagation();
|
|
526
|
+
closeSessionCtxMenu(true);
|
|
527
|
+
});
|
|
529
528
|
refreshIcons();
|
|
530
529
|
|
|
531
530
|
requestAnimationFrame(function () {
|
|
@@ -542,6 +541,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
542
541
|
if (menuRect.bottom > window.innerHeight - 8) {
|
|
543
542
|
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
|
|
544
543
|
}
|
|
544
|
+
var firstMenuItem = menu.querySelector("button:not(:disabled)");
|
|
545
|
+
if (firstMenuItem) firstMenuItem.focus();
|
|
545
546
|
});
|
|
546
547
|
}
|
|
547
548
|
|
|
@@ -718,13 +719,16 @@ export function getExpandedLoopRuns() {
|
|
|
718
719
|
|
|
719
720
|
// --- Context menu ---
|
|
720
721
|
|
|
721
|
-
function closeSessionCtxMenu() {
|
|
722
|
+
function closeSessionCtxMenu(restoreFocus) {
|
|
723
|
+
var anchor = sessionCtxMenuAnchor;
|
|
724
|
+
if (anchor) anchor.setAttribute("aria-expanded", "false");
|
|
722
725
|
sessionCtxMenuAnchor = null;
|
|
723
726
|
if (sessionCtxMenu) {
|
|
724
727
|
sessionCtxMenu.remove();
|
|
725
728
|
sessionCtxMenu = null;
|
|
726
729
|
sessionCtxSessionId = null;
|
|
727
730
|
}
|
|
731
|
+
if (restoreFocus && anchor && document.contains(anchor)) anchor.focus();
|
|
728
732
|
}
|
|
729
733
|
|
|
730
734
|
function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
package/lib/sessions.js
CHANGED
|
@@ -144,17 +144,32 @@ function createSessionManager(opts) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
function sessionFilePath(
|
|
148
|
-
return path.join(sessionsDir,
|
|
147
|
+
function sessionFilePath(recordId) {
|
|
148
|
+
return path.join(sessionsDir, recordId + ".jsonl");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// A Clay record must exist before a provider assigns its resumable id.
|
|
152
|
+
// Existing records keep their filename; fresh provider-less sessions use
|
|
153
|
+
// the already durable origin id and later store cliSessionId as metadata.
|
|
154
|
+
function sessionRecordId(session) {
|
|
155
|
+
if (!session) return null;
|
|
156
|
+
return session._sessionRecordId || session.cliSessionId || session.sessionOriginId || null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function existingSessionRecordId(session) {
|
|
160
|
+
if (!session) return null;
|
|
161
|
+
return session._sessionRecordId || session.cliSessionId || null;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
164
|
function saveSessionFile(session) {
|
|
152
|
-
if (!session.cliSessionId) return false;
|
|
153
165
|
try {
|
|
166
|
+
var provenanceMeta = sessionProvenance.metadata(session);
|
|
167
|
+
var recordId = sessionRecordId(session);
|
|
168
|
+
if (!recordId) return false;
|
|
154
169
|
var metaObj = {
|
|
155
170
|
type: "meta",
|
|
156
171
|
localId: session.localId,
|
|
157
|
-
cliSessionId: session.cliSessionId,
|
|
172
|
+
cliSessionId: session.cliSessionId || null,
|
|
158
173
|
title: session.title,
|
|
159
174
|
titleProvisional: session.titleProvisional === true,
|
|
160
175
|
titleAutoGenerated: session.titleAutoGenerated === true,
|
|
@@ -204,7 +219,6 @@ function createSessionManager(opts) {
|
|
|
204
219
|
if (session.homeDebateInitialTopic) metaObj.homeDebateInitialTopic = session.homeDebateInitialTopic;
|
|
205
220
|
if (session.homeClayEntryRequestId) metaObj.homeClayEntryRequestId = session.homeClayEntryRequestId;
|
|
206
221
|
if (session.homeClayEntryMode) metaObj.homeClayEntryMode = session.homeClayEntryMode;
|
|
207
|
-
var provenanceMeta = sessionProvenance.metadata(session);
|
|
208
222
|
metaObj.sessionOriginId = provenanceMeta.sessionOriginId;
|
|
209
223
|
if (provenanceMeta.sessionProvenance) metaObj.sessionProvenance = provenanceMeta.sessionProvenance;
|
|
210
224
|
if (Array.isArray(session._workerGenerations)) metaObj.workerGenerations = session._workerGenerations;
|
|
@@ -213,7 +227,7 @@ function createSessionManager(opts) {
|
|
|
213
227
|
for (var i = 0; i < session.history.length; i++) {
|
|
214
228
|
lines.push(JSON.stringify(session.history[i]));
|
|
215
229
|
}
|
|
216
|
-
var sfPath = sessionFilePath(
|
|
230
|
+
var sfPath = sessionFilePath(recordId);
|
|
217
231
|
// Atomic write: write to temp file then rename, so a crash mid-write
|
|
218
232
|
// cannot leave a truncated/corrupted session file.
|
|
219
233
|
var tmpPath = sfPath + ".tmp." + process.pid;
|
|
@@ -222,6 +236,7 @@ function createSessionManager(opts) {
|
|
|
222
236
|
try { fs.chmodSync(tmpPath, 0o600); } catch (chmodErr) {}
|
|
223
237
|
}
|
|
224
238
|
fs.renameSync(tmpPath, sfPath);
|
|
239
|
+
session._sessionRecordId = recordId;
|
|
225
240
|
return true;
|
|
226
241
|
} catch(e) {
|
|
227
242
|
console.error("[session] Failed to save session file:", e.message);
|
|
@@ -230,10 +245,11 @@ function createSessionManager(opts) {
|
|
|
230
245
|
}
|
|
231
246
|
|
|
232
247
|
function appendToSessionFile(session, obj) {
|
|
233
|
-
|
|
248
|
+
var recordId = existingSessionRecordId(session);
|
|
249
|
+
if (!recordId) return true;
|
|
234
250
|
session.lastActivity = Date.now();
|
|
235
251
|
try {
|
|
236
|
-
var afPath = sessionFilePath(
|
|
252
|
+
var afPath = sessionFilePath(recordId);
|
|
237
253
|
fs.appendFileSync(afPath, JSON.stringify(obj) + "\n");
|
|
238
254
|
if (process.platform !== "win32") {
|
|
239
255
|
try { fs.chmodSync(afPath, 0o600); } catch (chmodErr) {}
|
|
@@ -276,7 +292,8 @@ function createSessionManager(opts) {
|
|
|
276
292
|
|
|
277
293
|
var meta;
|
|
278
294
|
try { meta = JSON.parse(lines[0]); } catch { continue; }
|
|
279
|
-
|
|
295
|
+
var pendingOrigin = typeof meta.sessionOriginId === "string" && meta.sessionOriginId.trim() && meta.sessionOriginId.length <= 128;
|
|
296
|
+
if (meta.type !== "meta" || (!meta.cliSessionId && !pendingOrigin)) continue;
|
|
280
297
|
|
|
281
298
|
var history = [];
|
|
282
299
|
for (var j = 1; j < lines.length; j++) {
|
|
@@ -285,7 +302,7 @@ function createSessionManager(opts) {
|
|
|
285
302
|
|
|
286
303
|
var fileMtime = 0;
|
|
287
304
|
try { fileMtime = fs.statSync(path.join(sessionsDir, files[i])).mtimeMs; } catch {}
|
|
288
|
-
loaded.push({ meta: meta, history: history, mtime: fileMtime });
|
|
305
|
+
loaded.push({ meta: meta, history: history, mtime: fileMtime, recordId: files[i].slice(0, -".jsonl".length) });
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
loaded.sort(function(a, b) { return a.meta.createdAt - b.meta.createdAt; });
|
|
@@ -310,6 +327,7 @@ function createSessionManager(opts) {
|
|
|
310
327
|
queryInstance: null,
|
|
311
328
|
messageQueue: null,
|
|
312
329
|
cliSessionId: m.cliSessionId,
|
|
330
|
+
_sessionRecordId: loaded[i].recordId,
|
|
313
331
|
blocks: {},
|
|
314
332
|
sentToolResults: {},
|
|
315
333
|
pendingPermissions: {},
|
|
@@ -388,7 +406,7 @@ function createSessionManager(opts) {
|
|
|
388
406
|
saveSessionFile(session);
|
|
389
407
|
try {
|
|
390
408
|
var preservedTime = new Date(activityTime);
|
|
391
|
-
fs.utimesSync(sessionFilePath(session
|
|
409
|
+
fs.utimesSync(sessionFilePath(sessionRecordId(session)), preservedTime, preservedTime);
|
|
392
410
|
} catch (e) {}
|
|
393
411
|
delete session._provenanceMigrationNeeded;
|
|
394
412
|
});
|
|
@@ -917,10 +935,9 @@ function createSessionManager(opts) {
|
|
|
917
935
|
session.worker = null;
|
|
918
936
|
}
|
|
919
937
|
|
|
920
|
-
if (session.cliSessionId)
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
938
|
+
if (session.cliSessionId) rememberDeletedCliSession(session.cliSessionId);
|
|
939
|
+
var recordId = existingSessionRecordId(session);
|
|
940
|
+
if (recordId) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
|
|
924
941
|
|
|
925
942
|
sessions.delete(localId);
|
|
926
943
|
try { onSessionDeleted(localId, session); } catch (e) {
|
|
@@ -964,10 +981,9 @@ function createSessionManager(opts) {
|
|
|
964
981
|
try { session.worker.kill(); } catch(e) {}
|
|
965
982
|
session.worker = null;
|
|
966
983
|
}
|
|
967
|
-
if (session.cliSessionId)
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
}
|
|
984
|
+
if (session.cliSessionId) rememberDeletedCliSession(session.cliSessionId);
|
|
985
|
+
var recordId = existingSessionRecordId(session);
|
|
986
|
+
if (recordId) try { fs.unlinkSync(sessionFilePath(recordId)); } catch(e) {}
|
|
971
987
|
sessions.delete(localId);
|
|
972
988
|
try { onSessionDeleted(localId, session); } catch (e) {
|
|
973
989
|
console.error("[session] Session deletion hook failed:", e.message || e);
|
|
@@ -1093,7 +1109,8 @@ function createSessionManager(opts) {
|
|
|
1093
1109
|
}
|
|
1094
1110
|
|
|
1095
1111
|
function sendAndRecordDurably(session, obj, clientObj) {
|
|
1096
|
-
|
|
1112
|
+
var recordId = existingSessionRecordId(session);
|
|
1113
|
+
if (!recordId || !fs.existsSync(sessionFilePath(recordId))) return false;
|
|
1097
1114
|
var saved;
|
|
1098
1115
|
try { saved = doSendAndRecord(session, obj, clientObj); }
|
|
1099
1116
|
catch (error) {
|
|
@@ -1107,9 +1124,10 @@ function createSessionManager(opts) {
|
|
|
1107
1124
|
}
|
|
1108
1125
|
|
|
1109
1126
|
function hasDurableSessionRecord(session, predicate) {
|
|
1110
|
-
|
|
1127
|
+
var recordId = existingSessionRecordId(session);
|
|
1128
|
+
if (!recordId || typeof predicate !== "function") return false;
|
|
1111
1129
|
try {
|
|
1112
|
-
var lines = fs.readFileSync(sessionFilePath(
|
|
1130
|
+
var lines = fs.readFileSync(sessionFilePath(recordId), "utf8").split("\n");
|
|
1113
1131
|
for (var i = lines.length - 1; i >= 0; i--) {
|
|
1114
1132
|
if (!lines[i]) continue;
|
|
1115
1133
|
try { if (predicate(JSON.parse(lines[i]))) return true; } catch (error) {}
|
package/package.json
CHANGED