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.
Files changed (123) hide show
  1. package/lib/clay-product-control-input.js +2 -2
  2. package/lib/clay-product-control-tools.js +1 -1
  3. package/lib/clay-product-control.js +4 -3
  4. package/lib/daemon-project-access.js +10 -2
  5. package/lib/daemon-projects.js +16 -4
  6. package/lib/daemon-worktree-controls.js +171 -0
  7. package/lib/daemon.js +14 -63
  8. package/lib/personal-provider-install.js +85 -0
  9. package/lib/personal-provider-settings.js +94 -0
  10. package/lib/project-logs-service.js +2 -2
  11. package/lib/project-pair-task-control.js +1 -1
  12. package/lib/project-pending-message-queue.js +6 -2
  13. package/lib/project-product-control.js +1 -0
  14. package/lib/project-session-actions.js +46 -3
  15. package/lib/project-session-pair.js +2 -0
  16. package/lib/project-sessions.js +11 -3
  17. package/lib/project-user-message.js +5 -1
  18. package/lib/project-vendor-login.js +95 -32
  19. package/lib/project-worker-auth.js +71 -0
  20. package/lib/project-worker-conversation.js +11 -2
  21. package/lib/project.js +9 -0
  22. package/lib/public/app.js +22 -0
  23. package/lib/public/css/icon-strip.css +5 -0
  24. package/lib/public/css/overlays.css +18 -0
  25. package/lib/public/css/settings-shell.css +14 -0
  26. package/lib/public/css/settings-ui.css +6 -0
  27. package/lib/public/css/split-view-layout.css +16 -11
  28. package/lib/public/modules/agent-message-bubbles.js +33 -4
  29. package/lib/public/modules/app-messages.js +7 -1
  30. package/lib/public/modules/app-projects.js +3 -4
  31. package/lib/public/modules/clay-control-copy.js +1 -0
  32. package/lib/public/modules/clay-tool-presentation.js +1 -1
  33. package/lib/public/modules/generation-stop.js +4 -4
  34. package/lib/public/modules/pane-activation.js +2 -2
  35. package/lib/public/modules/pane-bridge.js +4 -2
  36. package/lib/public/modules/personal-provider-context.js +11 -0
  37. package/lib/public/modules/project-settings.js +1 -1
  38. package/lib/public/modules/settings-section-nav.js +58 -0
  39. package/lib/public/modules/settings-shell-registry.js +4 -2
  40. package/lib/public/modules/settings-shell.js +17 -6
  41. package/lib/public/modules/sidebar-projects.js +23 -9
  42. package/lib/public/modules/sidebar-sessions.js +3 -0
  43. package/lib/public/modules/split-pane-force-stop.js +47 -0
  44. package/lib/public/modules/split-pane-header-state.js +11 -1
  45. package/lib/public/modules/split-pane-renderer.js +2 -2
  46. package/lib/public/modules/split-view-arrange-menu.js +38 -0
  47. package/lib/public/modules/split-view-create-menu.js +9 -5
  48. package/lib/public/modules/split-view-member-actions.js +42 -0
  49. package/lib/public/modules/split-view-picker-actions.js +2 -2
  50. package/lib/public/modules/split-view-presentation-renderer.js +10 -35
  51. package/lib/public/modules/split-view-presentations.js +9 -8
  52. package/lib/public/modules/split-view-sidebar-menu.js +2 -1
  53. package/lib/public/modules/split-view-workers.js +32 -17
  54. package/lib/public/modules/stt-settings.js +21 -4
  55. package/lib/public/modules/user-settings-layout.js +11 -8
  56. package/lib/public/modules/user-settings-provider-install.js +42 -0
  57. package/lib/public/modules/user-settings-providers.js +102 -0
  58. package/lib/public/modules/user-settings-voice.js +50 -41
  59. package/lib/public/modules/vendor-login-browser.js +1 -1
  60. package/lib/public/modules/vendor-login.js +72 -12
  61. package/lib/public/modules/view-workspace-state.js +1 -1
  62. package/lib/public/modules/worker-activity-indicator.js +49 -0
  63. package/lib/public/modules/worker-message-previews.js +47 -11
  64. package/lib/public/modules/worker-stop-controls.js +36 -0
  65. package/lib/public/modules/worktree-controls.js +86 -0
  66. package/lib/public/pane-assets/{app-4IZ6F6ND.js → app-ANJS2LSK.js} +1 -1
  67. package/lib/public/pane-assets/{chunk-L3FCTD7U.js → chunk-32YIYF7R.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-RRYWV5JT.js → chunk-3EJ5IAHZ.js} +1 -1
  69. package/lib/public/pane-assets/{chunk-3E6LDNXF.js → chunk-3TIVBAKM.js} +1 -1
  70. package/lib/public/pane-assets/{chunk-I6X5J2V5.js → chunk-4QFNLHW5.js} +1 -1
  71. package/lib/public/pane-assets/{chunk-OB3KKICE.js → chunk-5XMZMIFG.js} +1 -1
  72. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
  73. package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
  74. package/lib/public/pane-assets/{chunk-A7OB7HQS.js → chunk-BOFNCPTN.js} +1 -1
  75. package/lib/public/pane-assets/{chunk-ELL4UBGY.js → chunk-CXWAJEN5.js} +1 -1
  76. package/lib/public/pane-assets/{chunk-SEELA2D2.js → chunk-EWTXBZJO.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-ADGKPVFD.js → chunk-G35ZOJGP.js} +1 -1
  78. package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
  79. package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
  80. package/lib/public/pane-assets/{chunk-6ZF2FJT3.js → chunk-JM46WGDH.js} +1 -1
  81. package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
  82. package/lib/public/pane-assets/{chunk-YXJ7ISEK.js → chunk-P3AK4ORJ.js} +1 -1
  83. package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
  84. package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
  85. package/lib/public/pane-assets/{chunk-YWHBNEB2.js → chunk-UGH5MCNK.js} +1 -1
  86. package/lib/public/pane-assets/{chunk-YFUORWA6.js → chunk-V5DLPSLT.js} +1 -1
  87. package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
  88. package/lib/public/pane-assets/{chunk-A4MLSYBJ.js → chunk-XSSTR6GC.js} +1 -1
  89. package/lib/public/pane-assets/{chunk-DJ5WXQKT.js → chunk-XVESKMU3.js} +1 -1
  90. package/lib/public/pane-assets/manifest.json +443 -433
  91. package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
  92. package/lib/sdk-bridge.js +40 -6
  93. package/lib/sdk-force-stop.js +62 -0
  94. package/lib/sdk-message-processor.js +2 -1
  95. package/lib/server-worktree-controls.js +34 -0
  96. package/lib/server.js +11 -1
  97. package/lib/session-force-stop-support.js +15 -0
  98. package/lib/sessions.js +6 -1
  99. package/lib/terminal-manager.js +1 -1
  100. package/lib/terminal.js +13 -1
  101. package/lib/vendor-login-browser-flow.js +1 -1
  102. package/lib/worker-activity.js +26 -0
  103. package/lib/worker-message-previews.js +12 -7
  104. package/lib/worktree-branch.js +57 -0
  105. package/lib/worktree-identity.js +39 -0
  106. package/lib/worktree-preferences.js +25 -0
  107. package/lib/worktree-ref-transaction.js +36 -0
  108. package/lib/yoke/adapters/antigravity.js +3 -0
  109. package/lib/yoke/force-termination.js +26 -0
  110. package/lib/yoke/interface.js +14 -0
  111. package/lib/yoke/process-force-termination.js +24 -0
  112. package/lib/yoke/provider-install-recipes.js +20 -0
  113. package/lib/yoke/vendor-registry.js +20 -1
  114. package/package.json +1 -1
  115. package/lib/public/pane-assets/chunk-6KO36HMU.js +0 -1
  116. package/lib/public/pane-assets/chunk-FILRACQB.js +0 -168
  117. package/lib/public/pane-assets/chunk-H6MU72QE.js +0 -1
  118. package/lib/public/pane-assets/chunk-PJURCIJQ.js +0 -1
  119. package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +0 -1
  120. package/lib/public/pane-assets/chunk-UTLQWAKG.js +0 -1
  121. package/lib/public/pane-assets/chunk-WE4NWXNK.js +0 -1
  122. package/lib/public/pane-assets/chunk-Z2MHS43B.js +0 -1
  123. 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 in the window below. Clay reconnects your sessions automatically.",
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
- next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null };
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 = true;
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 { bindAgentMessageBubble, messagePreviewKey, rememberAgentMessage } from './agent-message-bubbles.js';
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
- bindAgentMessageBubble(chip, messagePreviewKey('worker', [target.parentSessionOriginId, target.workerSessionOriginId, target.generation]));
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(); if (!store.get('connected') || !socket || socket.readyState !== 1) return;
21
- var list = targets(), signature = JSON.stringify([store.get('currentSlug'), store.get('myUserId'), store.get('projectSplitViewSocketGeneration'), list]);
22
- var previous = store.get('workerMessagePreviewSubscription'); if (previous && previous.signature === signature) return;
23
- var requestId = crypto.randomUUID(); store.set({ workerMessagePreviewSubscription: { requestId: requestId, signature: signature, targets: list } });
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'); if (!state || !store.get('connected') || message.requestId !== state.requestId) return;
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 chip = Array.from(document.querySelectorAll('.split-view-worker-chip[data-worker-parent]')).find(function (chip) { return chip.dataset.workerParent === message.parentSessionOriginId && chip.dataset.workerOrigin === message.workerSessionOriginId && Number(chip.dataset.workerGeneration) === message.generation; });
31
- if (!chip || !chip.getClientRects().length) return;
32
- rememberAgentMessage(messagePreviewKey('worker', [message.parentSessionOriginId, message.workerSessionOriginId, message.generation]), message.preview, message.live === true);
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.splitPanes !== previous.splitPanes || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId || state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration) syncWorkerMessagePreviews();
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
+ }