clay-server 4.2.0 → 4.3.0-beta.10
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/driver-continuation-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +86 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +3 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +117 -135
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-pair-result-capture.js +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-sessions.js +4 -0
- package/lib/project-user-message.js +21 -0
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +87 -3
- package/lib/public/app.js +1 -0
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/mobile-nav.css +29 -14
- package/lib/public/css/overlays.css +54 -25
- package/lib/public/css/pane.css +66 -12
- package/lib/public/css/sidebar.css +62 -7
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +165 -74
- package/lib/public/modules/app-messages.js +31 -3
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/message-delivery-ui.js +63 -0
- package/lib/public/modules/message-delivery.js +163 -6
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/permission-control.js +10 -5
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/session-creation-cta.js +46 -0
- package/lib/public/modules/sidebar-mobile.js +42 -30
- package/lib/public/modules/sidebar-sessions.js +77 -52
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-lifecycle.js +178 -0
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/server.js +1 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +46 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +30 -9
- package/lib/session-split-group-anchors.js +126 -0
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +293 -133
- package/lib/sessions.js +89 -22
- package/lib/ws-schema.js +8 -0
- package/package.json +1 -1
|
@@ -46,10 +46,11 @@ export function createPermissionControl(className, ariaLabel) {
|
|
|
46
46
|
control.setAttribute("role", "group");
|
|
47
47
|
control.setAttribute("aria-label", ariaLabel || "Session permission mode");
|
|
48
48
|
control.innerHTML = '<div class="session-permission-segmented">' +
|
|
49
|
-
'<button type="button" data-permission-mode="default">Ask</button>' +
|
|
50
|
-
'<button type="button" data-permission-mode="auto">Auto</button>' +
|
|
51
|
-
'<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label
|
|
49
|
+
'<button type="button" data-permission-mode="default" aria-label="Ask permissions" title="Ask permissions"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
50
|
+
'<button type="button" data-permission-mode="auto" aria-label="Auto permissions" title="Auto permissions"><span class="permission-label">Auto</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
51
|
+
'<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions" title="Skip permissions"><span class="permission-label">Skip</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
52
52
|
'</div><span class="session-permission-status" aria-live="polite"></span>';
|
|
53
|
+
control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true");
|
|
53
54
|
return control;
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -99,17 +100,21 @@ export function renderPermissionControl(control, state) {
|
|
|
99
100
|
control.classList.toggle("permission-auto-unsupported", autoVisible && !autoSupported);
|
|
100
101
|
control.classList.toggle("permission-globally-forced", globallyForced);
|
|
101
102
|
control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
|
|
103
|
+
control.setAttribute("aria-busy", pending ? "true" : "false");
|
|
104
|
+
var buttonTitles = { default: "Ask permissions", auto: "Auto permissions", bypassPermissions: "Skip permissions" };
|
|
102
105
|
var buttons = control.querySelectorAll("[data-permission-mode]");
|
|
103
106
|
for (var i = 0; i < buttons.length; i++) {
|
|
104
107
|
var mode = buttons[i].dataset.permissionMode;
|
|
105
108
|
var unsupported = mode === "auto" && autoVisible && !autoSupported;
|
|
109
|
+
var requested = !!pending && pending.mode === mode;
|
|
106
110
|
buttons[i].hidden = mode === "auto" && !autoVisible;
|
|
107
111
|
var selected = globallyForced ? mode === "bypassPermissions" : mode === state.permissionMode;
|
|
108
112
|
buttons[i].disabled = globallyForced || unsupported || !!pending || !connected || state.locked === true;
|
|
109
113
|
buttons[i].classList.toggle("active", selected);
|
|
114
|
+
buttons[i].classList.toggle("pending", requested);
|
|
110
115
|
buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
buttons[i].setAttribute("aria-busy", requested ? "true" : "false");
|
|
117
|
+
buttons[i].title = unsupported ? "Auto permissions are unavailable for this Claude session." : buttonTitles[mode];
|
|
113
118
|
}
|
|
114
119
|
var status = control.querySelector(".session-permission-status");
|
|
115
120
|
if (status) {
|
|
@@ -59,19 +59,22 @@ export function isProjectSessionReady(state, socket) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function splitPanesMatchProject(split, slug) {
|
|
62
|
-
if (!split || !Array.isArray(split.panes) || split.panes.length
|
|
63
|
-
|
|
62
|
+
if (!split || !Array.isArray(split.panes) || split.panes.length < 2 || split.panes.length > 3 || !slug) return false;
|
|
63
|
+
for (var i = 0; i < split.panes.length; i++) if (split.panes[i].slug !== slug) return false;
|
|
64
|
+
return true;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export function hasCurrentSplitGroup(split, groups) {
|
|
67
68
|
if (!split || !split.groupId) return true;
|
|
68
69
|
var members = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
|
|
69
|
-
if (!members || members.length
|
|
70
|
+
if (!members || members.length < 2 || members.length > 3) return false;
|
|
70
71
|
var available = Array.isArray(groups) ? groups : [];
|
|
71
72
|
for (var i = 0; i < available.length; i++) {
|
|
72
73
|
var group = available[i];
|
|
73
|
-
if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !==
|
|
74
|
-
|
|
74
|
+
if (group.id !== split.groupId || !Array.isArray(group.members) || group.members.length !== members.length) continue;
|
|
75
|
+
var matches = true;
|
|
76
|
+
for (var mi = 0; mi < members.length; mi++) if (group.members.indexOf(members[mi]) === -1) matches = false;
|
|
77
|
+
if (matches) return true;
|
|
75
78
|
}
|
|
76
79
|
return false;
|
|
77
80
|
}
|
|
@@ -84,8 +87,8 @@ export function reconcileRestoredSplit(split, groups) {
|
|
|
84
87
|
if (group.id !== split.groupId) continue;
|
|
85
88
|
var oldMembers = split.panes && split.panes.map(function (pane) { return pane.sessionId; });
|
|
86
89
|
var members = Array.isArray(group.members) ? group.members : [];
|
|
87
|
-
if (oldMembers && oldMembers.length ===
|
|
88
|
-
&&
|
|
90
|
+
if (oldMembers && oldMembers.length === members.length
|
|
91
|
+
&& oldMembers.every(function (id) { return members.indexOf(id) !== -1; })) {
|
|
89
92
|
return { action: "keep", group: group };
|
|
90
93
|
}
|
|
91
94
|
return { action: "rebuild", group: group };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Shared, presentation-only session creation controls for desktop and mobile.
|
|
2
|
+
|
|
3
|
+
import { iconHtml } from './icons.js';
|
|
4
|
+
|
|
5
|
+
var SESSION_VENDOR_LABELS = {
|
|
6
|
+
claude: "Claude",
|
|
7
|
+
codex: "Codex",
|
|
8
|
+
antigravity: "Antigravity",
|
|
9
|
+
opencode: "OpenCode",
|
|
10
|
+
kimi: "Kimi",
|
|
11
|
+
grok: "Grok",
|
|
12
|
+
copilot: "Copilot",
|
|
13
|
+
qwen: "Qwen",
|
|
14
|
+
junie: "Junie",
|
|
15
|
+
kiro: "Kiro",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function renderSessionCreationCta(options) {
|
|
19
|
+
var row = document.createElement("div");
|
|
20
|
+
row.className = options.rowClass + " session-create-cta";
|
|
21
|
+
|
|
22
|
+
var createButton = document.createElement("button");
|
|
23
|
+
createButton.className = options.createClass + " session-create-primary";
|
|
24
|
+
createButton.type = "button";
|
|
25
|
+
var vendorLabel = SESSION_VENDOR_LABELS[options.vendorKey] || options.vendorName;
|
|
26
|
+
createButton.innerHTML = '<img src="' + options.vendorAvatar +
|
|
27
|
+
'" class="' + options.iconClass + ' session-create-provider-icon" alt="">' +
|
|
28
|
+
'<span>Create new ' + vendorLabel + ' session</span>';
|
|
29
|
+
createButton.title = options.createTitle;
|
|
30
|
+
createButton.setAttribute("aria-label", options.createTitle);
|
|
31
|
+
createButton.addEventListener("click", options.onCreate);
|
|
32
|
+
row.appendChild(createButton);
|
|
33
|
+
|
|
34
|
+
var providerButton = document.createElement("button");
|
|
35
|
+
providerButton.className = options.providerClass + " session-create-provider";
|
|
36
|
+
providerButton.type = "button";
|
|
37
|
+
providerButton.title = options.providerTitle;
|
|
38
|
+
providerButton.setAttribute("aria-label", "Switch AI provider (current: " + options.vendorName + ")");
|
|
39
|
+
providerButton.setAttribute("aria-expanded", "false");
|
|
40
|
+
providerButton.setAttribute("aria-controls", options.menuId);
|
|
41
|
+
providerButton.innerHTML = iconHtml("chevron-down", "session-create-provider-chevron");
|
|
42
|
+
providerButton.addEventListener("click", options.onProvider);
|
|
43
|
+
row.appendChild(providerButton);
|
|
44
|
+
|
|
45
|
+
return { element: row, createButton: createButton, providerButton: providerButton };
|
|
46
|
+
}
|
|
@@ -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,50 @@ 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
|
+
vendorKey: mobileDefaultVendor,
|
|
693
|
+
vendorName: mobileVendorName,
|
|
694
|
+
vendorAvatar: VENDOR_AVATARS[mobileDefaultVendor] || VENDOR_AVATARS.claude,
|
|
695
|
+
createTitle: mobileCreateTitle,
|
|
696
|
+
providerTitle: "Switch AI provider (current: " + mobileVendorName + ")",
|
|
697
|
+
menuId: vendorList.id,
|
|
698
|
+
onCreate: function () { startNewSession(mobileDefaultVendor); closeMobileSheet(); },
|
|
699
|
+
onProvider: function () {
|
|
700
|
+
var opening = vendorList.classList.contains("hidden");
|
|
701
|
+
vendorList.classList.toggle("hidden", !opening);
|
|
702
|
+
controls.providerButton.classList.toggle("expanded", opening);
|
|
703
|
+
controls.providerButton.setAttribute("aria-expanded", opening ? "true" : "false");
|
|
704
|
+
if (opening) {
|
|
705
|
+
var first = vendorList.querySelector("button:not(:disabled)");
|
|
706
|
+
if (first) first.focus();
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
var newRow = controls.element;
|
|
711
|
+
vendorList.addEventListener("keydown", function (e) {
|
|
712
|
+
if (e.key !== "Escape") return;
|
|
713
|
+
e.preventDefault();
|
|
714
|
+
vendorList.classList.add("hidden");
|
|
715
|
+
controls.providerButton.classList.remove("expanded");
|
|
716
|
+
controls.providerButton.setAttribute("aria-expanded", "false");
|
|
717
|
+
controls.providerButton.focus();
|
|
708
718
|
});
|
|
709
|
-
newRow.appendChild(chevronBtn);
|
|
710
719
|
|
|
711
720
|
if (defaultVendorUnavailable(mobileInstalled) && mobileDefaultState.preference) {
|
|
712
721
|
var mobileFallbackNote = document.createElement("div");
|
|
@@ -727,7 +736,7 @@ function renderMobileSessionsInto(container) {
|
|
|
727
736
|
if (!isInstalled) vBtn.classList.add("disabled");
|
|
728
737
|
if (vendor === mobileDefaultVendor) vBtn.classList.add("active");
|
|
729
738
|
vBtn.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="mobile-session-new-icon" alt="">' +
|
|
730
|
-
'<span>' + name + '</span>' +
|
|
739
|
+
'<span>' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
|
|
731
740
|
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
732
741
|
(isInstalled ? "" : '<span class="mobile-vendor-note">Not installed</span>');
|
|
733
742
|
vBtn.addEventListener("click", function () {
|
|
@@ -735,6 +744,9 @@ function renderMobileSessionsInto(container) {
|
|
|
735
744
|
window.open(VENDOR_HOMEPAGES[vendor], "_blank", "noopener");
|
|
736
745
|
return;
|
|
737
746
|
}
|
|
747
|
+
vendorList.classList.add("hidden");
|
|
748
|
+
controls.providerButton.classList.remove("expanded");
|
|
749
|
+
controls.providerButton.setAttribute("aria-expanded", "false");
|
|
738
750
|
startNewSession(vendor);
|
|
739
751
|
closeMobileSheet();
|
|
740
752
|
});
|
|
@@ -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,50 @@ 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
|
-
|
|
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
|
+
vendorKey: defaultVendor,
|
|
376
|
+
vendorName: vendorName,
|
|
377
|
+
vendorAvatar: VENDOR_AVATARS[defaultVendor] || VENDOR_AVATARS.claude,
|
|
378
|
+
createTitle: createTitle,
|
|
379
|
+
providerTitle: "Switch AI provider (current: " + vendorName + ")",
|
|
380
|
+
menuId: "session-new-menu",
|
|
381
|
+
onCreate: function () { startNewSession(defaultVendor); },
|
|
382
|
+
onProvider: function (e) {
|
|
383
|
+
e.preventDefault();
|
|
384
|
+
e.stopPropagation();
|
|
385
|
+
if (sessionCtxMenu) { closeSessionCtxMenu(); return; }
|
|
386
|
+
showNewSessionMenu(controls.providerButton, defaultVendor);
|
|
387
|
+
},
|
|
394
388
|
});
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
wrap.appendChild(cell);
|
|
389
|
+
wrap.appendChild(controls.element);
|
|
398
390
|
return wrap;
|
|
399
391
|
}
|
|
400
392
|
|
|
401
|
-
//
|
|
402
|
-
// element/var so the global document click handler closes it.
|
|
393
|
+
// Provider disclosure anchored to the grouped provider control. Reuses the
|
|
394
|
+
// session-ctx-menu element/var so the global document click handler closes it.
|
|
403
395
|
function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
404
396
|
closeSessionCtxMenu();
|
|
405
397
|
|
|
406
398
|
var menu = document.createElement("div");
|
|
407
399
|
menu.className = "session-ctx-menu session-new-menu";
|
|
400
|
+
menu.id = "session-new-menu";
|
|
408
401
|
var installed = store.get('installedVendors') || [];
|
|
409
402
|
var defaultVendorState = store.get('defaultVendorState') || {};
|
|
410
403
|
sessionCtxMenuAnchor = anchorBtn;
|
|
404
|
+
anchorBtn.setAttribute("aria-expanded", "true");
|
|
411
405
|
|
|
412
406
|
if (defaultVendorUnavailable(installed) && defaultVendorState.preference) {
|
|
413
407
|
var fallbackNote = document.createElement("div");
|
|
@@ -430,7 +424,7 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
430
424
|
if (vendor === defaultVendor) item.classList.add("active");
|
|
431
425
|
item.type = "button";
|
|
432
426
|
item.innerHTML = '<img src="' + VENDOR_AVATARS[vendor] + '" class="session-top-action-icon" alt="">' +
|
|
433
|
-
'<span class="session-new-vendor-name">' + name + '</span>' +
|
|
427
|
+
'<span class="session-new-vendor-name">' + (isInstalled ? "Create with " : "Learn about ") + name + '</span>' +
|
|
434
428
|
(isExperimentalVendor(vendor) ? '<span class="vendor-experimental-badge" aria-label="Experimental integration" title="Experimental integration; not yet validated through direct use testing">🧪</span>' : '') +
|
|
435
429
|
(isInstalled ? "" : '<span class="session-new-vendor-note">Not installed</span>' + iconHtml("external-link"));
|
|
436
430
|
item.title = isInstalled
|
|
@@ -526,6 +520,12 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
526
520
|
|
|
527
521
|
document.body.appendChild(menu);
|
|
528
522
|
sessionCtxMenu = menu;
|
|
523
|
+
menu.addEventListener("keydown", function (e) {
|
|
524
|
+
if (e.key !== "Escape") return;
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
e.stopPropagation();
|
|
527
|
+
closeSessionCtxMenu(true);
|
|
528
|
+
});
|
|
529
529
|
refreshIcons();
|
|
530
530
|
|
|
531
531
|
requestAnimationFrame(function () {
|
|
@@ -542,6 +542,8 @@ function showNewSessionMenu(anchorBtn, defaultVendor) {
|
|
|
542
542
|
if (menuRect.bottom > window.innerHeight - 8) {
|
|
543
543
|
menu.style.top = (btnRect.top - menuRect.height - 2) + "px";
|
|
544
544
|
}
|
|
545
|
+
var firstMenuItem = menu.querySelector("button:not(:disabled)");
|
|
546
|
+
if (firstMenuItem) firstMenuItem.focus();
|
|
545
547
|
});
|
|
546
548
|
}
|
|
547
549
|
|
|
@@ -718,13 +720,16 @@ export function getExpandedLoopRuns() {
|
|
|
718
720
|
|
|
719
721
|
// --- Context menu ---
|
|
720
722
|
|
|
721
|
-
function closeSessionCtxMenu() {
|
|
723
|
+
function closeSessionCtxMenu(restoreFocus) {
|
|
724
|
+
var anchor = sessionCtxMenuAnchor;
|
|
725
|
+
if (anchor) anchor.setAttribute("aria-expanded", "false");
|
|
722
726
|
sessionCtxMenuAnchor = null;
|
|
723
727
|
if (sessionCtxMenu) {
|
|
724
728
|
sessionCtxMenu.remove();
|
|
725
729
|
sessionCtxMenu = null;
|
|
726
730
|
sessionCtxSessionId = null;
|
|
727
731
|
}
|
|
732
|
+
if (restoreFocus && anchor && document.contains(anchor)) anchor.focus();
|
|
728
733
|
}
|
|
729
734
|
|
|
730
735
|
function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
|
|
@@ -1429,14 +1434,20 @@ function renderSplitGroupItem(group, members) {
|
|
|
1429
1434
|
escapeHtml(VENDOR_NAMES[vendor] || vendor) + '">';
|
|
1430
1435
|
}
|
|
1431
1436
|
html += "</span>";
|
|
1432
|
-
|
|
1433
|
-
var splitBackgroundTaskCount =
|
|
1437
|
+
var splitProcessing = false;
|
|
1438
|
+
var splitBackgroundTaskCount = 0;
|
|
1439
|
+
for (var si = 0; si < members.length; si++) {
|
|
1440
|
+
if (members[si].isProcessing) splitProcessing = true;
|
|
1441
|
+
splitBackgroundTaskCount += members[si].backgroundTaskCount || 0;
|
|
1442
|
+
}
|
|
1443
|
+
if (splitProcessing) html += '<span class="session-processing"></span>';
|
|
1434
1444
|
if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
|
|
1435
1445
|
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
|
|
1436
1446
|
text.innerHTML = html;
|
|
1437
1447
|
el.appendChild(text);
|
|
1438
1448
|
|
|
1439
|
-
var unread =
|
|
1449
|
+
var unread = 0;
|
|
1450
|
+
for (var ui = 0; ui < members.length; ui++) unread += members[ui].unread || 0;
|
|
1440
1451
|
var badge = document.createElement("span");
|
|
1441
1452
|
badge.className = "session-unread-badge" + (unread > 0 ? " has-unread" : "");
|
|
1442
1453
|
badge.dataset.splitGroupId = group.id;
|
|
@@ -1532,15 +1543,24 @@ export function renderSessionList(sessions) {
|
|
|
1532
1543
|
}
|
|
1533
1544
|
for (var sg = 0; sg < splitGroups.length; sg++) {
|
|
1534
1545
|
var splitGroup = splitGroups[sg];
|
|
1535
|
-
var
|
|
1536
|
-
var
|
|
1537
|
-
|
|
1538
|
-
|
|
1546
|
+
var splitMembers = [];
|
|
1547
|
+
for (var smi = 0; smi < splitGroup.members.length; smi++) {
|
|
1548
|
+
var splitMember = sessionsById.get(splitGroup.members[smi]);
|
|
1549
|
+
if (splitMember) splitMembers.push(splitMember);
|
|
1550
|
+
}
|
|
1551
|
+
if (splitMembers.length !== splitGroup.members.length) continue;
|
|
1552
|
+
var hasHierarchyMember = false;
|
|
1553
|
+
for (var hmi = 0; hmi < splitMembers.length; hmi++) {
|
|
1554
|
+
if (hierarchySessionIds.has(splitMembers[hmi].id)) hasHierarchyMember = true;
|
|
1555
|
+
}
|
|
1556
|
+
if (hasHierarchyMember) continue;
|
|
1557
|
+
var splitLastActivity = 0;
|
|
1558
|
+
for (var lai = 0; lai < splitMembers.length; lai++) splitLastActivity = Math.max(splitLastActivity, splitMembers[lai].lastActivity || 0);
|
|
1539
1559
|
items.push({
|
|
1540
1560
|
type: "split-group",
|
|
1541
1561
|
group: splitGroup,
|
|
1542
|
-
members:
|
|
1543
|
-
lastActivity:
|
|
1562
|
+
members: splitMembers,
|
|
1563
|
+
lastActivity: splitLastActivity,
|
|
1544
1564
|
});
|
|
1545
1565
|
}
|
|
1546
1566
|
var groupKeys = Object.keys(loopGroups);
|
|
@@ -1567,8 +1587,13 @@ export function renderSessionList(sessions) {
|
|
|
1567
1587
|
continue;
|
|
1568
1588
|
}
|
|
1569
1589
|
if ((item.type === "driver-hierarchy" || item.type === "orphan-workers") && !hierarchyItemMatches(item, searchMatchIds)) continue;
|
|
1570
|
-
if (item.type === "split-group" && searchMatchIds !== null
|
|
1571
|
-
|
|
1590
|
+
if (item.type === "split-group" && searchMatchIds !== null) {
|
|
1591
|
+
var splitSearchMatch = false;
|
|
1592
|
+
for (var splitSearchIndex = 0; splitSearchIndex < item.members.length; splitSearchIndex++) {
|
|
1593
|
+
if (searchMatchIds.has(item.members[splitSearchIndex].id)) splitSearchMatch = true;
|
|
1594
|
+
}
|
|
1595
|
+
if (!splitSearchMatch) continue;
|
|
1596
|
+
}
|
|
1572
1597
|
if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
|
|
1573
1598
|
bookmarkedItems.push(item);
|
|
1574
1599
|
} else {
|
|
@@ -12,15 +12,87 @@ export function findSplitGroup(groups, memberIds) {
|
|
|
12
12
|
var list = groups || [];
|
|
13
13
|
for (var i = 0; i < list.length; i++) {
|
|
14
14
|
var members = list[i].members || [];
|
|
15
|
-
if (members.length
|
|
15
|
+
if (members.length !== memberIds.length) continue;
|
|
16
|
+
var matches = true;
|
|
17
|
+
for (var j = 0; j < memberIds.length; j++) {
|
|
18
|
+
if (members.indexOf(memberIds[j]) === -1) { matches = false; break; }
|
|
19
|
+
}
|
|
20
|
+
if (matches) return list[i];
|
|
16
21
|
}
|
|
17
22
|
return null;
|
|
18
23
|
}
|
|
19
24
|
|
|
25
|
+
// Resolve the visible order from the explicit role contract. Member array
|
|
26
|
+
// order is persistence detail and must never decide who is the Driver.
|
|
27
|
+
export function splitGroupRoles(group) {
|
|
28
|
+
if (!group || !Array.isArray(group.members)) return null;
|
|
29
|
+
var pair = group.pair;
|
|
30
|
+
if (!pair || typeof pair !== "object" || Array.isArray(pair)) return null;
|
|
31
|
+
var hasVersion = Object.prototype.hasOwnProperty.call(pair, "version");
|
|
32
|
+
var hasLegacyWorker = Object.prototype.hasOwnProperty.call(pair, "workerId");
|
|
33
|
+
var hasWorkerIds = Object.prototype.hasOwnProperty.call(pair, "workerIds");
|
|
34
|
+
if (hasVersion && pair.version !== 2) return null;
|
|
35
|
+
if (hasVersion && hasLegacyWorker) return null;
|
|
36
|
+
if (hasVersion && !hasWorkerIds) return null;
|
|
37
|
+
if (!hasVersion && hasWorkerIds) return null;
|
|
38
|
+
if (!hasVersion && !hasLegacyWorker) return null;
|
|
39
|
+
var driverId = pair.driverId;
|
|
40
|
+
var workerIds = hasWorkerIds ? (Array.isArray(pair.workerIds) ? pair.workerIds.slice() : []) :
|
|
41
|
+
(pair.workerId !== undefined ? [pair.workerId] : []);
|
|
42
|
+
if (!Number.isInteger(driverId) || workerIds.length < 1 || workerIds.length > 2) return null;
|
|
43
|
+
if (group.members.indexOf(driverId) === -1) return null;
|
|
44
|
+
var ordered = [driverId];
|
|
45
|
+
for (var i = 0; i < workerIds.length; i++) {
|
|
46
|
+
if (!Number.isInteger(workerIds[i]) || workerIds[i] === driverId || group.members.indexOf(workerIds[i]) === -1 || ordered.indexOf(workerIds[i]) !== -1) return null;
|
|
47
|
+
ordered.push(workerIds[i]);
|
|
48
|
+
}
|
|
49
|
+
if (group.members.length !== ordered.length) return null;
|
|
50
|
+
for (var mi = 0; mi < group.members.length; mi++) if (ordered.indexOf(group.members[mi]) === -1) return null;
|
|
51
|
+
return { driverId: driverId, workerIds: workerIds, orderedIds: ordered, version: pair.version || null };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function splitGroupMemberIds(group) {
|
|
55
|
+
var roles = splitGroupRoles(group);
|
|
56
|
+
return roles ? roles.orderedIds : (group && Array.isArray(group.members) ? group.members.slice() : []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function splitGroupActiveAnchor(group, activeId) {
|
|
60
|
+
var roles = splitGroupRoles(group);
|
|
61
|
+
if (roles) return roles.driverId;
|
|
62
|
+
var members = group && Array.isArray(group.members) ? group.members : [];
|
|
63
|
+
return members.indexOf(activeId) !== -1 ? activeId : (members[0] || null);
|
|
64
|
+
}
|
|
65
|
+
|
|
20
66
|
export function isConfiguredWorker(groups, sessionId) {
|
|
21
67
|
var list = groups || [];
|
|
22
68
|
for (var i = 0; i < list.length; i++) {
|
|
23
|
-
|
|
69
|
+
var roles = splitGroupRoles(list[i]);
|
|
70
|
+
if (roles && roles.workerIds.indexOf(sessionId) !== -1) return true;
|
|
24
71
|
}
|
|
25
72
|
return false;
|
|
26
73
|
}
|
|
74
|
+
|
|
75
|
+
export function splitWorkerCloseRequest(group, sessions, projectSlug, workerId, requestId) {
|
|
76
|
+
var roles = splitGroupRoles(group);
|
|
77
|
+
if (!roles || roles.version !== 2 || roles.workerIds.indexOf(workerId) === -1) return null;
|
|
78
|
+
var anchors = group && group.pairAnchors;
|
|
79
|
+
if (!anchors || anchors.version !== 2 || !anchors.driver || !Array.isArray(anchors.workers)) return null;
|
|
80
|
+
var workerIndex = roles.workerIds.indexOf(workerId);
|
|
81
|
+
var workerAnchor = anchors.workers[workerIndex];
|
|
82
|
+
var worker = null;
|
|
83
|
+
var list = sessions || [];
|
|
84
|
+
for (var i = 0; i < list.length; i++) if (list[i].id === workerId) worker = list[i];
|
|
85
|
+
if (!workerAnchor || !anchors.driver.origin || !workerAnchor.origin || !worker || !Number.isInteger(worker.workerGeneration)) return null;
|
|
86
|
+
return {
|
|
87
|
+
type: "split_worker_close",
|
|
88
|
+
requestId: requestId,
|
|
89
|
+
projectSlug: projectSlug,
|
|
90
|
+
groupId: group.id,
|
|
91
|
+
driverId: roles.driverId,
|
|
92
|
+
driverOriginId: anchors.driver.origin,
|
|
93
|
+
workerId: workerId,
|
|
94
|
+
workerOriginId: workerAnchor.origin,
|
|
95
|
+
generation: worker.workerGeneration,
|
|
96
|
+
expectedWorkerIds: roles.workerIds.slice(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -5,6 +5,7 @@ import { buildAgentVendorSelect, fillAgentModels, buildAgentEffortSelect, fillAg
|
|
|
5
5
|
import { showToast } from './utils.js';
|
|
6
6
|
import { getCachedSessions } from './sidebar-sessions.js';
|
|
7
7
|
import { appendWorkerRuntime } from './split-worker-runtime.js';
|
|
8
|
+
import { splitGroupRoles } from './split-group-helpers.js';
|
|
8
9
|
|
|
9
10
|
var pairDialog = null;
|
|
10
11
|
// Set when the dialog was opened via "Add Split Worker" on an existing session:
|
|
@@ -245,6 +246,8 @@ export function syncPairChrome(host, split) {
|
|
|
245
246
|
var paneEls = host.querySelectorAll(".split-pane");
|
|
246
247
|
for (var paneIndex = 0; paneIndex < paneEls.length; paneIndex++) {
|
|
247
248
|
paneEls[paneIndex].classList.remove("split-pane-role-driver", "split-pane-role-worker");
|
|
249
|
+
paneEls[paneIndex].style.removeProperty("--split-pane-order");
|
|
250
|
+
paneEls[paneIndex].style.removeProperty("--split-worker-row");
|
|
248
251
|
}
|
|
249
252
|
var runtimeHeaders = host.querySelectorAll(".has-worker-runtime");
|
|
250
253
|
for (var ri = 0; ri < runtimeHeaders.length; ri++) runtimeHeaders[ri].classList.remove("has-worker-runtime");
|
|
@@ -268,10 +271,23 @@ export function syncPairChrome(host, split) {
|
|
|
268
271
|
}
|
|
269
272
|
return;
|
|
270
273
|
}
|
|
274
|
+
var roles = splitGroupRoles(group);
|
|
275
|
+
if (!roles) return;
|
|
276
|
+
var workerIndex = 0;
|
|
271
277
|
for (var pi = 0; pi < split.panes.length && pi < headers.length; pi++) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
278
|
+
var desiredPane = split.panes[pi];
|
|
279
|
+
var desiredPaneEl = null;
|
|
280
|
+
for (var pei = 0; pei < paneEls.length; pei++) {
|
|
281
|
+
if (paneEls[pei].dataset.sessionId === String(desiredPane.sessionId)) desiredPaneEl = paneEls[pei];
|
|
282
|
+
}
|
|
283
|
+
if (!desiredPaneEl) continue;
|
|
284
|
+
(function (sessionId, header, paneEl) {
|
|
285
|
+
var isDriver = sessionId === roles.driverId;
|
|
286
|
+
if (paneEl) {
|
|
287
|
+
paneEl.classList.add(isDriver ? "split-pane-role-driver" : "split-pane-role-worker");
|
|
288
|
+
paneEl.style.setProperty("--split-pane-order", isDriver ? "2" : String(workerIndex));
|
|
289
|
+
if (!isDriver) paneEl.style.setProperty("--split-worker-row", String(workerIndex + 1));
|
|
290
|
+
}
|
|
275
291
|
var role = isDriver ? "Driver" : "Split Worker";
|
|
276
292
|
var badge = document.createElement("span");
|
|
277
293
|
badge.className = "split-pair-role split-pair-role-" + role.toLowerCase().replace(/ /g, "-");
|
|
@@ -287,18 +303,24 @@ export function syncPairChrome(host, split) {
|
|
|
287
303
|
if (sessions[wi].id === sessionId) { workerSession = sessions[wi]; break; }
|
|
288
304
|
}
|
|
289
305
|
appendWorkerRuntime(header, workerSession);
|
|
306
|
+
workerIndex++;
|
|
290
307
|
}
|
|
291
|
-
})(
|
|
308
|
+
})(desiredPane.sessionId, desiredPaneEl.querySelector(".split-pane-header"), desiredPaneEl);
|
|
292
309
|
}
|
|
293
310
|
var active = (store.get('splitDelegations') || {})[group.id];
|
|
294
311
|
if (!active || !active.active) return;
|
|
295
312
|
host.classList.add("split-delegating");
|
|
296
313
|
var fromIndex = split.panes[0].sessionId === active.from ? 0 : 1;
|
|
297
314
|
host.classList.add(fromIndex === 0 ? "split-direction-right" : "split-direction-left");
|
|
298
|
-
for (var si = 0; si < split.panes.length
|
|
315
|
+
for (var si = 0; si < split.panes.length; si++) {
|
|
316
|
+
var livePane = null;
|
|
317
|
+
for (var lpi = 0; lpi < paneEls.length; lpi++) {
|
|
318
|
+
if (paneEls[lpi].dataset.sessionId === String(split.panes[si].sessionId)) livePane = paneEls[lpi];
|
|
319
|
+
}
|
|
320
|
+
if (!livePane) continue;
|
|
299
321
|
var live = document.createElement("span");
|
|
300
322
|
live.className = "split-pair-live";
|
|
301
323
|
live.textContent = split.panes[si].sessionId === active.to ? "Following direction" : "Waiting";
|
|
302
|
-
|
|
324
|
+
livePane.querySelector(".split-pane-header").appendChild(live);
|
|
303
325
|
}
|
|
304
326
|
}
|