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,5 @@
1
+ import { appendWorkerStopControls, syncWorkerForceStop } from './worker-stop-controls.js';
2
+ import { createWorkerActivityIndicator, syncWorkerActivityIndicator, setWorkerActivityRuntime } from './worker-activity-indicator.js';
1
3
  import { syncWorkerMessagePreviews } from './worker-message-previews.js';
2
4
  import { appendWorkerRemoval, reconcileWorkerRemoval } from './split-view-worker-removal.js';
3
5
  import { workerCursorKey, workerCursorAdvances, shouldLoadWorkerPage, captureWorkerAnchor, restoreWorkerAnchor } from './split-view-worker-scroll.js';
@@ -52,14 +54,14 @@ function presentationSource(container, driver, connectionErrorBinding) {
52
54
  var inSingleton = !view && store.get('projectSplitViewSingletonOrigin') === driver.sessionOriginId;
53
55
  return inView || inSingleton ? { frame: frame, live: live, binding: binding, view: view, singleton: inSingleton } : null;
54
56
  }
55
- export function workerRemovalTarget(container, driverOrigin, workerOrigin, generation) {
57
+ export function workerRemovalTarget(container, driverOrigin, workerOrigin, generation, stopping) {
56
58
  var driver = exactDriver(driverOrigin), worker = exactWorker(driver, workerOrigin, generation);
57
59
  var source = presentationSource(container, driver);
58
60
  if (!worker || worker.hidden || !driver || driver.hidden || (driver.ownerId || null) !== (store.get('myUserId') || null) ||
59
61
  document.hidden || !source || !store.get('connected') || !container.isConnected || container.hidden || container.inert ||
60
- source.frame.hidden || source.frame.inert || store.get('permissions') && store.get('permissions').sessionDelete === false) return null;
62
+ source.frame.hidden || source.frame.inert || !stopping && store.get('permissions') && store.get('permissions').sessionDelete === false) return null;
61
63
  return { projectSlug: store.get('currentSlug'), ownerId: store.get('myUserId') || null, driverId: driver.id,
62
- driverOriginId: driverOrigin, workerId: worker.id, workerOriginId: workerOrigin, generation: generation,
64
+ driverOriginId: driverOrigin, workerId: worker.id, workerOriginId: workerOrigin, generation: generation, forceTermination: worker.forceTermination,
63
65
  bindingGeneration: source.binding.generation, bindingRequestId: source.binding.requestId,
64
66
  viewRevision: store.get('projectSplitViewRevision'), socketGeneration: store.get('projectSplitViewSocketGeneration') };
65
67
  }
@@ -204,6 +206,12 @@ function ensurePopover() {
204
206
  element.setAttribute("role", "dialog"); element.setAttribute("aria-label", "Read-only Worker conversation"); document.body.appendChild(element);
205
207
  var header = document.createElement("header"); header.className = "split-view-worker-popover-header";
206
208
  var title = document.createElement("strong"); title.className = "worker-popover-title"; header.appendChild(title);
209
+ appendWorkerStopControls(header, function () {
210
+ var state = store.get('splitViewWorkerConversation');
211
+ if (!state || state.status === 'error') return null;
212
+ var pane = document.querySelector('.split-pane[data-session-origin="' + CSS.escape(state.driverOriginId) + '"]');
213
+ return workerRemovalTarget(pane, state.driverOriginId, state.workerOriginId, state.generation, true);
214
+ });
207
215
  var closeButton = document.createElement("button"); closeButton.type = "button"; closeButton.setAttribute("aria-label", "Close Worker conversation"); closeButton.innerHTML = iconHtml("x");
208
216
  closeButton.addEventListener("click", function () { close(true); }); header.appendChild(closeButton); element.appendChild(header);
209
217
  var source = document.createElement("div"); source.className = "split-view-worker-popover-source"; element.appendChild(source);
@@ -227,6 +235,9 @@ function ensurePopover() {
227
235
  body.addEventListener('touchstart', scrollIntent, { passive: true });
228
236
  var footer = document.createElement("footer"); footer.className = "split-view-worker-popover-footer"; element.appendChild(footer);
229
237
  }
238
+ syncWorkerForceStop(element, worker);
239
+ var stopButtons = element.querySelectorAll('.worker-conversation-stop');
240
+ for (var i = 0; i < stopButtons.length; i++) stopButtons[i].disabled = stopButtons[i].dataset.workerForceStop === 'true' && stopButtons[i].disabled || !store.get('connected') || state.status === 'error';
230
241
  return element;
231
242
  }
232
243
  function renderContent() {
@@ -330,9 +341,9 @@ export function handleWorkerConversationChanged(message) {
330
341
  message.subscriptionId !== state.subscriptionId || message.parentSessionOriginId !== state.driverOriginId ||
331
342
  message.workerSessionOriginId !== state.workerOriginId || message.generation !== state.generation || !ensurePopover()) return false;
332
343
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
333
- if (chip.dataset.workerOrigin === state.workerOriginId) {
334
- var dot = chip.querySelector('.split-view-worker-dot');
335
- if (dot) dot.dataset.state = message.isProcessing ? 'working' : 'ready';
344
+ if (chip.dataset.workerParent === state.driverOriginId && chip.dataset.workerOrigin === state.workerOriginId && Number(chip.dataset.workerGeneration) === state.generation) {
345
+ setWorkerActivityRuntime(chip.dataset.messagePreview, message.isProcessing === true);
346
+ syncWorkerActivityIndicator(chip, { isProcessing: message.isProcessing === true });
336
347
  }
337
348
  });
338
349
  refreshWorkerConversation();
@@ -341,11 +352,6 @@ export function handleWorkerConversationChanged(message) {
341
352
  export function handleWorkerSessionActivity(message) {
342
353
  var worker = sessions().find(function (item) { return item.id === message.id && item.sessionRole === 'worker'; });
343
354
  if (!worker || !store.get('connected')) return;
344
- document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
345
- if (chip.dataset.workerOrigin !== worker.sessionOriginId) return;
346
- var dot = chip.querySelector('.split-view-worker-dot');
347
- if (dot) { dot.dataset.state = 'working'; dot.setAttribute('aria-label', 'Worker activity received'); }
348
- });
349
355
  var state = store.get('splitViewWorkerConversation');
350
356
  if (state && state.workerOriginId === worker.sessionOriginId && state.generation === worker.workerGeneration) refreshWorkerConversation();
351
357
  }
