clay-server 5.5.0-beta.1 → 5.5.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/clay-product-control-input.js +2 -2
- package/lib/clay-product-control-tools.js +1 -1
- package/lib/clay-product-control.js +4 -3
- package/lib/daemon-project-access.js +10 -2
- package/lib/daemon-projects.js +16 -4
- package/lib/daemon-worktree-controls.js +171 -0
- package/lib/daemon.js +14 -63
- package/lib/personal-provider-install.js +85 -0
- package/lib/personal-provider-settings.js +94 -0
- package/lib/project-logs-service.js +2 -2
- package/lib/project-pair-task-control.js +1 -1
- package/lib/project-pending-message-queue.js +6 -2
- package/lib/project-product-control.js +1 -0
- package/lib/project-session-actions.js +46 -3
- package/lib/project-session-pair.js +2 -0
- package/lib/project-sessions.js +11 -3
- package/lib/project-user-message.js +5 -1
- package/lib/project-vendor-login.js +95 -32
- package/lib/project-worker-auth.js +71 -0
- package/lib/project-worker-conversation.js +11 -2
- package/lib/project.js +9 -0
- package/lib/public/app.js +22 -0
- package/lib/public/css/icon-strip.css +5 -0
- package/lib/public/css/overlays.css +18 -0
- package/lib/public/css/settings-shell.css +14 -0
- package/lib/public/css/settings-ui.css +6 -0
- package/lib/public/css/split-view-layout.css +16 -11
- package/lib/public/modules/agent-message-bubbles.js +33 -4
- package/lib/public/modules/app-messages.js +7 -1
- package/lib/public/modules/app-projects.js +3 -4
- package/lib/public/modules/clay-control-copy.js +1 -0
- package/lib/public/modules/clay-tool-presentation.js +1 -1
- package/lib/public/modules/generation-stop.js +4 -4
- package/lib/public/modules/pane-activation.js +2 -2
- package/lib/public/modules/pane-bridge.js +4 -2
- package/lib/public/modules/personal-provider-context.js +11 -0
- package/lib/public/modules/project-settings.js +1 -1
- package/lib/public/modules/settings-section-nav.js +58 -0
- package/lib/public/modules/settings-shell-registry.js +4 -2
- package/lib/public/modules/settings-shell.js +17 -6
- package/lib/public/modules/sidebar-projects.js +23 -9
- package/lib/public/modules/sidebar-sessions.js +3 -0
- package/lib/public/modules/split-pane-force-stop.js +47 -0
- package/lib/public/modules/split-pane-header-state.js +11 -1
- package/lib/public/modules/split-pane-renderer.js +2 -2
- package/lib/public/modules/split-view-arrange-menu.js +38 -0
- package/lib/public/modules/split-view-create-menu.js +9 -5
- package/lib/public/modules/split-view-member-actions.js +42 -0
- package/lib/public/modules/split-view-picker-actions.js +2 -2
- package/lib/public/modules/split-view-presentation-renderer.js +10 -35
- package/lib/public/modules/split-view-presentations.js +9 -8
- package/lib/public/modules/split-view-sidebar-menu.js +2 -1
- package/lib/public/modules/split-view-workers.js +32 -17
- package/lib/public/modules/stt-settings.js +21 -4
- package/lib/public/modules/user-settings-layout.js +11 -8
- package/lib/public/modules/user-settings-provider-install.js +42 -0
- package/lib/public/modules/user-settings-providers.js +102 -0
- package/lib/public/modules/user-settings-voice.js +50 -41
- package/lib/public/modules/vendor-login-browser.js +1 -1
- package/lib/public/modules/vendor-login.js +72 -12
- package/lib/public/modules/view-workspace-state.js +1 -1
- package/lib/public/modules/worker-activity-indicator.js +49 -0
- package/lib/public/modules/worker-message-previews.js +47 -11
- package/lib/public/modules/worker-stop-controls.js +36 -0
- package/lib/public/modules/worktree-controls.js +86 -0
- package/lib/public/pane-assets/{app-4IZ6F6ND.js → app-ANJS2LSK.js} +1 -1
- package/lib/public/pane-assets/{chunk-L3FCTD7U.js → chunk-32YIYF7R.js} +1 -1
- package/lib/public/pane-assets/{chunk-RRYWV5JT.js → chunk-3EJ5IAHZ.js} +1 -1
- package/lib/public/pane-assets/{chunk-3E6LDNXF.js → chunk-3TIVBAKM.js} +1 -1
- package/lib/public/pane-assets/{chunk-I6X5J2V5.js → chunk-4QFNLHW5.js} +1 -1
- package/lib/public/pane-assets/{chunk-OB3KKICE.js → chunk-5XMZMIFG.js} +1 -1
- package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
- package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
- package/lib/public/pane-assets/{chunk-A7OB7HQS.js → chunk-BOFNCPTN.js} +1 -1
- package/lib/public/pane-assets/{chunk-ELL4UBGY.js → chunk-CXWAJEN5.js} +1 -1
- package/lib/public/pane-assets/{chunk-SEELA2D2.js → chunk-EWTXBZJO.js} +1 -1
- package/lib/public/pane-assets/{chunk-ADGKPVFD.js → chunk-G35ZOJGP.js} +1 -1
- package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
- package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
- package/lib/public/pane-assets/{chunk-6ZF2FJT3.js → chunk-JM46WGDH.js} +1 -1
- package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
- package/lib/public/pane-assets/{chunk-YXJ7ISEK.js → chunk-P3AK4ORJ.js} +1 -1
- package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
- package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
- package/lib/public/pane-assets/{chunk-YWHBNEB2.js → chunk-UGH5MCNK.js} +1 -1
- package/lib/public/pane-assets/{chunk-YFUORWA6.js → chunk-V5DLPSLT.js} +1 -1
- package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
- package/lib/public/pane-assets/{chunk-A4MLSYBJ.js → chunk-XSSTR6GC.js} +1 -1
- package/lib/public/pane-assets/{chunk-DJ5WXQKT.js → chunk-XVESKMU3.js} +1 -1
- package/lib/public/pane-assets/manifest.json +443 -433
- package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
- package/lib/sdk-bridge.js +40 -6
- package/lib/sdk-force-stop.js +62 -0
- package/lib/sdk-message-processor.js +2 -1
- package/lib/server-worktree-controls.js +34 -0
- package/lib/server.js +11 -1
- package/lib/session-force-stop-support.js +15 -0
- package/lib/sessions.js +6 -1
- package/lib/terminal-manager.js +1 -1
- package/lib/terminal.js +13 -1
- package/lib/vendor-login-browser-flow.js +1 -1
- package/lib/worker-activity.js +26 -0
- package/lib/worker-message-previews.js +12 -7
- package/lib/worktree-branch.js +57 -0
- package/lib/worktree-identity.js +39 -0
- package/lib/worktree-preferences.js +25 -0
- package/lib/worktree-ref-transaction.js +36 -0
- package/lib/yoke/adapters/antigravity.js +3 -0
- package/lib/yoke/force-termination.js +26 -0
- package/lib/yoke/interface.js +14 -0
- package/lib/yoke/process-force-termination.js +24 -0
- package/lib/yoke/provider-install-recipes.js +20 -0
- package/lib/yoke/vendor-registry.js +20 -1
- package/package.json +1 -1
- package/lib/public/pane-assets/chunk-6KO36HMU.js +0 -1
- package/lib/public/pane-assets/chunk-FILRACQB.js +0 -168
- package/lib/public/pane-assets/chunk-H6MU72QE.js +0 -1
- package/lib/public/pane-assets/chunk-PJURCIJQ.js +0 -1
- package/lib/public/pane-assets/chunk-Q7RFSS5Q.js +0 -1
- package/lib/public/pane-assets/chunk-UTLQWAKG.js +0 -1
- package/lib/public/pane-assets/chunk-WE4NWXNK.js +0 -1
- package/lib/public/pane-assets/chunk-Z2MHS43B.js +0 -1
- package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
|
@@ -1,3 +1,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
|
-
|
|
335
|
-
|
|
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
|
-
|
|
363
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
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');
|
|
24
|
-
var
|
|
25
|
-
|
|
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'));
|
|
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-
|
|
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-
|
|
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 = '<
|
|
8
|
-
var list = root.querySelector('
|
|
9
|
-
var
|
|
10
|
-
var
|
|
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')
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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 ? '
|
|
46
|
-
root.querySelector('#us-voice-
|
|
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
|
-
}
|
|
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
|
|
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 {
|
|
66
|
-
|
|
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
|
|
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) {
|
|
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"); };
|