clay-server 4.3.0-beta.1 → 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-pair.js +5 -1
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +3 -1
- 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 +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-sessions.js +4 -0
- package/lib/project-user-message.js +7 -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 +38 -1
- package/lib/public/app.js +1 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/mobile-nav.css +29 -14
- package/lib/public/css/overlays.css +22 -21
- package/lib/public/css/pane.css +64 -14
- 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 +17 -3
- 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 +5 -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/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 +70 -21
- package/lib/ws-schema.js +2 -0
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ import { handleIssuesMessageInContext as handleIssuesMessage } from './issues-co
|
|
|
4
4
|
// All dependencies are direct imports; no context injection needed.
|
|
5
5
|
|
|
6
6
|
import { store } from './store.js';
|
|
7
|
-
import { acknowledgeMessage,
|
|
7
|
+
import { acknowledgeMessage, activateDeliverySession, refreshDeliverySessionIdentity } from './message-delivery.js';
|
|
8
|
+
import { initMessageDeliveryUi } from './message-delivery-ui.js';
|
|
8
9
|
import { getWs } from './ws-ref.js';
|
|
9
10
|
|
|
10
11
|
// --- Leaf module imports ---
|
|
@@ -61,6 +62,7 @@ import { handleShellCommandResult } from './shell-command.js';
|
|
|
61
62
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
62
63
|
import { handleScheduledTaskMessage, handleScheduledTaskSessionSwitched } from './scheduled-tasks.js';
|
|
63
64
|
import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerationProgress, clearAllImageGenerationProgress } from './generated-images.js';
|
|
65
|
+
import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
|
|
64
66
|
|
|
65
67
|
// --- App module imports ---
|
|
66
68
|
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
@@ -98,6 +100,7 @@ var messagesEl = document.getElementById("messages");
|
|
|
98
100
|
var headerTitleEl = document.getElementById("header-title");
|
|
99
101
|
var inputEl = document.getElementById("input");
|
|
100
102
|
var connectOverlay = document.getElementById("connect-overlay");
|
|
103
|
+
initMessageDeliveryUi();
|
|
101
104
|
|
|
102
105
|
export function processMessage(msg) {
|
|
103
106
|
if (handleIssuesMessage(msg)) return;
|
|
@@ -241,6 +244,7 @@ export function processMessage(msg) {
|
|
|
241
244
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
242
245
|
if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
|
|
243
246
|
else markContextUnavailable();
|
|
247
|
+
|
|
244
248
|
// Restore accurate context data from the last result in full history
|
|
245
249
|
if (msg.lastUsage || msg.lastModelUsage) {
|
|
246
250
|
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
|
|
@@ -733,6 +737,10 @@ export function processMessage(msg) {
|
|
|
733
737
|
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
734
738
|
break;
|
|
735
739
|
|
|
740
|
+
case "split_worker_close_result":
|
|
741
|
+
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
742
|
+
break;
|
|
743
|
+
|
|
736
744
|
case "pair_session_options":
|
|
737
745
|
showPairDialog(msg);
|
|
738
746
|
break;
|
|
@@ -802,6 +810,10 @@ export function processMessage(msg) {
|
|
|
802
810
|
handleRemoteSelection(msg);
|
|
803
811
|
break;
|
|
804
812
|
|
|
813
|
+
case "pair_result_status":
|
|
814
|
+
handlePairResultStatus(msg);
|
|
815
|
+
break;
|
|
816
|
+
|
|
805
817
|
case "session_io":
|
|
806
818
|
blinkSessionDot(msg.id);
|
|
807
819
|
break;
|
|
@@ -821,6 +833,7 @@ export function processMessage(msg) {
|
|
|
821
833
|
break;
|
|
822
834
|
|
|
823
835
|
case "session_switched":
|
|
836
|
+
clearPairResultStatus();
|
|
824
837
|
syncAutonomousRunForSession(msg.autonomousRun || null);
|
|
825
838
|
closeWhatsNewArticle();
|
|
826
839
|
// Save draft from outgoing session
|
|
@@ -839,10 +852,10 @@ export function processMessage(msg) {
|
|
|
839
852
|
var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
|
|
840
853
|
? msg.runtimeTerminalId
|
|
841
854
|
: (typeof msg.terminalId === "number" ? msg.terminalId : null);
|
|
842
|
-
store.set({ activeSessionId: msg.id, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode'), effectivePermissionMode: msg.effectivePermissionMode || null, permissionCapabilities: msg.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
|
|
855
|
+
store.set({ activeSessionId: msg.id, sessionOriginId: msg.sessionOriginId || null, cliSessionId: msg.cliSessionId || null, currentModel: msg.model || "", currentEffort: msg.effort || store.get('currentEffort'), vendorCapabilities: msg.capabilities || {}, sessionIsProcessing: !!msg.isProcessing, activeSessionMode: _effectiveMode, activeTerminalId: _effectiveTerminalId, sessionHasHistory: !!msg.hasHistory, sessionVendorBound: !!msg.vendor || !!msg.hasHistory, sessionFullAccess: msg.permissionMode === "bypassPermissions", currentMode: msg.permissionMode || store.get('currentMode'), effectivePermissionMode: msg.effectivePermissionMode || null, permissionCapabilities: msg.permissionCapabilities || { auto: false, mcpOverride: false }, mcpPermissionModeOverrides: msg.mcpPermissionModeOverrides || {} });
|
|
843
856
|
handleScheduledTaskSessionSwitched(msg);
|
|
844
857
|
requestLoopInterviewState();
|
|
845
|
-
|
|
858
|
+
activateDeliverySession(msg.id, msg.cliSessionId || null, msg.sessionOriginId || null);
|
|
846
859
|
// TUI sessions swap the chat UI for an embedded xterm running
|
|
847
860
|
// `claude` inside a real PTY. Mount or tear down before the rest of
|
|
848
861
|
// the chat-side bookkeeping runs so we don't waste work on hidden DOM.
|
|
@@ -936,6 +949,7 @@ export function processMessage(msg) {
|
|
|
936
949
|
|
|
937
950
|
case "session_id":
|
|
938
951
|
store.set({ cliSessionId: msg.cliSessionId });
|
|
952
|
+
refreshDeliverySessionIdentity(msg.cliSessionId, store.get('sessionOriginId') || null);
|
|
939
953
|
break;
|
|
940
954
|
|
|
941
955
|
case "message_uuid":
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// message-delivery-ui.js - receipt recovery notices
|
|
2
|
+
|
|
3
|
+
import { store } from './store.js';
|
|
4
|
+
import { retryPendingMessage } from './message-delivery.js';
|
|
5
|
+
|
|
6
|
+
var initialized = false;
|
|
7
|
+
var rendered = {};
|
|
8
|
+
var unsubscribe = null;
|
|
9
|
+
|
|
10
|
+
function currentContext(receipt) {
|
|
11
|
+
var currentOriginId = store.get('sessionOriginId') || null;
|
|
12
|
+
return receipt && receipt.projectSlug === store.get('currentSlug') && String(receipt.sessionId) === String(store.get('activeSessionId')) && receipt.accountId === (store.get('myUserId') || "_default") && (!receipt.sessionOriginId || !currentOriginId || receipt.sessionOriginId === currentOriginId);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderReceipt(receipt) {
|
|
16
|
+
var messages = document.getElementById("messages");
|
|
17
|
+
if (!messages || !currentContext(receipt) || rendered[receipt.clientMessageId]) return;
|
|
18
|
+
var notice = document.createElement("div");
|
|
19
|
+
notice.className = "sys-msg error";
|
|
20
|
+
notice.dataset.deliveryReceipt = receipt.clientMessageId;
|
|
21
|
+
var text = document.createElement("span");
|
|
22
|
+
text.className = "sys-text";
|
|
23
|
+
text.textContent = "Receipt unconfirmed. Retry this message.";
|
|
24
|
+
notice.appendChild(text);
|
|
25
|
+
var retry = document.createElement("button");
|
|
26
|
+
retry.type = "button";
|
|
27
|
+
retry.className = "sys-retry";
|
|
28
|
+
retry.textContent = "Retry";
|
|
29
|
+
retry.addEventListener("click", function () { retryPendingMessage(receipt.clientMessageId); });
|
|
30
|
+
notice.appendChild(retry);
|
|
31
|
+
messages.appendChild(notice);
|
|
32
|
+
rendered[receipt.clientMessageId] = notice;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sync(state) {
|
|
36
|
+
var receipts = state.deliveryReceipts || {};
|
|
37
|
+
Object.keys(rendered).forEach(function (id) {
|
|
38
|
+
if (!rendered[id].parentNode || !receipts[id] || !currentContext(receipts[id])) {
|
|
39
|
+
if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
|
|
40
|
+
delete rendered[id];
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
Object.keys(receipts).forEach(function (id) { renderReceipt(receipts[id]); });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function initMessageDeliveryUi() {
|
|
47
|
+
if (initialized) return;
|
|
48
|
+
initialized = true;
|
|
49
|
+
unsubscribe = store.subscribe(function (state, previous) {
|
|
50
|
+
if (state.deliveryReceipts !== previous.deliveryReceipts || state.currentSlug !== previous.currentSlug || state.activeSessionId !== previous.activeSessionId || state.sessionOriginId !== previous.sessionOriginId || state.myUserId !== previous.myUserId || state.replayingHistory !== previous.replayingHistory) sync(state);
|
|
51
|
+
});
|
|
52
|
+
sync(store.snap());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function disposeMessageDeliveryUi() {
|
|
56
|
+
Object.keys(rendered).forEach(function (id) {
|
|
57
|
+
if (rendered[id] && rendered[id].parentNode) rendered[id].parentNode.removeChild(rendered[id]);
|
|
58
|
+
});
|
|
59
|
+
rendered = {};
|
|
60
|
+
if (unsubscribe) unsubscribe();
|
|
61
|
+
unsubscribe = null;
|
|
62
|
+
initialized = false;
|
|
63
|
+
}
|
|
@@ -4,7 +4,10 @@ import { store } from './store.js';
|
|
|
4
4
|
import { getWs } from './ws-ref.js';
|
|
5
5
|
|
|
6
6
|
var ACK_TIMEOUT_MS = 5000;
|
|
7
|
+
var ACK_RETRY_LIMIT = 3;
|
|
7
8
|
var ackTimers = {};
|
|
9
|
+
var socketRefs = {};
|
|
10
|
+
var activatedContext = null;
|
|
8
11
|
|
|
9
12
|
function createClientMessageId() {
|
|
10
13
|
if (window.crypto && typeof window.crypto.randomUUID === "function") {
|
|
@@ -21,6 +24,39 @@ function replacePending(messages) {
|
|
|
21
24
|
store.set({ pendingOutboundMessages: messages });
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
function receipts() {
|
|
28
|
+
return store.get('deliveryReceipts') || {};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setReceipt(entry) {
|
|
32
|
+
var next = Object.assign({}, receipts());
|
|
33
|
+
next[entry.payload.clientMessageId] = {
|
|
34
|
+
clientMessageId: entry.payload.clientMessageId,
|
|
35
|
+
projectSlug: entry.projectSlug,
|
|
36
|
+
sessionId: entry.sessionId,
|
|
37
|
+
accountId: entry.accountId,
|
|
38
|
+
sessionOriginId: entry.sessionOriginId || null,
|
|
39
|
+
attempts: entry.attempts
|
|
40
|
+
};
|
|
41
|
+
store.set({ deliveryReceipts: next });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function clearReceipt(clientMessageId) {
|
|
45
|
+
var current = receipts();
|
|
46
|
+
if (!current[clientMessageId]) return;
|
|
47
|
+
var next = Object.assign({}, current);
|
|
48
|
+
delete next[clientMessageId];
|
|
49
|
+
store.set({ deliveryReceipts: next });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function markUnconfirmed(entry) {
|
|
53
|
+
var unconfirmed = Object.assign({}, entry, { status: "unconfirmed" });
|
|
54
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
55
|
+
return candidate.payload.clientMessageId === entry.payload.clientMessageId ? unconfirmed : candidate;
|
|
56
|
+
}));
|
|
57
|
+
setReceipt(unconfirmed);
|
|
58
|
+
}
|
|
59
|
+
|
|
24
60
|
function clearAckTimer(clientMessageId) {
|
|
25
61
|
if (!ackTimers[clientMessageId]) return;
|
|
26
62
|
clearTimeout(ackTimers[clientMessageId]);
|
|
@@ -35,25 +71,92 @@ function isPending(clientMessageId) {
|
|
|
35
71
|
return false;
|
|
36
72
|
}
|
|
37
73
|
|
|
74
|
+
function pendingEntry(clientMessageId) {
|
|
75
|
+
var pending = pendingMessages();
|
|
76
|
+
for (var i = 0; i < pending.length; i++) {
|
|
77
|
+
if (pending[i].payload.clientMessageId === clientMessageId) return pending[i];
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchesCurrentContext(entry) {
|
|
83
|
+
return entry && entry.projectSlug === store.get('currentSlug') && String(entry.sessionId) === String(store.get('activeSessionId')) && entry.accountId === (store.get('myUserId') || "_default");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasAutomaticReplayIdentity(entry, socket) {
|
|
87
|
+
if (!activatedContext || activatedContext.socket !== socket) return false;
|
|
88
|
+
if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId !== activatedContext.sessionOriginId) return false;
|
|
89
|
+
if (entry.sessionOriginId && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return true;
|
|
90
|
+
if (entry.cliSessionId && activatedContext.cliSessionId) return entry.cliSessionId === activatedContext.cliSessionId;
|
|
91
|
+
return socketRefs[entry.payload.clientMessageId] === socket;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasKnownCliMismatch(entry) {
|
|
95
|
+
if (entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId) return false;
|
|
96
|
+
return !!entry.cliSessionId && !!activatedContext && !!activatedContext.cliSessionId && entry.cliSessionId !== activatedContext.cliSessionId;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function matchesActivatedContext(entry) {
|
|
100
|
+
var currentCliSessionId = store.get('cliSessionId') || null;
|
|
101
|
+
var sameOrigin = !!(entry.sessionOriginId && activatedContext && activatedContext.sessionOriginId && entry.sessionOriginId === activatedContext.sessionOriginId);
|
|
102
|
+
return !!activatedContext && activatedContext.socket === getWs() && matchesCurrentContext(entry) &&
|
|
103
|
+
activatedContext.projectSlug === store.get('currentSlug') && String(activatedContext.sessionId) === String(store.get('activeSessionId')) &&
|
|
104
|
+
activatedContext.accountId === (store.get('myUserId') || "_default") && (activatedContext.sessionOriginId || activatedContext.cliSessionId === currentCliSessionId) &&
|
|
105
|
+
(!entry.sessionOriginId || !activatedContext.sessionOriginId || entry.sessionOriginId === activatedContext.sessionOriginId) &&
|
|
106
|
+
(sameOrigin || !entry.cliSessionId || !activatedContext.cliSessionId || entry.cliSessionId === activatedContext.cliSessionId);
|
|
107
|
+
}
|
|
108
|
+
|
|
38
109
|
function armAckTimer(clientMessageId, socket) {
|
|
39
110
|
clearAckTimer(clientMessageId);
|
|
40
111
|
ackTimers[clientMessageId] = setTimeout(function () {
|
|
41
112
|
delete ackTimers[clientMessageId];
|
|
42
113
|
if (!isPending(clientMessageId) || getWs() !== socket) return;
|
|
114
|
+
var entry = pendingEntry(clientMessageId);
|
|
115
|
+
if (!matchesActivatedContext(entry)) return;
|
|
43
116
|
window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
|
|
44
117
|
detail: { socket: socket, clientMessageId: clientMessageId },
|
|
45
118
|
}));
|
|
119
|
+
var attempts = entry.attempts || 0;
|
|
120
|
+
if (attempts < ACK_RETRY_LIMIT && entry && socket.readyState === 1) {
|
|
121
|
+
var nextEntry = Object.assign({}, entry, { attempts: attempts + 1 });
|
|
122
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
123
|
+
return candidate.payload.clientMessageId === clientMessageId ? nextEntry : candidate;
|
|
124
|
+
}));
|
|
125
|
+
transmit(nextEntry, false);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (attempts >= ACK_RETRY_LIMIT) {
|
|
129
|
+
markUnconfirmed(entry);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
46
132
|
}, ACK_TIMEOUT_MS);
|
|
47
133
|
}
|
|
48
134
|
|
|
49
|
-
function transmit(entry) {
|
|
135
|
+
function transmit(entry, automatic) {
|
|
50
136
|
var socket = getWs();
|
|
51
|
-
if (!socket || socket.readyState !== 1) return false;
|
|
137
|
+
if (!matchesCurrentContext(entry) || !socket || socket.readyState !== 1) return false;
|
|
138
|
+
if (automatic && entry.status === "unconfirmed") return false;
|
|
139
|
+
if (automatic && !hasAutomaticReplayIdentity(entry, socket)) {
|
|
140
|
+
markUnconfirmed(entry);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (automatic) {
|
|
144
|
+
if ((entry.attempts || 0) >= ACK_RETRY_LIMIT) {
|
|
145
|
+
markUnconfirmed(entry);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
entry = Object.assign({}, entry, { attempts: (entry.attempts || 0) + 1 });
|
|
149
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
150
|
+
return candidate.payload.clientMessageId === entry.payload.clientMessageId ? entry : candidate;
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
socketRefs[entry.payload.clientMessageId] = socket;
|
|
52
154
|
try {
|
|
53
155
|
socket.send(JSON.stringify(entry.payload));
|
|
54
156
|
armAckTimer(entry.payload.clientMessageId, socket);
|
|
55
157
|
return true;
|
|
56
158
|
} catch (e) {
|
|
159
|
+
markUnconfirmed(entry);
|
|
57
160
|
return false;
|
|
58
161
|
}
|
|
59
162
|
}
|
|
@@ -63,10 +166,22 @@ export function sendAcknowledgedMessage(payload) {
|
|
|
63
166
|
var entry = {
|
|
64
167
|
projectSlug: store.get('currentSlug'),
|
|
65
168
|
sessionId: store.get('activeSessionId'),
|
|
169
|
+
accountId: store.get('myUserId') || "_default",
|
|
170
|
+
cliSessionId: store.get('cliSessionId') || null,
|
|
171
|
+
sessionOriginId: store.get('sessionOriginId') || null,
|
|
172
|
+
attempts: 0,
|
|
173
|
+
status: "pending",
|
|
66
174
|
payload: nextPayload,
|
|
67
175
|
};
|
|
176
|
+
nextPayload.projectSlug = entry.projectSlug;
|
|
177
|
+
nextPayload.sessionId = entry.sessionId;
|
|
178
|
+
nextPayload.accountId = entry.accountId;
|
|
179
|
+
nextPayload.cliSessionId = entry.cliSessionId;
|
|
180
|
+
nextPayload.sessionOriginId = entry.sessionOriginId;
|
|
181
|
+
entry.payload = nextPayload;
|
|
68
182
|
replacePending(pendingMessages().concat([entry]));
|
|
69
|
-
if (!transmit(entry)) {
|
|
183
|
+
if (!transmit(entry, false)) {
|
|
184
|
+
if (!getWs() || getWs().readyState !== 1) markUnconfirmed(entry);
|
|
70
185
|
window.dispatchEvent(new CustomEvent("clay-message-delivery-timeout", {
|
|
71
186
|
detail: { socket: getWs(), clientMessageId: nextPayload.clientMessageId },
|
|
72
187
|
}));
|
|
@@ -74,6 +189,21 @@ export function sendAcknowledgedMessage(payload) {
|
|
|
74
189
|
return nextPayload.clientMessageId;
|
|
75
190
|
}
|
|
76
191
|
|
|
192
|
+
export function retryPendingMessage(clientMessageId) {
|
|
193
|
+
var entry = pendingEntry(clientMessageId);
|
|
194
|
+
if (!matchesActivatedContext(entry) || hasKnownCliMismatch(entry)) return false;
|
|
195
|
+
var retryEntry = Object.assign({}, entry, { attempts: 0, status: "pending" });
|
|
196
|
+
replacePending(pendingMessages().map(function (candidate) {
|
|
197
|
+
return candidate.payload.clientMessageId === clientMessageId ? retryEntry : candidate;
|
|
198
|
+
}));
|
|
199
|
+
if (transmit(retryEntry, false)) {
|
|
200
|
+
clearReceipt(clientMessageId);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
markUnconfirmed(retryEntry);
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
77
207
|
export function acknowledgeMessage(clientMessageId) {
|
|
78
208
|
if (typeof clientMessageId !== "string") return null;
|
|
79
209
|
var pending = pendingMessages();
|
|
@@ -82,9 +212,14 @@ export function acknowledgeMessage(clientMessageId) {
|
|
|
82
212
|
if (entry.payload.clientMessageId === clientMessageId) acknowledged = entry;
|
|
83
213
|
return entry.payload.clientMessageId !== clientMessageId;
|
|
84
214
|
});
|
|
85
|
-
if (next.length === pending.length)
|
|
215
|
+
if (next.length === pending.length) {
|
|
216
|
+
clearReceipt(clientMessageId);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
86
219
|
clearAckTimer(clientMessageId);
|
|
87
220
|
replacePending(next);
|
|
221
|
+
delete socketRefs[clientMessageId];
|
|
222
|
+
clearReceipt(clientMessageId);
|
|
88
223
|
return acknowledged;
|
|
89
224
|
}
|
|
90
225
|
|
|
@@ -92,8 +227,30 @@ export function replayPendingMessages(sessionId) {
|
|
|
92
227
|
var pending = pendingMessages();
|
|
93
228
|
var projectSlug = store.get('currentSlug');
|
|
94
229
|
for (var i = 0; i < pending.length; i++) {
|
|
95
|
-
if (pending[i].projectSlug === projectSlug && pending[i].sessionId === sessionId) {
|
|
96
|
-
transmit(pending[i]);
|
|
230
|
+
if (pending[i].projectSlug === projectSlug && String(pending[i].sessionId) === String(sessionId) && matchesCurrentContext(pending[i])) {
|
|
231
|
+
transmit(pending[i], true);
|
|
97
232
|
}
|
|
98
233
|
}
|
|
99
234
|
}
|
|
235
|
+
|
|
236
|
+
export function activateDeliverySession(sessionId, cliSessionId, sessionOriginId) {
|
|
237
|
+
var socket = getWs();
|
|
238
|
+
activatedContext = {
|
|
239
|
+
socket: socket,
|
|
240
|
+
projectSlug: store.get('currentSlug'),
|
|
241
|
+
sessionId: sessionId,
|
|
242
|
+
accountId: store.get('myUserId') || "_default",
|
|
243
|
+
cliSessionId: cliSessionId || null,
|
|
244
|
+
sessionOriginId: sessionOriginId || null
|
|
245
|
+
};
|
|
246
|
+
replayPendingMessages(sessionId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function refreshDeliverySessionIdentity(cliSessionId, sessionOriginId) {
|
|
250
|
+
if (!activatedContext || activatedContext.socket !== getWs()) return false;
|
|
251
|
+
if (activatedContext.projectSlug !== store.get('currentSlug') || String(activatedContext.sessionId) !== String(store.get('activeSessionId')) || activatedContext.accountId !== (store.get('myUserId') || "_default")) return false;
|
|
252
|
+
if (activatedContext.sessionOriginId && sessionOriginId && activatedContext.sessionOriginId !== sessionOriginId) return false;
|
|
253
|
+
activatedContext.cliSessionId = cliSessionId || null;
|
|
254
|
+
if (!activatedContext.sessionOriginId && sessionOriginId) activatedContext.sessionOriginId = sessionOriginId;
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
@@ -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 };
|
|
@@ -46,9 +46,9 @@ 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"><span class="permission-label">Ask</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
|
|
50
|
-
'<button type="button" data-permission-mode="auto"><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
|
|
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
53
|
control.querySelector(".session-permission-status").setAttribute("aria-atomic", "true");
|
|
54
54
|
return control;
|
|
@@ -101,6 +101,7 @@ export function renderPermissionControl(control, state) {
|
|
|
101
101
|
control.classList.toggle("permission-globally-forced", globallyForced);
|
|
102
102
|
control.classList.toggle("permission-runtime-fallback", state.permissionMode === "auto" && !!state.effectivePermissionMode && state.effectivePermissionMode !== "auto");
|
|
103
103
|
control.setAttribute("aria-busy", pending ? "true" : "false");
|
|
104
|
+
var buttonTitles = { default: "Ask permissions", auto: "Auto permissions", bypassPermissions: "Skip permissions" };
|
|
104
105
|
var buttons = control.querySelectorAll("[data-permission-mode]");
|
|
105
106
|
for (var i = 0; i < buttons.length; i++) {
|
|
106
107
|
var mode = buttons[i].dataset.permissionMode;
|
|
@@ -113,8 +114,7 @@ export function renderPermissionControl(control, state) {
|
|
|
113
114
|
buttons[i].classList.toggle("pending", requested);
|
|
114
115
|
buttons[i].setAttribute("aria-pressed", selected ? "true" : "false");
|
|
115
116
|
buttons[i].setAttribute("aria-busy", requested ? "true" : "false");
|
|
116
|
-
|
|
117
|
-
else buttons[i].removeAttribute("title");
|
|
117
|
+
buttons[i].title = unsupported ? "Auto permissions are unavailable for this Claude session." : buttonTitles[mode];
|
|
118
118
|
}
|
|
119
119
|
var status = control.querySelector(".session-permission-status");
|
|
120
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
|
+
}
|