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.
Files changed (114) hide show
  1. package/lib/clay-product-control-input.js +2 -2
  2. package/lib/clay-product-control-tools.js +1 -1
  3. package/lib/clay-product-control.js +4 -3
  4. package/lib/daemon-project-access.js +10 -2
  5. package/lib/daemon-projects.js +16 -4
  6. package/lib/daemon-worktree-controls.js +171 -0
  7. package/lib/daemon.js +14 -63
  8. package/lib/personal-provider-install.js +85 -0
  9. package/lib/personal-provider-settings.js +94 -0
  10. package/lib/project-logs-service.js +2 -2
  11. package/lib/project-pair-task-control.js +1 -1
  12. package/lib/project-pending-message-queue.js +6 -2
  13. package/lib/project-product-control.js +1 -0
  14. package/lib/project-session-actions.js +46 -3
  15. package/lib/project-session-pair.js +2 -0
  16. package/lib/project-sessions.js +11 -3
  17. package/lib/project-user-message.js +5 -1
  18. package/lib/project-vendor-login.js +95 -32
  19. package/lib/project-worker-auth.js +71 -0
  20. package/lib/project-worker-conversation.js +11 -2
  21. package/lib/project.js +9 -0
  22. package/lib/public/app.js +22 -0
  23. package/lib/public/css/icon-strip.css +5 -0
  24. package/lib/public/css/overlays.css +18 -0
  25. package/lib/public/css/settings-shell.css +14 -0
  26. package/lib/public/css/settings-ui.css +6 -0
  27. package/lib/public/css/split-view-layout.css +16 -11
  28. package/lib/public/modules/agent-message-bubbles.js +33 -4
  29. package/lib/public/modules/app-messages.js +7 -1
  30. package/lib/public/modules/app-projects.js +3 -4
  31. package/lib/public/modules/clay-control-copy.js +1 -0
  32. package/lib/public/modules/clay-tool-presentation.js +1 -1
  33. package/lib/public/modules/generation-stop.js +4 -4
  34. package/lib/public/modules/pane-activation.js +2 -2
  35. package/lib/public/modules/pane-bridge.js +4 -2
  36. package/lib/public/modules/personal-provider-context.js +11 -0
  37. package/lib/public/modules/project-settings.js +1 -1
  38. package/lib/public/modules/settings-section-nav.js +58 -0
  39. package/lib/public/modules/settings-shell-registry.js +4 -2
  40. package/lib/public/modules/settings-shell.js +17 -6
  41. package/lib/public/modules/sidebar-projects.js +23 -9
  42. package/lib/public/modules/split-pane-force-stop.js +47 -0
  43. package/lib/public/modules/split-pane-header-state.js +11 -1
  44. package/lib/public/modules/split-pane-renderer.js +2 -2
  45. package/lib/public/modules/split-view-workers.js +32 -17
  46. package/lib/public/modules/stt-settings.js +21 -4
  47. package/lib/public/modules/user-settings-layout.js +11 -8
  48. package/lib/public/modules/user-settings-provider-install.js +42 -0
  49. package/lib/public/modules/user-settings-providers.js +102 -0
  50. package/lib/public/modules/user-settings-voice.js +50 -41
  51. package/lib/public/modules/vendor-login-browser.js +1 -1
  52. package/lib/public/modules/vendor-login.js +72 -12
  53. package/lib/public/modules/worker-activity-indicator.js +49 -0
  54. package/lib/public/modules/worker-message-previews.js +47 -11
  55. package/lib/public/modules/worker-stop-controls.js +36 -0
  56. package/lib/public/modules/worktree-controls.js +86 -0
  57. package/lib/public/pane-assets/{app-SEMVZ4AY.js → app-ANJS2LSK.js} +1 -1
  58. package/lib/public/pane-assets/{chunk-4YE4RU27.js → chunk-32YIYF7R.js} +1 -1
  59. package/lib/public/pane-assets/{chunk-BNNPJJVG.js → chunk-3EJ5IAHZ.js} +1 -1
  60. package/lib/public/pane-assets/{chunk-C6P7PICY.js → chunk-3TIVBAKM.js} +1 -1
  61. package/lib/public/pane-assets/{chunk-XYZGVYWW.js → chunk-4QFNLHW5.js} +1 -1
  62. package/lib/public/pane-assets/{chunk-FK3YLGAP.js → chunk-5XMZMIFG.js} +1 -1
  63. package/lib/public/pane-assets/chunk-6S2C7Q2N.js +1 -0
  64. package/lib/public/pane-assets/chunk-7B2GJI57.js +1 -0
  65. package/lib/public/pane-assets/{chunk-QBCM7QEA.js → chunk-BOFNCPTN.js} +1 -1
  66. package/lib/public/pane-assets/{chunk-CQNPEH2M.js → chunk-CXWAJEN5.js} +1 -1
  67. package/lib/public/pane-assets/{chunk-5UBN6GR7.js → chunk-EWTXBZJO.js} +1 -1
  68. package/lib/public/pane-assets/{chunk-YR5GH3GI.js → chunk-G35ZOJGP.js} +1 -1
  69. package/lib/public/pane-assets/chunk-HYT24UBT.js +1 -0
  70. package/lib/public/pane-assets/chunk-ISQEPZMY.js +1 -0
  71. package/lib/public/pane-assets/{chunk-E4VM2IDB.js → chunk-JM46WGDH.js} +1 -1
  72. package/lib/public/pane-assets/chunk-LBJG4MK2.js +174 -0
  73. package/lib/public/pane-assets/{chunk-K4PAN77B.js → chunk-P3AK4ORJ.js} +1 -1
  74. package/lib/public/pane-assets/chunk-QKPGE7DN.js +1 -0
  75. package/lib/public/pane-assets/chunk-TS5JUEVH.js +1 -0
  76. package/lib/public/pane-assets/{chunk-QWQB2YRF.js → chunk-UGH5MCNK.js} +1 -1
  77. package/lib/public/pane-assets/{chunk-YVVT3CTV.js → chunk-V5DLPSLT.js} +1 -1
  78. package/lib/public/pane-assets/chunk-VFKDU64W.js +1 -0
  79. package/lib/public/pane-assets/{chunk-U7EHYRJZ.js → chunk-XSSTR6GC.js} +1 -1
  80. package/lib/public/pane-assets/{chunk-M5CWIIIT.js → chunk-XVESKMU3.js} +1 -1
  81. package/lib/public/pane-assets/manifest.json +394 -386
  82. package/lib/public/pane-assets/style-VBVCWV44.css +1 -0
  83. package/lib/sdk-bridge.js +40 -6
  84. package/lib/sdk-force-stop.js +62 -0
  85. package/lib/sdk-message-processor.js +2 -1
  86. package/lib/server-worktree-controls.js +34 -0
  87. package/lib/server.js +11 -1
  88. package/lib/session-force-stop-support.js +15 -0
  89. package/lib/sessions.js +6 -1
  90. package/lib/terminal-manager.js +1 -1
  91. package/lib/terminal.js +13 -1
  92. package/lib/vendor-login-browser-flow.js +1 -1
  93. package/lib/worker-activity.js +26 -0
  94. package/lib/worker-message-previews.js +12 -7
  95. package/lib/worktree-branch.js +57 -0
  96. package/lib/worktree-identity.js +39 -0
  97. package/lib/worktree-preferences.js +25 -0
  98. package/lib/worktree-ref-transaction.js +36 -0
  99. package/lib/yoke/adapters/antigravity.js +3 -0
  100. package/lib/yoke/force-termination.js +26 -0
  101. package/lib/yoke/interface.js +14 -0
  102. package/lib/yoke/process-force-termination.js +24 -0
  103. package/lib/yoke/provider-install-recipes.js +20 -0
  104. package/lib/yoke/vendor-registry.js +20 -1
  105. package/package.json +1 -1
  106. package/lib/public/pane-assets/chunk-6AGSOAOD.js +0 -1
  107. package/lib/public/pane-assets/chunk-AATAIAXL.js +0 -1
  108. package/lib/public/pane-assets/chunk-D3CE5FZL.js +0 -1
  109. package/lib/public/pane-assets/chunk-GKSLBGVE.js +0 -168
  110. package/lib/public/pane-assets/chunk-J5PTSKJ6.js +0 -1
  111. package/lib/public/pane-assets/chunk-QPBO6ID4.js +0 -1
  112. package/lib/public/pane-assets/chunk-RA6VUJTY.js +0 -1
  113. package/lib/public/pane-assets/chunk-XOZUM5ET.js +0 -1
  114. 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 = '<h3>Voice providers</h3><p class="settings-hint">Manage your personal API keys here. Choose the voice model, transcription mode and language from the microphone menu in the composer. Browser speech needs no API key.</p><div class="us-voice-providers" aria-label="Voice providers"></div><div id="us-voice-connection" hidden><form id="us-voice-key-form"><label class="settings-label" for="us-voice-key">API key</label><input id="us-voice-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096" required><p id="us-voice-privacy" class="settings-hint"></p><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save key</button><button type="button" id="us-voice-key-cancel" class="settings-btn-sm">Cancel</button><a id="us-voice-get-key" target="_blank" rel="noopener noreferrer">Get API key ↗</a></div></form><button type="button" id="us-voice-remove" class="settings-btn-sm">Remove saved key</button></div><p class="settings-status" role="status" id="us-voice-status"></p><button type="button" class="settings-btn-sm" id="us-voice-retry" hidden>Reload providers</button>';
