clay-server 5.5.0-beta.1 → 5.5.0-beta.3
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/clay-product-control-input.js +2 -2
- package/lib/clay-product-control-tools.js +1 -1
- package/lib/clay-product-control.js +4 -3
- package/lib/daemon-project-access.js +10 -2
- package/lib/daemon-projects.js +16 -4
- package/lib/daemon-worktree-controls.js +171 -0
- package/lib/daemon.js +14 -63
- package/lib/personal-provider-install.js +85 -0
- package/lib/personal-provider-settings.js +94 -0
- package/lib/project-logs-service.js +2 -2
- package/lib/project-pair-task-control.js +1 -1
- package/lib/project-pending-message-queue.js +6 -2
- package/lib/project-product-control.js +1 -0
- package/lib/project-session-actions.js +46 -3
- package/lib/project-session-pair.js +2 -0
- package/lib/project-sessions.js +11 -3
- package/lib/project-user-message.js +5 -1
- package/lib/project-vendor-login.js +95 -32
- package/lib/project-worker-auth.js +71 -0
- package/lib/project-worker-conversation.js +11 -2
- package/lib/project.js +9 -0
- package/lib/public/app.js +22 -0
- package/lib/public/css/icon-strip.css +5 -0
- package/lib/public/css/overlays.css +18 -0
- package/lib/public/css/settings-shell.css +14 -0
- package/lib/public/css/settings-ui.css +6 -0
- package/lib/public/css/split-view-layout.css +16 -11
- package/lib/public/modules/agent-message-bubbles.js +33 -4
- package/lib/public/modules/app-messages.js +7 -1
- package/lib/public/modules/app-projects.js +3 -4
- package/lib/public/modules/clay-control-copy.js +1 -0
- package/lib/public/modules/clay-tool-presentation.js +1 -1
- package/lib/public/modules/generation-stop.js +4 -4
- package/lib/public/modules/pane-activation.js +2 -2
- package/lib/public/modules/pane-bridge.js +4 -2
- package/lib/public/modules/personal-provider-context.js +11 -0
- package/lib/public/modules/project-settings.js +1 -1
- package/lib/public/modules/settings-section-nav.js +58 -0
- package/lib/public/modules/settings-shell-registry.js +4 -2
- package/lib/public/modules/settings-shell.js +17 -6
- package/lib/public/modules/sidebar-projects.js +23 -9
- package/lib/public/modules/sidebar-sessions.js +3 -0
- package/lib/public/modules/split-pane-force-stop.js +47 -0
- package/lib/public/modules/split-pane-header-state.js +11 -1
- package/lib/public/modules/split-pane-renderer.js +2 -2
- package/lib/public/modules/split-view-arrange-menu.js +38 -0
- package/lib/public/modules/split-view-create-menu.js +9 -5
- package/lib/public/modules/split-view-member-actions.js +42 -0
- package/lib/public/modules/split-view-picker-actions.js +2 -2
- package/lib/public/modules/split-view-presentation-renderer.js +10 -35
- package/lib/public/modules/split-view-presentations.js +9 -8
- package/lib/public/modules/split-view-sidebar-menu.js +2 -1
- package/lib/public/modules/split-view-workers.js +32 -17
- package/lib/public/modules/stt-settings.js +21 -4
- package/lib/public/modules/user-settings-layout.js +11 -8
- package/lib/public/modules/user-settings-provider-install.js +42 -0
- package/lib/public/modules/user-settings-providers.js +102 -0
- package/lib/public/modules/user-settings-voice.js +50 -41
- package/lib/public/modules/vendor-login-browser.js +1 -1
- package/lib/public/modules/vendor-login.js +72 -12
- package/lib/public/modules/view-workspace-state.js +1 -1
- package/lib/public/modules/worker-activity-indicator.js +49 -0
- package/lib/public/modules/worker-message-previews.js +47 -11
- package/lib/public/modules/worker-stop-controls.js +36 -0
- package/lib/public/modules/worktree-controls.js +86 -0
- package/lib/public/pane-assets/{app-4IZ6F6ND.js → app-ANJS2LSK.js} +1 -1
- package/lib/public/pane-assets/{chunk-L3FCTD7U.js → chunk-32YIYF7R.js} +1 -1
- package/lib/public/pane-assets/{chunk-RRYWV5JT.js → chunk-3EJ5IAHZ.js} +1 -1
- package/lib/public/pane-assets/{chunk-3E6LDNXF.js → chunk-3TIVBAKM.js} +1 -1
- package/lib/public/pane-assets/{chunk-I6X5J2V5.js → chunk-4QFNLHW5.js} +1 -1
- package/lib/public/pane-assets/{chunk-OB3KKICE.js → chunk-5XMZMIFG.js} +1 -1
- package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
- package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
- package/lib/public/pane-assets/{chunk-A7OB7HQS.js → chunk-BOFNCPTN.js} +1 -1
- package/lib/public/pane-assets/{chunk-ELL4UBGY.js → chunk-CXWAJEN5.js} +1 -1
- package/lib/public/pane-assets/{chunk-SEELA2D2.js → chunk-EWTXBZJO.js} +1 -1
- package/lib/public/pane-assets/{chunk-ADGKPVFD.js → chunk-G35ZOJGP.js} +1 -1
- package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
- package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
- package/lib/public/pane-assets/{chunk-6ZF2FJT3.js → chunk-JM46WGDH.js} +1 -1
- package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
- package/lib/public/pane-assets/{chunk-YXJ7ISEK.js → chunk-P3AK4ORJ.js} +1 -1
- package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
- package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
- package/lib/public/pane-assets/{chunk-YWHBNEB2.js → chunk-UGH5MCNK.js} +1 -1
- package/lib/public/pane-assets/{chunk-YFUORWA6.js → chunk-V5DLPSLT.js} +1 -1
- package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
- package/lib/public/pane-assets/{chunk-A4MLSYBJ.js → chunk-XSSTR6GC.js} +1 -1
- package/lib/public/pane-assets/{chunk-DJ5WXQKT.js → chunk-XVESKMU3.js} +1 -1
- package/lib/public/pane-assets/manifest.json +443 -433
- package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
- package/lib/sdk-bridge.js +40 -6
- package/lib/sdk-force-stop.js +62 -0
- package/lib/sdk-message-processor.js +2 -1
- package/lib/server-worktree-controls.js +34 -0
- package/lib/server.js +11 -1
- package/lib/session-force-stop-support.js +15 -0
- package/lib/sessions.js +6 -1
- package/lib/terminal-manager.js +1 -1
- package/lib/terminal.js +13 -1
- package/lib/vendor-login-browser-flow.js +1 -1
- package/lib/worker-activity.js +26 -0
- package/lib/worker-message-previews.js +12 -7
- package/lib/worktree-branch.js +57 -0
- package/lib/worktree-identity.js +39 -0
- package/lib/worktree-preferences.js +25 -0
- package/lib/worktree-ref-transaction.js +36 -0
- package/lib/yoke/adapters/antigravity.js +3 -0
- package/lib/yoke/force-termination.js +26 -0
- package/lib/yoke/interface.js +14 -0
- package/lib/yoke/process-force-termination.js +24 -0
- package/lib/yoke/provider-install-recipes.js +20 -0
- package/lib/yoke/vendor-registry.js +20 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6KO36HMU.js +0 -1
- package/lib/public/pane-assets/chunk-FILRACQB.js +0 -168
- package/lib/public/pane-assets/chunk-H6MU72QE.js +0 -1
- package/lib/public/pane-assets/chunk-PJURCIJQ.js +0 -1
- package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +0 -1
- package/lib/public/pane-assets/chunk-UTLQWAKG.js +0 -1
- package/lib/public/pane-assets/chunk-WE4NWXNK.js +0 -1
- package/lib/public/pane-assets/chunk-Z2MHS43B.js +0 -1
- package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personalLoginCurrent } from './personal-provider-context.js';
|
|
1
2
|
// vendor-login.js - Vendor login (auth_required) recovery flow.
|
|
2
3
|
//
|
|
3
4
|
// The server owns the login terminal (see lib/project-vendor-login.js): exactly
|
|
@@ -16,6 +17,7 @@ import { refreshIcons, iconHtml } from './icons.js';
|
|
|
16
17
|
import { openTuiModal, closeTuiModal, getTuiModalTerminalId } from './tui-attention.js';
|
|
17
18
|
import { forwardPaneAuthRequired } from './pane-bridge.js';
|
|
18
19
|
import { openLoginBrowser, closeLoginBrowser } from './vendor-login-browser.js';
|
|
20
|
+
import { escapeHtml } from './utils.js';
|
|
19
21
|
|
|
20
22
|
// vendor -> { terminalId, startedAt } as last broadcast by the server.
|
|
21
23
|
var activeFlows = Object.create(null);
|
|
@@ -45,6 +47,17 @@ function currentProjectSlug() {
|
|
|
45
47
|
return "";
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
function workerContextMatches(target) {
|
|
51
|
+
return !!target && target.slug === store.get('currentSlug') && target.selectedSessionId === store.get('activeSessionId') &&
|
|
52
|
+
target.selectedOrigin === store.get('sessionOriginId') && target.socketGeneration === store.get('projectSplitViewSocketGeneration');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function forgetWorkerRequest(vendor) {
|
|
56
|
+
var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
|
|
57
|
+
delete requests[vendor];
|
|
58
|
+
store.set({ workerVendorLoginRequests: requests });
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
// The banner surface belongs to the notification center. Query it rather than
|
|
49
62
|
// importing app-notifications so the dependency stays one-directional
|
|
50
63
|
// (app-notifications -> vendor-login).
|
|
@@ -80,8 +93,8 @@ function showLoginBanner(opts) {
|
|
|
80
93
|
'<div class="notif-banner-icon">' + iconHtml(opts.icon || "check-circle") + '</div>' +
|
|
81
94
|
'<div class="notif-banner-body">' +
|
|
82
95
|
'<div class="notif-banner-project">CLAY</div>' +
|
|
83
|
-
'<div class="notif-banner-title">' + opts.title + '</div>' +
|
|
84
|
-
'<div class="notif-banner-text">' + opts.text + '</div>' +
|
|
96
|
+
'<div class="notif-banner-title">' + escapeHtml(opts.title) + '</div>' +
|
|
97
|
+
'<div class="notif-banner-text">' + escapeHtml(opts.text) + '</div>' +
|
|
85
98
|
'</div>' +
|
|
86
99
|
'<button class="notif-banner-close">' + iconHtml("x") + '</button>';
|
|
87
100
|
|
|
@@ -107,7 +120,7 @@ function showLoginStartedBanner(vendor) {
|
|
|
107
120
|
showLoginBanner({
|
|
108
121
|
icon: "check-circle",
|
|
109
122
|
title: vendorDisplayName(vendor) + " login started",
|
|
110
|
-
text: "Finish sign-in
|
|
123
|
+
text: "Finish sign-in below, then explicitly retry your task.",
|
|
111
124
|
onDismiss: function () { cancelVendorLogin(vendor); },
|
|
112
125
|
});
|
|
113
126
|
}
|
|
@@ -159,17 +172,28 @@ function closeModalForVendor(vendor, terminalId) {
|
|
|
159
172
|
export function requestVendorLogin(vendor, opts) {
|
|
160
173
|
var options = opts || {};
|
|
161
174
|
var target = vendor || "claude";
|
|
175
|
+
if (options.personalRequestId && !personalLoginCurrent({personalRequestId:options.personalRequestId})) return false;
|
|
162
176
|
if (options.auto && (activeFlows[target] || pendingStarts[target])) return false;
|
|
163
177
|
|
|
164
178
|
var ws = getWs();
|
|
165
179
|
if (!ws || ws.readyState !== 1) return false;
|
|
180
|
+
if (options.workerAuthRequestId) {
|
|
181
|
+
var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
|
|
182
|
+
requests[target] = { requestId: options.workerAuthRequestId, slug: store.get('currentSlug'),
|
|
183
|
+
selectedSessionId: store.get('activeSessionId'), selectedOrigin: store.get('sessionOriginId'),
|
|
184
|
+
socketGeneration: store.get('projectSplitViewSocketGeneration') };
|
|
185
|
+
store.set({ workerVendorLoginRequests: requests });
|
|
186
|
+
}
|
|
166
187
|
|
|
188
|
+
if (options.personalRequestId) store.set({personalProviderSigningIn:target});
|
|
167
189
|
pendingStarts[target] = true;
|
|
168
190
|
ws.send(JSON.stringify({
|
|
169
191
|
type: "vendor_login_start",
|
|
170
192
|
vendor: target,
|
|
171
193
|
auto: !!options.auto,
|
|
172
|
-
sessionId: options.sessionId || store.get('activeSessionId') || null,
|
|
194
|
+
sessionId: options.personalRequestId ? null : options.sessionId || store.get('activeSessionId') || null,
|
|
195
|
+
personalRequestId: options.personalRequestId || null,
|
|
196
|
+
workerAuthRequestId: options.workerAuthRequestId || null,
|
|
173
197
|
mode: options.mode || ((((store.get("vendorInfo") || {})[target] || {}).browserLoginSupported === true) ? "browser" : "terminal"),
|
|
174
198
|
}));
|
|
175
199
|
showLoginStartedBanner(target);
|
|
@@ -180,6 +204,7 @@ export function cancelVendorLogin(vendor) {
|
|
|
180
204
|
var target = vendor || modalVendor;
|
|
181
205
|
if (!target) return;
|
|
182
206
|
var flow = activeFlows[target];
|
|
207
|
+
forgetWorkerRequest(target);
|
|
183
208
|
var browser = store.get('vendorLoginBrowser');
|
|
184
209
|
var browserId = flow && flow.browserId || (browser && browser.vendor === target ? browser.browserId : null);
|
|
185
210
|
if (browser && browser.vendor === target) closeLoginBrowser();
|
|
@@ -189,7 +214,7 @@ export function cancelVendorLogin(vendor) {
|
|
|
189
214
|
if (modalVendor === target) closeModalInternally();
|
|
190
215
|
var ws = getWs();
|
|
191
216
|
if (ws && ws.readyState === 1) {
|
|
192
|
-
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId }));
|
|
217
|
+
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId, workerAuthRequestId: flow && flow.workerAuthRequestId, personalRequestId: store.get('personalProviderSigningIn') === target ? store.get('personalProviderRevision') : null }));
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
@@ -205,12 +230,24 @@ export function autoStartLoginIfNeeded(msg) {
|
|
|
205
230
|
if (store.get('paneMode')) {
|
|
206
231
|
return forwardPaneAuthRequired({
|
|
207
232
|
vendor: msg.vendor || "claude",
|
|
208
|
-
sessionId: store.get('activeSessionId') || null,
|
|
233
|
+
sessionId: msg.sessionId || store.get('activeSessionId') || null,
|
|
234
|
+
workerAuthRequestId: msg.workerAuthRequestId || null,
|
|
235
|
+
loginCommand: msg.loginCommand,
|
|
236
|
+
canAutoLogin: msg.canAutoLogin,
|
|
209
237
|
});
|
|
210
238
|
}
|
|
239
|
+
if (msg.canAutoLogin === false || msg.loginCommand === '') {
|
|
240
|
+
showLoginErrorBanner(msg.vendor, 'Automatic sign-in is unavailable. Sign in manually on the server, then explicitly retry the task.');
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
211
243
|
var vendor = msg.vendor || "claude";
|
|
212
244
|
if (activeFlows[vendor] || pendingStarts[vendor]) return false;
|
|
213
|
-
return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null });
|
|
245
|
+
return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null, workerAuthRequestId: msg.workerAuthRequestId || null });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function handleWorkerAuthRequired(msg) {
|
|
249
|
+
if (!msg || store.get('replayingHistory') || msg.slug !== store.get('currentSlug') || store.get('paneMode')) return false;
|
|
250
|
+
return autoStartLoginIfNeeded(msg);
|
|
214
251
|
}
|
|
215
252
|
|
|
216
253
|
// ========================================================
|
|
@@ -218,10 +255,12 @@ export function autoStartLoginIfNeeded(msg) {
|
|
|
218
255
|
// ========================================================
|
|
219
256
|
|
|
220
257
|
export function handleVendorLoginReady(msg) {
|
|
221
|
-
if (!msg || typeof msg.terminalId !== "number") return;
|
|
258
|
+
if (!msg || typeof msg.terminalId !== "number" || !personalLoginCurrent(msg)) return;
|
|
222
259
|
var vendor = msg.vendor || "claude";
|
|
260
|
+
var request = (store.get('workerVendorLoginRequests') || {})[vendor];
|
|
261
|
+
if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
|
|
223
262
|
delete pendingStarts[vendor];
|
|
224
|
-
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null };
|
|
263
|
+
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null, workerAuthRequestId: msg.workerAuthRequestId || null };
|
|
225
264
|
if (msg.browserId) {
|
|
226
265
|
openLoginBrowser(msg);
|
|
227
266
|
return;
|
|
@@ -246,10 +285,12 @@ export function handleVendorLoginReady(msg) {
|
|
|
246
285
|
|
|
247
286
|
export function handleVendorLoginState(msg) {
|
|
248
287
|
if (!msg) return;
|
|
288
|
+
pendingStarts = Object.create(null);
|
|
249
289
|
var next = Object.create(null);
|
|
250
290
|
var flows = msg.flows || [];
|
|
251
291
|
for (var i = 0; i < flows.length; i++) {
|
|
252
|
-
|
|
292
|
+
if (!personalLoginCurrent(flows[i])) continue;
|
|
293
|
+
next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null, workerAuthRequestId: flows[i].workerAuthRequestId || null };
|
|
253
294
|
delete pendingStarts[flows[i].vendor];
|
|
254
295
|
}
|
|
255
296
|
|
|
@@ -271,8 +312,11 @@ export function handleVendorLoginState(msg) {
|
|
|
271
312
|
}
|
|
272
313
|
|
|
273
314
|
export function handleAuthRefreshed(msg) {
|
|
274
|
-
if (!msg) return;
|
|
315
|
+
if (!msg || !personalLoginCurrent(msg)) return;
|
|
275
316
|
var vendor = msg.vendor || "claude";
|
|
317
|
+
var request = (store.get('workerVendorLoginRequests') || {})[vendor];
|
|
318
|
+
if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
|
|
319
|
+
forgetWorkerRequest(vendor);
|
|
276
320
|
var browser = store.get('vendorLoginBrowser');
|
|
277
321
|
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
278
322
|
var flow = activeFlows[vendor];
|
|
@@ -284,8 +328,9 @@ export function handleAuthRefreshed(msg) {
|
|
|
284
328
|
}
|
|
285
329
|
|
|
286
330
|
export function handleVendorLoginError(msg) {
|
|
287
|
-
if (!msg) return;
|
|
331
|
+
if (!msg || !personalLoginCurrent(msg)) return;
|
|
288
332
|
var vendor = msg.vendor || "claude";
|
|
333
|
+
forgetWorkerRequest(vendor);
|
|
289
334
|
var browser = store.get('vendorLoginBrowser');
|
|
290
335
|
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
291
336
|
delete pendingStarts[vendor];
|
|
@@ -304,3 +349,18 @@ export function requestVendorLoginState() {
|
|
|
304
349
|
export function getActiveLoginFlow(vendor) {
|
|
305
350
|
return activeFlows[vendor] || null;
|
|
306
351
|
}
|
|
352
|
+
|
|
353
|
+
store.subscribe(function (state, previous) {
|
|
354
|
+
var requests = state.workerVendorLoginRequests || {};
|
|
355
|
+
Object.keys(requests).forEach(function (vendor) {
|
|
356
|
+
if (workerContextMatches(requests[vendor]) && state.connected !== false) return;
|
|
357
|
+
if (requests[vendor].slug === state.currentSlug && requests[vendor].socketGeneration === state.projectSplitViewSocketGeneration) cancelVendorLogin(vendor);
|
|
358
|
+
else forgetWorkerRequest(vendor);
|
|
359
|
+
});
|
|
360
|
+
if (state.connected !== false || previous.connected === false) return;
|
|
361
|
+
pendingStarts = Object.create(null);
|
|
362
|
+
activeFlows = Object.create(null);
|
|
363
|
+
closeLoginBrowser();
|
|
364
|
+
closeModalInternally();
|
|
365
|
+
clearLoginBanners();
|
|
366
|
+
});
|
|
@@ -81,7 +81,7 @@ export function prepareViewMutation(type, message) {
|
|
|
81
81
|
if (type === 'project_split_view_create' || type === 'project_split_view_update') {
|
|
82
82
|
if (!message.workspace) message.workspace = recomposeWorkspace(view && view.workspace, message.tree, message.activeSessionOriginId);
|
|
83
83
|
message.tree = sessionTree(message.workspace);
|
|
84
|
-
message.active =
|
|
84
|
+
message.active = message.active !== false;
|
|
85
85
|
}
|
|
86
86
|
if (type === 'project_split_view_move') {
|
|
87
87
|
var target = views.find(function (item) { return item.id === message.targetViewId; });
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml } from './icons.js';
|
|
3
|
+
|
|
4
|
+
export function validActivity(value) {
|
|
5
|
+
return !!(value && typeof value.epoch === 'string' && value.epoch.length <= 64 && Number.isSafeInteger(value.sequence) && value.sequence > 0 &&
|
|
6
|
+
Number.isSafeInteger(value.queryGeneration) && value.queryGeneration >= 0 && Number.isFinite(value.timestamp) && value.timestamp > 0 &&
|
|
7
|
+
typeof value.summary === 'string' && value.summary.length <= 80);
|
|
8
|
+
}
|
|
9
|
+
export function observeWorkerActivity(key, activity, live) {
|
|
10
|
+
var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
|
|
11
|
+
if (activity && !validActivity(activity)) return null;
|
|
12
|
+
if (previous && activity && previous.cursor && activity.epoch === previous.cursor.epoch &&
|
|
13
|
+
(activity.sequence <= previous.cursor.sequence || activity.queryGeneration < previous.cursor.queryGeneration)) return null;
|
|
14
|
+
// Initial subscriptions/reconnects establish a baseline; never animate cached activity.
|
|
15
|
+
var fresh = !!(live && previous && activity);
|
|
16
|
+
if (previous && previous.activity && activity && previous.activity.epoch !== activity.epoch) fresh = false;
|
|
17
|
+
var generationChanged = previous && previous.activity && (!activity || previous.activity.queryGeneration !== activity.queryGeneration);
|
|
18
|
+
var angle = previous && !generationChanged ? previous.angle : 0, now = performance.now();
|
|
19
|
+
var nextStepAt = previous && !generationChanged ? previous.nextStepAt : 0;
|
|
20
|
+
if (fresh && now >= nextStepAt) { angle += 30; nextStepAt = now + 350; }
|
|
21
|
+
all[key] = { processing: previous && previous.processing, fresh: fresh, cursor: activity || previous && previous.cursor || null, activity: activity || null, angle: angle, nextStepAt: nextStepAt };
|
|
22
|
+
store.set({ workerActivityIndicators: all });
|
|
23
|
+
return fresh;
|
|
24
|
+
}
|
|
25
|
+
export function setWorkerActivityRuntime(key, processing) {
|
|
26
|
+
var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
|
|
27
|
+
if (!previous || typeof processing !== 'boolean' || previous.processing === processing) return;
|
|
28
|
+
all[key] = Object.assign({}, previous, { processing: processing }); store.set({ workerActivityIndicators: all });
|
|
29
|
+
}
|
|
30
|
+
export function syncWorkerActivityIndicator(chip, worker) {
|
|
31
|
+
var icon = chip.querySelector('.split-view-worker-loop'); if (!icon) return;
|
|
32
|
+
var state = (store.get('workerActivityIndicators') || {})[chip.dataset.messagePreview];
|
|
33
|
+
var angle = state ? state.angle : 0;
|
|
34
|
+
var scope = chip.dataset.messagePreview + ':' + (state && state.activity ? state.activity.epoch + ':' + state.activity.queryGeneration : 'unknown');
|
|
35
|
+
if (icon.dataset.activityScope !== scope) {
|
|
36
|
+
var initial = icon.dataset.activityScope == null ? angle : state && state.fresh ? 0 : angle;
|
|
37
|
+
icon.style.transition = 'none'; icon.style.transform = 'rotate(' + initial + 'deg)';
|
|
38
|
+
void icon.offsetWidth;
|
|
39
|
+
icon.style.transition = ''; icon.dataset.activityScope = scope;
|
|
40
|
+
}
|
|
41
|
+
icon.style.transform = 'rotate(' + angle + 'deg)';
|
|
42
|
+
var processing = state && typeof state.processing === 'boolean' ? state.processing : worker && worker.isProcessing;
|
|
43
|
+
icon.dataset.state = !store.get('connected') ? 'disconnected' : processing ? 'working' : 'ready';
|
|
44
|
+
icon.setAttribute('aria-label', !store.get('connected') ? 'Disconnected' : processing ? 'Worker running; motion indicates received activity' : 'Worker idle');
|
|
45
|
+
}
|
|
46
|
+
export function createWorkerActivityIndicator() {
|
|
47
|
+
var icon = document.createElement('span'); icon.className = 'split-view-worker-loop';
|
|
48
|
+
icon.innerHTML = iconHtml('refresh-cw'); icon.setAttribute('role', 'img'); return icon;
|
|
49
|
+
}
|
|
@@ -1,39 +1,75 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { bindAgentMessageBubble, messagePreviewKey, rememberAgentMessage, rememberAgentActivity, forgetAgentMessage } from './agent-message-bubbles.js';
|
|
5
|
+
import { observeWorkerActivity, syncWorkerActivityIndicator, setWorkerActivityRuntime } from './worker-activity-indicator.js';
|
|
4
6
|
|
|
5
7
|
var scheduled = false;
|
|
8
|
+
function key(target) { return messagePreviewKey('worker', [target.parentSessionOriginId, target.workerSessionOriginId, target.generation]); }
|
|
9
|
+
function exactWorker(target) {
|
|
10
|
+
var list = getCachedSessions() || [];
|
|
11
|
+
var driver = list.find(function (item) { return item.sessionOriginId === target.parentSessionOriginId && item.sessionRole === 'driver' && (item.ownerId || null) === (store.get('myUserId') || null); });
|
|
12
|
+
return driver && list.find(function (item) { return item.sessionOriginId === target.workerSessionOriginId && item.sessionRole === 'worker' &&
|
|
13
|
+
item.parentAvailable && item.parentSessionId === driver.id && item.workerGeneration === target.generation && !item.hidden && (item.ownerId || null) === (driver.ownerId || null); });
|
|
14
|
+
}
|
|
6
15
|
function targets() {
|
|
7
16
|
var found = [];
|
|
17
|
+
if (document.hidden) return found;
|
|
8
18
|
document.querySelectorAll('.split-view-worker-chip[data-worker-parent]').forEach(function (chip) {
|
|
9
19
|
if (!chip.getClientRects().length || chip.closest('[hidden], .split-pane-cached')) return;
|
|
10
20
|
var target = { parentSessionOriginId: chip.dataset.workerParent, workerSessionOriginId: chip.dataset.workerOrigin, generation: Number(chip.dataset.workerGeneration) };
|
|
11
|
-
|
|
21
|
+
var worker = exactWorker(target); if (!worker) return;
|
|
22
|
+
bindAgentMessageBubble(chip, key(target)); syncWorkerActivityIndicator(chip, worker);
|
|
12
23
|
found.push(target);
|
|
13
24
|
});
|
|
14
25
|
return found.slice(0, 32);
|
|
15
26
|
}
|
|
27
|
+
function cleanup(list) {
|
|
28
|
+
var keep = new Set(list.map(key)), all = Object.assign({}, store.get('workerActivityIndicators') || {}), changed = false;
|
|
29
|
+
Object.keys(all).forEach(function (id) { if (!keep.has(id)) { delete all[id]; forgetAgentMessage(id); changed = true; } });
|
|
30
|
+
if (changed) store.set({ workerActivityIndicators: all });
|
|
31
|
+
}
|
|
16
32
|
export function syncWorkerMessagePreviews() {
|
|
17
33
|
if (scheduled || store.get('paneMode')) return;
|
|
18
34
|
scheduled = true;
|
|
19
35
|
requestAnimationFrame(function () {
|
|
20
|
-
scheduled = false; var socket = getWs()
|
|
21
|
-
var list = targets()
|
|
22
|
-
|
|
23
|
-
var
|
|
36
|
+
scheduled = false; var socket = getWs(), connected = store.get('connected') && socket && socket.readyState === 1;
|
|
37
|
+
var list = connected ? targets() : []; cleanup(list);
|
|
38
|
+
if (!connected) { store.set({ workerMessagePreviewSubscription: null }); return; }
|
|
39
|
+
var signature = JSON.stringify([store.get('currentSlug'), store.get('myUserId'), store.get('projectSplitViewSocketGeneration'), list]);
|
|
40
|
+
var previous = store.get('workerMessagePreviewSubscription'); if (previous && previous.signature === signature && previous.socket === socket) return;
|
|
41
|
+
var requestId = crypto.randomUUID();
|
|
42
|
+
store.set({ workerMessagePreviewSubscription: { requestId: requestId, signature: signature, targets: list, socket: socket,
|
|
43
|
+
slug: store.get('currentSlug'), owner: store.get('myUserId'), socketGeneration: store.get('projectSplitViewSocketGeneration') } });
|
|
24
44
|
socket.send(JSON.stringify({ type: 'split_worker_preview_request', requestId: requestId, targets: list }));
|
|
25
45
|
});
|
|
26
46
|
}
|
|
27
47
|
export function handleWorkerMessagePreview(message) {
|
|
28
|
-
var state = store.get('workerMessagePreviewSubscription');
|
|
48
|
+
var state = store.get('workerMessagePreviewSubscription');
|
|
49
|
+
if (!state || !store.get('connected') || document.hidden || message.requestId !== state.requestId || state.socket !== getWs() ||
|
|
50
|
+
state.slug !== store.get('currentSlug') || state.owner !== store.get('myUserId') || state.socketGeneration !== store.get('projectSplitViewSocketGeneration')) return;
|
|
29
51
|
if (!state.targets.some(function (target) { return target.parentSessionOriginId === message.parentSessionOriginId && target.workerSessionOriginId === message.workerSessionOriginId && target.generation === message.generation; })) return;
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
var worker = exactWorker(message); if (!worker) return;
|
|
53
|
+
var chips = Array.from(document.querySelectorAll('.split-view-worker-chip[data-worker-parent]')).filter(function (chip) {
|
|
54
|
+
return chip.dataset.workerParent === message.parentSessionOriginId && chip.dataset.workerOrigin === message.workerSessionOriginId &&
|
|
55
|
+
Number(chip.dataset.workerGeneration) === message.generation && chip.getClientRects().length && !chip.closest('[hidden], .split-pane-cached');
|
|
56
|
+
});
|
|
57
|
+
if (!chips.length) return;
|
|
58
|
+
var id = key(message), observed = observeWorkerActivity(id, message.activity || null, message.live === true);
|
|
59
|
+
var current = (store.get('workerActivityIndicators') || {})[id];
|
|
60
|
+
if (observed !== null || current && current.cursor && message.activity && current.cursor.epoch === message.activity.epoch && current.cursor.sequence === message.activity.sequence && current.cursor.queryGeneration === message.activity.queryGeneration) setWorkerActivityRuntime(id, message.isProcessing);
|
|
61
|
+
if (observed !== null) rememberAgentActivity(id, message.activity || null, message.vendor, message.observedAt);
|
|
62
|
+
rememberAgentMessage(id, message.preview, message.live === true);
|
|
63
|
+
chips.forEach(function (chip) { syncWorkerActivityIndicator(chip, Object.assign({}, worker, { isProcessing: typeof message.isProcessing === 'boolean' ? message.isProcessing : worker.isProcessing })); });
|
|
33
64
|
}
|
|
34
65
|
store.subscribe(function (state, previous) {
|
|
35
|
-
if (state.
|
|
66
|
+
if (state.paneBindingSessions !== previous.paneBindingSessions && state.connected) targets().forEach(function (target) {
|
|
67
|
+
var worker = exactWorker(target); if (worker) setWorkerActivityRuntime(key(target), worker.isProcessing === true);
|
|
68
|
+
});
|
|
69
|
+
if (state.splitPanes !== previous.splitPanes || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId || state.paneBindingSessions !== previous.paneBindingSessions ||
|
|
70
|
+
state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.connected !== previous.connected || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration) syncWorkerMessagePreviews();
|
|
36
71
|
});
|
|
37
72
|
var observer = new MutationObserver(syncWorkerMessagePreviews);
|
|
38
73
|
function observe() { var root = document.getElementById('split-host'); if (root) observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden'] }); }
|
|
39
74
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', observe); else observe();
|
|
75
|
+
document.addEventListener('visibilitychange', syncWorkerMessagePreviews);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getWs } from './ws-ref.js';
|
|
2
|
+
import { iconHtml } from './icons.js';
|
|
3
|
+
|
|
4
|
+
export function syncWorkerForceStop(header, worker) {
|
|
5
|
+
var button = header.querySelector('[data-worker-force-stop]');
|
|
6
|
+
if (!button) return;
|
|
7
|
+
var support = worker && worker.forceTermination;
|
|
8
|
+
button.disabled = !support || support.supported !== true;
|
|
9
|
+
button.title = button.disabled ? support && support.reason || 'Force termination is not yet supported for this vendor' : 'Force stop Worker';
|
|
10
|
+
button.setAttribute('aria-label', 'Force stop Worker' + (button.disabled ? ': ' + button.title : ''));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// The caller revalidates the visible Driver and exact Worker on every action.
|
|
14
|
+
export function appendWorkerStopControls(header, target) {
|
|
15
|
+
['stop', 'force_stop'].forEach(function (type) {
|
|
16
|
+
var button = document.createElement('button'), pressed = null;
|
|
17
|
+
button.type = 'button'; button.className = 'worker-conversation-stop';
|
|
18
|
+
button.setAttribute('aria-label', type === 'stop' ? 'Stop Worker' : 'Force stop Worker');
|
|
19
|
+
button.title = type === 'stop' ? 'Stop Worker' : 'Force stop Worker';
|
|
20
|
+
button.innerHTML = iconHtml(type === 'stop' ? 'square' : 'octagon-x');
|
|
21
|
+
function stamp(value) { return value && JSON.stringify(value); }
|
|
22
|
+
button.addEventListener('pointerdown', function () { pressed = stamp(target()); });
|
|
23
|
+
button.addEventListener('keydown', function () { pressed = stamp(target()); });
|
|
24
|
+
button.addEventListener('click', function (event) {
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
var current = target(), ws = getWs();
|
|
27
|
+
if (button.disabled || type === 'force_stop' && (!current || !current.forceTermination || current.forceTermination.supported !== true)) return;
|
|
28
|
+
if (!current || !ws || ws.readyState !== 1 || pressed && pressed !== stamp(current)) return;
|
|
29
|
+
pressed = null;
|
|
30
|
+
ws.send(JSON.stringify({ type: type, sessionId: current.workerId, sessionOriginId: current.workerOriginId,
|
|
31
|
+
workerGeneration: current.generation, parentSessionOriginId: current.driverOriginId }));
|
|
32
|
+
});
|
|
33
|
+
if (type === 'force_stop') button.dataset.workerForceStop = 'true';
|
|
34
|
+
header.appendChild(button);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { showConfirm } from './app-misc.js';
|
|
3
|
+
import { showToast } from './utils.js';
|
|
4
|
+
|
|
5
|
+
async function request(stage, data) {
|
|
6
|
+
var result;
|
|
7
|
+
try {
|
|
8
|
+
var response = await fetch('/api/worktree-removal/' + stage, {
|
|
9
|
+
method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Clay-Worktree-Control': '1' },
|
|
10
|
+
credentials: 'same-origin', body: JSON.stringify(data),
|
|
11
|
+
});
|
|
12
|
+
result = await response.json();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
throw new Error(stage === 'execute' ? 'The removal outcome is unknown. Inspect the worktree directory and pending operation before trying again.' : 'Could not inspect the worktree. Try again when the server is reachable.');
|
|
15
|
+
}
|
|
16
|
+
if (!result.ok) {
|
|
17
|
+
var detail = result.filesDeleted ? ' The directory was removed, but cleanup is pending.' + (result.deleteBranch && !result.branchDeleted ? ' Local branch deletion was not confirmed.' : '') : '';
|
|
18
|
+
throw new Error((result.error || 'Worktree removal failed.') + detail + (result.operationId ? ' Operation: ' + result.operationId + '. Inspect it before retrying.' : ''));
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
}
|
|
22
|
+
export async function reviewWorktreeRemoval(slug, action, anchor) {
|
|
23
|
+
if (store.get('worktreeRemoval')) return;
|
|
24
|
+
var pending = { slug: slug, action: action, projectSlug: store.get('currentSlug'), sequence: 0, deleteBranch: false, preview: null };
|
|
25
|
+
store.set({ worktreeRemoval: pending });
|
|
26
|
+
function active() { return store.get('worktreeRemoval') === pending && store.get('currentSlug') === pending.projectSlug; }
|
|
27
|
+
function cleanup() {
|
|
28
|
+
document.getElementById('confirm-text').classList.remove('worktree-removal-review');
|
|
29
|
+
var option = document.getElementById('worktree-branch-option'); if (option) option.remove();
|
|
30
|
+
document.getElementById('confirm-ok').disabled = false;
|
|
31
|
+
}
|
|
32
|
+
function reset() {
|
|
33
|
+
if (store.get('worktreeRemoval') !== pending) return;
|
|
34
|
+
cleanup(); store.set({ worktreeRemoval: null });
|
|
35
|
+
if (anchor && anchor.isConnected) anchor.focus();
|
|
36
|
+
}
|
|
37
|
+
function description(preview) {
|
|
38
|
+
return (action === 'delete' ? 'Delete worktree' : 'Exclude worktree') + '\nBranch: ' + preview.branch + '\nDirectory: ' + preview.path + '\n\n' +
|
|
39
|
+
(action === 'delete' ? 'Stop its sessions and Workers, then remove this linked Git worktree and directory from disk. ' +
|
|
40
|
+
(preview.deleteBranch ? 'Then delete the local branch ' + preview.branch + '. If that step fails, the directory stays removed and cleanup remains pending.' : 'The local branch is kept.') +
|
|
41
|
+
' Stored history is kept. Remote branches are untouched. Changed, untracked, ignored or locked worktrees cannot be deleted.' +
|
|
42
|
+
(preview.localBranch ? '' : '\nDetached HEAD has no local branch to delete.') :
|
|
43
|
+
'Stop its sessions and Workers and hide it from Clay Studio. The worktree directory, Git branch and stored history are kept. This exclusion is saved across restarts.');
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
pending.preview = await request('preview', { slug: slug, action: action, deleteBranch: false });
|
|
47
|
+
if (!active()) { reset(); return; }
|
|
48
|
+
showConfirm(description(pending.preview), async function () {
|
|
49
|
+
if (!active() || !pending.preview || pending.preview.deleteBranch !== pending.deleteBranch) { reset(); return; }
|
|
50
|
+
var reviewed = pending.preview; cleanup();
|
|
51
|
+
try {
|
|
52
|
+
showToast('Closing worktree runtimes…', 'info');
|
|
53
|
+
var result = await request('execute', { token: reviewed.token, deleteBranch: pending.deleteBranch });
|
|
54
|
+
showToast(result.filesDeleted ? (result.branchDeleted ? 'Worktree directory and local branch deleted' : 'Worktree directory deleted; branch kept') : 'Worktree excluded; files kept', 'success');
|
|
55
|
+
if (store.get('currentSlug') === slug) window.location.assign('/p/' + encodeURIComponent(slug.split('--')[0]) + '/');
|
|
56
|
+
} catch (error) { showConfirm(error.message, function () {}, 'Close', false); }
|
|
57
|
+
finally { reset(); }
|
|
58
|
+
}, action === 'delete' ? 'Delete worktree' : 'Exclude', true, reset);
|
|
59
|
+
var container = document.getElementById('confirm-text');
|
|
60
|
+
container.classList.add('worktree-removal-review');
|
|
61
|
+
if (action === 'delete' && pending.preview.localBranch) {
|
|
62
|
+
var copy = document.createElement('span'); copy.textContent = description(pending.preview); container.textContent = ''; container.appendChild(copy);
|
|
63
|
+
var label = document.createElement('label'); label.id = 'worktree-branch-option'; label.className = 'worktree-branch-option';
|
|
64
|
+
var checkbox = document.createElement('input'); checkbox.type = 'checkbox'; checkbox.checked = false;
|
|
65
|
+
var caption = document.createElement('span'); caption.textContent = 'Also delete the local branch';
|
|
66
|
+
label.appendChild(checkbox); label.appendChild(caption); container.appendChild(label);
|
|
67
|
+
checkbox.addEventListener('change', async function () {
|
|
68
|
+
pending.deleteBranch = checkbox.checked; pending.preview = null; pending.sequence++;
|
|
69
|
+
var sequence = pending.sequence, choice = pending.deleteBranch;
|
|
70
|
+
document.getElementById('confirm-ok').disabled = true;
|
|
71
|
+
copy.textContent = 'Checking the selected local branch choice…'; store.set({ worktreeRemoval: pending });
|
|
72
|
+
try {
|
|
73
|
+
var refreshed = await request('preview', { slug: slug, action: action, deleteBranch: choice });
|
|
74
|
+
if (!active() || pending.sequence !== sequence) return;
|
|
75
|
+
if (refreshed.deleteBranch !== choice) throw new Error('The branch choice changed. Review again.');
|
|
76
|
+
pending.preview = refreshed; copy.textContent = description(refreshed);
|
|
77
|
+
document.getElementById('confirm-ok').disabled = false;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (!active() || pending.sequence !== sequence) return;
|
|
80
|
+
copy.textContent = error.message + '\nUncheck the option to keep the local branch, or Cancel.';
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
var cancel = document.getElementById('confirm-cancel'); if (cancel) cancel.focus();
|
|
85
|
+
} catch (error) { reset(); showToast(error.message, 'error'); }
|
|
86
|
+
}
|