clay-server 4.2.0 → 4.3.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/driver-continuation-access.js +54 -0
- package/lib/driver-continuation-lease.js +52 -0
- package/lib/driver-continuation-pair-transfer.js +86 -0
- package/lib/driver-continuation-pair.js +86 -0
- package/lib/driver-continuation-record.js +40 -0
- package/lib/driver-continuation-startup.js +68 -0
- package/lib/driver-continuation-transaction.js +61 -0
- package/lib/driver-continuation-trigger.js +110 -0
- package/lib/multi-worker-feature.js +11 -0
- package/lib/project-connection.js +3 -1
- package/lib/project-driver-continuation.js +499 -0
- package/lib/project-message-delivery.js +20 -0
- package/lib/project-pair-autonomous-stop.js +14 -8
- package/lib/project-pair-close-transaction.js +59 -0
- package/lib/project-pair-global-stop.js +18 -0
- package/lib/project-pair-lifecycle-status.js +34 -0
- package/lib/project-pair-lifecycle.js +117 -135
- package/lib/project-pair-message.js +1 -1
- package/lib/project-pair-owned-stop.js +7 -4
- package/lib/project-pair-replacement-state.js +5 -1
- package/lib/project-pair-result-capture.js +116 -0
- package/lib/project-pair-result-delivery.js +140 -0
- package/lib/project-pair-result-outbox.js +400 -0
- package/lib/project-pair-result-pipeline.js +16 -0
- package/lib/project-pair-result-recovery.js +229 -0
- package/lib/project-pair-structural-close.js +20 -0
- package/lib/project-pair-task-control.js +45 -18
- package/lib/project-pair-worker-close.js +68 -0
- package/lib/project-session-handoff.js +48 -55
- package/lib/project-session-pair-prompt.js +26 -0
- package/lib/project-session-pair.js +87 -106
- package/lib/project-sessions.js +4 -0
- package/lib/project-user-message.js +21 -0
- package/lib/project-worker-creation-proposal.js +176 -0
- package/lib/project-worker-permission.js +7 -8
- package/lib/project-worker-proposal-runner.js +45 -0
- package/lib/project-worker-proposal.js +79 -115
- package/lib/project.js +87 -3
- package/lib/public/app.js +1 -0
- package/lib/public/css/driver-continuation.css +56 -0
- package/lib/public/css/messages.css +12 -0
- package/lib/public/css/mobile-nav.css +29 -14
- package/lib/public/css/overlays.css +54 -25
- package/lib/public/css/pane.css +66 -12
- package/lib/public/css/sidebar.css +62 -7
- package/lib/public/index.html +3 -3
- package/lib/public/modules/app-connection.js +165 -74
- package/lib/public/modules/app-messages.js +31 -3
- package/lib/public/modules/driver-continuation-state.js +76 -0
- package/lib/public/modules/driver-continuation.js +260 -0
- package/lib/public/modules/message-delivery-ui.js +63 -0
- package/lib/public/modules/message-delivery.js +163 -6
- package/lib/public/modules/pair-result-status.js +87 -0
- package/lib/public/modules/permission-control.js +10 -5
- package/lib/public/modules/project-activation.js +10 -7
- package/lib/public/modules/session-creation-cta.js +46 -0
- package/lib/public/modules/sidebar-mobile.js +42 -30
- package/lib/public/modules/sidebar-sessions.js +77 -52
- package/lib/public/modules/split-group-helpers.js +74 -2
- package/lib/public/modules/split-pair-ui.js +28 -6
- package/lib/public/modules/split-pane-reconciler.js +28 -0
- package/lib/public/modules/split-pane-renderer.js +107 -0
- package/lib/public/modules/split-view.js +58 -188
- package/lib/public/modules/websocket-lifecycle.js +178 -0
- package/lib/public/modules/websocket-watchdog.js +126 -0
- package/lib/public/modules/worker-pane-lock.js +13 -3
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +77 -0
- package/lib/server.js +1 -0
- package/lib/session-handoff-discovery.js +443 -0
- package/lib/session-handoff-mcp-server.js +32 -4
- package/lib/session-pair-factory.js +66 -0
- package/lib/session-pair-mcp-server.js +10 -5
- package/lib/session-pair-prompts.js +46 -25
- package/lib/session-pair-target.js +42 -0
- package/lib/session-pair-turn-control.js +30 -9
- package/lib/session-split-group-anchors.js +126 -0
- package/lib/session-split-group-persistence.js +136 -0
- package/lib/session-split-group-roles.js +164 -0
- package/lib/session-split-group-v2-store.js +146 -0
- package/lib/session-split-groups.js +293 -133
- package/lib/sessions.js +89 -22
- package/lib/ws-schema.js +8 -0
- package/package.json +1 -1
|
@@ -1262,26 +1262,81 @@
|
|
|
1262
1262
|
opacity: 1;
|
|
1263
1263
|
}
|
|
1264
1264
|
|
|
1265
|
-
|
|
1265
|
+
.session-top-action:focus-visible {
|
|
1266
|
+
outline: 2px solid var(--accent);
|
|
1267
|
+
outline-offset: 2px;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/* Primary creation action with an adjacent icon-only provider selector. */
|
|
1266
1271
|
.session-top-action-split {
|
|
1267
1272
|
display: flex;
|
|
1273
|
+
flex-wrap: nowrap;
|
|
1268
1274
|
align-items: stretch;
|
|
1269
|
-
gap:
|
|
1275
|
+
gap: 0;
|
|
1270
1276
|
min-width: 0;
|
|
1271
1277
|
}
|
|
1272
1278
|
|
|
1273
1279
|
.session-top-action-split .split-main {
|
|
1274
1280
|
flex: 1 1 auto;
|
|
1275
1281
|
width: auto;
|
|
1282
|
+
min-width: 0;
|
|
1283
|
+
padding: 0 9px;
|
|
1284
|
+
gap: 6px;
|
|
1285
|
+
font-size: 12px;
|
|
1286
|
+
line-height: 1.2;
|
|
1287
|
+
border: 1px solid var(--border-subtle);
|
|
1288
|
+
background: color-mix(in srgb, var(--sidebar-hover) 46%, transparent);
|
|
1289
|
+
color: var(--text);
|
|
1290
|
+
font-weight: 500;
|
|
1291
|
+
white-space: normal;
|
|
1292
|
+
text-align: left;
|
|
1293
|
+
border-radius: 9px 0 0 9px;
|
|
1276
1294
|
}
|
|
1277
1295
|
|
|
1278
|
-
.session-top-action-split .split-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1296
|
+
.session-top-action-split .split-main:hover {
|
|
1297
|
+
border-color: var(--border);
|
|
1298
|
+
background: var(--sidebar-hover);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
.session-top-action-split .split-provider {
|
|
1302
|
+
flex: 0 0 26px;
|
|
1303
|
+
width: 26px;
|
|
1304
|
+
min-width: 26px;
|
|
1282
1305
|
padding: 0;
|
|
1306
|
+
border: 1px solid var(--border-subtle);
|
|
1307
|
+
border-left-color: var(--border);
|
|
1308
|
+
background: color-mix(in srgb, var(--sidebar-hover) 46%, transparent);
|
|
1309
|
+
border-radius: 0 9px 9px 0;
|
|
1310
|
+
display: inline-flex;
|
|
1311
|
+
align-items: center;
|
|
1312
|
+
gap: 2px;
|
|
1283
1313
|
justify-content: center;
|
|
1284
|
-
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
.session-top-action-split .split-provider:hover {
|
|
1317
|
+
background: var(--sidebar-hover);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.session-create-provider-icon {
|
|
1321
|
+
width: 16px;
|
|
1322
|
+
height: 16px;
|
|
1323
|
+
margin-right: 0;
|
|
1324
|
+
border-radius: 4px;
|
|
1325
|
+
object-fit: cover;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
.session-create-provider-chevron {
|
|
1329
|
+
display: inline-flex;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
.session-create-provider-chevron .lucide,
|
|
1333
|
+
.session-create-provider-chevron svg {
|
|
1334
|
+
width: 14px;
|
|
1335
|
+
height: 14px;
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
.session-create-provider[aria-expanded="true"] {
|
|
1339
|
+
background: var(--sidebar-hover);
|
|
1285
1340
|
}
|
|
1286
1341
|
|
|
1287
1342
|
.session-favorites-section {
|
package/lib/public/index.html
CHANGED
|
@@ -410,9 +410,9 @@
|
|
|
410
410
|
<button id="header-info-btn" type="button" title="Session info"><i data-lucide="info"></i></button>
|
|
411
411
|
<div id="header-full-access-btn" class="session-permission-control hidden" role="group" aria-label="Session permission mode">
|
|
412
412
|
<div class="session-permission-segmented">
|
|
413
|
-
<button type="button" data-permission-mode="default">Ask</button>
|
|
414
|
-
<button type="button" data-permission-mode="auto">Auto</button>
|
|
415
|
-
<button type="button" data-permission-mode="bypassPermissions" aria-label="Skip permissions"><span class="permission-label
|
|
413
|
+
<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>
|
|
414
|
+
<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>
|
|
415
|
+
<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>
|
|
416
416
|
</div>
|
|
417
417
|
<span class="session-permission-status hidden" aria-live="polite"></span>
|
|
418
418
|
</div>
|
|
@@ -22,10 +22,9 @@ import { beginDefaultVendorConnection, requestDefaultVendor } from './default-ve
|
|
|
22
22
|
import { clearProjectSplitState } from './split-session-boundary.js';
|
|
23
23
|
import { clearPermissionModePending } from './permission-control.js';
|
|
24
24
|
import { clearMcpPermissionModePending } from './mcp-ui.js';
|
|
25
|
+
import { createWebSocketLifecycle } from './websocket-lifecycle.js';
|
|
26
|
+
import { createWebSocketWatchdog } from './websocket-watchdog.js';
|
|
25
27
|
|
|
26
|
-
var reconnectTimer = null;
|
|
27
|
-
var reconnectDelay = 1000;
|
|
28
|
-
var connectTimeoutId = null;
|
|
29
28
|
var connectOverlay = null;
|
|
30
29
|
var hasConnectedOnce = false;
|
|
31
30
|
|
|
@@ -44,9 +43,32 @@ var overlayGraceTimer = null;
|
|
|
44
43
|
// server-global-ws.js. Well under the common 60s proxy read timeout, and one
|
|
45
44
|
// timer per live socket.
|
|
46
45
|
var HEARTBEAT_MS = 25000;
|
|
47
|
-
var heartbeatTimer = null;
|
|
48
|
-
var heartbeatDeadlineTimer = null;
|
|
49
46
|
var disconnectedAt = 0;
|
|
47
|
+
var attemptStartedAt = 0;
|
|
48
|
+
var attemptSocket = null;
|
|
49
|
+
var lifecycle = createWebSocketLifecycle({
|
|
50
|
+
handshakeTimeoutMs: 12000,
|
|
51
|
+
onHandshakeTimeout: function (epoch) {
|
|
52
|
+
if (lifecycle.current(epoch) && attemptSocket) {
|
|
53
|
+
forceReplaceSocket("handshake_timeout", 0, epoch);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
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
|
+
});
|
|
50
72
|
|
|
51
73
|
function clearOverlayGrace() {
|
|
52
74
|
if (overlayGraceTimer) {
|
|
@@ -64,15 +86,54 @@ function showOverlayNow() {
|
|
|
64
86
|
connectOverlay.classList.remove("hidden");
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
89
|
+
function recordConnectionDiagnostic(reason, code, latency) {
|
|
90
|
+
var safeReasons = { close: true, error: true, handshake_timeout: true, heartbeat_timeout: true, heartbeat_error: true, probe_error: true, probe_timeout: true, auth_timeout: true, "ack-timeout": true, online: true, visible: true };
|
|
91
|
+
if (!safeReasons[reason]) reason = "unknown";
|
|
92
|
+
console.warn("[clay] WebSocket diagnostic", {
|
|
93
|
+
reason: reason,
|
|
94
|
+
code: typeof code === "number" ? code : 0,
|
|
95
|
+
retry: lifecycle.getRetryAttempt(),
|
|
96
|
+
latencyMs: typeof latency === "number" ? Math.max(0, Math.min(latency, 600000)) : null
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function forceReplaceSocket(reason, code, epoch, latency) {
|
|
101
|
+
if (!lifecycle.current(epoch)) return;
|
|
102
|
+
var socket = getWs();
|
|
103
|
+
watchdog.clearProbe();
|
|
104
|
+
stopHeartbeat();
|
|
105
|
+
if (socket) {
|
|
106
|
+
socket.onopen = null;
|
|
107
|
+
socket.onmessage = null;
|
|
108
|
+
socket.onerror = null;
|
|
109
|
+
socket.onclose = null;
|
|
110
|
+
try { socket.close(); } catch (e) {}
|
|
71
111
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
112
|
+
setWs(null);
|
|
113
|
+
recordConnectionDiagnostic(reason, code, latency);
|
|
114
|
+
setStatus("disconnected");
|
|
115
|
+
lifecycle.invalidate(epoch);
|
|
116
|
+
scheduleReconnect(reason);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function stopHeartbeat() {
|
|
120
|
+
watchdog.stopHeartbeat();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function suspendSocketForOffline() {
|
|
124
|
+
watchdog.clearProbe();
|
|
125
|
+
stopHeartbeat();
|
|
126
|
+
var socket = getWs();
|
|
127
|
+
attemptSocket = null;
|
|
128
|
+
if (socket) {
|
|
129
|
+
socket.onopen = null;
|
|
130
|
+
socket.onmessage = null;
|
|
131
|
+
socket.onerror = null;
|
|
132
|
+
socket.onclose = null;
|
|
133
|
+
try { socket.close(); } catch (e) {}
|
|
75
134
|
}
|
|
135
|
+
setWs(null);
|
|
136
|
+
if (socket || store.get('connected')) setStatus("disconnected");
|
|
76
137
|
}
|
|
77
138
|
|
|
78
139
|
// Started once per live socket. The socket it was started for is captured, so
|
|
@@ -80,28 +141,51 @@ export function stopHeartbeat() {
|
|
|
80
141
|
// connection. The pong the server sends back keeps a proxy's upstream read
|
|
81
142
|
// timeout from expiring. A missing pong also identifies a half-open socket,
|
|
82
143
|
// which browsers can otherwise leave OPEN while silently losing sends.
|
|
83
|
-
export function startHeartbeat(socket) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
144
|
+
export function startHeartbeat(socket, attemptEpoch) {
|
|
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);
|
|
98
159
|
}
|
|
99
160
|
|
|
100
161
|
export function initConnection() {
|
|
101
162
|
connectOverlay = document.getElementById("connect-overlay");
|
|
163
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) lifecycle.setOffline(true);
|
|
164
|
+
if (document.hidden) suspendSocketWatchdogs();
|
|
102
165
|
window.addEventListener("clay-message-delivery-timeout", function (event) {
|
|
103
166
|
var socket = event.detail && event.detail.socket;
|
|
104
|
-
|
|
167
|
+
recordConnectionDiagnostic("ack-timeout", 0, null);
|
|
168
|
+
if (!socket || getWs() === socket) probeReconnect("ack-timeout");
|
|
169
|
+
});
|
|
170
|
+
window.addEventListener("offline", function () {
|
|
171
|
+
lifecycle.setOffline(true);
|
|
172
|
+
suspendSocketForOffline();
|
|
173
|
+
});
|
|
174
|
+
window.addEventListener("online", function () {
|
|
175
|
+
lifecycle.wake(function () {
|
|
176
|
+
if (document.hidden) return;
|
|
177
|
+
resumeSocketWatchdogs("online");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
document.addEventListener("visibilitychange", function () {
|
|
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");
|
|
105
189
|
});
|
|
106
190
|
|
|
107
191
|
// --- Reactive UI sync for connected/processing state ---
|
|
@@ -224,6 +308,9 @@ function onConnected() {
|
|
|
224
308
|
}
|
|
225
309
|
|
|
226
310
|
export function connect() {
|
|
311
|
+
if (lifecycle.isOffline()) return;
|
|
312
|
+
var attemptEpoch = lifecycle.beginAttempt();
|
|
313
|
+
attemptStartedAt = Date.now();
|
|
227
314
|
var ws = getWs();
|
|
228
315
|
// Tear down the previous socket's heartbeat before a new socket exists, so
|
|
229
316
|
// two sockets can never both be pinging.
|
|
@@ -236,37 +323,24 @@ export function connect() {
|
|
|
236
323
|
ws.onclose = null;
|
|
237
324
|
ws.close();
|
|
238
325
|
}
|
|
239
|
-
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
240
|
-
|
|
241
326
|
var protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
|
242
327
|
var socketPath = store.get('wsPath');
|
|
243
328
|
if (store.get('socketPath') && store.get('socketPath') !== socketPath) clearProjectSplitState();
|
|
244
329
|
store.set({ socketPath: socketPath, activeProjectSlug: null, sessionActivatedProjectSlug: null, sessionListProjectSlug: null, splitGroupsProjectSlug: null });
|
|
245
330
|
var newWs = new WebSocket(protocol + "//" + location.host + socketPath);
|
|
246
331
|
setWs(newWs);
|
|
247
|
-
|
|
248
|
-
// If not connected within 3s, force retry
|
|
249
|
-
connectTimeoutId = setTimeout(function () {
|
|
250
|
-
if (getWs() !== newWs) return;
|
|
251
|
-
if (!store.get('connected')) {
|
|
252
|
-
newWs.onclose = null;
|
|
253
|
-
newWs.onerror = null;
|
|
254
|
-
newWs.close();
|
|
255
|
-
connect();
|
|
256
|
-
}
|
|
257
|
-
}, 3000);
|
|
332
|
+
attemptSocket = newWs;
|
|
258
333
|
|
|
259
334
|
newWs.onopen = function () {
|
|
260
|
-
if (getWs() !== newWs) return;
|
|
261
|
-
if (
|
|
335
|
+
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
336
|
+
if (!lifecycle.markOpen(attemptEpoch)) return;
|
|
262
337
|
if (hasConnectedOnce && disconnectedAt) {
|
|
263
338
|
console.log("[clay] WebSocket reconnected after " + (Date.now() - disconnectedAt) + "ms");
|
|
264
339
|
}
|
|
265
340
|
disconnectedAt = 0;
|
|
266
341
|
setStatus("connected");
|
|
267
|
-
startHeartbeat(newWs);
|
|
268
|
-
|
|
269
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
342
|
+
startHeartbeat(newWs, attemptEpoch);
|
|
343
|
+
lifecycle.clearRetry();
|
|
270
344
|
|
|
271
345
|
// A pane pin is one-shot per WebSocket, not per page lifetime. The server
|
|
272
346
|
// intentionally does not restore pane presence after a daemon restart.
|
|
@@ -292,64 +366,81 @@ export function connect() {
|
|
|
292
366
|
};
|
|
293
367
|
|
|
294
368
|
newWs.onclose = function (e) {
|
|
295
|
-
if (getWs() !== newWs) return;
|
|
296
|
-
if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
|
|
369
|
+
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
297
370
|
stopHeartbeat();
|
|
371
|
+
watchdog.clearProbe();
|
|
298
372
|
disconnectedAt = Date.now();
|
|
299
|
-
|
|
300
|
-
// deliberate navigation or a server shutdown the user already knows about.
|
|
301
|
-
if (e && !e.wasClean) {
|
|
302
|
-
console.warn("[clay] WebSocket closed abnormally: code=" + e.code +
|
|
303
|
-
" reason=" + (e.reason || "(none)") + " wasClean=false");
|
|
304
|
-
}
|
|
373
|
+
recordConnectionDiagnostic("close", e && e.code, attemptStartedAt ? Date.now() - attemptStartedAt : null);
|
|
305
374
|
closeDmUserPicker();
|
|
306
375
|
store.set({ activeProjectSlug: null, sessionActivatedProjectSlug: null });
|
|
307
376
|
setStatus("disconnected");
|
|
308
377
|
setActivity(null);
|
|
309
|
-
|
|
378
|
+
lifecycle.invalidate(attemptEpoch);
|
|
379
|
+
scheduleReconnect("close");
|
|
310
380
|
};
|
|
311
381
|
|
|
312
|
-
newWs.onerror = function () {
|
|
382
|
+
newWs.onerror = function () {
|
|
383
|
+
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
384
|
+
recordConnectionDiagnostic("error", 0, null);
|
|
385
|
+
forceReplaceSocket("error", 0, attemptEpoch);
|
|
386
|
+
};
|
|
313
387
|
|
|
314
388
|
newWs.onmessage = function (event) {
|
|
315
|
-
if (getWs() !== newWs) return;
|
|
389
|
+
if (getWs() !== newWs || !lifecycle.current(attemptEpoch)) return;
|
|
316
390
|
// Backup: if we're receiving messages, we're connected
|
|
317
391
|
if (!store.get('connected')) {
|
|
318
392
|
setStatus("connected");
|
|
319
|
-
|
|
320
|
-
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
|
|
393
|
+
lifecycle.clearRetry();
|
|
321
394
|
}
|
|
322
395
|
|
|
323
396
|
blinkIO();
|
|
324
397
|
var msg;
|
|
325
398
|
try { msg = JSON.parse(event.data); } catch (e) { return; }
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
heartbeatDeadlineTimer = null;
|
|
329
|
-
}
|
|
399
|
+
lifecycle.markLive(attemptEpoch);
|
|
400
|
+
if (msg.type === "pong") watchdog.acceptPong(newWs, attemptEpoch);
|
|
330
401
|
processMessage(msg);
|
|
331
402
|
};
|
|
332
403
|
}
|
|
333
404
|
|
|
334
405
|
export function cancelReconnect() {
|
|
335
|
-
|
|
406
|
+
watchdog.clearProbe();
|
|
407
|
+
stopHeartbeat();
|
|
408
|
+
lifecycle.cancel();
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function probeReconnect(reason) {
|
|
412
|
+
if (lifecycle.isOffline() || !lifecycle.current(lifecycle.getEpoch())) return;
|
|
413
|
+
var socket = getWs();
|
|
414
|
+
if (socket && socket.readyState === 0) return;
|
|
415
|
+
if (socket && socket.readyState === 1) {
|
|
416
|
+
watchdog.probe(socket, lifecycle.getEpoch());
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
scheduleReconnect(reason);
|
|
336
420
|
}
|
|
337
421
|
|
|
338
|
-
export function scheduleReconnect() {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
422
|
+
export function scheduleReconnect(reason) {
|
|
423
|
+
lifecycle.schedule(reason || "unknown", function (epoch) {
|
|
424
|
+
var authEpoch = epoch;
|
|
425
|
+
var controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
426
|
+
var authToken = lifecycle.beginAuth(authEpoch, function () {
|
|
427
|
+
if (controller) controller.abort();
|
|
428
|
+
scheduleReconnect("auth_timeout");
|
|
429
|
+
}, function () {
|
|
430
|
+
if (controller) controller.abort();
|
|
431
|
+
});
|
|
432
|
+
if (!authToken) return;
|
|
433
|
+
fetch("/info", controller ? { signal: controller.signal } : undefined).then(function (res) {
|
|
434
|
+
if (!lifecycle.finishAuth(authEpoch, authToken)) return;
|
|
435
|
+
if (!lifecycle.canProceed(authEpoch)) return;
|
|
344
436
|
if (res.status === 401) {
|
|
345
437
|
location.reload();
|
|
346
438
|
return;
|
|
347
439
|
}
|
|
348
440
|
connect();
|
|
349
441
|
}).catch(function () {
|
|
350
|
-
|
|
351
|
-
connect();
|
|
442
|
+
var authFinished = lifecycle.finishAuth(authEpoch, authToken);
|
|
443
|
+
if (authFinished && lifecycle.canProceed(authEpoch)) connect();
|
|
352
444
|
});
|
|
353
|
-
}
|
|
354
|
-
reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
|
|
445
|
+
});
|
|
355
446
|
}
|
|
@@ -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 ---
|
|
@@ -30,6 +31,7 @@ import { syncPaneTitles, maybeRestoreSplitGroup, openGroup } from './split-view.
|
|
|
30
31
|
import { showPairDialog, handlePairCreated, handleSplitDelegation, showWorkerDelegationNotice, hideWorkerDelegationNotice } from './split-pair-ui.js';
|
|
31
32
|
import { handleSessionActionMessage } from './session-actions.js';
|
|
32
33
|
import { renderWorkerProposal, updateWorkerProposal } from './worker-proposal.js';
|
|
34
|
+
import { renderDriverContinuation, updateDriverContinuation, renderDriverContinuationContext } from './driver-continuation.js';
|
|
33
35
|
import { handleInputSync, autoResize, builtinCommands, setScheduleBtnDisabled, sendShellResultToAgent, handleAcceptedOrdinaryMessage, handleScheduleMessageResult } from './input.js';
|
|
34
36
|
import { startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, closeToolGroup, removeToolFromGroup, resetToolState, getTools, getPlanContent, setPlanContent, renderPlanBanner, renderPlanCard, getTodoTools, handleTodoWrite, handleTaskCreate, handleTaskUpdate, applyDeadSessionTodoCompaction, isPlanFilePath, enableMainInput, addTurnMeta, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, initSubagentStop, updateSubagentProgress, updateSubagentTaskStatus, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionCancelled, markPermissionResolved, renderElicitationRequest, markElicitationResolved, renderUserDialogRequest, markUserDialogResolved, updateThinkingTokens } from './tools.js';
|
|
35
37
|
import { finishThinkingTurn, resumeThinkingAfterReplay } from './thinking-lifecycle.js';
|
|
@@ -60,6 +62,7 @@ import { handleShellCommandResult } from './shell-command.js';
|
|
|
60
62
|
import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
|
|
61
63
|
import { handleScheduledTaskMessage, handleScheduledTaskSessionSwitched } from './scheduled-tasks.js';
|
|
62
64
|
import { renderGeneratedImage, renderImageGenerationProgress, clearImageGenerationProgress, clearAllImageGenerationProgress } from './generated-images.js';
|
|
65
|
+
import { clearPairResultStatus, handlePairResultStatus } from './pair-result-status.js';
|
|
63
66
|
|
|
64
67
|
// --- App module imports ---
|
|
65
68
|
import { scrollToBottom, addToMessages, addUserMessage, addSystemMessage, removeMatePreThinking, appendDelta, finalizeAssistantBlock, addConflictMessage, addContextOverflowMessage, showSuggestionChips, armStickyBottom, VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
|
|
@@ -97,6 +100,7 @@ var messagesEl = document.getElementById("messages");
|
|
|
97
100
|
var headerTitleEl = document.getElementById("header-title");
|
|
98
101
|
var inputEl = document.getElementById("input");
|
|
99
102
|
var connectOverlay = document.getElementById("connect-overlay");
|
|
103
|
+
initMessageDeliveryUi();
|
|
100
104
|
|
|
101
105
|
export function processMessage(msg) {
|
|
102
106
|
if (handleIssuesMessage(msg)) return;
|
|
@@ -240,6 +244,7 @@ export function processMessage(msg) {
|
|
|
240
244
|
// Restore cached rich context usage BEFORE updateContextPanel runs
|
|
241
245
|
if (msg.contextUsage && !msg.contextUsage.unavailable) store.set({ richContextUsage: msg.contextUsage });
|
|
242
246
|
else markContextUnavailable();
|
|
247
|
+
|
|
243
248
|
// Restore accurate context data from the last result in full history
|
|
244
249
|
if (msg.lastUsage || msg.lastModelUsage) {
|
|
245
250
|
accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
|
|
@@ -732,6 +737,10 @@ export function processMessage(msg) {
|
|
|
732
737
|
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
733
738
|
break;
|
|
734
739
|
|
|
740
|
+
case "split_worker_close_result":
|
|
741
|
+
if (!msg.ok && msg.error) showToast(msg.error, "error");
|
|
742
|
+
break;
|
|
743
|
+
|
|
735
744
|
case "pair_session_options":
|
|
736
745
|
showPairDialog(msg);
|
|
737
746
|
break;
|
|
@@ -756,6 +765,19 @@ export function processMessage(msg) {
|
|
|
756
765
|
updateWorkerProposal(msg);
|
|
757
766
|
break;
|
|
758
767
|
|
|
768
|
+
case "driver_continuation_proposal":
|
|
769
|
+
renderDriverContinuation(msg);
|
|
770
|
+
break;
|
|
771
|
+
|
|
772
|
+
case "driver_continuation_update":
|
|
773
|
+
case "driver_continuation_result":
|
|
774
|
+
updateDriverContinuation(msg);
|
|
775
|
+
break;
|
|
776
|
+
|
|
777
|
+
case "driver_continuation_context":
|
|
778
|
+
renderDriverContinuationContext(msg);
|
|
779
|
+
break;
|
|
780
|
+
|
|
759
781
|
case "split_delegation":
|
|
760
782
|
handleSplitDelegation(msg);
|
|
761
783
|
syncPaneTitles();
|
|
@@ -788,6 +810,10 @@ export function processMessage(msg) {
|
|
|
788
810
|
handleRemoteSelection(msg);
|
|
789
811
|
break;
|
|
790
812
|
|
|
813
|
+
case "pair_result_status":
|
|
814
|
+
handlePairResultStatus(msg);
|
|
815
|
+
break;
|
|
816
|
+
|
|
791
817
|
case "session_io":
|
|
792
818
|
blinkSessionDot(msg.id);
|
|
793
819
|
break;
|
|
@@ -807,6 +833,7 @@ export function processMessage(msg) {
|
|
|
807
833
|
break;
|
|
808
834
|
|
|
809
835
|
case "session_switched":
|
|
836
|
+
clearPairResultStatus();
|
|
810
837
|
syncAutonomousRunForSession(msg.autonomousRun || null);
|
|
811
838
|
closeWhatsNewArticle();
|
|
812
839
|
// Save draft from outgoing session
|
|
@@ -825,10 +852,10 @@ export function processMessage(msg) {
|
|
|
825
852
|
var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
|
|
826
853
|
? msg.runtimeTerminalId
|
|
827
854
|
: (typeof msg.terminalId === "number" ? msg.terminalId : null);
|
|
828
|
-
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 || {} });
|
|
829
856
|
handleScheduledTaskSessionSwitched(msg);
|
|
830
857
|
requestLoopInterviewState();
|
|
831
|
-
|
|
858
|
+
activateDeliverySession(msg.id, msg.cliSessionId || null, msg.sessionOriginId || null);
|
|
832
859
|
// TUI sessions swap the chat UI for an embedded xterm running
|
|
833
860
|
// `claude` inside a real PTY. Mount or tear down before the rest of
|
|
834
861
|
// the chat-side bookkeeping runs so we don't waste work on hidden DOM.
|
|
@@ -922,6 +949,7 @@ export function processMessage(msg) {
|
|
|
922
949
|
|
|
923
950
|
case "session_id":
|
|
924
951
|
store.set({ cliSessionId: msg.cliSessionId });
|
|
952
|
+
refreshDeliverySessionIdentity(msg.cliSessionId, store.get('sessionOriginId') || null);
|
|
925
953
|
break;
|
|
926
954
|
|
|
927
955
|
case "message_uuid":
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
function clean(value) {
|
|
2
|
+
return value === undefined || value === null ? "" : String(value);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function continuationKey(message) {
|
|
6
|
+
if (!message) return "";
|
|
7
|
+
var projectSlug = clean(message.projectSlug);
|
|
8
|
+
var sourceOriginId = clean(message.sourceOriginId);
|
|
9
|
+
var proposalId = clean(message.proposalId);
|
|
10
|
+
if (!projectSlug || !sourceOriginId || !proposalId) return "";
|
|
11
|
+
return projectSlug + "\n" + sourceOriginId + "\n" + proposalId;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function authoritativeState(message) {
|
|
15
|
+
return {
|
|
16
|
+
key: continuationKey(message),
|
|
17
|
+
projectSlug: clean(message.projectSlug),
|
|
18
|
+
sourceOriginId: clean(message.sourceOriginId),
|
|
19
|
+
proposalId: clean(message.proposalId),
|
|
20
|
+
sourceSessionId: Number(message.sourceSessionId),
|
|
21
|
+
status: message.status || "pending",
|
|
22
|
+
requestId: "",
|
|
23
|
+
requestKind: "",
|
|
24
|
+
inflight: false,
|
|
25
|
+
error: message.error || "",
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function beginRequest(state, kind, requestId, connected) {
|
|
30
|
+
if (!state || !state.key || state.inflight || connected !== true) return null;
|
|
31
|
+
if (kind === "decision" && state.status !== "pending") return null;
|
|
32
|
+
var next = Object.assign({}, state, {
|
|
33
|
+
inflight: true,
|
|
34
|
+
requestId: clean(requestId),
|
|
35
|
+
requestKind: kind,
|
|
36
|
+
error: "",
|
|
37
|
+
});
|
|
38
|
+
return next.requestId ? next : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function applyResponse(state, message) {
|
|
42
|
+
if (!state || !message || continuationKey(message) !== state.key) return null;
|
|
43
|
+
if (message.requestId && clean(message.requestId) !== state.requestId) return null;
|
|
44
|
+
var next = Object.assign({}, state, { inflight: false, requestId: "", requestKind: "" });
|
|
45
|
+
var sourceSessionId = Number(message.sourceSessionId);
|
|
46
|
+
if (Number.isInteger(sourceSessionId) && sourceSessionId > 0) next.sourceSessionId = sourceSessionId;
|
|
47
|
+
if (message.status) next.status = message.status;
|
|
48
|
+
next.error = message.error || "";
|
|
49
|
+
return next;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function failRequest(state, requestId, error) {
|
|
53
|
+
if (!state || !state.inflight || clean(requestId) !== state.requestId) return null;
|
|
54
|
+
return Object.assign({}, state, {
|
|
55
|
+
inflight: false,
|
|
56
|
+
requestId: "",
|
|
57
|
+
requestKind: "",
|
|
58
|
+
error: clean(error) || "The request could not be sent. Try again.",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function sendContinuationPayload(ws, payload) {
|
|
63
|
+
if (!ws || ws.readyState !== 1) return { ok: false, error: "The connection is unavailable. Try again." };
|
|
64
|
+
try {
|
|
65
|
+
if (ws.send(JSON.stringify(payload)) === false) {
|
|
66
|
+
return { ok: false, error: "The request was not accepted by the connection. Try again." };
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return { ok: false, error: "The request could not be sent. Try again." };
|
|
70
|
+
}
|
|
71
|
+
return { ok: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function continuationCanAct(state, connected) {
|
|
75
|
+
return !!state && state.status === "pending" && !state.inflight && connected === true;
|
|
76
|
+
}
|