8
- var list = root.querySelector('.us-voice-providers'); var editor = root.querySelector('#us-voice-connection');
9
- var form = root.querySelector('form'); var key = form.querySelector('input'); var status = root.querySelector('[role=status]');
10
- var remove = root.querySelector('#us-voice-remove'); var retry = root.querySelector('#us-voice-retry');
6
+ var root = document.getElementById('us-voice-settings'); if (!root) return;
7
+ root.innerHTML = '<p class="settings-hint">Audio transcription: convert microphone audio into text for voice input. Choose the model, mode and language in the microphone menu. Browser speech needs no API key.</p><h4>Your connections</h4><div id="us-api-connections"></div><p id="us-api-empty" class="settings-hint">No API connections yet. Choose a provider below to add an audio transcription key.</p><section class="us-integration-catalog"><h4>Available providers</h4><div class="us-integration-tiles"></div></section><div id="us-api-setup"></div><div id="us-voice-connection" hidden><form id="us-voice-key-form"><label class="settings-label" for="us-voice-key">API key</label><input id="us-voice-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096"><p id="us-voice-key-help" class="settings-hint"></p><p id="us-voice-privacy" class="settings-hint"></p><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save connection</button><button type="button" id="us-voice-key-cancel" class="settings-btn-sm">Cancel</button><button type="button" id="us-voice-remove" class="settings-btn-sm">Remove saved key</button><a id="us-voice-get-key" target="_blank" rel="noopener noreferrer">Get API key ↗</a></div></form></div><p class="settings-status" role="status" aria-live="polite" id="us-voice-status"></p><button type="button" class="settings-btn-sm" id="us-voice-retry" hidden>Reload providers</button>';
8
+ var list = root.querySelector('#us-api-connections'), tiles = root.querySelector('.us-integration-tiles');
9
+ var editor = root.querySelector('#us-voice-connection'), form = root.querySelector('form');
10
+ var key = form.querySelector('input'), status = root.querySelector('[role=status]');
11
+ var remove = root.querySelector('#us-voice-remove'), retry = root.querySelector('#us-voice-retry');
11
12
  function sync() {
12
- var settings = store.get('speechSettings'); var busy = !!store.get('speechSettingsSaving');
13
+ var settings = store.get('speechSettings'), busy = !!store.get('speechSettingsSaving');
14
+ root.appendChild(editor); retry.before(status); list.replaceChildren(); tiles.replaceChildren();
13
15
  if (settings) {
14
- var signature = settings.providers.map(function (provider) { return provider.id; }).join(',');
15
- if (list.dataset.catalog !== signature) {
16
- root.appendChild(editor); list.replaceChildren(); list.dataset.catalog = signature;
17
- settings.providers.forEach(function (provider) {
18
- var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.voiceProvider = provider.id;
19
- var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
20
- var image = document.createElement('img'); image.className = 'us-voice-provider-logo'; image.src = provider.icon; image.alt = '';
21
- var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
22
- var name = document.createElement('strong'); name.textContent = provider.name;
23
- var state = document.createElement('small'); state.className = 'us-voice-key-state'; copy.appendChild(name); copy.appendChild(state);
24
- var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm us-voice-key-action'; button.dataset.voiceKey = provider.id;
25
- button.setAttribute('aria-controls','us-voice-connection');
26
- button.onclick = function () { discardThen(function () { store.set({userVoiceKeyProvider:provider.id}); status.textContent = ''; sync(); key.focus(); }); };
27
- controls.appendChild(image); controls.appendChild(copy); controls.appendChild(button); row.appendChild(controls); list.appendChild(row);
28
- });
29
- }
30
- list.querySelectorAll('[data-voice-provider]').forEach(function (row) {
31
- var id = row.dataset.voiceProvider; var connected = settings.configured.indexOf(id) >= 0; var button = row.querySelector('[data-voice-key]');
32
- row.querySelector('.us-voice-key-state').textContent = connected ? 'Key saved' : 'Not connected';
33
- button.textContent = connected ? 'API key' : 'Connect'; button.disabled = busy;
34
- button.setAttribute('aria-label', (connected ? 'Manage ' : 'Connect ') + row.querySelector('strong').textContent + ' API key');
35
- button.setAttribute('aria-expanded',String(store.get('userVoiceKeyProvider') === id));
16
+ root.querySelector('#us-api-empty').hidden = !!settings.configured.length;
17
+ settings.providers.forEach(function (provider) {
18
+ var connected = settings.configured.indexOf(provider.id) >= 0;
19
+ var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.voiceProvider = provider.id;
20
+ var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
21
+ var image = document.createElement('img'); image.className = 'us-voice-provider-logo'; image.src = provider.icon; image.alt = '';
22
+ var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
23
+ var name = document.createElement('strong'); name.textContent = provider.name; copy.appendChild(name);
24
+ var button = document.createElement('button'); button.type = 'button'; button.className = connected ? 'settings-btn-sm' : 'us-integration-tile'; button.dataset.voiceKey = provider.id;
25
+ button.setAttribute('aria-label', (connected ? 'Manage ' : 'Add ') + provider.name + ' API key');
26
+ button.setAttribute('aria-controls','us-voice-connection'); button.setAttribute('aria-expanded',String(store.get('userVoiceKeyProvider') === provider.id)); button.disabled = busy;
27
+ button.onclick = function () { discardThen(function () { key.value = ''; store.set({userVoiceKeyProvider:provider.id,userVoiceDirty:false}); status.textContent = ''; sync(); key.focus(); }); };
28
+ if (connected) {
29
+ var purpose = document.createElement('small'); purpose.textContent = 'Audio transcription'; copy.appendChild(purpose);
30
+ var saved = document.createElement('span'); saved.className = 'us-integration-state'; saved.textContent = 'Key saved'; button.textContent = 'Manage';
31
+ controls.append(image,copy,saved,button); row.appendChild(controls); list.appendChild(row);
32
+ } else {
33
+ var title = document.createElement('span'); title.textContent = provider.name; button.append(image,title); tiles.appendChild(button);
34
+ }
36
35
  });
37
36
  var provider = settings.providers.find(function (item) { return item.id === store.get('userVoiceKeyProvider'); });
38
37
  editor.hidden = !provider;
39
38
  if (provider) {
40
39
  var connected = settings.configured.indexOf(provider.id) >= 0;
41
40
  var row = Array.from(list.children).find(function (item) { return item.dataset.voiceProvider === provider.id; });
42
- if (editor.parentElement !== row) row.appendChild(editor);
43
- if (status.parentElement !== editor) editor.appendChild(status);
41
+ (row || root.querySelector('#us-api-setup')).appendChild(editor); editor.appendChild(status);
44
42
  form.querySelector('label').textContent = provider.name + ' API key';
45
- key.placeholder = connected ? 'Enter a replacement key' : 'Enter API key'; remove.hidden = !connected;
46
- root.querySelector('#us-voice-privacy').textContent = 'Stored privately on your Clay server and shared by all ' + provider.name + ' voice models. Audio is sent to ' + provider.name + '; usage is billed to your account.';
43
+ key.required = !connected; key.placeholder = connected ? '•••••••• — saved key' : 'Enter API key'; remove.hidden = !connected;
44
+ root.querySelector('#us-voice-key-help').textContent = connected ? 'Leave blank to keep the saved key. Key saved does not verify provider access.' : 'Enter an API key for audio transcription. Saving does not verify provider access.';
45
+ root.querySelector('#us-voice-privacy').textContent = 'Stored privately on your Clay Studio server for your account and shared by all ' + provider.name + ' voice models. Audio is sent to ' + provider.name + '; usage is billed to your provider account.';
47
46
  root.querySelector('#us-voice-get-key').href = provider.url;
48
- } else { if (editor.parentElement !== root) root.appendChild(editor); retry.before(status); }
49
- } else list.textContent = 'Loading providers…';
47
+ }
48
+ } else { list.textContent = store.get('speechSettingsError') ? 'Provider collection unavailable.' : 'Loading providers…'; root.querySelector('#us-api-empty').hidden = true; editor.hidden = true; }
50
49
  editor.querySelectorAll('input,button').forEach(function (control) { control.disabled = busy || !settings; });
51
50
  retry.hidden = !store.get('speechSettingsError'); retry.disabled = busy;
52
- if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; }
51
+ if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; status.dataset.loadError = 'true'; }
52
+ else if (status.dataset.loadError) { status.textContent = ''; status.dataset.state = ''; delete status.dataset.loadError; }
53
53
  }
