clay-server 5.1.0-beta.1 → 6.0.0-beta.1

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 (222) hide show
  1. package/LICENSES/README.md +4 -0
  2. package/LICENSES/dicebear-avatars.md +16 -0
  3. package/bin/cli.js +9 -91
  4. package/lib/daemon-project-access.js +7 -1
  5. package/lib/daemon-settings.js +11 -0
  6. package/lib/daemon.js +10 -37
  7. package/lib/dicebear-avatar.js +62 -0
  8. package/lib/knowledge-db-service.js +175 -0
  9. package/lib/knowledge-db-validation.js +0 -0
  10. package/lib/knowledge-document-index.js +49 -0
  11. package/lib/knowledge-draft-store.js +56 -0
  12. package/lib/knowledge-file-store.js +199 -0
  13. package/lib/knowledge-markdown.js +128 -0
  14. package/lib/linear-store.js +35 -8
  15. package/lib/mate-knowledge-mcp-server.js +29 -6
  16. package/lib/mate-knowledge-service.js +23 -2
  17. package/lib/mate-knowledge-sync.js +36 -21
  18. package/lib/mate-workspace-color.js +4 -0
  19. package/lib/mates.js +18 -0
  20. package/lib/os-users.js +5 -16
  21. package/lib/project-connection.js +36 -9
  22. package/lib/project-debate-proposal.js +6 -0
  23. package/lib/project-debate-workspace.js +43 -0
  24. package/lib/project-debate.js +1 -0
  25. package/lib/project-filesystem.js +21 -39
  26. package/lib/project-knowledge.js +315 -136
  27. package/lib/project-mate-creation-proposal.js +1 -0
  28. package/lib/project-mate-interaction.js +2 -2
  29. package/lib/project-server-settings.js +31 -0
  30. package/lib/project-sessions.js +92 -182
  31. package/lib/project-user-knowledge.js +60 -0
  32. package/lib/project.js +59 -18
  33. package/lib/public/app.js +79 -71
  34. package/lib/public/clay-studio-wordmark-bold.svg +20 -0
  35. package/lib/public/css/clay-workspace.css +105 -0
  36. package/lib/public/css/debates-workbench.css +47 -0
  37. package/lib/public/css/home-debate-live.css +26 -0
  38. package/lib/public/css/home-hub.css +3 -1
  39. package/lib/public/css/home-mate-model-picker.css +15 -131
  40. package/lib/public/css/home-mate-settings.css +168 -16
  41. package/lib/public/css/icon-strip.css +249 -32
  42. package/lib/public/css/input.css +3 -1
  43. package/lib/public/css/mate-conversation.css +176 -0
  44. package/lib/public/css/mate-knowledge-workbench.css +118 -0
  45. package/lib/public/css/mate-workspace-color.css +16 -0
  46. package/lib/public/css/mates.css +28 -28
  47. package/lib/public/css/messages.css +1 -114
  48. package/lib/public/css/mobile-nav.css +16 -16
  49. package/lib/public/css/overlays.css +8 -0
  50. package/lib/public/css/project-logs.css +7 -0
  51. package/lib/public/css/project-mate-creation.css +105 -0
  52. package/lib/public/css/project-settings-modal.css +56 -0
  53. package/lib/public/css/right-workbench.css +3 -3
  54. package/lib/public/css/server-settings-system.css +125 -0
  55. package/lib/public/css/session-folders.css +80 -21
  56. package/lib/public/css/session-github.css +25 -16
  57. package/lib/public/css/settings-ui.css +247 -22
  58. package/lib/public/css/shared-browser.css +2 -0
  59. package/lib/public/css/sidebar.css +183 -107
  60. package/lib/public/css/title-bar.css +156 -1
  61. package/lib/public/css/worker-delegation.css +47 -0
  62. package/lib/public/css/worker-proposal.css +72 -150
  63. package/lib/public/css/you.css +74 -0
  64. package/lib/public/index.html +203 -547
  65. package/lib/public/modules/account-island.js +232 -0
  66. package/lib/public/modules/admin-settings-email.js +197 -0
  67. package/lib/public/modules/admin-settings-invitations.js +183 -0
  68. package/lib/public/modules/admin-settings-projects.js +198 -0
  69. package/lib/public/modules/admin-settings-ui.js +100 -0
  70. package/lib/public/modules/admin-settings-users.js +331 -0
  71. package/lib/public/modules/admin.js +19 -949
  72. package/lib/public/modules/app-cursors.js +19 -43
  73. package/lib/public/modules/app-debate-ui.js +4 -3
  74. package/lib/public/modules/app-dm.js +0 -2
  75. package/lib/public/modules/app-header.js +106 -0
  76. package/lib/public/modules/app-message-router.js +12 -3
  77. package/lib/public/modules/app-messages.js +47 -61
  78. package/lib/public/modules/app-notifications.js +4 -7
  79. package/lib/public/modules/app-projects.js +5 -38
  80. package/lib/public/modules/app-rate-limit.js +70 -12
  81. package/lib/public/modules/app-rendering.js +12 -13
  82. package/lib/public/modules/avatar.js +25 -9
  83. package/lib/public/modules/chat-render-runtime.js +16 -4
  84. package/lib/public/modules/command-palette.js +2 -15
  85. package/lib/public/modules/context-sources.js +27 -18
  86. package/lib/public/modules/cursor-sharing-preference.js +3 -3
  87. package/lib/public/modules/debate-controls.js +299 -0
  88. package/lib/public/modules/debate.js +2 -0
  89. package/lib/public/modules/debates-workbench.js +339 -0
  90. package/lib/public/modules/default-ai.js +88 -65
  91. package/lib/public/modules/header-tui-font.js +10 -20
  92. package/lib/public/modules/home-debate-controls.js +18 -289
  93. package/lib/public/modules/home-mate-avatar-editor.js +228 -0
  94. package/lib/public/modules/home-mate-color-picker.js +66 -0
  95. package/lib/public/modules/home-mate-creation.js +1 -1
  96. package/lib/public/modules/home-mate-instructions.js +90 -0
  97. package/lib/public/modules/home-mate-model-picker.js +80 -271
  98. package/lib/public/modules/home-mate-profile-editor.js +72 -0
  99. package/lib/public/modules/home-mate-settings.js +118 -125
  100. package/lib/public/modules/home-shell.js +3 -0
  101. package/lib/public/modules/home-surface-boot.js +1 -9
  102. package/lib/public/modules/icon-strip-tooltip.js +95 -0
  103. package/lib/public/modules/knowledge-db-edit-state.js +35 -0
  104. package/lib/public/modules/knowledge-db-workbench.js +234 -0
  105. package/lib/public/modules/knowledge-history-state.js +55 -0
  106. package/lib/public/modules/knowledge-markdown-editor.js +57 -0
  107. package/lib/public/modules/knowledge-preview-links.js +70 -0
  108. package/lib/public/modules/knowledge-workbench-shell.js +21 -0
  109. package/lib/public/modules/linear-settings.js +3 -2
  110. package/lib/public/modules/markdown.js +5 -6
  111. package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
  112. package/lib/public/modules/mate-knowledge-workbench.js +500 -0
  113. package/lib/public/modules/mate-knowledge.js +11 -3
  114. package/lib/public/modules/mate-management.js +54 -10
  115. package/lib/public/modules/mate-sidebar.js +11 -1
  116. package/lib/public/modules/mate-workspace-color.js +33 -0
  117. package/lib/public/modules/mention.js +5 -13
  118. package/lib/public/modules/notification-settings.js +81 -0
  119. package/lib/public/modules/notifications.js +44 -28
  120. package/lib/public/modules/personal-settings-dialog.js +51 -0
  121. package/lib/public/modules/profile.js +66 -24
  122. package/lib/public/modules/project-chat-identity.js +79 -0
  123. package/lib/public/modules/project-debate-planning-workspace.js +250 -0
  124. package/lib/public/modules/project-debate-workspace.js +185 -0
  125. package/lib/public/modules/project-header-identity.js +127 -0
  126. package/lib/public/modules/project-logs-render.js +9 -4
  127. package/lib/public/modules/project-mate-creation-workspace.js +414 -0
  128. package/lib/public/modules/project-mate-navigation.js +238 -0
  129. package/lib/public/modules/project-session-navigation.js +29 -0
  130. package/lib/public/modules/project-settings-environment.js +106 -0
  131. package/lib/public/modules/project-settings.js +72 -628
  132. package/lib/public/modules/project-switcher.js +6 -8
  133. package/lib/public/modules/projects-hub-view.js +5 -6
  134. package/lib/public/modules/projects-hub.js +2 -6
  135. package/lib/public/modules/qrcode.js +27 -8
  136. package/lib/public/modules/search-clay-chat.js +8 -11
  137. package/lib/public/modules/server-settings-controls.js +316 -0
  138. package/lib/public/modules/server-settings-environment.js +194 -0
  139. package/lib/public/modules/server-settings-navigation.js +39 -0
  140. package/lib/public/modules/server-settings-state.js +54 -0
  141. package/lib/public/modules/server-settings.js +116 -788
  142. package/lib/public/modules/server-storage-settings.js +84 -0
  143. package/lib/public/modules/session-activity-tooltip.js +12 -0
  144. package/lib/public/modules/session-create-form.js +86 -14
  145. package/lib/public/modules/session-create.js +50 -7
  146. package/lib/public/modules/session-github.js +2 -1
  147. package/lib/public/modules/session-list-visibility.js +41 -0
  148. package/lib/public/modules/session-presence-ui.js +3 -0
  149. package/lib/public/modules/session-title-marquee.js +51 -0
  150. package/lib/public/modules/settings-env-values.js +34 -0
  151. package/lib/public/modules/settings-select.js +121 -0
  152. package/lib/public/modules/shared-browser.js +10 -7
  153. package/lib/public/modules/sidebar-mates.js +3 -50
  154. package/lib/public/modules/sidebar-mobile.js +63 -60
  155. package/lib/public/modules/sidebar-projects.js +84 -97
  156. package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
  157. package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
  158. package/lib/public/modules/sidebar-sessions.js +49 -50
  159. package/lib/public/modules/sidebar.js +11 -13
  160. package/lib/public/modules/stt-picker.js +14 -5
  161. package/lib/public/modules/terminal-prefs.js +22 -0
  162. package/lib/public/modules/theme.js +88 -52
  163. package/lib/public/modules/thinking-lifecycle.js +7 -6
  164. package/lib/public/modules/tool-palette-order.js +4 -3
  165. package/lib/public/modules/tool-palette.js +13 -3
  166. package/lib/public/modules/tools.js +22 -22
  167. package/lib/public/modules/user-settings-account.js +318 -0
  168. package/lib/public/modules/user-settings-appearance.js +55 -0
  169. package/lib/public/modules/user-settings-editors.js +24 -0
  170. package/lib/public/modules/user-settings-integrations.js +104 -0
  171. package/lib/public/modules/user-settings-layout.js +80 -0
  172. package/lib/public/modules/user-settings-preferences.js +48 -0
  173. package/lib/public/modules/user-settings-profile.js +53 -0
  174. package/lib/public/modules/user-settings-themes.js +38 -0
  175. package/lib/public/modules/user-settings-voice.js +75 -0
  176. package/lib/public/modules/user-settings.js +53 -480
  177. package/lib/public/modules/worker-proposal.js +42 -8
  178. package/lib/public/modules/worker-runtime-picker.js +115 -0
  179. package/lib/public/modules/you-activity.js +121 -0
  180. package/lib/public/modules/you.js +251 -0
  181. package/lib/public/style.css +20 -8
  182. package/lib/public/vendor/codemirror/LICENSE +21 -0
  183. package/lib/public/vendor/codemirror/codemirror.css +344 -0
  184. package/lib/public/vendor/codemirror/codemirror.js +9884 -0
  185. package/lib/public/vendor/codemirror/continuelist.js +101 -0
  186. package/lib/public/vendor/codemirror/markdown.js +886 -0
  187. package/lib/public/vendor/codemirror/show-hint.css +37 -0
  188. package/lib/public/vendor/codemirror/show-hint.js +523 -0
  189. package/lib/sdk-bridge.js +15 -3
  190. package/lib/sdk-message-processor.js +16 -4
  191. package/lib/sdk-quota-snapshot.js +37 -0
  192. package/lib/server-admin.js +11 -0
  193. package/lib/server-appearance-settings.js +71 -0
  194. package/lib/server-home-chat-events.js +1 -1
  195. package/lib/server-home-chat.js +25 -3
  196. package/lib/server-home-debate-planning.js +4 -1
  197. package/lib/server-home-debates.js +2 -0
  198. package/lib/server-home-mate-creation.js +4 -1
  199. package/lib/server-linear.js +14 -8
  200. package/lib/server-mate-instructions.js +44 -0
  201. package/lib/server-mates.js +12 -3
  202. package/lib/server-notification-settings.js +67 -0
  203. package/lib/server-palette.js +1 -1
  204. package/lib/server-settings.js +69 -1
  205. package/lib/server-storage.js +56 -0
  206. package/lib/server-user-knowledge.js +119 -0
  207. package/lib/server.js +31 -3
  208. package/lib/session-visibility.js +7 -2
  209. package/lib/sessions.js +34 -9
  210. package/lib/startup-projects.js +40 -0
  211. package/lib/storage-usage.js +127 -0
  212. package/lib/user-knowledge-service.js +167 -0
  213. package/lib/users-permissions.js +7 -0
  214. package/lib/users-preferences.js +0 -74
  215. package/lib/users-project-mate-filter-preferences.js +39 -0
  216. package/lib/users.js +6 -4
  217. package/lib/workspace-query-access.js +4 -1
  218. package/lib/ws-schema.js +52 -4
  219. package/lib/yoke/adapters/codex.js +8 -45
  220. package/lib/yoke/codex-model-catalog.js +40 -0
  221. package/lib/yoke/codex-rate-limits.js +26 -0
  222. package/package.json +12 -3