@@ -359,8 +365,12 @@ export function renderPaneWorkers(container, driver) {
359
365
  var source = presentationSource(container, driver);
360
366
  root.hidden = !source || data.items.length === 0;
361
367
  if (!source) { if (store.get('splitViewWorkerConversation') && store.get('splitViewWorkerConversation').driverOriginId === driver.sessionOriginId) ensurePopover(); return; }
362
- root.textContent = "";
363
- data.items.forEach(function (worker) {
368
+ var signature = JSON.stringify([store.get('myUserId'), store.get('currentSlug'), driver.sessionOriginId, driver.title, store.get('projectSplitViewSocketGeneration'), data.items.map(function (worker) {
369
+ return [worker.id, worker.sessionOriginId, worker.workerGeneration, worker.vendor, worker.model, worker.title, data.currentIds.has(worker.id)];
370
+ })]);
371
+ var rebuild = root.dataset.workerSignature !== signature;
372
+ if (rebuild) { root.textContent = ''; root.dataset.workerSignature = signature; }
373
+ if (rebuild) data.items.forEach(function (worker) {
364
374
  var pill = document.createElement("span"); pill.className = "split-view-worker-pill";
365
375
  var button = document.createElement("button"); button.type = "button"; button.className = "split-view-worker-chip";
366
376
  button.dataset.workerParent = driver.sessionOriginId; button.dataset.workerOrigin = worker.sessionOriginId; button.dataset.workerGeneration = String(worker.workerGeneration);
@@ -369,8 +379,7 @@ export function renderPaneWorkers(container, driver) {
369
379
  button.title = (VENDOR_NAMES[worker.vendor] || worker.vendor || "Unknown vendor") + " · " + modelLabel + " · Split Worker #" + worker.id +
370
380
  (current ? "" : " · Archived · Gen " + worker.workerGeneration) + " · " + (worker.title || "Worker");
371
381
  button.setAttribute("aria-label", button.title + " conversation");
372
- var dot = document.createElement("span"); dot.className = "split-view-worker-dot"; dot.dataset.state = !store.get('connected') ? 'disconnected' : worker.isProcessing ? 'working' : 'ready';
373
- dot.setAttribute('aria-label', !store.get('connected') ? 'Disconnected' : worker.isProcessing ? 'Worker is working' : 'Worker is idle');
382
+ var dot = createWorkerActivityIndicator();
374
383
  if (VENDOR_AVATARS[worker.vendor]) {
375
384
  var vendorIcon = document.createElement("img"); vendorIcon.className = "split-view-worker-vendor";
376
385
  vendorIcon.src = VENDOR_AVATARS[worker.vendor]; vendorIcon.alt = ""; vendorIcon.setAttribute("aria-hidden", "true"); button.appendChild(vendorIcon);
@@ -389,8 +398,15 @@ export function renderPaneWorkers(container, driver) {
389
398
  button.addEventListener("click", function () { if (presentationSource(container, exactDriver(driver.sessionOriginId))) open(button, driver, worker); }); pill.appendChild(button); root.appendChild(pill);
390
399
  appendWorkerRemoval(pill, button, container, driver, worker);
391
400
  });
401
+ root.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
402
+ var worker = data.items.find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
403
+ syncWorkerActivityIndicator(chip, worker);
404
+ var conversation = store.get('splitViewWorkerConversation');
405
+ chip.setAttribute('aria-expanded', conversation && conversation.driverOriginId === driver.sessionOriginId && conversation.workerOriginId === chip.dataset.workerOrigin ? 'true' : 'false');
406
+ });
392
407
  introduceWorkerViewer(container, driver);
393
408
  syncWorkerMessagePreviews();
409
+ if (rebuild) refreshIcons();
394
410
  var conversation = store.get('splitViewWorkerConversation');
395
411
  if (conversation) ensurePopover();
396
412
  }
@@ -417,8 +433,7 @@ store.subscribe(function (state, previous) {
417
433
  if (state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected) {
418
434
  document.querySelectorAll('.split-view-worker-chip').forEach(function (chip) {
419
435
  var worker = sessions().find(function (item) { return item.sessionOriginId === chip.dataset.workerOrigin; });
420
- var dot = chip.querySelector('.split-view-worker-dot');
421
- if (dot) dot.dataset.state = !state.connected ? 'disconnected' : worker && worker.isProcessing ? 'working' : 'ready';
436
+ syncWorkerActivityIndicator(chip, worker);
422
437
  });
423
438
  if (state.connected && state.splitViewWorkerConversation) refreshWorkerConversation();
424
439
  }
@@ -1,3 +1,4 @@
1
+ import { getWs } from './ws-ref.js';
1
2
  import { store } from './store.js';
2
3
  import { stopSTT } from './stt-controller.js';
3
4
  export var speechLanguages = [
@@ -16,35 +17,51 @@ export function voiceProvider(id) {
16
17
  var settings = store.get('speechSettings');
17
18
  return settings && settings.providers.find(function (provider) { return provider.id === id; });
18
19
  }
20
+ function context() { return JSON.stringify([store.get('myUserId'), store.get('speechSettingsContextGeneration')]); }
21
+ function lostAccess() {
22
+ store.set({speechPopup:null,speechRetry:null,speechSettings:null,speechSettingsSaving:false,speechSettingsError:'Sign in again to manage audio transcription keys.',speechSettingsContextGeneration:(store.get('speechSettingsContextGeneration') || 0)+1,speechSettingsRequest:(store.get('speechSettingsRequest') || 0)+1});
23
+ }
19
24
  export async function loadSpeechSettings() {
20
25
  if (store.get('speechSettingsSaving')) return;
26
+ var actor = context(), socket = getWs();
21
27
  var request = (store.get('speechSettingsRequest') || 0) + 1;
22
28
  store.set({ speechSettingsRequest: request });
23
29
  try {
24
30
  var response = await fetch('/api/speech/settings');
31
+ if ((response.status === 401 || response.status === 403) && actor === context() && socket === getWs() && store.get('speechSettingsRequest') === request) { lostAccess(); return; }
25
32
  if (!response.ok) throw new Error('Voice settings unavailable. Please try again.');
26
33
  var settings = await response.json();
27
- if (store.get('speechSettingsRequest') !== request) return;
34
+ if (actor !== context() || socket !== getWs() || store.get('speechSettingsRequest') !== request) return;
28
35
  var previous = store.get('speechSettings');
29
36
  if ((previous && JSON.stringify(previous) !== JSON.stringify(settings)) ||
30
37
  (!previous && settings.selected !== 'browser')) stopSTT();
31
38
  if (!previous || JSON.stringify(previous) !== JSON.stringify(settings)) store.set({ speechSettings: settings, speechLang: settings.language || 'en-US', speechSettingsError: '' });
32
39
  else if (store.get('speechSettingsError')) store.set({ speechSettingsError: '' });
33
- } catch (error) { if (store.get('speechSettingsRequest') === request) store.set({ speechSettingsError: 'Voice settings unavailable. Please try again.' }); }
40
+ } catch (error) { if (actor === context() && socket === getWs() && store.get('speechSettingsRequest') === request) store.set({ speechSettingsError: 'Voice settings unavailable. Please try again.' }); }
34
41
  }
35
42
  export async function saveSpeechSettings(change) {
36
43
  if (store.get('speechSettingsSaving')) throw new Error('Voice settings are being saved. Please wait.');
44
+ var actor = context(), socket = getWs(), request = (store.get('speechSettingsRequest') || 0) + 1;
37
45
  stopSTT();
38
- store.set({ speechRetry: null, speechSettingsSaving: true, speechSettingsRequest: (store.get('speechSettingsRequest') || 0) + 1 });
46
+ store.set({ speechRetry: null, speechSettingsSaving: true, speechSettingsRequest: request });
39
47
  try {
40
48
  var response = await fetch('/api/speech/settings', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(change) });
41
49
  var result = await response.json();
50
+ if (actor !== context() || socket !== getWs() || store.get('speechSettingsRequest') !== request) throw new Error('Account or connection changed. Reopen API connections.');
51
+ if (response.status === 401 || response.status === 403) { lostAccess(); throw new Error('Sign in again to manage audio transcription keys.'); }
42
52
  if (!response.ok) throw new Error(result.error || 'Could not save voice settings.');
43
53
  store.set({ speechSettings: result, speechLang: result.language || 'en-US', speechSettingsError: '' });
44
- } finally { store.set({ speechSettingsSaving: false }); }
54
+ } finally { if (actor === context() && store.get('speechSettingsRequest') === request) store.set({ speechSettingsSaving: false }); }
45
55
  try { window.top.document.dispatchEvent(new window.top.Event('clay:speech-settings-saved')); } catch (error) { /* Standalone host. */ }
46
56
  }
47
57
  export async function saveSpeechLanguage(code) {
48
58
  await saveSpeechSettings({ language: code });
49
59
  window.dispatchEvent(new CustomEvent('clay:speech-language-saved', { detail: code }));
50
60
  }
61
+
62
+ store.subscribe(function (state, previous) {
63
+ if (state.myUserId !== previous.myUserId || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration || state.currentSlug !== previous.currentSlug || state.connected === false && previous.connected !== false) {
64
+ stopSTT();
65
+ store.set({speechPopup:null,speechRetry:null,speechSettings:null,speechSettingsSaving:false,speechSettingsError:state.connected === false ? 'Reconnect before loading audio transcription providers.' : 'Account or connection changed. Reload providers to continue.',speechSettingsContextGeneration:(state.speechSettingsContextGeneration || 0)+1,speechSettingsRequest:(state.speechSettingsRequest || 0)+1});
66
+ }
67
+ });
@@ -1,3 +1,4 @@
1
+ import { initPersonalProviders, loadPersonalProviders, leavePersonalProviders } from './user-settings-providers.js';
1
2
  import { initPersonalIntegrations, loadPersonalIntegrations } from './user-settings-integrations.js';
2
3
  import { getWs } from './ws-ref.js';
3
4
  import { store } from './store.js';
@@ -20,13 +21,14 @@ export function buildUserSettingsLayout() {
20
21
  var push=document.getElementById('settings-notif-push').closest('label');
21
22
  heading(notifications,'This device','Browser permission and push delivery are configured separately on each browser.');notifications.appendChild(push);
22
23
  var accountNotifications=document.createElement('h3');accountNotifications.textContent='Account preferences';notifications.querySelector('.settings-card').before(accountNotifications);
23
- var chat = section('us-chat'); chat.querySelector('h2').textContent = 'Conversation';
24
- var voice = document.getElementById('us-lang-select').closest('.settings-card');
25
- voice.innerHTML = '<div id="us-voice-settings"></div>';
24
+ var chat = section('us-chat');
25
+ var providers = document.createElement('div'); providers.className = 'us-section'; providers.dataset.section = 'us-providers';
26
+ providers.innerHTML = '<h2>AI providers</h2><p class="settings-hint">Set up coding agents on the server and manage your personal audio transcription API keys.</p><div class="settings-section-heading"><h3 id="coding-agents" data-settings-section>Coding agents</h3><span id="us-provider-refresh"></span></div><div id="us-coding-agents"></div><h3 id="api-connections" data-settings-section>API connections</h3><div id="us-voice-settings"></div>';
27
+ root.querySelector('.us-modal-content').appendChild(providers);
26
28
  var ai = document.createElement('div'); ai.className = 'us-section'; ai.dataset.section = 'us-ai';
27
29
  ai.innerHTML = '<h2>AI preferences</h2><p class="settings-hint">Defaults for new Clay interviews and built-in AI assistance. Project defaults, individual Mates and existing conversation runtimes stay separate.</p><div id="us-default-ai"></div>';
28
30
  root.querySelector('.us-modal-content').appendChild(ai);
29
- heading(ai, 'Execution behavior');
31
+ heading(ai, 'Execution behavior'); ai.lastElementChild.id = 'execution-behavior';
30
32
  ['us-auto-continue'].forEach(function (id) {ai.appendChild(document.getElementById(id).closest('.settings-card'));});
31
33
  document.getElementById('us-auto-continue').closest('label').querySelector('.settings-hint').textContent = 'When a rate-limited conversation becomes eligible to retry, Clay can send a continuation automatically. This may resume work and consume usage without another message from you.';
32
34
  heading(ai, 'Command permissions', 'These are personal Claude CLI allow rules, separate from model selection and Clay session approval modes. Managed defaults are shown for reference. Server access restrictions still apply. On an OS-user server these install in your OS home; otherwise they install in the shared server process home and can affect other Claude CLI launches there.');
@@ -36,7 +38,8 @@ export function buildUserSettingsLayout() {
36
38
  root.querySelector('.us-modal-content').appendChild(agent);
37
39
  section('us-mates').remove();
38
40
  var experiments = document.createElement('details'); experiments.className = 'us-experiments'; experiments.innerHTML = '<summary>Experimental features</summary><p class="settings-hint">Personal opt-ins for features that may change.</p>';
39
- experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));chat.appendChild(experiments);section('us-experimental').remove();
41
+ experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));ai.appendChild(experiments);experiments.id = 'experimental-features';section('us-experimental').remove();chat.remove();
42
+ initPersonalProviders();
40
43
  section('us-email').remove();
41
44
  initPersonalIntegrations();
42
45
  var account = section('us-account');heading(account, 'Sign out');account.appendChild(document.getElementById('us-logout-btn').closest('.settings-card'));section('us-logout').remove();
@@ -47,12 +50,12 @@ export function buildUserSettingsLayout() {
47
50
  }
48
51
  export function loadUserSettingsExtras(page) {
49
52
  if (page === 'us-integrations') loadPersonalIntegrations();
50
- if (page === 'us-chat') loadSpeechSettings();
53
+ if (page === 'us-providers') { loadSpeechSettings(); loadPersonalProviders(); } else leavePersonalProviders();
51
54
  if (page === 'us-agent') mountAgentBehavior(document.getElementById('us-agent-behavior'));
52
55
  if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
53
56
  }
54
57
  export function leaveAgentBehaviorPage() {unmountAgentBehavior();resetAgentBehavior();}
55
- export function finishUserSettingsExtras() {leaveAgentBehaviorPage();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
58
+ export function finishUserSettingsExtras() {leavePersonalProviders();leaveAgentBehaviorPage();closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
56
59
  // Returns the draft prompt for leaving one personal page (or every page when
57
60
  // pageId is omitted); the shell shows it with the shared confirmation.
58
61
  export function personalLeavePrompt(pageId) {
@@ -64,7 +67,7 @@ export function personalLeavePrompt(pageId) {
64
67
  var key = document.getElementById('us-linear-key');
65
68
  var keyDraft = key && key.value && store.get('personalLinearEditing') && !(key.form && key.form.hidden);
66
69
  var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || keyDraft) ||
67
- leaving('us-chat') && store.get('userVoiceDirty') ||
70
+ leaving('us-providers') && store.get('userVoiceDirty') ||
68
71
  leaving('us-notifications') && store.get('notificationDraft') ||
69
72
  leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
70
73
  leaving('us-account') && (store.get('userProfileDirty') || pinDirty) ||
@@ -0,0 +1,42 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { personalProviderContext } from './personal-provider-context.js';
4
+ function send(type,provider,reviewId,render) {
5
+ var ws = getWs();
6
+ if (!ws || ws.readyState !== 1 || !store.get('personalProvidersOpen') || store.get('personalProviderContext') !== personalProviderContext()) return false;
7
+ var requestId = type === 'stop' || type === 'cancel' ? store.get('personalProviderInstallRequest') : crypto.randomUUID();
8
+ store.set({personalProviderInstallRequest:requestId});
9
+ ws.send(JSON.stringify({requestId:requestId,type:'personal_provider_install_'+type,vendor:provider.id,personalRequestId:store.get('personalProviderRevision'),reviewId:reviewId}));
10
+ if (type === 'review' || type === 'start') setTimeout(function () { if (store.get('personalProviderInstallRequest') === requestId && store.get('personalProviderInstallAwaiting') === requestId) { store.set({personalProviderInstallBusy:false,personalProviderInstall:{vendor:provider.id,state:'unknown',message:'Installation response timed out. Reopen settings and refresh status; never assume an installation completed.'}}); if (render) render(); } },10000);
11
+ store.set({personalProviderInstallAwaiting:requestId}); return true;
12
+ }
13
+ export function resetProviderInstall() { store.set({personalProviderInstall:null,personalProviderInstallBusy:false,personalProviderInstallRequest:null,personalProviderInstallAwaiting:null}); }
14
+ export function handleProviderInstall(message,render) {
15
+ if (message.type !== 'personal_provider_install') return false;
16
+ if (!store.get('personalProvidersOpen') || !store.get('personalProviderRevision') || message.requestId !== store.get('personalProviderInstallRequest') || message.personalRequestId !== store.get('personalProviderRevision') || store.get('personalProviderContext') !== personalProviderContext()) return true;
17
+ store.set({personalProviderInstallAwaiting:null,personalProviderInstall:message,personalProviderInstallBusy:['review','started','stopping','unknown'].indexOf(message.state)>=0}); render();
18
+ var editor=document.getElementById('provider-setup-'+message.vendor); var status=editor && editor.querySelector('[data-install-status]'); if (status) status.focus({preventScroll:true}); return true;
19
+ }
20
+ export function renderProviderInstall(editor,provider,render) {
21
+ if (!provider.canInstall) return;
22
+ var item = store.get('personalProviderInstall');
23
+ var actions = document.createElement('div'); actions.className = 'settings-form-actions';
24
+ if (!item || item.vendor !== provider.id || ['review','started','stopping','unknown'].indexOf(item.state)<0) {
25
+ var review = document.createElement('button'); review.type='button'; review.className='settings-btn-sm'; review.textContent='Review installation'; review.disabled=!!store.get('personalProviderInstallBusy') || !!store.get('personalProviderSigningIn');
26
+ review.onclick=function () { if (send('review',provider,null,render)) { store.set({personalProviderInstallBusy:true}); review.disabled=true; } }; actions.appendChild(review);
27
+ }
28
+ if (item && item.vendor === provider.id) {
29
+ var status=document.createElement('p'); status.className='settings-status'; status.role='status'; status.tabIndex=-1; status.dataset.installStatus=''; status.textContent=item.message; editor.appendChild(status);
30
+ if (item.state==='review') {
31
+ var details=document.createElement('p'); details.className='settings-hint'; details.textContent='Runtime account: '+item.runtime.user+' (UID '+item.runtime.uid+'), HOME '+item.runtime.home+'. '+item.effects; editor.appendChild(details);
32
+ var command=document.createElement('pre'); command.className='settings-hint'; command.style.whiteSpace='pre-wrap'; command.style.overflowWrap='anywhere'; command.textContent=item.command; editor.appendChild(command);
33
+ var source=document.createElement('a'); source.className='settings-link'; source.href=item.source; source.target='_blank'; source.rel='noopener noreferrer'; source.textContent='Official setup instructions'; editor.appendChild(source);
34
+ var confirm=document.createElement('button'); confirm.type='button'; confirm.className='settings-btn-sm'; confirm.textContent='Install for this account'; confirm.onclick=function () { if (send('start',provider,item.reviewId,render)) { confirm.disabled=true; } }; actions.appendChild(confirm);
35
+ var cancel=document.createElement('button'); cancel.type='button'; cancel.className='settings-btn-sm'; cancel.textContent='Cancel installation'; cancel.onclick=function () { send('cancel',provider,item.reviewId); resetProviderInstall(); render(); }; actions.appendChild(cancel);
36
+ }
37
+ if (item.state==='started') {
38
+ var stop=document.createElement('button'); stop.type='button'; stop.className='settings-btn-sm'; stop.textContent='Stop installation'; stop.onclick=function () { send('stop',provider); stop.disabled=true; }; actions.appendChild(stop);
39
+ }
40
+ }
41
+ editor.appendChild(actions);
42
+ }
@@ -0,0 +1,102 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { requestVendorLogin, cancelVendorLogin } from './vendor-login.js';
4
+ import { renderProviderInstall, handleProviderInstall, resetProviderInstall } from './user-settings-provider-install.js';
5
+ import { personalProviderContext } from './personal-provider-context.js';
6
+
7
+ function render() {
8
+ var root = document.getElementById('us-coding-agents'); if (!root) return;
9
+ root.replaceChildren();
10
+ var status = document.createElement('p'); status.className = 'settings-status'; status.role = 'status';
11
+ status.textContent = store.get('personalProvidersError') || store.get('personalProvidersNotice') || (store.get('personalProvidersLoading') ? 'Checking installations…' : '');
12
+ status.dataset.state = store.get('personalProvidersError') ? 'error' : store.get('personalProvidersNotice') ? 'info' : ''; root.appendChild(status);
13
+ var refresh = document.createElement('button'); refresh.type = 'button'; refresh.className = 'settings-btn-sm';
14
+ refresh.textContent = 'Refresh status'; refresh.disabled = !!store.get('personalProvidersLoading') || !!store.get('personalProviderSigningIn') || !!store.get('personalProviderInstallBusy');
15
+ refresh.onclick = loadPersonalProviders;
16
+ var headingActions=document.getElementById('us-provider-refresh'); if (headingActions) headingActions.replaceChildren(refresh);
17
+ var catalog = store.get('personalProviderCatalog'); if (!catalog) return;
18
+ var runtime=catalog.runtime;
19
+ var scope = document.createElement('p'); scope.className = 'settings-hint'; scope.textContent = 'Account: ' + (catalog.accountName || catalog.owner) + ' · ' + (runtime.shared ? 'Shared server OS account: ' : 'Mapped OS account: ') + runtime.user; root.appendChild(scope);
20
+ if (runtime.shared) { var shared=document.createElement('p'); shared.className='settings-hint'; shared.textContent='Provider credentials are shared across Clay Studio accounts.'; root.appendChild(shared); }
21
+ var details=document.createElement('details'); details.className='settings-runtime-details settings-hint'; details.open=!!store.get('personalProviderRuntimeDetailsOpen');
22
+ var summary=document.createElement('summary'); summary.textContent='Runtime details';
23
+ var exact=document.createElement('p'); exact.className='settings-hint'; exact.dataset.providerRuntime=''; exact.textContent='OS account: '+runtime.user+' · UID '+runtime.uid+' · HOME '+runtime.home;
24
+ details.append(summary,exact); details.ontoggle=function () { if (details.isConnected && store.get('personalProviderCatalog') === catalog) store.set({personalProviderRuntimeDetailsOpen:details.open}); }; root.appendChild(details);
25
+ catalog.providers.forEach(function (provider) {
26
+ var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.codingProvider = provider.id;
27
+ var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
28
+ var logo = document.createElement('img'); logo.src = provider.icon; logo.alt = ''; logo.className = 'us-voice-provider-logo';
29
+ var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
30
+ var name = document.createElement('strong'); name.textContent = provider.name;
31
+ var state = document.createElement('small'); state.textContent = (provider.installed === true ? 'Installed' : provider.installed === false ? 'Not installed' : 'Installation unavailable') + ((store.get('personalProviderSignInResults') || {})[provider.id] ? ' · Sign-in reported success · Not tested' : ' · Authentication unknown · Not tested');
32
+ copy.append(name, state);
33
+ var manage = document.createElement('button'); manage.type = 'button'; manage.className = 'settings-btn-sm';
34
+ manage.textContent = provider.installed ? 'Manage' : provider.canInstall ? 'Install' : 'Setup instructions'; manage.setAttribute('aria-label', manage.textContent + ' ' + provider.name);
35
+ manage.setAttribute('aria-expanded', String(store.get('personalProviderEditing') === provider.id));
36
+ manage.setAttribute('aria-controls', 'provider-setup-' + provider.id);
37
+ manage.onclick = function () { store.set({personalProviderEditing:store.get('personalProviderEditing') === provider.id ? null : provider.id}); render(); var button = root.querySelector('[data-coding-provider="' + provider.id + '"] button'); if (button) button.focus(); };
38
+ controls.append(logo, copy, manage); row.appendChild(controls);
39
+ var editor = document.createElement('div'); editor.id = 'provider-setup-' + provider.id; editor.className = 'settings-card'; editor.hidden = store.get('personalProviderEditing') !== provider.id;
40
+ var detail = document.createElement('p'); detail.className = 'settings-hint'; detail.textContent = provider.reason || (provider.installed ? 'Runtime: '+runtime.user+' · HOME '+runtime.home+'. Sign-in uses this account. It can replace that OS account’s provider credentials. Sign-in does not send or replay any task. Provider logout and credential inspection are unavailable here.' : provider.setup); editor.appendChild(detail);
41
+ if (provider.reason && !provider.installed) { var setup = document.createElement('p'); setup.className = 'settings-hint'; setup.textContent = provider.setup; editor.appendChild(setup); }
42
+ if (!provider.installed && !provider.canInstall && provider.setupUrl) { var source=document.createElement('a'); source.className='settings-link'; source.href=provider.setupUrl; source.target='_blank'; source.rel='noopener noreferrer'; source.textContent='Official setup instructions'; editor.appendChild(source); }
43
+ renderProviderInstall(editor,provider,render);
44
+ if (provider.canSignIn) {
45
+ function signIn(mode) {
46
+ if (store.get('personalProviderSigningIn') || !store.get('personalProviderRevision') || store.get('personalProvidersLoading')) return;
47
+ var results=Object.assign({},store.get('personalProviderSignInResults') || {}); delete results[provider.id];
48
+ store.set({personalProviderSigningIn:provider.id,personalProvidersError:'',personalProvidersNotice:'',personalProviderSignInResults:results});
49
+ if (!requestVendorLogin(provider.id, {personalRequestId:store.get('personalProviderRevision'),mode:mode})) {
50
+ store.set({personalProviderSigningIn:null,personalProvidersError:'Reconnect the workspace and refresh status before signing in.'}); render();
51
+ }
52
+ render();
53
+ }
54
+ var actions = document.createElement('div'); actions.className = 'settings-form-actions';
55
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm'; button.textContent = 'Sign in'; button.disabled = !!store.get('personalProviderSigningIn') || !store.get('personalProviderRevision');
56
+ button.onclick = function () { signIn(provider.browser ? 'browser' : 'terminal'); }; actions.appendChild(button);
57
+ if (provider.browser) { var terminal = document.createElement('button'); terminal.type = 'button'; terminal.className = 'settings-btn-sm'; terminal.textContent = 'Use terminal sign-in'; terminal.disabled = !!store.get('personalProviderSigningIn') || !store.get('personalProviderRevision'); terminal.onclick = function () { signIn('terminal'); }; actions.appendChild(terminal); }
58
+ if (store.get('personalProviderSigningIn') === provider.id) { var cancel = document.createElement('button'); cancel.type = 'button'; cancel.className = 'settings-btn-sm'; cancel.textContent = 'Cancel sign-in'; cancel.onclick = function () { cancelVendorLogin(provider.id); store.set({personalProviderSigningIn:null}); render(); }; actions.appendChild(cancel); }
59
+ editor.appendChild(actions);
60
+ }
61
+ row.appendChild(editor); root.appendChild(row);
62
+ });
63
+ }
64
+ export function loadPersonalProviders() {
65
+ resetProviderInstall();
66
+ var ws = getWs();
67
+ var id = crypto.randomUUID();
68
+ store.set({personalProvidersOpen:true,personalProviderRequest:id,personalProviderContext:personalProviderContext(),personalProviderRevision:null,personalProvidersLoading:true,personalProvidersError:'',personalProvidersNotice:'',personalProviderSignInResults:{}});
69
+ if (!ws || ws.readyState !== 1) { store.set({personalProvidersLoading:false,personalProvidersError:'Connect to a project workspace to check provider installations and sign in.'}); render(); return; }
70
+ ws.send(JSON.stringify({type:'vendor_login_catalog_request', requestId:id})); render();
71
+ setTimeout(function () { if (store.get('personalProviderRequest') === id && store.get('personalProvidersLoading')) { store.set({personalProvidersLoading:false,personalProvidersError:'Provider status timed out. Reconnect, then refresh status.'}); render(); } },10000);
72
+ }
73
+ export function handlePersonalProviderMessage(message) {
74
+ if (handleProviderInstall(message,render)) return true;
75
+ if (message.type !== 'vendor_login_catalog') {
76
+ if (message.personalRequestId && store.get('personalProvidersOpen') && store.get('personalProviderContext') === personalProviderContext() && message.personalRequestId === store.get('personalProviderRevision') && ['vendor_login_ready','vendor_login_error','auth_refreshed'].indexOf(message.type) >= 0) {
77
+ var succeeded=message.type === 'auth_refreshed' && !message.error;
78
+ var results=Object.assign({},store.get('personalProviderSignInResults') || {});
79
+ if (succeeded) results[message.vendor]=true; else if (message.type === 'vendor_login_error') delete results[message.vendor];
80
+ store.set({personalProviderSigningIn:message.type === 'vendor_login_ready' ? message.vendor : null,personalProviderSignInResults:results,personalProvidersError:message.error || '',personalProvidersNotice:succeeded ? 'Sign-in reported success. Explicitly retry your task to test provider access.' : ''}); render();
81
+ }
82
+ if (message.type === 'vendor_login_state' && store.get('personalProviderSigningIn') && !(message.flows || []).some(function (flow) { return flow.personalRequestId === store.get('personalProviderRevision'); })) { store.set({personalProviderSigningIn:null}); render(); }
83
+ return false;
84
+ }
85
+ if (!store.get('personalProvidersOpen') || message.requestId !== store.get('personalProviderRequest') || store.get('personalProviderContext') !== personalProviderContext()) return true;
86
+ store.set({personalProvidersLoading:false,personalProvidersError:message.error || '',personalProviderCatalog:message.error ? null : message,personalProviderRevision:message.revision || null}); render(); return true;
87
+ }
88
+ export function leavePersonalProviders() {
89
+ if (!store.get('personalProvidersOpen')) return;
90
+ resetProviderInstall();
91
+ var signing = store.get('personalProviderSigningIn'); if (signing) cancelVendorLogin(signing);
92
+ var ws = getWs(); if (ws && ws.readyState === 1) ws.send(JSON.stringify({type:'vendor_login_personal_release'}));
93
+ store.set({personalProvidersOpen:false,personalProviderRequest:null,personalProviderRevision:null,personalProviderSigningIn:null,personalProvidersLoading:false});
94
+ }
95
+ export function initPersonalProviders() {
96
+ store.subscribe(function (state, previous) {
97
+ if (state.myUserId !== previous.myUserId || state.currentSlug !== previous.currentSlug || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration || state.connected === false && previous.connected !== false) {
98
+ leavePersonalProviders(); store.set({personalProviderCatalog:null,personalProviderEditing:null,personalProviderRuntimeDetailsOpen:false,personalProvidersNotice:'',personalProviderSignInResults:{},personalProvidersError:'Connection or account changed. Refresh status to continue.'}); render();
99
+ }
100
+ });
101
+ render();
102
+ }
@@ -3,73 +3,82 @@ import { saveSpeechSettings, loadSpeechSettings } from './stt-settings.js';
3
3
  import { confirmSettingsAction } from './server-settings-state.js';
4
4
 
5
5
  export function initPersonalVoice() {
6
- var root = document.getElementById('us-voice-settings');
7
- root.innerHTML = '<h3>Voice providers</h3><p class="settings-hint">Manage your personal API keys here. Choose the voice model, transcription mode and language from the microphone menu in the composer. Browser speech needs no API key.</p><div class="us-voice-providers" aria-label="Voice providers"></div><div id="us-voice-connection" hidden><form id="us-voice-key-form"><label class="settings-label" for="us-voice-key">API key</label><input id="us-voice-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096" required><p id="us-voice-privacy" class="settings-hint"></p><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save key</button><button type="button" id="us-voice-key-cancel" class="settings-btn-sm">Cancel</button><a id="us-voice-get-key" target="_blank" rel="noopener noreferrer">Get API key ↗</a></div></form><button type="button" id="us-voice-remove" class="settings-btn-sm">Remove saved key</button></div><p class="settings-status" role="status" id="us-voice-status"></p><button type="button" class="settings-btn-sm" id="us-voice-retry" hidden>Reload providers</button>';
8
- var list = root.querySelector('.us-voice-providers'); var editor = root.querySelector('#us-voice-connection');
9
- var form = root.querySelector('form'); var key = form.querySelector('input'); var status = root.querySelector('[role=status]');
10
- var remove = root.querySelector('#us-voice-remove'); var retry = root.querySelector('#us-voice-retry');
6
+ var root = document.getElementById('us-voice-settings'); if (!root) return;
7
+ root.innerHTML = '<p class="settings-hint">Audio transcription: convert microphone audio into text for voice input. Choose the model, mode and language in the microphone menu. Browser speech needs no API key.</p><h4>Your connections</h4><div id="us-api-connections"></div><p id="us-api-empty" class="settings-hint">No API connections yet. Choose a provider below to add an audio transcription key.</p><section class="us-integration-catalog"><h4>Available providers</h4><div class="us-integration-tiles"></div></section><div id="us-api-setup"></div><div id="us-voice-connection" hidden><form id="us-voice-key-form"><label class="settings-label" for="us-voice-key">API key</label><input id="us-voice-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096"><p id="us-voice-key-help" class="settings-hint"></p><p id="us-voice-privacy" class="settings-hint"></p><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save connection</button><button type="button" id="us-voice-key-cancel" class="settings-btn-sm">Cancel</button><button type="button" id="us-voice-remove" class="settings-btn-sm">Remove saved key</button><a id="us-voice-get-key" target="_blank" rel="noopener noreferrer">Get API key ↗</a></div></form></div><p class="settings-status" role="status" aria-live="polite" id="us-voice-status"></p><button type="button" class="settings-btn-sm" id="us-voice-retry" hidden>Reload providers</button>';
8
+ var list = root.querySelector('#us-api-connections'), tiles = root.querySelector('.us-integration-tiles');
9
+ var editor = root.querySelector('#us-voice-connection'), form = root.querySelector('form');
10
+ var key = form.querySelector('input'), status = root.querySelector('[role=status]');
11
+ var remove = root.querySelector('#us-voice-remove'), retry = root.querySelector('#us-voice-retry');
11
12
  function sync() {
12
- var settings = store.get('speechSettings'); var busy = !!store.get('speechSettingsSaving');
13
+ var settings = store.get('speechSettings'), busy = !!store.get('speechSettingsSaving');
14
+ root.appendChild(editor); retry.before(status); list.replaceChildren(); tiles.replaceChildren();
13
15
  if (settings) {
14
- var signature = settings.providers.map(function (provider) { return provider.id; }).join(',');
15
- if (list.dataset.catalog !== signature) {
16
- root.appendChild(editor); list.replaceChildren(); list.dataset.catalog = signature;
17
- settings.providers.forEach(function (provider) {
18
- var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.voiceProvider = provider.id;
19
- var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
20
- var image = document.createElement('img'); image.className = 'us-voice-provider-logo'; image.src = provider.icon; image.alt = '';
21
- var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
22
- var name = document.createElement('strong'); name.textContent = provider.name;
23
- var state = document.createElement('small'); state.className = 'us-voice-key-state'; copy.appendChild(name); copy.appendChild(state);
24
- var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm us-voice-key-action'; button.dataset.voiceKey = provider.id;
25
- button.setAttribute('aria-controls','us-voice-connection');
26
- button.onclick = function () { discardThen(function () { store.set({userVoiceKeyProvider:provider.id}); status.textContent = ''; sync(); key.focus(); }); };
27
- controls.appendChild(image); controls.appendChild(copy); controls.appendChild(button); row.appendChild(controls); list.appendChild(row);
28
- });
29
- }
30
- list.querySelectorAll('[data-voice-provider]').forEach(function (row) {
31
- var id = row.dataset.voiceProvider; var connected = settings.configured.indexOf(id) >= 0; var button = row.querySelector('[data-voice-key]');
32
- row.querySelector('.us-voice-key-state').textContent = connected ? 'Key saved' : 'Not connected';
33
- button.textContent = connected ? 'API key' : 'Connect'; button.disabled = busy;
34
- button.setAttribute('aria-label', (connected ? 'Manage ' : 'Connect ') + row.querySelector('strong').textContent + ' API key');
35
- button.setAttribute('aria-expanded',String(store.get('userVoiceKeyProvider') === id));
16
+ root.querySelector('#us-api-empty').hidden = !!settings.configured.length;
17
+ settings.providers.forEach(function (provider) {
18
+ var connected = settings.configured.indexOf(provider.id) >= 0;
19
+ var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.voiceProvider = provider.id;
20
+ var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
21
+ var image = document.createElement('img'); image.className = 'us-voice-provider-logo'; image.src = provider.icon; image.alt = '';
22
+ var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
23
+ var name = document.createElement('strong'); name.textContent = provider.name; copy.appendChild(name);
24
+ var button = document.createElement('button'); button.type = 'button'; button.className = connected ? 'settings-btn-sm' : 'us-integration-tile'; button.dataset.voiceKey = provider.id;
25
+ button.setAttribute('aria-label', (connected ? 'Manage ' : 'Add ') + provider.name + ' API key');
26
+ button.setAttribute('aria-controls','us-voice-connection'); button.setAttribute('aria-expanded',String(store.get('userVoiceKeyProvider') === provider.id)); button.disabled = busy;
27
+ button.onclick = function () { discardThen(function () { key.value = ''; store.set({userVoiceKeyProvider:provider.id,userVoiceDirty:false}); status.textContent = ''; sync(); key.focus(); }); };
28
+ if (connected) {
29
+ var purpose = document.createElement('small'); purpose.textContent = 'Audio transcription'; copy.appendChild(purpose);
30
+ var saved = document.createElement('span'); saved.className = 'us-integration-state'; saved.textContent = 'Key saved'; button.textContent = 'Manage';
31
+ controls.append(image,copy,saved,button); row.appendChild(controls); list.appendChild(row);
32
+ } else {
33
+ var title = document.createElement('span'); title.textContent = provider.name; button.append(image,title); tiles.appendChild(button);
34
+ }
36
35
  });
37
36
  var provider = settings.providers.find(function (item) { return item.id === store.get('userVoiceKeyProvider'); });
38
37
  editor.hidden = !provider;
39
38
  if (provider) {
40
39
  var connected = settings.configured.indexOf(provider.id) >= 0;
41
40
  var row = Array.from(list.children).find(function (item) { return item.dataset.voiceProvider === provider.id; });
42
- if (editor.parentElement !== row) row.appendChild(editor);
43
- if (status.parentElement !== editor) editor.appendChild(status);
41
+ (row || root.querySelector('#us-api-setup')).appendChild(editor); editor.appendChild(status);
44
42
  form.querySelector('label').textContent = provider.name + ' API key';
45
- key.placeholder = connected ? 'Enter a replacement key' : 'Enter API key'; remove.hidden = !connected;
46
- root.querySelector('#us-voice-privacy').textContent = 'Stored privately on your Clay server and shared by all ' + provider.name + ' voice models. Audio is sent to ' + provider.name + '; usage is billed to your account.';
43
+ key.required = !connected; key.placeholder = connected ? '•••••••• — saved key' : 'Enter API key'; remove.hidden = !connected;
44
+ root.querySelector('#us-voice-key-help').textContent = connected ? 'Leave blank to keep the saved key. Key saved does not verify provider access.' : 'Enter an API key for audio transcription. Saving does not verify provider access.';
45
+ root.querySelector('#us-voice-privacy').textContent = 'Stored privately on your Clay Studio server for your account and shared by all ' + provider.name + ' voice models. Audio is sent to ' + provider.name + '; usage is billed to your provider account.';
47
46
  root.querySelector('#us-voice-get-key').href = provider.url;
48
- } else { if (editor.parentElement !== root) root.appendChild(editor); retry.before(status); }
49
- } else list.textContent = 'Loading providers…';
47
+ }
48
+ } else { list.textContent = store.get('speechSettingsError') ? 'Provider collection unavailable.' : 'Loading providers…'; root.querySelector('#us-api-empty').hidden = true; editor.hidden = true; }
50
49
  editor.querySelectorAll('input,button').forEach(function (control) { control.disabled = busy || !settings; });
51
50
  retry.hidden = !store.get('speechSettingsError'); retry.disabled = busy;
52
- if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; }
51
+ if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; status.dataset.loadError = 'true'; }
52
+ else if (status.dataset.loadError) { status.textContent = ''; status.dataset.state = ''; delete status.dataset.loadError; }
53
53
  }
54
54
  function discardThen(action) {
55
55
  if (!key.value) { action(); return; }
56
- confirmSettingsAction('Discard the unsaved voice API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
56
+ confirmSettingsAction('Discard the unsaved audio transcription API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
57
57
  }
58
58
  function closeEditor() {
59
59
  var id = store.get('userVoiceKeyProvider'); key.value = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); sync();
60
60
  var button = root.querySelector('[data-voice-key="' + id + '"]'); if (button) button.focus();
61
61
  }
62
62
  async function save(value) {
63
- var provider = store.get('userVoiceKeyProvider'); if (!provider) return;
63
+ var provider = store.get('userVoiceKeyProvider'); if (!provider || store.get('speechSettingsSaving')) return;
64
+ var actor = store.get('myUserId'), generation = store.get('speechSettingsContextGeneration');
65
+ var configured = (store.get('speechSettings') || {}).configured || [];
66
+ if (value === '' && configured.indexOf(provider) >= 0) { closeEditor(); status.textContent = 'No key changes. Your voice model and language are unchanged.'; return; }
64
67
  status.textContent = 'Saving…'; status.dataset.state = '';
65
- try { await saveSpeechSettings({provider:provider,key:value}); closeEditor(); status.textContent = value === null ? 'Key removed. Reconnect this provider before using its voice models.' : 'Key saved. Your voice model and language are unchanged.'; }
66
- catch (error) { status.textContent = error.message; status.dataset.state = 'error'; sync(); }
68
+ try {
69
+ await saveSpeechSettings({provider:provider,key:value});
70
+ if (actor !== store.get('myUserId') || generation !== store.get('speechSettingsContextGeneration')) return;
71
+ closeEditor(); status.textContent = value === null ? 'Key removed. Reconnect this provider before using its voice models.' : 'Key saved. Your voice model and language are unchanged.';
72
+ } catch (error) { if (actor === store.get('myUserId') && generation === store.get('speechSettingsContextGeneration')) { status.textContent = error.message; status.dataset.state = 'error'; sync(); } }
67
73
  }
68
74
  key.oninput = function () { store.set({userVoiceDirty:!!key.value}); };
69
75
  form.onsubmit = function (event) { event.preventDefault(); save(key.value); };
70
76
  root.querySelector('#us-voice-key-cancel').onclick = function () { discardThen(function () { closeEditor(); status.textContent = ''; }); };
71
- remove.onclick = function () { confirmSettingsAction('Remove this provider’s saved voice key? Models using it will require reconnection.', 'Remove key', function () { save(null); }); };
77
+ remove.onclick = function () { confirmSettingsAction('Remove this provider’s saved audio transcription key? Models using it will require reconnection.', 'Remove key', function () { save(null); }); };
72
78
  retry.onclick = loadSpeechSettings;
73
- store.subscribe(function (state, previous) { if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError) sync(); });
79
+ store.subscribe(function (state, previous) {
80
+ if (state.speechSettingsContextGeneration !== previous.speechSettingsContextGeneration) { key.value = ''; status.textContent = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); }
81
+ if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError || state.speechSettingsContextGeneration !== previous.speechSettingsContextGeneration) sync();
82
+ });
74
83
  sync();
75
84
  }
@@ -81,7 +81,7 @@ export function openLoginBrowser(msg) {
81
81
  }
82
82
  cancel();
83
83
  if (isMcp) ws.send(JSON.stringify({ type: 'mcp_connection_action', action: 'signin', id: msg.connectionId }));
84
- else requestVendorLogin(msg.vendor, { mode: mode });
84
+ else requestVendorLogin(msg.vendor, { mode: mode, sessionId: msg.sessionId, workerAuthRequestId: msg.workerAuthRequestId, personalRequestId: msg.personalRequestId });
85
85
  }
86
86
  dialog.querySelector('[data-action="cancel"]').onclick = cancel;
87
87
  dialog.querySelector('[data-action="retry"]').onclick = function () { restart("browser"); };