54
54
  function discardThen(action) {
55
55
  if (!key.value) { action(); return; }
56
- confirmSettingsAction('Discard the unsaved voice API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
56
+ confirmSettingsAction('Discard the unsaved audio transcription API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
57
57
  }
58
58
  function closeEditor() {
59
59
  var id = store.get('userVoiceKeyProvider'); key.value = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); sync();
60
60
  var button = root.querySelector('[data-voice-key="' + id + '"]'); if (button) button.focus();
61
61
  }
62
62
  async function save(value) {
63
- var provider = store.get('userVoiceKeyProvider'); if (!provider) return;
63
+ var provider = store.get('userVoiceKeyProvider'); if (!provider || store.get('speechSettingsSaving')) return;
64
+ var actor = store.get('myUserId'), generation = store.get('speechSettingsContextGeneration');
65
+ var configured = (store.get('speechSettings') || {}).configured || [];
66
+ if (value === '' && configured.indexOf(provider) >= 0) { closeEditor(); status.textContent = 'No key changes. Your voice model and language are unchanged.'; return; }
64
67
  status.textContent = 'Saving…'; status.dataset.state = '';
65
- try { await saveSpeechSettings({provider:provider,key:value}); closeEditor(); status.textContent = value === null ? 'Key removed. Reconnect this provider before using its voice models.' : 'Key saved. Your voice model and language are unchanged.'; }
66
- catch (error) { status.textContent = error.message; status.dataset.state = 'error'; sync(); }
68
+ try {
69
+ await saveSpeechSettings({provider:provider,key:value});
70
+ if (actor !== store.get('myUserId') || generation !== store.get('speechSettingsContextGeneration')) return;
71
+ closeEditor(); status.textContent = value === null ? 'Key removed. Reconnect this provider before using its voice models.' : 'Key saved. Your voice model and language are unchanged.';
72
+ } catch (error) { if (actor === store.get('myUserId') && generation === store.get('speechSettingsContextGeneration')) { status.textContent = error.message; status.dataset.state = 'error'; sync(); } }
67
73
  }
68
74
  key.oninput = function () { store.set({userVoiceDirty:!!key.value}); };
69
75
  form.onsubmit = function (event) { event.preventDefault(); save(key.value); };
70
76
  root.querySelector('#us-voice-key-cancel').onclick = function () { discardThen(function () { closeEditor(); status.textContent = ''; }); };
71
- remove.onclick = function () { confirmSettingsAction('Remove this provider’s saved voice key? Models using it will require reconnection.', 'Remove key', function () { save(null); }); };
77
+ remove.onclick = function () { confirmSettingsAction('Remove this provider’s saved audio transcription key? Models using it will require reconnection.', 'Remove key', function () { save(null); }); };
72
78
  retry.onclick = loadSpeechSettings;
73
- store.subscribe(function (state, previous) { if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError) sync(); });
79
+ store.subscribe(function (state, previous) {
80
+ if (state.speechSettingsContextGeneration !== previous.speechSettingsContextGeneration) { key.value = ''; status.textContent = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); }
81
+ if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError || state.speechSettingsContextGeneration !== previous.speechSettingsContextGeneration) sync();
82
+ });
74
83
  sync();
