clay-server 5.5.0-beta.2 → 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/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-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/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-SEMVZ4AY.js → app-ANJS2LSK.js} +1 -1
- package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-32YIYF7R.js} +1 -1
- package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-3EJ5IAHZ.js} +1 -1
- package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-3TIVBAKM.js} +1 -1
- package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-4QFNLHW5.js} +1 -1
- package/lib/public/pane-assets/{chunk-FK3YLGAP.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-QBCM7QEA.js → chunk-BOFNCPTN.js} +1 -1
- package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-CXWAJEN5.js} +1 -1
- package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-EWTXBZJO.js} +1 -1
- package/lib/public/pane-assets/{chunk-YR5GH3GI.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-E4VM2IDB.js → chunk-JM46WGDH.js} +1 -1
- package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
- package/lib/public/pane-assets/{chunk-K4PAN77B.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-QWQB2YRF.js → chunk-UGH5MCNK.js} +1 -1
- package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-V5DLPSLT.js} +1 -1
- package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
- package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-XSSTR6GC.js} +1 -1
- package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-XVESKMU3.js} +1 -1
- package/lib/public/pane-assets/manifest.json +394 -386
- 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-6AGSOAOD.js +0 -1
- package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
- package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
- package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
- package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
- package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
- package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
- package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
- package/lib/public/pane-assets/style-DZX6JAMF.css +0 -1
|
@@ -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"); };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { personalLoginCurrent } from './personal-provider-context.js';
|
|
1
2
|
// vendor-login.js - Vendor login (auth_required) recovery flow.
|
|
2
3
|
//
|
|
3
4
|
// The server owns the login terminal (see lib/project-vendor-login.js): exactly
|
|
@@ -16,6 +17,7 @@ import { refreshIcons, iconHtml } from './icons.js';
|
|
|
16
17
|
import { openTuiModal, closeTuiModal, getTuiModalTerminalId } from './tui-attention.js';
|
|
17
18
|
import { forwardPaneAuthRequired } from './pane-bridge.js';
|
|
18
19
|
import { openLoginBrowser, closeLoginBrowser } from './vendor-login-browser.js';
|
|
20
|
+
import { escapeHtml } from './utils.js';
|
|
19
21
|
|
|
20
22
|
// vendor -> { terminalId, startedAt } as last broadcast by the server.
|
|
21
23
|
var activeFlows = Object.create(null);
|
|
@@ -45,6 +47,17 @@ function currentProjectSlug() {
|
|
|
45
47
|
return "";
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
function workerContextMatches(target) {
|
|
51
|
+
return !!target && target.slug === store.get('currentSlug') && target.selectedSessionId === store.get('activeSessionId') &&
|
|
52
|
+
target.selectedOrigin === store.get('sessionOriginId') && target.socketGeneration === store.get('projectSplitViewSocketGeneration');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function forgetWorkerRequest(vendor) {
|
|
56
|
+
var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
|
|
57
|
+
delete requests[vendor];
|
|
58
|
+
store.set({ workerVendorLoginRequests: requests });
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
// The banner surface belongs to the notification center. Query it rather than
|
|
49
62
|
// importing app-notifications so the dependency stays one-directional
|
|
50
63
|
// (app-notifications -> vendor-login).
|
|
@@ -80,8 +93,8 @@ function showLoginBanner(opts) {
|
|
|
80
93
|
'<div class="notif-banner-icon">' + iconHtml(opts.icon || "check-circle") + '</div>' +
|
|
81
94
|
'<div class="notif-banner-body">' +
|
|
82
95
|
'<div class="notif-banner-project">CLAY</div>' +
|
|
83
|
-
'<div class="notif-banner-title">' + opts.title + '</div>' +
|
|
84
|
-
'<div class="notif-banner-text">' + opts.text + '</div>' +
|
|
96
|
+
'<div class="notif-banner-title">' + escapeHtml(opts.title) + '</div>' +
|
|
97
|
+
'<div class="notif-banner-text">' + escapeHtml(opts.text) + '</div>' +
|
|
85
98
|
'</div>' +
|
|
86
99
|
'<button class="notif-banner-close">' + iconHtml("x") + '</button>';
|
|
87
100
|
|
|
@@ -107,7 +120,7 @@ function showLoginStartedBanner(vendor) {
|
|
|
107
120
|
showLoginBanner({
|
|
108
121
|
icon: "check-circle",
|
|
109
122
|
title: vendorDisplayName(vendor) + " login started",
|
|
110
|
-
text: "Finish sign-in
|
|
123
|
+
text: "Finish sign-in below, then explicitly retry your task.",
|
|
111
124
|
onDismiss: function () { cancelVendorLogin(vendor); },
|
|
112
125
|
});
|
|
113
126
|
}
|
|
@@ -159,17 +172,28 @@ function closeModalForVendor(vendor, terminalId) {
|
|
|
159
172
|
export function requestVendorLogin(vendor, opts) {
|
|
160
173
|
var options = opts || {};
|
|
161
174
|
var target = vendor || "claude";
|
|
175
|
+
if (options.personalRequestId && !personalLoginCurrent({personalRequestId:options.personalRequestId})) return false;
|
|
162
176
|
if (options.auto && (activeFlows[target] || pendingStarts[target])) return false;
|
|
163
177
|
|
|
164
178
|
var ws = getWs();
|
|
165
179
|
if (!ws || ws.readyState !== 1) return false;
|
|
180
|
+
if (options.workerAuthRequestId) {
|
|
181
|
+
var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
|
|
182
|
+
requests[target] = { requestId: options.workerAuthRequestId, slug: store.get('currentSlug'),
|
|
183
|
+
selectedSessionId: store.get('activeSessionId'), selectedOrigin: store.get('sessionOriginId'),
|
|
184
|
+
socketGeneration: store.get('projectSplitViewSocketGeneration') };
|
|
185
|
+
store.set({ workerVendorLoginRequests: requests });
|
|
186
|
+
}
|
|
166
187
|
|
|
188
|
+
if (options.personalRequestId) store.set({personalProviderSigningIn:target});
|
|
167
189
|
pendingStarts[target] = true;
|
|
168
190
|
ws.send(JSON.stringify({
|
|
169
191
|
type: "vendor_login_start",
|
|
170
192
|
vendor: target,
|
|
171
193
|
auto: !!options.auto,
|
|
172
|
-
sessionId: options.sessionId || store.get('activeSessionId') || null,
|
|
194
|
+
sessionId: options.personalRequestId ? null : options.sessionId || store.get('activeSessionId') || null,
|
|
195
|
+
personalRequestId: options.personalRequestId || null,
|
|
196
|
+
workerAuthRequestId: options.workerAuthRequestId || null,
|
|
173
197
|
mode: options.mode || ((((store.get("vendorInfo") || {})[target] || {}).browserLoginSupported === true) ? "browser" : "terminal"),
|
|
174
198
|
}));
|
|
175
199
|
showLoginStartedBanner(target);
|
|
@@ -180,6 +204,7 @@ export function cancelVendorLogin(vendor) {
|
|
|
180
204
|
var target = vendor || modalVendor;
|
|
181
205
|
if (!target) return;
|
|
182
206
|
var flow = activeFlows[target];
|
|
207
|
+
forgetWorkerRequest(target);
|
|
183
208
|
var browser = store.get('vendorLoginBrowser');
|
|
184
209
|
var browserId = flow && flow.browserId || (browser && browser.vendor === target ? browser.browserId : null);
|
|
185
210
|
if (browser && browser.vendor === target) closeLoginBrowser();
|
|
@@ -189,7 +214,7 @@ export function cancelVendorLogin(vendor) {
|
|
|
189
214
|
if (modalVendor === target) closeModalInternally();
|
|
190
215
|
var ws = getWs();
|
|
191
216
|
if (ws && ws.readyState === 1) {
|
|
192
|
-
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId }));
|
|
217
|
+
ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId, workerAuthRequestId: flow && flow.workerAuthRequestId, personalRequestId: store.get('personalProviderSigningIn') === target ? store.get('personalProviderRevision') : null }));
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
@@ -205,12 +230,24 @@ export function autoStartLoginIfNeeded(msg) {
|
|
|
205
230
|
if (store.get('paneMode')) {
|
|
206
231
|
return forwardPaneAuthRequired({
|
|
207
232
|
vendor: msg.vendor || "claude",
|
|
208
|
-
sessionId: store.get('activeSessionId') || null,
|
|
233
|
+
sessionId: msg.sessionId || store.get('activeSessionId') || null,
|
|
234
|
+
workerAuthRequestId: msg.workerAuthRequestId || null,
|
|
235
|
+
loginCommand: msg.loginCommand,
|
|
236
|
+
canAutoLogin: msg.canAutoLogin,
|
|
209
237
|
});
|
|
210
238
|
}
|
|
239
|
+
if (msg.canAutoLogin === false || msg.loginCommand === '') {
|
|
240
|
+
showLoginErrorBanner(msg.vendor, 'Automatic sign-in is unavailable. Sign in manually on the server, then explicitly retry the task.');
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
211
243
|
var vendor = msg.vendor || "claude";
|
|
212
244
|
if (activeFlows[vendor] || pendingStarts[vendor]) return false;
|
|
213
|
-
return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null });
|
|
245
|
+
return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null, workerAuthRequestId: msg.workerAuthRequestId || null });
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function handleWorkerAuthRequired(msg) {
|
|
249
|
+
if (!msg || store.get('replayingHistory') || msg.slug !== store.get('currentSlug') || store.get('paneMode')) return false;
|
|
250
|
+
return autoStartLoginIfNeeded(msg);
|
|
214
251
|
}
|
|
215
252
|
|
|
216
253
|
// ========================================================
|
|
@@ -218,10 +255,12 @@ export function autoStartLoginIfNeeded(msg) {
|
|
|
218
255
|
// ========================================================
|
|
219
256
|
|
|
220
257
|
export function handleVendorLoginReady(msg) {
|
|
221
|
-
if (!msg || typeof msg.terminalId !== "number") return;
|
|
258
|
+
if (!msg || typeof msg.terminalId !== "number" || !personalLoginCurrent(msg)) return;
|
|
222
259
|
var vendor = msg.vendor || "claude";
|
|
260
|
+
var request = (store.get('workerVendorLoginRequests') || {})[vendor];
|
|
261
|
+
if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
|
|
223
262
|
delete pendingStarts[vendor];
|
|
224
|
-
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null };
|
|
263
|
+
activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null, workerAuthRequestId: msg.workerAuthRequestId || null };
|
|
225
264
|
if (msg.browserId) {
|
|
226
265
|
openLoginBrowser(msg);
|
|
227
266
|
return;
|
|
@@ -246,10 +285,12 @@ export function handleVendorLoginReady(msg) {
|
|
|
246
285
|
|
|
247
286
|
export function handleVendorLoginState(msg) {
|
|
248
287
|
if (!msg) return;
|
|
288
|
+
pendingStarts = Object.create(null);
|
|
249
289
|
var next = Object.create(null);
|
|
250
290
|
var flows = msg.flows || [];
|
|
251
291
|
for (var i = 0; i < flows.length; i++) {
|
|
252
|
-
|
|
292
|
+
if (!personalLoginCurrent(flows[i])) continue;
|
|
293
|
+
next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null, workerAuthRequestId: flows[i].workerAuthRequestId || null };
|
|
253
294
|
delete pendingStarts[flows[i].vendor];
|
|
254
295
|
}
|
|
255
296
|
|
|
@@ -271,8 +312,11 @@ export function handleVendorLoginState(msg) {
|
|
|
271
312
|
}
|
|
272
313
|
|
|
273
314
|
export function handleAuthRefreshed(msg) {
|
|
274
|
-
if (!msg) return;
|
|
315
|
+
if (!msg || !personalLoginCurrent(msg)) return;
|
|
275
316
|
var vendor = msg.vendor || "claude";
|
|
317
|
+
var request = (store.get('workerVendorLoginRequests') || {})[vendor];
|
|
318
|
+
if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
|
|
319
|
+
forgetWorkerRequest(vendor);
|
|
276
320
|
var browser = store.get('vendorLoginBrowser');
|
|
277
321
|
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
278
322
|
var flow = activeFlows[vendor];
|
|
@@ -284,8 +328,9 @@ export function handleAuthRefreshed(msg) {
|
|
|
284
328
|
}
|
|
285
329
|
|
|
286
330
|
export function handleVendorLoginError(msg) {
|
|
287
|
-
if (!msg) return;
|
|
331
|
+
if (!msg || !personalLoginCurrent(msg)) return;
|
|
288
332
|
var vendor = msg.vendor || "claude";
|
|
333
|
+
forgetWorkerRequest(vendor);
|
|
289
334
|
var browser = store.get('vendorLoginBrowser');
|
|
290
335
|
if (browser && browser.vendor === vendor) closeLoginBrowser();
|
|
291
336
|
delete pendingStarts[vendor];
|
|
@@ -304,3 +349,18 @@ export function requestVendorLoginState() {
|
|
|
304
349
|
export function getActiveLoginFlow(vendor) {
|
|
305
350
|
return activeFlows[vendor] || null;
|
|
306
351
|
}
|
|
352
|
+
|
|
353
|
+
store.subscribe(function (state, previous) {
|
|
354
|
+
var requests = state.workerVendorLoginRequests || {};
|
|
355
|
+
Object.keys(requests).forEach(function (vendor) {
|
|
356
|
+
if (workerContextMatches(requests[vendor]) && state.connected !== false) return;
|
|
357
|
+
if (requests[vendor].slug === state.currentSlug && requests[vendor].socketGeneration === state.projectSplitViewSocketGeneration) cancelVendorLogin(vendor);
|
|
358
|
+
else forgetWorkerRequest(vendor);
|
|
359
|
+
});
|
|
360
|
+
if (state.connected !== false || previous.connected === false) return;
|
|
361
|
+
pendingStarts = Object.create(null);
|
|
362
|
+
activeFlows = Object.create(null);
|
|
363
|
+
closeLoginBrowser();
|
|
364
|
+
closeModalInternally();
|
|
365
|
+
clearLoginBanners();
|
|
366
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { iconHtml } from './icons.js';
|
|
3
|
+
|
|
4
|
+
export function validActivity(value) {
|
|
5
|
+
return !!(value && typeof value.epoch === 'string' && value.epoch.length <= 64 && Number.isSafeInteger(value.sequence) && value.sequence > 0 &&
|
|
6
|
+
Number.isSafeInteger(value.queryGeneration) && value.queryGeneration >= 0 && Number.isFinite(value.timestamp) && value.timestamp > 0 &&
|
|
7
|
+
typeof value.summary === 'string' && value.summary.length <= 80);
|
|
8
|
+
}
|
|
9
|
+
export function observeWorkerActivity(key, activity, live) {
|
|
10
|
+
var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
|
|
11
|
+
if (activity && !validActivity(activity)) return null;
|
|
12
|
+
if (previous && activity && previous.cursor && activity.epoch === previous.cursor.epoch &&
|
|
13
|
+
(activity.sequence <= previous.cursor.sequence || activity.queryGeneration < previous.cursor.queryGeneration)) return null;
|
|
14
|
+
// Initial subscriptions/reconnects establish a baseline; never animate cached activity.
|
|
15
|
+
var fresh = !!(live && previous && activity);
|
|
16
|
+
if (previous && previous.activity && activity && previous.activity.epoch !== activity.epoch) fresh = false;
|
|
17
|
+
var generationChanged = previous && previous.activity && (!activity || previous.activity.queryGeneration !== activity.queryGeneration);
|
|
18
|
+
var angle = previous && !generationChanged ? previous.angle : 0, now = performance.now();
|
|
19
|
+
var nextStepAt = previous && !generationChanged ? previous.nextStepAt : 0;
|
|
20
|
+
if (fresh && now >= nextStepAt) { angle += 30; nextStepAt = now + 350; }
|
|
21
|
+
all[key] = { processing: previous && previous.processing, fresh: fresh, cursor: activity || previous && previous.cursor || null, activity: activity || null, angle: angle, nextStepAt: nextStepAt };
|
|
22
|
+
store.set({ workerActivityIndicators: all });
|
|
23
|
+
return fresh;
|
|
24
|
+
}
|
|
25
|
+
export function setWorkerActivityRuntime(key, processing) {
|
|
26
|
+
var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
|
|
27
|
+
if (!previous || typeof processing !== 'boolean' || previous.processing === processing) return;
|
|
28
|
+
all[key] = Object.assign({}, previous, { processing: processing }); store.set({ workerActivityIndicators: all });
|
|
29
|
+
}
|
|
30
|
+
export function syncWorkerActivityIndicator(chip, worker) {
|
|
31
|
+
var icon = chip.querySelector('.split-view-worker-loop'); if (!icon) return;
|
|
32
|
+
var state = (store.get('workerActivityIndicators') || {})[chip.dataset.messagePreview];
|
|
33
|
+
var angle = state ? state.angle : 0;
|
|
34
|
+
var scope = chip.dataset.messagePreview + ':' + (state && state.activity ? state.activity.epoch + ':' + state.activity.queryGeneration : 'unknown');
|
|
35
|
+
if (icon.dataset.activityScope !== scope) {
|
|
36
|
+
var initial = icon.dataset.activityScope == null ? angle : state && state.fresh ? 0 : angle;
|
|
37
|
+
icon.style.transition = 'none'; icon.style.transform = 'rotate(' + initial + 'deg)';
|
|
38
|
+
void icon.offsetWidth;
|
|
39
|
+
icon.style.transition = ''; icon.dataset.activityScope = scope;
|
|
40
|
+
}
|
|
41
|
+
icon.style.transform = 'rotate(' + angle + 'deg)';
|
|
42
|
+
var processing = state && typeof state.processing === 'boolean' ? state.processing : worker && worker.isProcessing;
|
|
43
|
+
icon.dataset.state = !store.get('connected') ? 'disconnected' : processing ? 'working' : 'ready';
|
|
44
|
+
icon.setAttribute('aria-label', !store.get('connected') ? 'Disconnected' : processing ? 'Worker running; motion indicates received activity' : 'Worker idle');
|
|
45
|
+
}
|
|
46
|
+
export function createWorkerActivityIndicator() {
|
|
47
|
+
var icon = document.createElement('span'); icon.className = 'split-view-worker-loop';
|
|
48
|
+
icon.innerHTML = iconHtml('refresh-cw'); icon.setAttribute('role', 'img'); return icon;
|
|
49
|
+
}
|
|
@@ -1,39 +1,75 @@
|
|
|
1
1
|
import { store } from './store.js';
|
|
2
2
|
import { getWs } from './ws-ref.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getCachedSessions } from './sidebar-sessions.js';
|
|
4
|
+
import { bindAgentMessageBubble, messagePreviewKey, rememberAgentMessage, rememberAgentActivity, forgetAgentMessage } from './agent-message-bubbles.js';
|
|
5
|
+
import { observeWorkerActivity, syncWorkerActivityIndicator, setWorkerActivityRuntime } from './worker-activity-indicator.js';
|
|
4
6
|
|
|
5
7
|
var scheduled = false;
|
|
8
|
+
function key(target) { return messagePreviewKey('worker', [target.parentSessionOriginId, target.workerSessionOriginId, target.generation]); }
|
|
9
|
+
function exactWorker(target) {
|
|
10
|
+
var list = getCachedSessions() || [];
|
|
11
|
+
var driver = list.find(function (item) { return item.sessionOriginId === target.parentSessionOriginId && item.sessionRole === 'driver' && (item.ownerId || null) === (store.get('myUserId') || null); });
|
|
12
|
+
return driver && list.find(function (item) { return item.sessionOriginId === target.workerSessionOriginId && item.sessionRole === 'worker' &&
|
|
13
|
+
item.parentAvailable && item.parentSessionId === driver.id && item.workerGeneration === target.generation && !item.hidden && (item.ownerId || null) === (driver.ownerId || null); });
|
|
14
|
+
}
|
|
6
15
|
function targets() {
|
|
7
16
|
var found = [];
|
|
17
|
+
if (document.hidden) return found;
|
|
8
18
|
document.querySelectorAll('.split-view-worker-chip[data-worker-parent]').forEach(function (chip) {
|
|
9
19
|
if (!chip.getClientRects().length || chip.closest('[hidden], .split-pane-cached')) return;
|
|
10
20
|
var target = { parentSessionOriginId: chip.dataset.workerParent, workerSessionOriginId: chip.dataset.workerOrigin, generation: Number(chip.dataset.workerGeneration) };
|
|
11
|
-
|
|
21
|
+
var worker = exactWorker(target); if (!worker) return;
|
|
22
|
+
bindAgentMessageBubble(chip, key(target)); syncWorkerActivityIndicator(chip, worker);
|
|
12
23
|
found.push(target);
|
|
13
24
|
});
|
|
14
25
|
return found.slice(0, 32);
|
|
15
26
|
}
|
|
27
|
+
function cleanup(list) {
|
|
28
|
+
var keep = new Set(list.map(key)), all = Object.assign({}, store.get('workerActivityIndicators') || {}), changed = false;
|
|
29
|
+
Object.keys(all).forEach(function (id) { if (!keep.has(id)) { delete all[id]; forgetAgentMessage(id); changed = true; } });
|
|
30
|
+
if (changed) store.set({ workerActivityIndicators: all });
|
|
31
|
+
}
|
|
16
32
|
export function syncWorkerMessagePreviews() {
|
|
17
33
|
if (scheduled || store.get('paneMode')) return;
|
|
18
34
|
scheduled = true;
|
|
19
35
|
requestAnimationFrame(function () {
|
|
20
|
-
scheduled = false; var socket = getWs()
|
|
21
|
-
var list = targets()
|
|
22
|
-
|
|
23
|
-
var
|
|
36
|
+
scheduled = false; var socket = getWs(), connected = store.get('connected') && socket && socket.readyState === 1;
|
|
37
|
+
var list = connected ? targets() : []; cleanup(list);
|
|
38
|
+
if (!connected) { store.set({ workerMessagePreviewSubscription: null }); return; }
|
|
39
|
+
var signature = JSON.stringify([store.get('currentSlug'), store.get('myUserId'), store.get('projectSplitViewSocketGeneration'), list]);
|
|
40
|
+
var previous = store.get('workerMessagePreviewSubscription'); if (previous && previous.signature === signature && previous.socket === socket) return;
|
|
41
|
+
var requestId = crypto.randomUUID();
|
|
42
|
+
store.set({ workerMessagePreviewSubscription: { requestId: requestId, signature: signature, targets: list, socket: socket,
|
|
43
|
+
slug: store.get('currentSlug'), owner: store.get('myUserId'), socketGeneration: store.get('projectSplitViewSocketGeneration') } });
|
|
24
44
|
socket.send(JSON.stringify({ type: 'split_worker_preview_request', requestId: requestId, targets: list }));
|
|
25
45
|
});
|
|
26
46
|
}
|
|
27
47
|
export function handleWorkerMessagePreview(message) {
|
|
28
|
-
var state = store.get('workerMessagePreviewSubscription');
|
|
48
|
+
var state = store.get('workerMessagePreviewSubscription');
|
|
49
|
+
if (!state || !store.get('connected') || document.hidden || message.requestId !== state.requestId || state.socket !== getWs() ||
|
|
50
|
+
state.slug !== store.get('currentSlug') || state.owner !== store.get('myUserId') || state.socketGeneration !== store.get('projectSplitViewSocketGeneration')) return;
|
|
29
51
|
if (!state.targets.some(function (target) { return target.parentSessionOriginId === message.parentSessionOriginId && target.workerSessionOriginId === message.workerSessionOriginId && target.generation === message.generation; })) return;
|
|
30
|
-
var
|
|
31
|
-
|
|
32
|
-
|
|
52
|
+
var worker = exactWorker(message); if (!worker) return;
|
|
53
|
+
var chips = Array.from(document.querySelectorAll('.split-view-worker-chip[data-worker-parent]')).filter(function (chip) {
|
|
54
|
+
return chip.dataset.workerParent === message.parentSessionOriginId && chip.dataset.workerOrigin === message.workerSessionOriginId &&
|
|
55
|
+
Number(chip.dataset.workerGeneration) === message.generation && chip.getClientRects().length && !chip.closest('[hidden], .split-pane-cached');
|
|
56
|
+
});
|
|
57
|
+
if (!chips.length) return;
|
|
58
|
+
var id = key(message), observed = observeWorkerActivity(id, message.activity || null, message.live === true);
|
|
59
|
+
var current = (store.get('workerActivityIndicators') || {})[id];
|
|
60
|
+
if (observed !== null || current && current.cursor && message.activity && current.cursor.epoch === message.activity.epoch && current.cursor.sequence === message.activity.sequence && current.cursor.queryGeneration === message.activity.queryGeneration) setWorkerActivityRuntime(id, message.isProcessing);
|
|
61
|
+
if (observed !== null) rememberAgentActivity(id, message.activity || null, message.vendor, message.observedAt);
|
|
62
|
+
rememberAgentMessage(id, message.preview, message.live === true);
|
|
63
|
+
chips.forEach(function (chip) { syncWorkerActivityIndicator(chip, Object.assign({}, worker, { isProcessing: typeof message.isProcessing === 'boolean' ? message.isProcessing : worker.isProcessing })); });
|
|
33
64
|
}
|
|
34
65
|
store.subscribe(function (state, previous) {
|
|
35
|
-
if (state.
|
|
66
|
+
if (state.paneBindingSessions !== previous.paneBindingSessions && state.connected) targets().forEach(function (target) {
|
|
67
|
+
var worker = exactWorker(target); if (worker) setWorkerActivityRuntime(key(target), worker.isProcessing === true);
|
|
68
|
+
});
|
|
69
|
+
if (state.splitPanes !== previous.splitPanes || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId || state.paneBindingSessions !== previous.paneBindingSessions ||
|
|
70
|
+
state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.connected !== previous.connected || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration) syncWorkerMessagePreviews();
|
|
36
71
|
});
|
|
37
72
|
var observer = new MutationObserver(syncWorkerMessagePreviews);
|
|
38
73
|
function observe() { var root = document.getElementById('split-host'); if (root) observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden'] }); }
|
|
39
74
|
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', observe); else observe();
|
|
75
|
+
document.addEventListener('visibilitychange', syncWorkerMessagePreviews);
|