@@ -0,0 +1,104 @@
1
+ import { linearIconMarkup } from './session-github.js';
2
+ import { iconHtml } from './icons.js';
3
+ import { store } from './store.js';
4
+ import { confirmSettingsAction } from './server-settings-state.js';
5
+
6
+ export function initPersonalIntegrations() {
7
+ var root = document.querySelector('[data-section="us-integrations"].us-section');
8
+ root.innerHTML = '<h2>Integrations</h2><p class="settings-hint">Your private accounts and API keys.</p><h3>Your connections</h3><div id="us-email-list"></div><div id="us-linear-connections"></div><p id="us-connections-empty" class="settings-hint">No connections yet. Choose a service below to get started.</p><p id="us-linear-collection-status" class="settings-status" role="status" aria-live="polite"></p><button id="us-linear-retry" type="button" class="settings-btn-sm" hidden>Retry Linear</button><section class="us-integration-catalog"><h3>Available integrations</h3><div class="us-integration-tiles"><button type="button" class="us-integration-tile" data-email-provider="gmail"><img src="/icons/email/gmail.svg" alt=""><span>Gmail</span></button><button type="button" class="us-integration-tile" data-email-provider="outlook"><img src="/icons/email/outlook.svg" alt=""><span>Outlook Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="yahoo"><img src="/icons/email/yahoo.svg" alt=""><span>Yahoo Mail</span></button><button type="button" class="us-integration-tile" data-email-provider="custom">' + iconHtml('mail') + '<span>Custom email</span></button><button type="button" class="us-integration-tile" id="us-linear-add" aria-label="Add Linear connection">' + linearIconMarkup() + '<span>Linear</span></button></div></section><div id="us-integration-setup"></div><form id="us-linear-editor" class="settings-form" hidden><div class="settings-field"><label class="settings-label" for="us-linear-name">Connection name</label><input id="us-linear-name" class="us-text-input" maxlength="80" required placeholder="Work, personal, or client name"></div><div class="settings-field"><label class="settings-label" for="us-linear-key">Personal API key</label><input id="us-linear-key" class="us-text-input" type="password" autocomplete="off" spellcheck="false" maxlength="4096"><p id="us-linear-key-help" class="settings-hint">Existing keys stay masked.</p></div><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save connection</button><button type="button" class="settings-btn-sm" id="us-linear-cancel">Cancel</button><button type="button" class="settings-btn-sm" id="us-linear-remove">Disconnect</button></div></form>';
9
+ var list = root.querySelector('#us-linear-connections'), form = root.querySelector('form');
10
+ var name = root.querySelector('#us-linear-name'), key = root.querySelector('#us-linear-key');
11
+ var add = root.querySelector('#us-linear-add'), remove = root.querySelector('#us-linear-remove');
12
+ var status = root.querySelector('[role=status]'), retry = root.querySelector('#us-linear-retry');
13
+ function message(text, error) { status.textContent = text; status.dataset.state = error ? 'error' : ''; }
14
+ function busy(value) {
15
+ store.set({ personalLinearSaving: value });
16
+ root.querySelectorAll('input,button').forEach(function (el) { el.disabled = value; });
17
+ }
18
+ function render() {
19
+ root.insertBefore(form, status); list.replaceChildren();
20
+ var connections = store.get('personalLinearConnections') || [];
21
+
22
+ connections.forEach(function (connection) {
23
+ var row = document.createElement('div'); row.className = 'us-voice-row'; row.dataset.connectionId = connection.id;
24
+ var controls = document.createElement('div'); controls.className = 'us-voice-row-controls';
25
+ var copy = document.createElement('div'); copy.className = 'us-voice-provider-copy';
26
+ var title = document.createElement('strong'); title.textContent = connection.name;
27
+ var detail = document.createElement('small'); detail.textContent = 'Linear · ' + (connection.workspace && connection.workspace.name || 'Linear') + (connection.legacy ? '' : ' · Not assigned');
28
+ copy.append(title, detail);
29
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'settings-btn-sm'; button.textContent = 'Manage';
30
+ button.setAttribute('aria-label', 'Manage ' + connection.name); button.setAttribute('aria-controls', 'us-linear-editor');
31
+ button.setAttribute('aria-expanded', String(store.get('personalLinearEditing') === connection.id));
32
+ button.onclick = function () { discardThen(function () { edit(connection); }); };
33
+ var brand = document.createElement('span'); brand.className = 'us-integration-brand'; brand.innerHTML = linearIconMarkup();
34
+ var saved = document.createElement('span'); saved.className = 'us-integration-state'; saved.textContent = 'Key saved';
35
+ controls.append(brand, copy, saved, button); row.appendChild(controls); list.appendChild(row);
36
+ if (store.get('personalLinearEditing') === connection.id) row.appendChild(form);
37
+ });
38
+ if (store.get('personalLinearEditing') === 'new') root.querySelector('#us-integration-setup').appendChild(form);
39
+ }
40
+ function edit(connection) {
41
+ store.set({personalLinearGeneration:(store.get('personalLinearGeneration') || 0)+1, personalLinearEditing: connection ? connection.id : 'new', personalLinearDirty: false, personalLinearOriginalName: connection ? connection.name : '', personalLinearNewId: connection ? null : crypto.randomUUID()});
42
+ name.value = connection ? connection.name : ''; key.value = ''; key.required = !connection;
43
+ root.querySelector('#us-linear-key-help').textContent = connection ? 'Leave blank to keep the saved key.' : 'Enter your personal Linear API key. It stays private to your account.';
44
+ key.placeholder = connection ? 'Enter a replacement key' : 'Enter API key'; remove.hidden = !connection;
45
+ form.hidden = false; message(''); render(); name.focus();
46
+ }
47
+ function close() {
48
+ var id = store.get('personalLinearEditing');
49
+ store.set({personalLinearEditing:null,personalLinearDirty:false}); form.reset(); form.hidden = true; render();
50
+ var row = Array.from(list.children).find(function (el) { return el.dataset.connectionId === id; });
51
+ (row ? row.querySelector('button') : add).focus();
52
+ }
53
+ function discardThen(action) {
54
+ if (!store.get('personalLinearDirty')) { action(); return; }
55
+ confirmSettingsAction('Discard this unsaved connection draft?', 'Discard', action);
56
+ }
57
+ form.oninput = function () { store.set({personalLinearDirty: !!key.value || name.value !== store.get('personalLinearOriginalName')}); };
58
+ async function save(disconnect) {
59
+ if (store.get('personalLinearSaving')) return;
60
+ var actor = store.get('myUserId'), generation = store.get('personalLinearGeneration');
61
+ var id = store.get('personalLinearEditing'); if (!id) return;
62
+ if (id === 'new') id = store.get('personalLinearNewId');
63
+ busy(true); message(disconnect ? 'Disconnecting…' : 'Saving connection…');
64
+ try {
65
+ var response = await fetch('/api/linear/connections/' + id, {method:disconnect ? 'DELETE' : 'PUT', headers:{'Content-Type':'application/json'}, body:disconnect ? undefined : JSON.stringify({name:name.value,key:key.value})});
66
+ var data = await response.json();
67
+ if (actor !== store.get('myUserId') || generation !== store.get('personalLinearGeneration')) return;
68
+ if (!response.ok) throw new Error(data.error || 'Could not save connection.');
69
+ var entries = (store.get('personalLinearConnections') || []).filter(function (entry) { return entry.id !== id; });
70
+ if (!disconnect) entries.push(Object.assign({id:id,name:name.value.trim(),legacy:id === 'default'},data));
71
+ store.set({personalLinearConnections:entries, githubWorkRefresh:null}); close(); message(disconnect ? 'Connection removed.' : 'Connection saved.');
72
+ } catch (error) { if (actor === store.get('myUserId') && generation === store.get('personalLinearGeneration')) message(error.message,true); }
73
+ finally { if (generation === store.get('personalLinearGeneration')) busy(false); }
74
+ }
75
+ form.onsubmit = function (event) { event.preventDefault(); save(false); };
76
+ add.onclick = function () { discardThen(function () { edit(null); }); };
77
+ root.querySelector('#us-linear-cancel').onclick = function () { discardThen(close); };
78
+ remove.onclick = function () { confirmSettingsAction('Disconnect this Linear connection? Work using this key will need to be reconnected. Existing issue links are retained.', 'Disconnect', function () { save(true); }); };
79
+ retry.onclick = loadPersonalIntegrations;
80
+ store.subscribe(function (state, previous) {
81
+ if (state.myUserId !== previous.myUserId) {
82
+ store.set({personalLinearConnections:null,personalLinearEditing:null,personalLinearDirty:false,personalLinearSaving:false,personalLinearGeneration:(state.personalLinearGeneration || 0)+1});
83
+ root.dataset.connectionsLoaded = 'false'; form.reset(); form.hidden = true; busy(false); render(); message('');
84
+ } else if (state.personalLinearConnections !== previous.personalLinearConnections) render();
85
+ });
86
+ }
87
+
88
+ export async function loadPersonalIntegrations() {
89
+ if (store.get('personalLinearSaving') || store.get('personalLinearEditing')) return;
90
+ var actor = store.get('myUserId'), generation = (store.get('personalLinearGeneration') || 0) + 1;
91
+ store.set({personalLinearGeneration:generation});
92
+ var status = document.getElementById('us-linear-collection-status'), retry = document.getElementById('us-linear-retry');
93
+ status.textContent = 'Loading connections…'; retry.hidden = true;
94
+ try {
95
+ var response = await fetch('/api/linear/connections');
96
+ if (!response.ok) throw new Error('Could not load connections. Try again.');
97
+ var data = await response.json();
98
+ if (actor !== store.get('myUserId') || generation !== store.get('personalLinearGeneration')) return;
99
+ store.set({personalLinearConnections:data.connections}); document.querySelector('.us-section[data-section="us-integrations"]').dataset.connectionsLoaded = 'true'; status.textContent = ''; status.dataset.state = '';
100
+ } catch (error) {
101
+ if (actor !== store.get('myUserId') || generation !== store.get('personalLinearGeneration')) return;
102
+ status.textContent = error.message; status.dataset.state = 'error'; retry.hidden = false;
103
+ }
104
+ }
@@ -0,0 +1,80 @@
1
+ import { initPersonalIntegrations, loadPersonalIntegrations } from './user-settings-integrations.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { store } from './store.js';
4
+ import { closeSpeechPicker } from './stt-picker.js';
5
+ import { loadSpeechSettings } from './stt-settings.js';
6
+ import { mountDefaultAi, unmountDefaultAi, requestDefaultAi } from './default-ai.js';
7
+ import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
8
+
9
+ export var USER_SETTINGS_PAGES = [
10
+ ['us-account', 'Account'], ['us-appearance', 'Appearance'], ['us-chat', 'Conversation'],
11
+ ['us-notifications', 'Notifications'], ['us-ai', 'AI preferences'], ['us-integrations', 'Integrations']
12
+ ];
13
+ export function resolveUserSettingsPage(id) {
14
+ return ({'us-mates':'us-chat','us-experimental':'us-chat','us-connections':'us-integrations','us-email':'us-integrations','us-logout':'us-account'})[id] || id;
15
+ }
16
+ function section(id) { return document.querySelector('#user-settings .us-section[data-section="' + id + '"]'); }
17
+ function heading(target, text, copy) {
18
+ var title = document.createElement('h3'); title.textContent = text; target.appendChild(title);
19
+ if (copy) { var p = document.createElement('p'); p.className = 'settings-hint'; p.textContent = copy; target.appendChild(p); }
20
+ }
21
+ export function buildUserSettingsLayout() {
22
+ var root = document.getElementById('user-settings');
23
+ root.querySelector('.us-modal').setAttribute('role','dialog');root.querySelector('.us-modal').setAttribute('aria-modal','true');root.querySelector('.us-modal').setAttribute('aria-label','User settings');
24
+ var nav = root.querySelector('.us-modal-nav-items'); nav.replaceChildren();
25
+ var dropdown = document.getElementById('user-settings-nav-dropdown'); dropdown.replaceChildren();
26
+ USER_SETTINGS_PAGES.forEach(function (page) {
27
+ var button = document.createElement('button'); button.className = 'us-nav-item'; button.dataset.section = page[0]; button.textContent = page[1]; button.type = 'button'; nav.appendChild(button);
28
+ var option = document.createElement('option'); option.value = page[0]; option.textContent = page[1]; dropdown.appendChild(option);
29
+ });
30
+ dropdown.setAttribute('aria-label', 'Personal settings page');
31
+ root.querySelector('.us-modal-nav').setAttribute('aria-label', 'Personal settings pages');
32
+ var notifications=section('us-notifications');
33
+ var push=document.getElementById('settings-notif-push').closest('label');
34
+ heading(notifications,'This device','Browser permission and push delivery are configured separately on each browser.');notifications.appendChild(push);
35
+ var accountNotifications=document.createElement('h3');accountNotifications.textContent='Account preferences';notifications.querySelector('.settings-card').before(accountNotifications);
36
+ var chat = section('us-chat'); chat.querySelector('h2').textContent = 'Conversation';
37
+ var voice = document.getElementById('us-lang-select').closest('.settings-card');
38
+ voice.innerHTML = '<div id="us-voice-settings"></div>';
39
+ var ai = document.createElement('div'); ai.className = 'us-section'; ai.dataset.section = 'us-ai';
40
+ ai.innerHTML = '<h2>AI preferences</h2><p class="settings-hint">Defaults for new Clay interviews and built-in AI assistance. Project defaults, individual Mates and existing conversation runtimes stay separate.</p><div id="us-default-ai"></div>';
41
+ root.querySelector('.us-modal-content').appendChild(ai);
42
+ heading(ai, 'Execution behavior');
43
+ ['us-auto-continue'].forEach(function (id) {ai.appendChild(document.getElementById(id).closest('.settings-card'));});
44
+ document.getElementById('us-auto-continue').closest('label').querySelector('.settings-hint').textContent = 'When a rate-limited conversation becomes eligible to retry, Clay can send a continuation automatically. This may resume work and consume usage without another message from you.';
45
+ heading(ai, 'Command permissions', 'These are personal Claude CLI allow rules, separate from model selection and Clay session approval modes. Managed defaults are shown for reference. Server access restrictions still apply. On an OS-user server these install in your OS home; otherwise they install in the shared server process home and can affect other Claude CLI launches there.');
46
+ ai.appendChild(document.getElementById('us-claude-allow-list').closest('.settings-card'));
47
+ section('us-mates').remove();
48
+ var experiments = document.createElement('details'); experiments.className = 'us-experiments'; experiments.innerHTML = '<summary>Experimental features</summary><p class="settings-hint">Personal opt-ins for features that may change.</p>';
49
+ experiments.appendChild(document.getElementById('us-capsules-enabled').closest('.settings-card'));chat.appendChild(experiments);section('us-experimental').remove();
50
+ section('us-email').remove();
51
+ initPersonalIntegrations();
52
+ var account = section('us-account');heading(account, 'Sign out');account.appendChild(document.getElementById('us-logout-btn').closest('.settings-card'));section('us-logout').remove();
53
+ document.getElementById('us-logout-btn').textContent = 'Sign out';
54
+ document.getElementById('us-logout-btn').closest('.settings-card').classList.remove('settings-card-danger');
55
+ document.getElementById('us-pin-set-btn').parentElement.querySelector('.settings-hint').textContent = 'Change your personal account PIN. Server sign-in policy is managed separately by an administrator.';
56
+ root.querySelectorAll('.layout-option-icon').forEach(function (el) {el.remove();});
57
+ var back = document.createElement('button');back.type = 'button';back.className = 'settings-btn-sm us-back';back.textContent = '← Settings';root.querySelector('.us-modal-content').prepend(back);
58
+ back.onclick = function () {root.classList.add('us-show-navigation');nav.querySelector('.active').focus();};
59
+ nav.addEventListener('keydown', function (event) {
60
+ if (['ArrowDown','ArrowUp','Home','End'].indexOf(event.key) < 0) return;
61
+ var items = Array.from(nav.querySelectorAll('button'));var index = items.indexOf(document.activeElement);if(index < 0)return;
62
+ event.preventDefault();index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length-1 : (index+(event.key === 'ArrowDown'?1:-1)+items.length)%items.length;items[index].focus();
63
+ });
64
+ }
65
+ export function loadUserSettingsExtras(page) {
66
+ document.getElementById('user-settings').classList.remove('us-show-navigation');
67
+ if (page === 'us-integrations') loadPersonalIntegrations();
68
+ if (page === 'us-chat') loadSpeechSettings();
69
+ if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
70
+ }
71
+ export function finishUserSettingsExtras() {closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
72
+ export function guardUserSettingsLeave(done) {
73
+ var pin = document.getElementById('us-pin-form');
74
+ var pinDirty = pin && !pin.classList.contains('hidden') && Array.from(pin.querySelectorAll('input')).some(function (input) {return !!input.value;});
75
+ var key = document.getElementById('us-linear-key');
76
+ if(store.get('speechPopup'))return;
77
+ var dirty = store.get('personalEmailDirty') || store.get('personalLinearDirty') || store.get('userVoiceDirty') || store.get('notificationDraft') || store.get('appearanceSaveError') || store.get('userProfileDirty') || store.get('userTerminalDirty') || pinDirty || key && key.value || store.get('defaultAiDraftDirty') || store.get('userAllowDirty');
78
+ if (!dirty) {done();return;}
79
+ showConfirmDialog('Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', 'Keep draft and leave', done);
80
+ }
@@ -0,0 +1,48 @@
1
+ import { store } from './store.js';
2
+ import { getWs } from './ws-ref.js';
3
+ import { getChatLayout, setChatLayout } from './theme.js';
4
+
5
+ export function preferenceStatus(id, text, error) {
6
+ var control = document.getElementById(id); if (!control) return;
7
+ var status = document.getElementById(id + '-receipt');
8
+ if (!status) {status = document.createElement('p');status.id=id+'-receipt';status.className='settings-status';status.setAttribute('role','status');control.closest('.settings-card').appendChild(status);}
9
+ status.textContent=text;status.dataset.state=error?'error':'';
10
+ }
11
+ async function saveToggle(id, url, apply) {
12
+ var input=document.getElementById(id);var want=input.checked;
13
+ input.disabled=true;preferenceStatus(id,'Saving…');
14
+ try {
15
+ var response=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:want})});var data=await response.json();
16
+ if(!response.ok || !data.ok)throw new Error(data.error || 'Could not save.');
17
+ if(apply)apply(want);preferenceStatus(id,'Saved.');
18
+ } catch(error) {input.checked=!want;preferenceStatus(id,error.message+' Try again.',true);}
19
+ finally {input.disabled=false;}
20
+ }
21
+ export function initPersonalPreferences() {
22
+ document.getElementById('us-auto-continue').onchange=function(){saveToggle(this.id,'/api/user/auto-continue');};
23
+
24
+ document.querySelectorAll('#us-layout-switcher button').forEach(function(button){button.onclick=async function(){
25
+ var layout=button.dataset.layout;var controls=button.parentElement.querySelectorAll('button');controls.forEach(function(el){el.disabled=true;});preferenceStatus('us-layout-switcher','Saving…');
26
+ try {var response=await fetch('/api/user/chat-layout',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({layout:layout})});if(!response.ok)throw new Error('Could not save layout.');setChatLayout(layout);controls.forEach(function(el){el.classList.toggle('selected',el.dataset.layout===layout);el.setAttribute('aria-pressed',el.dataset.layout===layout);});preferenceStatus('us-layout-switcher','Saved. Applied to conversations in this view.');}
27
+ catch(error){preferenceStatus('us-layout-switcher',error.message+' Try again.',true);}
28
+ finally{controls.forEach(function(el){el.disabled=false;});}
29
+ };});
30
+ var textarea=document.getElementById('us-claude-allow-list');textarea.oninput=function(){store.set({userAllowDirty:true});};
31
+ document.getElementById('us-claude-allow-save').onclick=function(){
32
+ var ws=getWs();if(!ws || ws.readyState!==1){preferenceStatus('us-claude-allow-list','Disconnected. Your draft is retained.',true);return;}
33
+ this.disabled=true;preferenceStatus('us-claude-allow-list','Saving…');ws.send(JSON.stringify({type:'set_claude_user_allow_list',patterns:textarea.value.split('\n').map(function(line){return line.trim();}).filter(Boolean)}));
34
+ setTimeout(function(){var save=document.getElementById('us-claude-allow-save');if(save.disabled){save.disabled=false;preferenceStatus('us-claude-allow-list','No acknowledgement. Your draft is retained; reconnect and retry.',true);}},12000);
35
+ };
36
+ }
37
+ export function handlePersonalSettingsMessage(msg) {
38
+ if(msg.type==='claude_allow_list') {
39
+ store.set({userAllowSaved:msg.user || []});
40
+ if(store.get('userAllowDirty'))return;
41
+ }
42
+ if(msg.type==='set_claude_user_allow_list_result') {
43
+ document.getElementById('us-claude-allow-save').disabled=false;
44
+ if(msg.ok)store.set({userAllowDirty:msg.effective===false,userAllowSaved:msg.patterns || []});
45
+ var message=msg.ok ? (msg.effective===false ? 'Saved to your account, but not installed in Claude. Check filesystem access, then Save to retry.' : 'Saved. Applies on the next Claude invocation.') : msg.error || 'Save failed. Your draft is retained.';
46
+ preferenceStatus('us-claude-allow-list',message,!msg.ok || msg.effective===false);
47
+ }
48
+ }
@@ -0,0 +1,53 @@
1
+ import { store } from './store.js';
2
+ import { getProfile, saveProfileUpdates, uploadAvatarBlob, showAvatarPositioner } from './profile.js';
3
+ import { populateAccount } from './user-settings-account.js';
4
+ import { confirmSettingsAction } from './server-settings-state.js';
5
+ import { iconHtml, refreshIcons } from './icons.js';
6
+
7
+ export function initPersonalProfile() {
8
+ var hero = document.querySelector('#user-settings .us-account-hero');
9
+ var name = document.getElementById('us-account-name');
10
+ var edit = document.createElement('button'); edit.id = 'us-profile-edit'; edit.type = 'button'; edit.className = 'settings-btn-sm'; edit.textContent = 'Edit name'; hero.appendChild(edit);
11
+ var form = document.createElement('form'); form.hidden = true; form.className = 'us-profile-form';
12
+ form.innerHTML = '<label class="settings-label" for="us-profile-name">Display name</label><input id="us-profile-name" class="us-text-input" maxlength="100" required autocomplete="name"><div class="settings-form-actions"><button type="submit" class="settings-btn-sm">Save</button><button type="button" class="settings-btn-sm" data-cancel>Cancel</button></div><p role="status" class="settings-status"></p>';
13
+ name.after(form); var input = form.querySelector('input');
14
+ function close() { form.hidden = true; name.hidden = false; edit.hidden = false; edit.focus(); }
15
+ edit.onclick = function () {
16
+ if (!store.get('userProfileDirty')) input.value = getProfile().name || name.textContent;
17
+ form.hidden = false; name.hidden = true; edit.hidden = true; input.focus(); input.select();
18
+ };
19
+ input.oninput = function () { store.set({ userProfileDirty: true }); };
20
+ form.onsubmit = async function (event) {
21
+ event.preventDefault(); var save = form.querySelector('[type=submit]'); var status = form.querySelector('[role=status]');
22
+ if (!input.value.trim()) { status.textContent = 'Enter a display name.'; return; }
23
+ save.disabled = true; status.textContent = 'Saving…';
24
+ try { await saveProfileUpdates({ name: input.value.trim() }); store.set({ userProfileDirty: false }); close(); populateAccount(); }
25
+ catch (error) { status.textContent = error.message; status.dataset.state = 'error'; }
26
+ finally { save.disabled = false; }
27
+ };
28
+ form.querySelector('[data-cancel]').onclick = function () {
29
+ function discard() { store.set({ userProfileDirty: false }); close(); }
30
+ if (store.get('userProfileDirty')) confirmSettingsAction('Discard your unsaved name?', 'Discard', discard); else discard();
31
+ };
32
+ var visual = hero.querySelector('.us-account-avatar');
33
+ var avatar = document.createElement('button'); avatar.id = 'us-profile-avatar'; avatar.type = 'button'; avatar.className = 'us-avatar-edit'; avatar.setAttribute('aria-label', 'Change profile picture'); avatar.title = 'Change profile picture';
34
+ visual.before(avatar); avatar.appendChild(visual);
35
+ avatar.insertAdjacentHTML('beforeend', '<span class="us-avatar-edit-mark" aria-hidden="true">' + iconHtml('camera') + '</span>');
36
+ refreshIcons();
37
+ var file = document.createElement('input'); file.type = 'file'; file.accept = 'image/*'; file.hidden = true; hero.after(file);
38
+ var status = document.createElement('p'); status.className = 'settings-status'; status.setAttribute('role', 'status'); hero.after(status);
39
+ avatar.onclick = function () { file.click(); };
40
+ file.onchange = function () {
41
+ var selected = file.files[0]; if (!selected) return;
42
+ var url = URL.createObjectURL(selected); var image = new Image();
43
+ image.onload = function () {
44
+ showAvatarPositioner(image, url, function (blob) {
45
+ URL.revokeObjectURL(url); status.textContent = 'Uploading picture…';
46
+ uploadAvatarBlob(blob).then(function () { status.textContent = 'Picture updated.'; populateAccount(); avatar.focus(); })
47
+ .catch(function (error) { status.textContent = error.message; status.dataset.state = 'error'; });
48
+ });
49
+ };
50
+ image.onerror = function () { URL.revokeObjectURL(url); status.textContent = 'Could not read that image. Choose another file.'; };
51
+ image.src = url; file.value = '';
52
+ };
53
+ }
@@ -0,0 +1,38 @@
1
+ import { store } from './store.js';
2
+ import { getThemes, retryAppearanceSave, getAppearanceChoices, selectAppearanceMode, selectAppearanceTheme, onThemeChange, importLegacyAppearance } from './theme.js';
3
+ import { enhanceSettingsSelects } from './settings-select.js';
4
+
5
+ export function initPersonalThemes(root) {
6
+ root.innerHTML = '<h3>Theme</h3><div class="us-preference-row"><label class="settings-label" for="us-theme-mode">Appearance</label><select id="us-theme-mode" class="settings-select"><option value="system">Follow system</option><option value="light">Light</option><option value="dark">Dark</option></select></div><p class="settings-hint">Choose a palette for each appearance. Switching light and dark keeps both choices.</p><div class="us-theme-pair"></div><p id="us-appearance-status" class="settings-status" role="status"></p>';
7
+ var mode = root.querySelector('select');
8
+ mode.onchange = function () { selectAppearanceMode(mode.value); };
9
+ ['light', 'dark'].forEach(function (variant) {
10
+ var group = document.createElement('div'); group.className = 'us-theme-choice';
11
+ group.innerHTML = '<label class="settings-label" for="us-theme-' + variant + '">' + (variant === 'light' ? 'Light palette' : 'Dark palette') + '</label><select class="settings-select" id="us-theme-' + variant + '"></select><div class="us-theme-preview" aria-hidden="true"><div class="us-theme-preview-nav"></div><div class="us-theme-preview-content"><strong>Aa</strong><div class="us-theme-swatches"></div></div></div>';
12
+ group.querySelector('select').onchange = function () { selectAppearanceTheme(variant, this.value); };
13
+ root.querySelector('.us-theme-pair').appendChild(group);
14
+ });
15
+ function sync() {
16
+ var themes = getThemes(); var choices = getAppearanceChoices(); mode.value = choices.mode; mode.dispatchEvent(new Event('settings-sync'));
17
+ ['light', 'dark'].forEach(function (variant) {
18
+ var select = root.querySelector('#us-theme-' + variant); select.replaceChildren();
19
+ Object.keys(themes).filter(function (id) { return themes[id].variant === variant; }).sort(function (a,b) { return themes[a].name.localeCompare(themes[b].name); }).forEach(function (id) {
20
+ var option = document.createElement('option'); option.value = id; option.textContent = themes[id].name; select.appendChild(option);
21
+ });
22
+ select.value = choices[variant + 'Theme']; select.dispatchEvent(new Event('settings-sync'));
23
+ var theme = themes[select.value]; if (!theme) return;
24
+ var preview = select.parentElement.querySelector('.us-theme-preview');
25
+ preview.style.background = '#' + theme.base00; preview.style.color = '#' + theme.base06;
26
+ preview.querySelector('.us-theme-preview-nav').style.background = '#' + theme.base01;
27
+ var swatches = preview.querySelector('.us-theme-swatches'); swatches.replaceChildren();
28
+ ['base08','base09','base0A','base0B','base0D','base0E'].forEach(function (key) { var dot = document.createElement('span'); dot.style.background = '#' + theme[key]; swatches.appendChild(dot); });
29
+ });
30
+ }
31
+ var retry = document.createElement('button'); retry.type = 'button'; retry.className = 'settings-btn-sm'; retry.textContent = 'Retry save'; retry.hidden = !store.get('appearanceSaveError'); retry.onclick = retryAppearanceSave; root.appendChild(retry);
32
+ var legacy = document.createElement('button'); legacy.type = 'button'; legacy.className = 'settings-btn-sm'; legacy.textContent = "Import this browser's previous theme"; legacy.onclick = importLegacyAppearance; root.appendChild(legacy);
33
+ store.subscribe(function (state, previous) {
34
+ legacy.hidden = !state.legacyAppearanceAvailable; retry.hidden = !state.appearanceSaveError;
35
+ if (state.appearanceDraft !== previous.appearanceDraft || state.appearancePreference !== previous.appearancePreference) sync();
36
+ });
37
+ legacy.hidden = !store.get('legacyAppearanceAvailable'); onThemeChange(sync); sync(); enhanceSettingsSelects(root);
38
+ }
@@ -0,0 +1,75 @@
1
+ import { store } from './store.js';
2
+ import { saveSpeechSettings, loadSpeechSettings } from './stt-settings.js';
3
+ import { confirmSettingsAction } from './server-settings-state.js';
4
+
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');
11
+ function sync() {
12
+ var settings = store.get('speechSettings'); var busy = !!store.get('speechSettingsSaving');
13
+ 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));
36
+ });
37
+ var provider = settings.providers.find(function (item) { return item.id === store.get('userVoiceKeyProvider'); });
38
+ editor.hidden = !provider;
39
+ if (provider) {
40
+ var connected = settings.configured.indexOf(provider.id) >= 0;
41
+ 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);
44
+ 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.';
47
+ 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…';
50
+ editor.querySelectorAll('input,button').forEach(function (control) { control.disabled = busy || !settings; });
51
+ retry.hidden = !store.get('speechSettingsError'); retry.disabled = busy;
52
+ if (store.get('speechSettingsError')) { status.textContent = store.get('speechSettingsError'); status.dataset.state = 'error'; }
53
+ }
54
+ function discardThen(action) {
55
+ if (!key.value) { action(); return; }
56
+ confirmSettingsAction('Discard the unsaved voice API key?', 'Discard', function () { key.value = ''; store.set({userVoiceDirty:false}); action(); });
57
+ }
58
+ function closeEditor() {
59
+ var id = store.get('userVoiceKeyProvider'); key.value = ''; store.set({userVoiceKeyProvider:null,userVoiceDirty:false}); sync();
60
+ var button = root.querySelector('[data-voice-key="' + id + '"]'); if (button) button.focus();
61
+ }
62
+ async function save(value) {
63
+ var provider = store.get('userVoiceKeyProvider'); if (!provider) return;
64
+ 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(); }
67
+ }
68
+ key.oninput = function () { store.set({userVoiceDirty:!!key.value}); };
69
+ form.onsubmit = function (event) { event.preventDefault(); save(key.value); };
70
+ 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); }); };
72
+ retry.onclick = loadSpeechSettings;
73
+ store.subscribe(function (state, previous) { if (state.speechSettings !== previous.speechSettings || state.speechSettingsSaving !== previous.speechSettingsSaving || state.speechSettingsError !== previous.speechSettingsError) sync(); });
74
+ sync();
75
+ }