75
84
  }
@@ -81,7 +81,7 @@ export function openLoginBrowser(msg) {
81
81
  }
82
82
  cancel();
83
83
  if (isMcp) ws.send(JSON.stringify({ type: 'mcp_connection_action', action: 'signin', id: msg.connectionId }));
84
- else requestVendorLogin(msg.vendor, { mode: mode });
84
+ else requestVendorLogin(msg.vendor, { mode: mode, sessionId: msg.sessionId, workerAuthRequestId: msg.workerAuthRequestId, personalRequestId: msg.personalRequestId });
85
85
  }
86
86
  dialog.querySelector('[data-action="cancel"]').onclick = cancel;
87
87
  dialog.querySelector('[data-action="retry"]').onclick = function () { restart("browser"); };
@@ -1,3 +1,4 @@
1
+ import { personalLoginCurrent } from './personal-provider-context.js';
1
2
  // vendor-login.js - Vendor login (auth_required) recovery flow.
2
3
  //
3
4
  // The server owns the login terminal (see lib/project-vendor-login.js): exactly
@@ -16,6 +17,7 @@ import { refreshIcons, iconHtml } from './icons.js';
16
17
  import { openTuiModal, closeTuiModal, getTuiModalTerminalId } from './tui-attention.js';
17
18
  import { forwardPaneAuthRequired } from './pane-bridge.js';
18
19
  import { openLoginBrowser, closeLoginBrowser } from './vendor-login-browser.js';
20
+ import { escapeHtml } from './utils.js';
19
21
 
20
22
  // vendor -> { terminalId, startedAt } as last broadcast by the server.
21
23
  var activeFlows = Object.create(null);
@@ -45,6 +47,17 @@ function currentProjectSlug() {
45
47
  return "";
46
48
  }
47
49
 
