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.
Files changed (67) hide show
  1. package/lib/driver-continuation-pair.js +5 -1
  2. package/lib/multi-worker-feature.js +11 -0
  3. package/lib/project-connection.js +3 -1
  4. package/lib/project-message-delivery.js +20 -0
  5. package/lib/project-pair-autonomous-stop.js +14 -8
  6. package/lib/project-pair-close-transaction.js +59 -0
  7. package/lib/project-pair-global-stop.js +18 -0
  8. package/lib/project-pair-lifecycle-status.js +34 -0
  9. package/lib/project-pair-lifecycle.js +58 -76
  10. package/lib/project-pair-message.js +1 -1
  11. package/lib/project-pair-owned-stop.js +7 -4
  12. package/lib/project-pair-result-capture.js +116 -0
  13. package/lib/project-pair-result-delivery.js +140 -0
  14. package/lib/project-pair-result-outbox.js +400 -0
  15. package/lib/project-pair-result-pipeline.js +16 -0
  16. package/lib/project-pair-result-recovery.js +229 -0
  17. package/lib/project-pair-structural-close.js +20 -0
  18. package/lib/project-pair-task-control.js +45 -18
  19. package/lib/project-pair-worker-close.js +68 -0
  20. package/lib/project-session-pair-prompt.js +26 -0
  21. package/lib/project-session-pair.js +87 -106
  22. package/lib/project-sessions.js +4 -0
  23. package/lib/project-user-message.js +7 -0
  24. package/lib/project-worker-creation-proposal.js +176 -0
  25. package/lib/project-worker-permission.js +7 -8
  26. package/lib/project-worker-proposal-runner.js +45 -0
  27. package/lib/project-worker-proposal.js +79 -115
  28. package/lib/project.js +38 -1
  29. package/lib/public/app.js +1 -0
  30. package/lib/public/css/messages.css +12 -0
  31. package/lib/public/css/mobile-nav.css +29 -14
  32. package/lib/public/css/overlays.css +22 -21
  33. package/lib/public/css/pane.css +64 -14
  34. package/lib/public/css/sidebar.css +62 -7
  35. package/lib/public/index.html +3 -3
  36. package/lib/public/modules/app-connection.js +165 -74
  37. package/lib/public/modules/app-messages.js +17 -3
  38. package/lib/public/modules/message-delivery-ui.js +63 -0
  39. package/lib/public/modules/message-delivery.js +163 -6
  40. package/lib/public/modules/pair-result-status.js +87 -0
  41. package/lib/public/modules/permission-control.js +5 -5
  42. package/lib/public/modules/project-activation.js +10 -7
  43. package/lib/public/modules/session-creation-cta.js +46 -0
  44. package/lib/public/modules/sidebar-mobile.js +42 -30
  45. package/lib/public/modules/sidebar-sessions.js +77 -52
  46. package/lib/public/modules/split-group-helpers.js +74 -2
  47. package/lib/public/modules/split-pair-ui.js +28 -6
  48. package/lib/public/modules/split-pane-reconciler.js +28 -0
  49. package/lib/public/modules/split-pane-renderer.js +107 -0
  50. package/lib/public/modules/split-view.js +58 -188
  51. package/lib/public/modules/websocket-lifecycle.js +178 -0
  52. package/lib/public/modules/websocket-watchdog.js +126 -0
  53. package/lib/public/modules/worker-pane-lock.js +13 -3
  54. package/lib/server.js +1 -0
  55. package/lib/session-pair-factory.js +66 -0
  56. package/lib/session-pair-mcp-server.js +10 -5
  57. package/lib/session-pair-prompts.js +41 -25
  58. package/lib/session-pair-target.js +42 -0
  59. package/lib/session-pair-turn-control.js +11 -6
  60. package/lib/session-split-group-anchors.js +89 -7
  61. package/lib/session-split-group-persistence.js +136 -0
  62. package/lib/session-split-group-roles.js +164 -0
  63. package/lib/session-split-group-v2-store.js +146 -0
  64. package/lib/session-split-groups.js +109 -99
  65. package/lib/sessions.js +70 -21
  66. package/lib/ws-schema.js +2 -0
  67. 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, replayPendingMessages } from './message-delivery.js';
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
- replayPendingMessages(msg.id);
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) return null;
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-long permission-label">Skip permissions</span><span class="permission-label-short permission-label" aria-hidden="true">Skip</span><span class="permission-spinner" aria-hidden="true"></span></button>' +
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
- if (unsupported) buttons[i].title = "Auto permissions are unavailable for this Claude session.";
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 !== 2 || !slug) return false;
63
- return split.panes[0].slug === slug && split.panes[1].slug === slug;
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 !== 2) return false;
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 !== 2) continue;
74
- if (group.members.indexOf(members[0]) !== -1 && group.members.indexOf(members[1]) !== -1) return true;
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 === 2 && members.length === 2
88
- && members.indexOf(oldMembers[0]) !== -1 && members.indexOf(oldMembers[1]) !== -1) {
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
+ }