clay-server 4.3.0-beta.4 → 4.3.0-beta.6
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-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +2 -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 +58 -76
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- 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-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- 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 +38 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/pane.css +48 -2
- package/lib/public/modules/app-connection.js +49 -76
- package/lib/public/modules/app-messages.js +11 -0
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/sidebar-sessions.js +31 -11
- 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-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/server.js +1 -0
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +41 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +11 -6
- package/lib/session-split-group-anchors.js +89 -7
- 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 +109 -99
- package/lib/sessions.js +33 -2
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -23,6 +23,7 @@ import { clearProjectSplitState } from './split-session-boundary.js';
|
|
|
23
23
|
import { clearPermissionModePending } from './permission-control.js';
|
|
24
24
|
import { clearMcpPermissionModePending } from './mcp-ui.js';
|
|
25
25
|
import { createWebSocketLifecycle } from './websocket-lifecycle.js';
|
|
26
|
+
import { createWebSocketWatchdog } from './websocket-watchdog.js';
|
|
26
27
|
|
|
27
28
|
var connectOverlay = null;
|
|
28
29
|
var hasConnectedOnce = false;
|
|
@@ -42,16 +43,9 @@ var overlayGraceTimer = null;
|
|
|
42
43
|
// server-global-ws.js. Well under the common 60s proxy read timeout, and one
|
|
43
44
|
// timer per live socket.
|
|
44
45
|
var HEARTBEAT_MS = 25000;
|
|
45
|
-
var heartbeatTimer = null;
|
|
46
|
-
var heartbeatDeadlineTimer = null;
|
|
47
|
-
var heartbeatAwaitingSocket = null;
|
|
48
46
|
var disconnectedAt = 0;
|
|
49
47
|
var attemptStartedAt = 0;
|
|
50
48
|
var attemptSocket = null;
|
|
51
|
-
var probeTimer = null;
|
|
52
|
-
var probeSocket = null;
|
|
53
|
-
var probeEpoch = 0;
|
|
54
|
-
var probeStartedAt = 0;
|
|
55
49
|
var lifecycle = createWebSocketLifecycle({
|
|
56
50
|
handshakeTimeoutMs: 12000,
|
|
57
51
|
onHandshakeTimeout: function (epoch) {
|
|
@@ -60,6 +54,21 @@ var lifecycle = createWebSocketLifecycle({
|
|
|
60
54
|
}
|
|
61
55
|
}
|
|
62
56
|
});
|
|
57
|
+
var watchdog = createWebSocketWatchdog({
|
|
58
|
+
heartbeatIntervalMs: HEARTBEAT_MS,
|
|
59
|
+
heartbeatDeadlineMs: 10000,
|
|
60
|
+
probeTimeoutMs: 4000,
|
|
61
|
+
now: function () { return Date.now(); },
|
|
62
|
+
isCurrent: function (socket, epoch) {
|
|
63
|
+
return getWs() === socket && socket.readyState === 1 && lifecycle.current(epoch);
|
|
64
|
+
},
|
|
65
|
+
sendPing: function (socket) {
|
|
66
|
+
socket.send(JSON.stringify({ type: "ping" }));
|
|
67
|
+
},
|
|
68
|
+
onFailure: function (reason, epoch, latency) {
|
|
69
|
+
forceReplaceSocket(reason, 0, epoch, latency);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
63
72
|
|
|
64
73
|
function clearOverlayGrace() {
|
|
65
74
|
if (overlayGraceTimer) {
|
|
@@ -88,19 +97,10 @@ function recordConnectionDiagnostic(reason, code, latency) {
|
|
|
88
97
|
});
|
|
89
98
|
}
|
|
90
99
|
|
|
91
|
-
function
|
|
92
|
-
if (probeTimer) clearTimeout(probeTimer);
|
|
93
|
-
probeTimer = null;
|
|
94
|
-
probeSocket = null;
|
|
95
|
-
probeEpoch = 0;
|
|
96
|
-
probeStartedAt = 0;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function forceReplaceSocket(reason, code, epoch) {
|
|
100
|
+
function forceReplaceSocket(reason, code, epoch, latency) {
|
|
100
101
|
if (!lifecycle.current(epoch)) return;
|
|
101
102
|
var socket = getWs();
|
|
102
|
-
|
|
103
|
-
clearProbe();
|
|
103
|
+
watchdog.clearProbe();
|
|
104
104
|
stopHeartbeat();
|
|
105
105
|
if (socket) {
|
|
106
106
|
socket.onopen = null;
|
|
@@ -117,19 +117,11 @@ function forceReplaceSocket(reason, code, epoch) {
|
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
export function stopHeartbeat() {
|
|
120
|
-
|
|
121
|
-
clearInterval(heartbeatTimer);
|
|
122
|
-
heartbeatTimer = null;
|
|
123
|
-
}
|
|
124
|
-
if (heartbeatDeadlineTimer) {
|
|
125
|
-
clearTimeout(heartbeatDeadlineTimer);
|
|
126
|
-
heartbeatDeadlineTimer = null;
|
|
127
|
-
}
|
|
128
|
-
heartbeatAwaitingSocket = null;
|
|
120
|
+
watchdog.stopHeartbeat();
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
function suspendSocketForOffline() {
|
|
132
|
-
clearProbe();
|
|
124
|
+
watchdog.clearProbe();
|
|
133
125
|
stopHeartbeat();
|
|
134
126
|
var socket = getWs();
|
|
135
127
|
attemptSocket = null;
|
|
@@ -150,31 +142,26 @@ function suspendSocketForOffline() {
|
|
|
150
142
|
// timeout from expiring. A missing pong also identifies a half-open socket,
|
|
151
143
|
// which browsers can otherwise leave OPEN while silently losing sends.
|
|
152
144
|
export function startHeartbeat(socket, attemptEpoch) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
heartbeatDeadlineTimer = null;
|
|
168
|
-
if (getWs() === socket && socket.readyState === 1 && lifecycle.current(attemptEpoch)) {
|
|
169
|
-
forceReplaceSocket("heartbeat_timeout", 0, attemptEpoch);
|
|
170
|
-
}
|
|
171
|
-
}, 10000);
|
|
172
|
-
}, HEARTBEAT_MS);
|
|
145
|
+
watchdog.startHeartbeat(socket, attemptEpoch);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function suspendSocketWatchdogs() {
|
|
149
|
+
// A frozen page cannot promise proxy keepalive. Retire its deadlines without
|
|
150
|
+
// closing the socket; resume performs a fresh bounded liveness check.
|
|
151
|
+
watchdog.suspend();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resumeSocketWatchdogs(reason) {
|
|
155
|
+
var resumed = watchdog.resume();
|
|
156
|
+
var socket = getWs();
|
|
157
|
+
if (socket && socket.readyState === 1 && resumed) startHeartbeat(socket, lifecycle.getEpoch());
|
|
158
|
+
probeReconnect(reason);
|
|
173
159
|
}
|
|
174
160
|
|
|
175
161
|
export function initConnection() {
|
|
176
162
|
connectOverlay = document.getElementById("connect-overlay");
|
|
177
163
|
if (typeof navigator !== "undefined" && navigator.onLine === false) lifecycle.setOffline(true);
|
|
164
|
+
if (document.hidden) suspendSocketWatchdogs();
|
|
178
165
|
window.addEventListener("clay-message-delivery-timeout", function (event) {
|
|
179
166
|
var socket = event.detail && event.detail.socket;
|
|
180
167
|
recordConnectionDiagnostic("ack-timeout", 0, null);
|
|
@@ -186,17 +173,19 @@ export function initConnection() {
|
|
|
186
173
|
});
|
|
187
174
|
window.addEventListener("online", function () {
|
|
188
175
|
lifecycle.wake(function () {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
probeReconnect("online");
|
|
176
|
+
if (document.hidden) return;
|
|
177
|
+
resumeSocketWatchdogs("online");
|
|
192
178
|
});
|
|
193
179
|
});
|
|
194
180
|
document.addEventListener("visibilitychange", function () {
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
181
|
+
if (document.hidden) suspendSocketWatchdogs();
|
|
182
|
+
else resumeSocketWatchdogs("visible");
|
|
183
|
+
});
|
|
184
|
+
document.addEventListener("freeze", function () {
|
|
185
|
+
suspendSocketWatchdogs();
|
|
186
|
+
});
|
|
187
|
+
document.addEventListener("resume", function () {
|
|
188
|
+
if (!document.hidden) resumeSocketWatchdogs("visible");
|
|
200
189
|
});
|
|
201
190
|
|
|
202
191
|
// --- Reactive UI sync for connected/processing state ---
|
|
@@ -379,7 +368,7 @@ export function connect() {
|
|
|
379
368
|
newWs.onclose = function (e) {
|
|
380
369
|
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
381
370
|
stopHeartbeat();
|
|
382
|
-
clearProbe();
|
|
371
|
+
watchdog.clearProbe();
|
|
383
372
|
disconnectedAt = Date.now();
|
|
384
373
|
recordConnectionDiagnostic("close", e && e.code, attemptStartedAt ? Date.now() - attemptStartedAt : null);
|
|
385
374
|
closeDmUserPicker();
|
|
@@ -408,18 +397,13 @@ export function connect() {
|
|
|
408
397
|
var msg;
|
|
409
398
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
410
399
|
lifecycle.markLive(attemptEpoch);
|
|
411
|
-
if (msg.type === "pong"
|
|
412
|
-
clearTimeout(heartbeatDeadlineTimer);
|
|
413
|
-
heartbeatDeadlineTimer = null;
|
|
414
|
-
heartbeatAwaitingSocket = null;
|
|
415
|
-
}
|
|
416
|
-
if (msg.type === "pong" && probeSocket === newWs && lifecycle.current(probeEpoch)) clearProbe();
|
|
400
|
+
if (msg.type === "pong") watchdog.acceptPong(newWs, attemptEpoch);
|
|
417
401
|
processMessage(msg);
|
|
418
402
|
};
|
|
419
403
|
}
|
|
420
404
|
|
|
421
405
|
export function cancelReconnect() {
|
|
422
|
-
clearProbe();
|
|
406
|
+
watchdog.clearProbe();
|
|
423
407
|
stopHeartbeat();
|
|
424
408
|
lifecycle.cancel();
|
|
425
409
|
}
|
|
@@ -429,18 +413,7 @@ function probeReconnect(reason) {
|
|
|
429
413
|
var socket = getWs();
|
|
430
414
|
if (socket && socket.readyState === 0) return;
|
|
431
415
|
if (socket && socket.readyState === 1) {
|
|
432
|
-
|
|
433
|
-
probeSocket = socket;
|
|
434
|
-
probeEpoch = lifecycle.getEpoch();
|
|
435
|
-
probeStartedAt = Date.now();
|
|
436
|
-
try { socket.send(JSON.stringify({ type: "ping" })); } catch (e) {
|
|
437
|
-
forceReplaceSocket("probe_error", 0, probeEpoch);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
probeTimer = setTimeout(function () {
|
|
441
|
-
probeTimer = null;
|
|
442
|
-
if (probeSocket === socket && lifecycle.current(probeEpoch) && getWs() === socket && socket.readyState === 1) forceReplaceSocket("probe_timeout", 0, probeEpoch);
|
|
443
|
-
}, 4000);
|
|
416
|
+
watchdog.probe(socket, lifecycle.getEpoch());
|
|
444
417
|
return;
|
|
445
418
|
}
|
|
446
419
|
scheduleReconnect(reason);
|
|
@@ -62,6 +62,7 @@ import { handleShellCommandResult } from './shell-command.js';
|
|
|
62
62
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
63
63
|
import { handleScheduledTaskMessage, handleScheduledTaskSessionSwitched } from './scheduled-tasks.js';
|
|
64
64
|
import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerationProgress, clearAllImageGenerationProgress } from './generated-images.js';
|
|
65
|
+
import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
|
|
65
66
|
|
|
66
67
|
// --- App module imports ---
|
|
67
68
|
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
@@ -243,6 +244,7 @@ export function processMessage(msg) {
|
|
|
243
244
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
244
245
|
if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
|
|
245
246
|
else markContextUnavailable();
|
|
247
|
+
|
|
246
248
|
// Restore accurate context data from the last result in full history
|
|
247
249
|
if (msg.lastUsage || msg.lastModelUsage) {
|
|
248
250
|
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
|
|
@@ -735,6 +737,10 @@ export function processMessage(msg) {
|
|
|
735
737
|
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
736
738
|
break;
|
|
737
739
|
|
|
740
|
+
case "split_worker_close_result":
|
|
741
|
+
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
742
|
+
break;
|
|
743
|
+
|
|
738
744
|
case "pair_session_options":
|
|
739
745
|
showPairDialog(msg);
|
|
740
746
|
break;
|
|
@@ -804,6 +810,10 @@ export function processMessage(msg) {
|
|
|
804
810
|
handleRemoteSelection(msg);
|
|
805
811
|
break;
|
|
806
812
|
|
|
813
|
+
case "pair_result_status":
|
|
814
|
+
handlePairResultStatus(msg);
|
|
815
|
+
break;
|
|
816
|
+
|
|
807
817
|
case "session_io":
|
|
808
818
|
blinkSessionDot(msg.id);
|
|
809
819
|
break;
|
|
@@ -823,6 +833,7 @@ export function processMessage(msg) {
|
|
|
823
833
|
break;
|
|
824
834
|
|
|
825
835
|
case "session_switched":
|
|
836
|
+
clearPairResultStatus();
|
|
826
837
|
syncAutonomousRunForSession(msg.autonomousRun || null);
|
|
827
838
|
closeWhatsNewArticle();
|
|
828
839
|
// Save draft from outgoing session
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { showConfirm } from './app-misc.js';
|
|
4
|
+
|
|
5
|
+
var statusGeneration = 0;
|
|
6
|
+
|
|
7
|
+
function retrySource(item) {
|
|
8
|
+
return { item: item, generation: statusGeneration, projectSlug: store.get("currentSlug") || store.get("activeProjectSlug") || null,
|
|
9
|
+
sessionId: store.get("activeSessionId"), userId: store.get("myUserId") || null, ws: getWs() };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sendRetry(source, confirmed) {
|
|
13
|
+
var ws = getWs();
|
|
14
|
+
var projectSlug = store.get("currentSlug") || store.get("activeProjectSlug") || null;
|
|
15
|
+
var items = store.get("pairResultStatus") || [];
|
|
16
|
+
var stillVisible = items.some(function (item) { return item.id === source.item.id; });
|
|
17
|
+
if (!ws || ws !== source.ws || ws.readyState !== 1 || statusGeneration !== source.generation ||
|
|
18
|
+
store.get("activeSessionId") !== source.sessionId || projectSlug !== source.projectSlug ||
|
|
19
|
+
(store.get("myUserId") || null) !== source.userId || !stillVisible) return false;
|
|
20
|
+
ws.send(JSON.stringify({ type: "pair_result_retry", id: source.item.id, confirmDuplicate: confirmed === true,
|
|
21
|
+
sessionId: source.sessionId, projectSlug: source.projectSlug }));
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function retry(item) {
|
|
26
|
+
var source = retrySource(item);
|
|
27
|
+
if (item.confirmDuplicate) {
|
|
28
|
+
showConfirm("The Driver may already have received this completed result. Retry delivery anyway? This can cause a duplicate result receipt.", function () {
|
|
29
|
+
sendRetry(source, true);
|
|
30
|
+
}, "Retry delivery", false);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
sendRetry(source, false);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function renderPairResultStatus() {
|
|
37
|
+
var existing = document.getElementById("pair-result-status");
|
|
38
|
+
if (existing) existing.remove();
|
|
39
|
+
var items = store.get("pairResultStatus") || [];
|
|
40
|
+
if (!items.length) return;
|
|
41
|
+
var messages = document.getElementById("messages");
|
|
42
|
+
if (!messages || !messages.parentElement) return;
|
|
43
|
+
var section = document.createElement("section");
|
|
44
|
+
section.id = "pair-result-status";
|
|
45
|
+
section.className = "pair-result-status";
|
|
46
|
+
section.setAttribute("role", "status");
|
|
47
|
+
for (var i = 0; i < items.length; i++) {
|
|
48
|
+
var item = items[i], card = document.createElement("div");
|
|
49
|
+
card.className = "pair-result-status-card pair-result-status-" + item.state;
|
|
50
|
+
var title = document.createElement("strong");
|
|
51
|
+
title.textContent = item.state === "blocked" ? "Split Worker result blocked" :
|
|
52
|
+
(item.state === "uncertain" ? (item.hasCompletedResult === false ? "Split Worker recovery uncertain" : "Split Worker result delivery uncertain") : "Split Worker result pending");
|
|
53
|
+
var message = document.createElement("span");
|
|
54
|
+
message.textContent = item.message || "The completed result is retained.";
|
|
55
|
+
card.appendChild(title); card.appendChild(message);
|
|
56
|
+
if (item.hasCompletedResult !== false) {
|
|
57
|
+
var details = document.createElement("details"), summary = document.createElement("summary"), body = document.createElement("pre");
|
|
58
|
+
summary.textContent = "View completed result";
|
|
59
|
+
body.textContent = item.error ? "Error: " + item.error : (item.response || "No text response was recorded.");
|
|
60
|
+
details.appendChild(summary); details.appendChild(body); card.appendChild(details);
|
|
61
|
+
}
|
|
62
|
+
if (item.canRetry) {
|
|
63
|
+
var button = document.createElement("button");
|
|
64
|
+
button.type = "button"; button.className = "confirm-btn confirm-ok pair-result-status-retry"; button.textContent = "Retry delivery";
|
|
65
|
+
button.addEventListener("click", retry.bind(null, item));
|
|
66
|
+
card.appendChild(button);
|
|
67
|
+
}
|
|
68
|
+
section.appendChild(card);
|
|
69
|
+
}
|
|
70
|
+
messages.parentElement.insertBefore(section, messages);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handlePairResultStatus(msg) {
|
|
74
|
+
if (!msg || msg.sessionId !== store.get("activeSessionId")) return false;
|
|
75
|
+
statusGeneration++;
|
|
76
|
+
store.set({ pairResultStatus: Array.isArray(msg.items) ? msg.items : [] });
|
|
77
|
+
renderPairResultStatus();
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clearPairResultStatus() {
|
|
82
|
+
statusGeneration++;
|
|
83
|
+
store.set({ pairResultStatus: [] });
|
|
84
|
+
renderPairResultStatus();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export { clearPairResultStatus, handlePairResultStatus, renderPairResultStatus };
|
|
@@ -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 };
|
|
@@ -1429,14 +1429,20 @@ function renderSplitGroupItem(group, members) {
|
|
|
1429
1429
|
escapeHtml(VENDOR_NAMES[vendor] || vendor) + '">';
|
|
1430
1430
|
}
|
|
1431
1431
|
html += "</span>";
|
|
1432
|
-
|
|
1433
|
-
var splitBackgroundTaskCount =
|
|
1432
|
+
var splitProcessing = false;
|
|
1433
|
+
var splitBackgroundTaskCount = 0;
|
|
1434
|
+
for (var si = 0; si < members.length; si++) {
|
|
1435
|
+
if (members[si].isProcessing) splitProcessing = true;
|
|
1436
|
+
splitBackgroundTaskCount += members[si].backgroundTaskCount || 0;
|
|
1437
|
+
}
|
|
1438
|
+
if (splitProcessing) html += '<span class="session-processing"></span>';
|
|
1434
1439
|
if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
|
|
1435
1440
|
html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", searchQuery) + "</span>";
|
|
1436
1441
|
text.innerHTML = html;
|
|
1437
1442
|
el.appendChild(text);
|
|
1438
1443
|
|
|
1439
|
-
var unread =
|
|
1444
|
+
var unread = 0;
|
|
1445
|
+
for (var ui = 0; ui < members.length; ui++) unread += members[ui].unread || 0;
|
|
1440
1446
|
var badge = document.createElement("span");
|
|
1441
1447
|
badge.className = "session-unread-badge" + (unread > 0 ? " has-unread" : "");
|
|
1442
1448
|
badge.dataset.splitGroupId = group.id;
|
|
@@ -1532,15 +1538,24 @@ export function renderSessionList(sessions) {
|
|
|
1532
1538
|
}
|
|
1533
1539
|
for (var sg = 0; sg < splitGroups.length; sg++) {
|
|
1534
1540
|
var splitGroup = splitGroups[sg];
|
|
1535
|
-
var
|
|
1536
|
-
var
|
|
1537
|
-
|
|
1538
|
-
|
|
1541
|
+
var splitMembers = [];
|
|
1542
|
+
for (var smi = 0; smi < splitGroup.members.length; smi++) {
|
|
1543
|
+
var splitMember = sessionsById.get(splitGroup.members[smi]);
|
|
1544
|
+
if (splitMember) splitMembers.push(splitMember);
|
|
1545
|
+
}
|
|
1546
|
+
if (splitMembers.length !== splitGroup.members.length) continue;
|
|
1547
|
+
var hasHierarchyMember = false;
|
|
1548
|
+
for (var hmi = 0; hmi < splitMembers.length; hmi++) {
|
|
1549
|
+
if (hierarchySessionIds.has(splitMembers[hmi].id)) hasHierarchyMember = true;
|
|
1550
|
+
}
|
|
1551
|
+
if (hasHierarchyMember) continue;
|
|
1552
|
+
var splitLastActivity = 0;
|
|
1553
|
+
for (var lai = 0; lai < splitMembers.length; lai++) splitLastActivity = Math.max(splitLastActivity, splitMembers[lai].lastActivity || 0);
|
|
1539
1554
|
items.push({
|
|
1540
1555
|
type: "split-group",
|
|
1541
1556
|
group: splitGroup,
|
|
1542
|
-
members:
|
|
1543
|
-
lastActivity:
|
|
1557
|
+
members: splitMembers,
|
|
1558
|
+
lastActivity: splitLastActivity,
|
|
1544
1559
|
});
|
|
1545
1560
|
}
|
|
1546
1561
|
var groupKeys = Object.keys(loopGroups);
|
|
@@ -1567,8 +1582,13 @@ export function renderSessionList(sessions) {
|
|
|
1567
1582
|
continue;
|
|
1568
1583
|
}
|
|
1569
1584
|
if ((item.type === "driver-hierarchy" || item.type === "orphan-workers") && !hierarchyItemMatches(item, searchMatchIds)) continue;
|
|
1570
|
-
if (item.type === "split-group" && searchMatchIds !== null
|
|
1571
|
-
|
|
1585
|
+
if (item.type === "split-group" && searchMatchIds !== null) {
|
|
1586
|
+
var splitSearchMatch = false;
|
|
1587
|
+
for (var splitSearchIndex = 0; splitSearchIndex < item.members.length; splitSearchIndex++) {
|
|
1588
|
+
if (searchMatchIds.has(item.members[splitSearchIndex].id)) splitSearchMatch = true;
|
|
1589
|
+
}
|
|
1590
|
+
if (!splitSearchMatch) continue;
|
|
1591
|
+
}
|
|
1572
1592
|
if ((item.type === "session" || item.type === "driver-hierarchy") && item.data && item.data.bookmarked) {
|
|
1573
1593
|
bookmarkedItems.push(item);
|
|
1574
1594
|
} 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
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Keyed DOM reconciliation for the parent split shell. Existing pane nodes
|
|
2
|
+
// (and their iframe browsing contexts) are reused by stable project/session
|
|
3
|
+
// identity while metadata and visual order are refreshed.
|
|
4
|
+
|
|
5
|
+
export function splitPaneIdentity(pane) {
|
|
6
|
+
return String(pane && pane.slug || "") + "#" + String(pane && pane.sessionId || "");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function reconcileSplitPanes(host, panes, createPane, updatePane) {
|
|
10
|
+
var desired = Array.isArray(panes) ? panes : [];
|
|
11
|
+
var existing = host.querySelectorAll(".split-pane");
|
|
12
|
+
var byKey = new Map();
|
|
13
|
+
for (var i = 0; i < existing.length; i++) byKey.set(existing[i].dataset.splitPaneKey, existing[i]);
|
|
14
|
+
var used = new Set();
|
|
15
|
+
for (var pi = 0; pi < desired.length; pi++) {
|
|
16
|
+
var pane = desired[pi];
|
|
17
|
+
var key = splitPaneIdentity(pane);
|
|
18
|
+
var paneEl = byKey.get(key);
|
|
19
|
+
if (!paneEl) paneEl = createPane(pane, pi);
|
|
20
|
+
paneEl.dataset.splitPaneKey = key;
|
|
21
|
+
updatePane(paneEl, pane, pi);
|
|
22
|
+
used.add(paneEl);
|
|
23
|
+
if (!paneEl.parentNode) host.appendChild(paneEl);
|
|
24
|
+
}
|
|
25
|
+
for (var ei = existing.length - 1; ei >= 0; ei--) {
|
|
26
|
+
if (!used.has(existing[ei])) existing[ei].remove();
|
|
27
|
+
}
|
|
28
|
+
}
|