50
+ function workerContextMatches(target) {
51
+ return !!target && target.slug === store.get('currentSlug') && target.selectedSessionId === store.get('activeSessionId') &&
52
+ target.selectedOrigin === store.get('sessionOriginId') && target.socketGeneration === store.get('projectSplitViewSocketGeneration');
53
+ }
54
+
55
+ function forgetWorkerRequest(vendor) {
56
+ var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
57
+ delete requests[vendor];
58
+ store.set({ workerVendorLoginRequests: requests });
59
+ }
60
+
48
61
  // The banner surface belongs to the notification center. Query it rather than
49
62
  // importing app-notifications so the dependency stays one-directional
50
63
  // (app-notifications -> vendor-login).
@@ -80,8 +93,8 @@ function showLoginBanner(opts) {
80
93
  '<div class="notif-banner-icon">' + iconHtml(opts.icon || "check-circle") + '</div>' +
81
94
  '<div class="notif-banner-body">' +
82
95
  '<div class="notif-banner-project">CLAY</div>' +
83
- '<div class="notif-banner-title">' + opts.title + '</div>' +
84
- '<div class="notif-banner-text">' + opts.text + '</div>' +
96
+ '<div class="notif-banner-title">' + escapeHtml(opts.title) + '</div>' +
97
+ '<div class="notif-banner-text">' + escapeHtml(opts.text) + '</div>' +
85
98
  '</div>' +
86
99
  '<button class="notif-banner-close">' + iconHtml("x") + '</button>';
87
100
 
@@ -107,7 +120,7 @@ function showLoginStartedBanner(vendor) {
107
120
  showLoginBanner({
108
121
  icon: "check-circle",
109
122
  title: vendorDisplayName(vendor) + " login started",
110
- text: "Finish sign-in in the window below. Clay reconnects your sessions automatically.",
123
+ text: "Finish sign-in below, then explicitly retry your task.",
111
124
  onDismiss: function () { cancelVendorLogin(vendor); },
112
125
  });
113
126
  }
@@ -159,17 +172,28 @@ function closeModalForVendor(vendor, terminalId) {
159
172
  export function requestVendorLogin(vendor, opts) {
160
173
  var options = opts || {};
161
174
  var target = vendor || "claude";
175
+ if (options.personalRequestId && !personalLoginCurrent({personalRequestId:options.personalRequestId})) return false;
162
176
  if (options.auto && (activeFlows[target] || pendingStarts[target])) return false;
163
177
 
164
178
  var ws = getWs();
165
179
  if (!ws || ws.readyState !== 1) return false;
180
+ if (options.workerAuthRequestId) {
181
+ var requests = Object.assign({}, store.get('workerVendorLoginRequests') || {});
182
+ requests[target] = { requestId: options.workerAuthRequestId, slug: store.get('currentSlug'),
183
+ selectedSessionId: store.get('activeSessionId'), selectedOrigin: store.get('sessionOriginId'),
184
+ socketGeneration: store.get('projectSplitViewSocketGeneration') };
185
+ store.set({ workerVendorLoginRequests: requests });
186
+ }
166
187
 
188
+ if (options.personalRequestId) store.set({personalProviderSigningIn:target});
167
189
  pendingStarts[target] = true;
168
190
  ws.send(JSON.stringify({
169
191
  type: "vendor_login_start",
170
192
  vendor: target,
171
193
  auto: !!options.auto,
172
- sessionId: options.sessionId || store.get('activeSessionId') || null,
194
+ sessionId: options.personalRequestId ? null : options.sessionId || store.get('activeSessionId') || null,
195
+ personalRequestId: options.personalRequestId || null,
196
+ workerAuthRequestId: options.workerAuthRequestId || null,
173
197
  mode: options.mode || ((((store.get("vendorInfo") || {})[target] || {}).browserLoginSupported === true) ? "browser" : "terminal"),
174
198
  }));
175
199
  showLoginStartedBanner(target);
@@ -180,6 +204,7 @@ export function cancelVendorLogin(vendor) {
180
204
  var target = vendor || modalVendor;
181
205
  if (!target) return;
182
206
  var flow = activeFlows[target];
207
+ forgetWorkerRequest(target);
183
208
  var browser = store.get('vendorLoginBrowser');
184
209
  var browserId = flow && flow.browserId || (browser && browser.vendor === target ? browser.browserId : null);
185
210
  if (browser && browser.vendor === target) closeLoginBrowser();
@@ -189,7 +214,7 @@ export function cancelVendorLogin(vendor) {
189
214
  if (modalVendor === target) closeModalInternally();
190
215
  var ws = getWs();
191
216
  if (ws && ws.readyState === 1) {
192
- ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId }));
217
+ ws.send(JSON.stringify({ type: "vendor_login_cancel", vendor: target, browserId: browserId, workerAuthRequestId: flow && flow.workerAuthRequestId, personalRequestId: store.get('personalProviderSigningIn') === target ? store.get('personalProviderRevision') : null }));
193
218
  }
194
219
  }
195
220
 
@@ -205,12 +230,24 @@ export function autoStartLoginIfNeeded(msg) {
205
230
  if (store.get('paneMode')) {
206
231
  return forwardPaneAuthRequired({
207
232
  vendor: msg.vendor || "claude",
208
- sessionId: store.get('activeSessionId') || null,
233
+ sessionId: msg.sessionId || store.get('activeSessionId') || null,
234
+ workerAuthRequestId: msg.workerAuthRequestId || null,
235
+ loginCommand: msg.loginCommand,
236
+ canAutoLogin: msg.canAutoLogin,
209
237
  });
210
238
  }
239
+ if (msg.canAutoLogin === false || msg.loginCommand === '') {
240
+ showLoginErrorBanner(msg.vendor, 'Automatic sign-in is unavailable. Sign in manually on the server, then explicitly retry the task.');
241
+ return false;
242
+ }
211
243
  var vendor = msg.vendor || "claude";
212
244
  if (activeFlows[vendor] || pendingStarts[vendor]) return false;
213
- return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null });
245
+ return requestVendorLogin(vendor, { auto: true, sessionId: msg.sessionId || null, workerAuthRequestId: msg.workerAuthRequestId || null });
246
+ }
247
+
248
+ export function handleWorkerAuthRequired(msg) {
249
+ if (!msg || store.get('replayingHistory') || msg.slug !== store.get('currentSlug') || store.get('paneMode')) return false;
250
+ return autoStartLoginIfNeeded(msg);
214
251
  }
215
252
 
216
253
  // ========================================================
@@ -218,10 +255,12 @@ export function autoStartLoginIfNeeded(msg) {
218
255
  // ========================================================
219
256
 
220
257
  export function handleVendorLoginReady(msg) {
221
- if (!msg || typeof msg.terminalId !== "number") return;
258
+ if (!msg || typeof msg.terminalId !== "number" || !personalLoginCurrent(msg)) return;
222
259
  var vendor = msg.vendor || "claude";
260
+ var request = (store.get('workerVendorLoginRequests') || {})[vendor];
261
+ if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
223
262
  delete pendingStarts[vendor];
224
- activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null };
263
+ activeFlows[vendor] = { terminalId: msg.terminalId, startedAt: Date.now(), browserId: msg.browserId || null, workerAuthRequestId: msg.workerAuthRequestId || null };
225
264
  if (msg.browserId) {
226
265
  openLoginBrowser(msg);
227
266
  return;
@@ -246,10 +285,12 @@ export function handleVendorLoginReady(msg) {
246
285
 
247
286
  export function handleVendorLoginState(msg) {
248
287
  if (!msg) return;
288
+ pendingStarts = Object.create(null);
249
289
  var next = Object.create(null);
250
290
  var flows = msg.flows || [];
251
291
  for (var i = 0; i < flows.length; i++) {
252
- next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null };
292
+ if (!personalLoginCurrent(flows[i])) continue;
293
+ next[flows[i].vendor] = { terminalId: flows[i].terminalId, startedAt: flows[i].startedAt, browserId: flows[i].browserId || null, workerAuthRequestId: flows[i].workerAuthRequestId || null };
253
294
  delete pendingStarts[flows[i].vendor];
254
295
  }
255
296
 
@@ -271,8 +312,11 @@ export function handleVendorLoginState(msg) {
271
312
  }
272
313
 
273
314
  export function handleAuthRefreshed(msg) {
274
- if (!msg) return;
315
+ if (!msg || !personalLoginCurrent(msg)) return;
275
316
  var vendor = msg.vendor || "claude";
317
+ var request = (store.get('workerVendorLoginRequests') || {})[vendor];
318
+ if (msg.workerAuthRequestId && (!workerContextMatches(request) || request.requestId !== msg.workerAuthRequestId)) return;
319
+ forgetWorkerRequest(vendor);
276
320
  var browser = store.get('vendorLoginBrowser');
277
321
  if (browser && browser.vendor === vendor) closeLoginBrowser();
278
322
  var flow = activeFlows[vendor];
@@ -284,8 +328,9 @@ export function handleAuthRefreshed(msg) {
284
328
  }
285
329
 
286
330
  export function handleVendorLoginError(msg) {
287
- if (!msg) return;
331
+ if (!msg || !personalLoginCurrent(msg)) return;
288
332
  var vendor = msg.vendor || "claude";
333
+ forgetWorkerRequest(vendor);
289
334
  var browser = store.get('vendorLoginBrowser');
290
335
  if (browser && browser.vendor === vendor) closeLoginBrowser();
291
336
  delete pendingStarts[vendor];
@@ -304,3 +349,18 @@ export function requestVendorLoginState() {
304
349
  export function getActiveLoginFlow(vendor) {
305
350
  return activeFlows[vendor] || null;
306
351
  }
352
+
353
+ store.subscribe(function (state, previous) {
354
+ var requests = state.workerVendorLoginRequests || {};
355
+ Object.keys(requests).forEach(function (vendor) {
356
+ if (workerContextMatches(requests[vendor]) && state.connected !== false) return;
357
+ if (requests[vendor].slug === state.currentSlug && requests[vendor].socketGeneration === state.projectSplitViewSocketGeneration) cancelVendorLogin(vendor);
358
+ else forgetWorkerRequest(vendor);
359
+ });
360
+ if (state.connected !== false || previous.connected === false) return;
361
+ pendingStarts = Object.create(null);
362
+ activeFlows = Object.create(null);
363
+ closeLoginBrowser();
364
+ closeModalInternally();
365
+ clearLoginBanners();
366
+ });
@@ -0,0 +1,49 @@
1
+ import { store } from './store.js';
2
+ import { iconHtml } from './icons.js';
3
+
4
+ export function validActivity(value) {
5
+ return !!(value && typeof value.epoch === 'string' && value.epoch.length <= 64 && Number.isSafeInteger(value.sequence) && value.sequence > 0 &&
6
+ Number.isSafeInteger(value.queryGeneration) && value.queryGeneration >= 0 && Number.isFinite(value.timestamp) && value.timestamp > 0 &&
7
+ typeof value.summary === 'string' && value.summary.length <= 80);
8
+ }
9
+ export function observeWorkerActivity(key, activity, live) {
10
+ var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
11
+ if (activity && !validActivity(activity)) return null;
12
+ if (previous && activity && previous.cursor && activity.epoch === previous.cursor.epoch &&
13
+ (activity.sequence <= previous.cursor.sequence || activity.queryGeneration < previous.cursor.queryGeneration)) return null;
14
+ // Initial subscriptions/reconnects establish a baseline; never animate cached activity.
15
+ var fresh = !!(live && previous && activity);
16
+ if (previous && previous.activity && activity && previous.activity.epoch !== activity.epoch) fresh = false;
17
+ var generationChanged = previous && previous.activity && (!activity || previous.activity.queryGeneration !== activity.queryGeneration);
18
+ var angle = previous && !generationChanged ? previous.angle : 0, now = performance.now();
19
+ var nextStepAt = previous && !generationChanged ? previous.nextStepAt : 0;
20
+ if (fresh && now >= nextStepAt) { angle += 30; nextStepAt = now + 350; }
21
+ all[key] = { processing: previous && previous.processing, fresh: fresh, cursor: activity || previous && previous.cursor || null, activity: activity || null, angle: angle, nextStepAt: nextStepAt };
22
+ store.set({ workerActivityIndicators: all });
23
+ return fresh;
24
+ }
25
+ export function setWorkerActivityRuntime(key, processing) {
26
+ var all = Object.assign({}, store.get('workerActivityIndicators') || {}), previous = all[key];
27
+ if (!previous || typeof processing !== 'boolean' || previous.processing === processing) return;
28
+ all[key] = Object.assign({}, previous, { processing: processing }); store.set({ workerActivityIndicators: all });
29
+ }
30
+ export function syncWorkerActivityIndicator(chip, worker) {
31
+ var icon = chip.querySelector('.split-view-worker-loop'); if (!icon) return;
32
+ var state = (store.get('workerActivityIndicators') || {})[chip.dataset.messagePreview];
33
+ var angle = state ? state.angle : 0;
34
+ var scope = chip.dataset.messagePreview + ':' + (state && state.activity ? state.activity.epoch + ':' + state.activity.queryGeneration : 'unknown');
35
+ if (icon.dataset.activityScope !== scope) {
36
+ var initial = icon.dataset.activityScope == null ? angle : state && state.fresh ? 0 : angle;
37
+ icon.style.transition = 'none'; icon.style.transform = 'rotate(' + initial + 'deg)';
38
+ void icon.offsetWidth;
39
+ icon.style.transition = ''; icon.dataset.activityScope = scope;
40
+ }
41
+ icon.style.transform = 'rotate(' + angle + 'deg)';
42
+ var processing = state && typeof state.processing === 'boolean' ? state.processing : worker && worker.isProcessing;
43
+ icon.dataset.state = !store.get('connected') ? 'disconnected' : processing ? 'working' : 'ready';
44
+ icon.setAttribute('aria-label', !store.get('connected') ? 'Disconnected' : processing ? 'Worker running; motion indicates received activity' : 'Worker idle');
45
+ }
46
+ export function createWorkerActivityIndicator() {
47
+ var icon = document.createElement('span'); icon.className = 'split-view-worker-loop';
48
+ icon.innerHTML = iconHtml('refresh-cw'); icon.setAttribute('role', 'img'); return icon;
49
+ }
@@ -1,39 +1,75 @@
1
1
  import { store } from './store.js';
2
2
  import { getWs } from './ws-ref.js';
3
- import { bindAgentMessageBubble, messagePreviewKey, rememberAgentMessage } from './agent-message-bubbles.js';
3
+ import { getCachedSessions } from './sidebar-sessions.js';
4
+ import { bindAgentMessageBubble, messagePreviewKey, rememberAgentMessage, rememberAgentActivity, forgetAgentMessage } from './agent-message-bubbles.js';
5
+ import { observeWorkerActivity, syncWorkerActivityIndicator, setWorkerActivityRuntime } from './worker-activity-indicator.js';
4
6
 
5
7
  var scheduled = false;
8
+ function key(target) { return messagePreviewKey('worker', [target.parentSessionOriginId, target.workerSessionOriginId, target.generation]); }
9
+ function exactWorker(target) {
10
+ var list = getCachedSessions() || [];
11
+ var driver = list.find(function (item) { return item.sessionOriginId === target.parentSessionOriginId && item.sessionRole === 'driver' && (item.ownerId || null) === (store.get('myUserId') || null); });
12
+ return driver && list.find(function (item) { return item.sessionOriginId === target.workerSessionOriginId && item.sessionRole === 'worker' &&
13
+ item.parentAvailable && item.parentSessionId === driver.id && item.workerGeneration === target.generation && !item.hidden && (item.ownerId || null) === (driver.ownerId || null); });
14
+ }
6
15
  function targets() {
7
16
  var found = [];
17
+ if (document.hidden) return found;
8
18
  document.querySelectorAll('.split-view-worker-chip[data-worker-parent]').forEach(function (chip) {
9
19
  if (!chip.getClientRects().length || chip.closest('[hidden], .split-pane-cached')) return;
10
20
  var target = { parentSessionOriginId: chip.dataset.workerParent, workerSessionOriginId: chip.dataset.workerOrigin, generation: Number(chip.dataset.workerGeneration) };
11
- bindAgentMessageBubble(chip, messagePreviewKey('worker', [target.parentSessionOriginId, target.workerSessionOriginId, target.generation]));
21
+ var worker = exactWorker(target); if (!worker) return;
22
+ bindAgentMessageBubble(chip, key(target)); syncWorkerActivityIndicator(chip, worker);
12
23
  found.push(target);
13
24
  });
14
25
  return found.slice(0, 32);
15
26
  }
27
+ function cleanup(list) {
28
+ var keep = new Set(list.map(key)), all = Object.assign({}, store.get('workerActivityIndicators') || {}), changed = false;
29
+ Object.keys(all).forEach(function (id) { if (!keep.has(id)) { delete all[id]; forgetAgentMessage(id); changed = true; } });
30
+ if (changed) store.set({ workerActivityIndicators: all });
31
+ }
16
32
  export function syncWorkerMessagePreviews() {
17
33
  if (scheduled || store.get('paneMode')) return;
18
34
  scheduled = true;
19
35
  requestAnimationFrame(function () {
20
- scheduled = false; var socket = getWs(); if (!store.get('connected') || !socket || socket.readyState !== 1) return;
21
- var list = targets(), signature = JSON.stringify([store.get('currentSlug'), store.get('myUserId'), store.get('projectSplitViewSocketGeneration'), list]);
22
- var previous = store.get('workerMessagePreviewSubscription'); if (previous && previous.signature === signature) return;
23
- var requestId = crypto.randomUUID(); store.set({ workerMessagePreviewSubscription: { requestId: requestId, signature: signature, targets: list } });
36
+ scheduled = false; var socket = getWs(), connected = store.get('connected') && socket && socket.readyState === 1;
37
+ var list = connected ? targets() : []; cleanup(list);
38
+ if (!connected) { store.set({ workerMessagePreviewSubscription: null }); return; }
39
+ var signature = JSON.stringify([store.get('currentSlug'), store.get('myUserId'), store.get('projectSplitViewSocketGeneration'), list]);
40
+ var previous = store.get('workerMessagePreviewSubscription'); if (previous && previous.signature === signature && previous.socket === socket) return;
41
+ var requestId = crypto.randomUUID();
42
+ store.set({ workerMessagePreviewSubscription: { requestId: requestId, signature: signature, targets: list, socket: socket,
43
+ slug: store.get('currentSlug'), owner: store.get('myUserId'), socketGeneration: store.get('projectSplitViewSocketGeneration') } });
24
44
  socket.send(JSON.stringify({ type: 'split_worker_preview_request', requestId: requestId, targets: list }));
25
45
  });
26
46
  }
27
47
  export function handleWorkerMessagePreview(message) {
28
- var state = store.get('workerMessagePreviewSubscription'); if (!state || !store.get('connected') || message.requestId !== state.requestId) return;
48
+ var state = store.get('workerMessagePreviewSubscription');
49
+ if (!state || !store.get('connected') || document.hidden || message.requestId !== state.requestId || state.socket !== getWs() ||
50
+ state.slug !== store.get('currentSlug') || state.owner !== store.get('myUserId') || state.socketGeneration !== store.get('projectSplitViewSocketGeneration')) return;
29
51
  if (!state.targets.some(function (target) { return target.parentSessionOriginId === message.parentSessionOriginId && target.workerSessionOriginId === message.workerSessionOriginId && target.generation === message.generation; })) return;
30
- var chip = Array.from(document.querySelectorAll('.split-view-worker-chip[data-worker-parent]')).find(function (chip) { return chip.dataset.workerParent === message.parentSessionOriginId && chip.dataset.workerOrigin === message.workerSessionOriginId && Number(chip.dataset.workerGeneration) === message.generation; });
31
- if (!chip || !chip.getClientRects().length) return;
32
- rememberAgentMessage(messagePreviewKey('worker', [message.parentSessionOriginId, message.workerSessionOriginId, message.generation]), message.preview, message.live === true);
52
+ var worker = exactWorker(message); if (!worker) return;
53
+ var chips = Array.from(document.querySelectorAll('.split-view-worker-chip[data-worker-parent]')).filter(function (chip) {
54
+ return chip.dataset.workerParent === message.parentSessionOriginId && chip.dataset.workerOrigin === message.workerSessionOriginId &&
55
+ Number(chip.dataset.workerGeneration) === message.generation && chip.getClientRects().length && !chip.closest('[hidden], .split-pane-cached');
56
+ });
57
+ if (!chips.length) return;
58
+ var id = key(message), observed = observeWorkerActivity(id, message.activity || null, message.live === true);
59
+ var current = (store.get('workerActivityIndicators') || {})[id];
60
+ if (observed !== null || current && current.cursor && message.activity && current.cursor.epoch === message.activity.epoch && current.cursor.sequence === message.activity.sequence && current.cursor.queryGeneration === message.activity.queryGeneration) setWorkerActivityRuntime(id, message.isProcessing);
61
+ if (observed !== null) rememberAgentActivity(id, message.activity || null, message.vendor, message.observedAt);
62
+ rememberAgentMessage(id, message.preview, message.live === true);
63
+ chips.forEach(function (chip) { syncWorkerActivityIndicator(chip, Object.assign({}, worker, { isProcessing: typeof message.isProcessing === 'boolean' ? message.isProcessing : worker.isProcessing })); });
33
64
  }
34
65
  store.subscribe(function (state, previous) {
35
- if (state.splitPanes !== previous.splitPanes || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId || state.paneBindingSessions !== previous.paneBindingSessions || state.connected !== previous.connected || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration) syncWorkerMessagePreviews();
66
+ if (state.paneBindingSessions !== previous.paneBindingSessions && state.connected) targets().forEach(function (target) {
67
+ var worker = exactWorker(target); if (worker) setWorkerActivityRuntime(key(target), worker.isProcessing === true);
68
+ });
69
+ if (state.splitPanes !== previous.splitPanes || state.projectSplitViewActiveId !== previous.projectSplitViewActiveId || state.paneBindingSessions !== previous.paneBindingSessions ||
70
+ state.currentSlug !== previous.currentSlug || state.myUserId !== previous.myUserId || state.connected !== previous.connected || state.projectSplitViewSocketGeneration !== previous.projectSplitViewSocketGeneration) syncWorkerMessagePreviews();
36
71
  });
37
72
  var observer = new MutationObserver(syncWorkerMessagePreviews);
38
73
  function observe() { var root = document.getElementById('split-host'); if (root) observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden'] }); }
39
74
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', observe); else observe();
75
+ document.addEventListener('visibilitychange', syncWorkerMessagePreviews);