clay-server 5.1.0-beta.2 → 5.1.0

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 (101) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/builtin-mates.js +7 -7
  3. package/lib/daemon-settings.js +11 -0
  4. package/lib/daemon.js +8 -37
  5. package/lib/linear-store.js +35 -8
  6. package/lib/mate-identity-file.js +39 -0
  7. package/lib/mate-knowledge-sync.js +1 -1
  8. package/lib/mate-ready-creation.js +2 -1
  9. package/lib/mates-identity.js +4 -4
  10. package/lib/mates-prompts.js +4 -3
  11. package/lib/mates.js +12 -11
  12. package/lib/os-users.js +5 -16
  13. package/lib/project-connection.js +0 -7
  14. package/lib/project-debate.js +3 -2
  15. package/lib/project-filesystem.js +21 -39
  16. package/lib/project-mate-interaction.js +7 -6
  17. package/lib/project-server-settings.js +31 -0
  18. package/lib/project-sessions.js +57 -184
  19. package/lib/project.js +3 -3
  20. package/lib/public/app.js +6 -37
  21. package/lib/public/css/input.css +3 -1
  22. package/lib/public/css/menus.css +3 -0
  23. package/lib/public/css/overlays.css +7 -0
  24. package/lib/public/css/project-settings-modal.css +56 -0
  25. package/lib/public/css/server-settings-system.css +125 -0
  26. package/lib/public/css/session-github.css +25 -16
  27. package/lib/public/css/settings-ui.css +247 -22
  28. package/lib/public/css/shared-browser.css +7 -3
  29. package/lib/public/css/title-bar.css +5 -1
  30. package/lib/public/index.html +173 -447
  31. package/lib/public/modules/account-island.js +9 -7
  32. package/lib/public/modules/admin-settings-email.js +197 -0
  33. package/lib/public/modules/admin-settings-invitations.js +183 -0
  34. package/lib/public/modules/admin-settings-projects.js +198 -0
  35. package/lib/public/modules/admin-settings-ui.js +100 -0
  36. package/lib/public/modules/admin-settings-users.js +331 -0
  37. package/lib/public/modules/admin.js +19 -949
  38. package/lib/public/modules/app-dm.js +0 -2
  39. package/lib/public/modules/app-messages.js +13 -52
  40. package/lib/public/modules/app-rate-limit.js +70 -12
  41. package/lib/public/modules/avatar.js +6 -1
  42. package/lib/public/modules/context-sources.js +27 -18
  43. package/lib/public/modules/default-ai.js +14 -7
  44. package/lib/public/modules/header-tui-font.js +10 -20
  45. package/lib/public/modules/home-mate-avatar-editor.js +2 -2
  46. package/lib/public/modules/linear-settings.js +3 -2
  47. package/lib/public/modules/markdown.js +5 -6
  48. package/lib/public/modules/notification-settings.js +81 -0
  49. package/lib/public/modules/notifications.js +44 -28
  50. package/lib/public/modules/personal-settings-dialog.js +51 -0
  51. package/lib/public/modules/profile.js +15 -6
  52. package/lib/public/modules/project-settings-environment.js +106 -0
  53. package/lib/public/modules/project-settings.js +72 -628
  54. package/lib/public/modules/server-settings-controls.js +316 -0
  55. package/lib/public/modules/server-settings-environment.js +194 -0
  56. package/lib/public/modules/server-settings-navigation.js +39 -0
  57. package/lib/public/modules/server-settings-state.js +54 -0
  58. package/lib/public/modules/server-settings.js +116 -788
  59. package/lib/public/modules/server-storage-settings.js +84 -0
  60. package/lib/public/modules/session-github.js +2 -1
  61. package/lib/public/modules/session-presence-ui.js +1 -1
  62. package/lib/public/modules/settings-env-values.js +34 -0
  63. package/lib/public/modules/settings-select.js +121 -0
  64. package/lib/public/modules/shared-browser.js +22 -12
  65. package/lib/public/modules/stt-picker.js +14 -5
  66. package/lib/public/modules/terminal-prefs.js +22 -0
  67. package/lib/public/modules/theme.js +88 -52
  68. package/lib/public/modules/user-settings-account.js +318 -0
  69. package/lib/public/modules/user-settings-appearance.js +55 -0
  70. package/lib/public/modules/user-settings-editors.js +24 -0
  71. package/lib/public/modules/user-settings-integrations.js +104 -0
  72. package/lib/public/modules/user-settings-layout.js +80 -0
  73. package/lib/public/modules/user-settings-preferences.js +48 -0
  74. package/lib/public/modules/user-settings-profile.js +53 -0
  75. package/lib/public/modules/user-settings-themes.js +38 -0
  76. package/lib/public/modules/user-settings-voice.js +75 -0
  77. package/lib/public/modules/user-settings.js +53 -480
  78. package/lib/public/style.css +4 -0
  79. package/lib/sdk-message-processor.js +16 -4
  80. package/lib/sdk-quota-snapshot.js +37 -0
  81. package/lib/server-appearance-settings.js +71 -0
  82. package/lib/server-linear.js +14 -8
  83. package/lib/server-mate-instructions.js +2 -2
  84. package/lib/server-mates.js +2 -1
  85. package/lib/server-notification-settings.js +67 -0
  86. package/lib/server-settings.js +4 -0
  87. package/lib/server-storage.js +57 -0
  88. package/lib/server.js +10 -2
  89. package/lib/sessions.js +3 -4
  90. package/lib/startup-projects.js +40 -0
  91. package/lib/storage-native-walk.js +85 -0
  92. package/lib/storage-usage.js +144 -0
  93. package/lib/users-preferences.js +0 -74
  94. package/lib/users.js +0 -4
  95. package/lib/ws-schema.js +2 -4
  96. package/lib/yoke/acp-process-manager.js +2 -2
  97. package/lib/yoke/adapters/codex.js +12 -52
  98. package/lib/yoke/codex-app-server.js +2 -2
  99. package/lib/yoke/codex-model-catalog.js +52 -0
  100. package/lib/yoke/codex-rate-limits.js +26 -0
  101. package/package.json +3 -3
@@ -0,0 +1,318 @@
1
+ import { refreshIcons } from './icons.js';
2
+ import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
3
+ import { store } from './store.js';
4
+ import { getWs } from './ws-ref.js';
5
+ import { showToast } from './utils.js';
6
+ import { getChatLayout, setChatLayout } from './theme.js';
7
+ import { applyTerminalFont } from './terminal-prefs.js';
8
+ import { userAvatarUrl } from './avatar.js';
9
+ import { syncCapsulePreference } from './capsule-preference.js';
10
+ import { getEmailAccountListCache } from './context-sources.js';
11
+ function applyProjectMateDmsClass(enabled) { document.body.classList.toggle('project-mate-dms-disabled', !enabled); }
12
+ export function showPinForm() {
13
+ var pinForm = document.getElementById('us-pin-form');
14
+ if (!pinForm) return;
15
+ if(!pinForm.classList.contains('hidden'))return;
16
+ pinForm.classList.remove('hidden');
17
+ var pinCurrentRow = document.getElementById('us-pin-current-row');
18
+ var pinInput = document.getElementById('us-pin-input');
19
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
20
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
21
+ var pinSave = document.getElementById('us-pin-save');
22
+ var pinMsg = document.getElementById('us-pin-msg');
23
+ var pinFormTitle = document.getElementById('us-pin-form-title');
24
+ if (pinFormTitle) {
25
+ pinFormTitle.textContent = store.get('userPinEnabled') ? 'Change PIN' : 'Set PIN';
26
+ }
27
+ if (pinCurrentRow) {
28
+ pinCurrentRow.classList.toggle('hidden', !store.get('userPinEnabled'));
29
+ }
30
+ if (pinInput) {
31
+ pinInput.value = '';
32
+ }
33
+ if (pinConfirmInput) pinConfirmInput.value = '';
34
+ if (pinCurrentInput) pinCurrentInput.value = '';
35
+ if (pinSave) pinSave.disabled = true;
36
+ if (pinSave) pinSave.textContent = 'Save';
37
+ if (pinMsg) {
38
+ pinMsg.textContent = '';
39
+ pinMsg.className = 'settings-hint settings-pin-error hidden';
40
+ }
41
+ if (store.get('userPinEnabled') && pinCurrentInput) {
42
+ pinCurrentInput.focus();
43
+ } else if (pinInput) {
44
+ pinInput.focus();
45
+ }
46
+ }
47
+
48
+ export function hidePinForm(force) {
49
+ var fields = document.querySelectorAll('#us-pin-form input');
50
+ if (!force && Array.from(fields).some(function (input) {return !!input.value;})) {
51
+ showConfirmDialog('Discard the unsaved PIN change?', 'Discard', function () {hidePinForm(true);});return;
52
+ }
53
+ var pinForm = document.getElementById('us-pin-form');
54
+ if (!pinForm) return;
55
+ pinForm.classList.add('hidden');
56
+ var pinCurrentRow = document.getElementById('us-pin-current-row');
57
+ var pinInput = document.getElementById('us-pin-input');
58
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
59
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
60
+ var pinSave = document.getElementById('us-pin-save');
61
+ var pinMsg = document.getElementById('us-pin-msg');
62
+ if (pinCurrentRow) {
63
+ pinCurrentRow.classList.remove('hidden');
64
+ }
65
+ if (pinInput) pinInput.value = '';
66
+ if (pinConfirmInput) pinConfirmInput.value = '';
67
+ if (pinCurrentInput) pinCurrentInput.value = '';
68
+ if (pinSave) pinSave.disabled = true;
69
+ if (pinSave) pinSave.textContent = 'Save';
70
+ if (pinMsg) {
71
+ pinMsg.textContent = '';
72
+ pinMsg.className = 'settings-hint settings-pin-error hidden';
73
+ }
74
+ }
75
+
76
+ // --- Account population ---
77
+
78
+ export function populateAccount() {
79
+ fetch('/api/profile').then(function (r) {
80
+ if (!r.ok) return null;
81
+ return r.json();
82
+ }).then(function (data) {
83
+ if (!data) return;
84
+ var displayName = data.name || data.displayName || data.username || 'Clay User';
85
+ var avatarImg = document.getElementById('us-account-avatar-img');
86
+ var avatarFallback = document.getElementById('us-account-avatar-fallback');
87
+ var accountName = document.getElementById('us-account-name');
88
+ var accountSubline = document.getElementById('us-account-subline');
89
+ var langSelect = document.getElementById('us-lang-select');
90
+ var pinSetBtnEl = document.getElementById('us-pin-set-btn');
91
+ var pinFormTitle = document.getElementById('us-pin-form-title');
92
+ if (accountName) {
93
+ accountName.textContent = displayName;
94
+ }
95
+ if (accountSubline) {
96
+ var parts = [];
97
+ if (data.username) parts.push('@' + data.username);
98
+ if (data.role) parts.push(data.role.charAt(0).toUpperCase() + data.role.slice(1));
99
+ accountSubline.textContent = parts.length ? parts.join(' · ') : 'Local profile';
100
+ }
101
+ store.set({userPinEnabled:!!data.pinEnabled});
102
+ if (pinSetBtnEl) {
103
+ pinSetBtnEl.textContent = store.get('userPinEnabled') ? 'Change PIN' : 'Set PIN';
104
+ }
105
+ if (pinFormTitle) {
106
+ pinFormTitle.textContent = store.get('userPinEnabled') ? 'Change PIN' : 'Set PIN';
107
+ }
108
+ if (langSelect) {
109
+ langSelect.value = store.get('speechLang') || 'en-US';
110
+ langSelect.dispatchEvent(new Event('settings-sync'));
111
+ }
112
+ if (avatarImg && avatarFallback) {
113
+ var avatarUrl = getCurrentUserAvatarSrc(data);
114
+ avatarImg.alt = displayName;
115
+ avatarImg.classList.remove('hidden');
116
+ avatarFallback.classList.remove('hidden');
117
+ avatarFallback.textContent = avatarInitials(displayName);
118
+ avatarImg.onload = function () {
119
+ avatarFallback.classList.add('hidden');
120
+ };
121
+ avatarImg.onerror = function () {
122
+ avatarFallback.classList.remove('hidden');
123
+ };
124
+ avatarImg.src = avatarUrl;
125
+ if (avatarImg.complete && avatarImg.naturalWidth > 0) {
126
+ avatarFallback.classList.add('hidden');
127
+ }
128
+ }
129
+ // Hide account section in single-user mode (no username)
130
+ var accountNav = document.getElementById('user-settings').querySelector('[data-section="us-account"]');
131
+ if (accountNav) accountNav.style.display = '';
132
+ if (pinSetBtnEl) pinSetBtnEl.disabled = !data.username;
133
+ document.getElementById('us-claude-allow-save').disabled=!data.username;
134
+ if(!data.username)document.getElementById('us-claude-allow-status').textContent='Personal command rules require a signed-in account.';
135
+ // Terminal font: seed in-memory prefs from server. Header-mounted
136
+ // controls (header-tui-font.js) listen to applyTerminalFont and
137
+ // sync their UI - we don't touch any DOM here.
138
+ if (data.terminalFont && typeof data.terminalFont === "object") {
139
+ applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
140
+ }
141
+ // Auto-continue toggle
142
+ var acToggle = document.getElementById('us-auto-continue');
143
+ if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
144
+ // Theme picker self-syncs active selection via theme.js's applyTheme
145
+ // change callbacks (updatePickerActive). Nothing to do here.
146
+ // Layout switcher: sync from server response
147
+ // Sync mate onboarding state from server
148
+ if (data.mateOnboardingShown) {
149
+ store.set({mateOnboardingShown:true});
150
+ }
151
+ // Keep project Mate DM shortcuts available for legacy profiles too.
152
+ var matesOn = true;
153
+ var matesToggle = document.getElementById('us-mates-enabled');
154
+ if (matesToggle) matesToggle.checked = matesOn;
155
+ store.set({ projectMateDmsEnabled: matesOn });
156
+ applyProjectMateDmsClass(matesOn);
157
+ syncCapsulePreference(data.capsulesEnabled === true);
158
+ if (data.chatLayout) {
159
+ setChatLayout(data.chatLayout); // update local cache + CSS
160
+ }
161
+ var lSwitcher = document.getElementById('us-layout-switcher');
162
+ if (lSwitcher) {
163
+ var currentLayout = getChatLayout();
164
+ var lBtns = lSwitcher.querySelectorAll('.layout-option');
165
+ for (var li = 0; li < lBtns.length; li++) {
166
+ lBtns[li].classList.toggle('selected', lBtns[li].dataset.layout === currentLayout);
167
+ }
168
+ }
169
+ store.set({userSettingsProfile:data});
170
+ }).catch(function () {showToast('Could not load your settings. Close and reopen to retry.');});
171
+ }
172
+
173
+ function avatarInitials(value) {
174
+ var text = (value || '').trim();
175
+ if (!text) return '?';
176
+ var parts = text.split(/\s+/);
177
+ var first = parts[0].charAt(0);
178
+ var second = parts.length > 1 ? parts[1].charAt(0) : parts[0].charAt(1);
179
+ var out = first + (second || '');
180
+ return out.toUpperCase();
181
+ }
182
+
183
+ function getCurrentUserAvatarSrc(data) {
184
+ var islandAvatar = document.querySelector('.user-island-avatar img');
185
+ if (islandAvatar && islandAvatar.src) return islandAvatar.src;
186
+ if (data && data.avatarCustom) return data.avatarCustom;
187
+ return userAvatarUrl(data, 96);
188
+ }
189
+
190
+ export function savePin(pin) {
191
+ var pinInput = document.getElementById('us-pin-input');
192
+ var pinConfirmInput = document.getElementById('us-pin-confirm-input');
193
+ var pinCurrentInput = document.getElementById('us-pin-current-input');
194
+ var pinSave = document.getElementById('us-pin-save');
195
+ var pinMsg = document.getElementById('us-pin-msg');
196
+ var currentPin = pinCurrentInput ? pinCurrentInput.value.trim() : '';
197
+ var confirmPin = pinConfirmInput ? pinConfirmInput.value.trim() : '';
198
+ var newPin = pinInput ? pinInput.value.trim() : '';
199
+
200
+ if (!/^\d{6}$/.test(newPin)) {
201
+ if (pinMsg) {
202
+ pinMsg.textContent = 'PIN must be exactly 6 digits.';
203
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
204
+ pinMsg.classList.remove('hidden');
205
+ }
206
+ return;
207
+ }
208
+ if (confirmPin !== newPin) {
209
+ if (pinMsg) {
210
+ pinMsg.textContent = 'PINs do not match.';
211
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
212
+ pinMsg.classList.remove('hidden');
213
+ }
214
+ return;
215
+ }
216
+ if (store.get('userPinEnabled') && !/^\d{6}$/.test(currentPin)) {
217
+ if (pinMsg) {
218
+ pinMsg.textContent = 'Current PIN is required.';
219
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
220
+ pinMsg.classList.remove('hidden');
221
+ }
222
+ return;
223
+ }
224
+
225
+ pinSave.disabled = true;
226
+ pinSave.textContent = 'Saving\u2026';
227
+
228
+ fetch('/api/user/pin', {
229
+ method: 'PUT',
230
+ headers: { 'Content-Type': 'application/json' },
231
+ body: JSON.stringify({ currentPin: currentPin, newPin: newPin }),
232
+ }).then(function (r) { return r.json(); }).then(function (data) {
233
+ if (data.ok) {
234
+ showToast('PIN changed');
235
+ hidePinForm(true);
236
+ store.set({userPinEnabled:true});
237
+ } else {
238
+ pinSave.disabled = false;
239
+ pinSave.textContent = 'Save';
240
+ if (pinMsg) {
241
+ pinMsg.textContent = data.error || 'Could not change your PIN. Please try again.';
242
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
243
+ pinMsg.classList.remove('hidden');
244
+ }
245
+ }
246
+ }).catch(function () {
247
+ pinSave.disabled = false;
248
+ pinSave.textContent = 'Save';
249
+ if (pinMsg) {
250
+ pinMsg.textContent = 'Connection lost. Check your network and try again.';
251
+ pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
252
+ pinMsg.classList.remove('hidden');
253
+ }
254
+ });
255
+ }
256
+
257
+ // --- Email settings ---
258
+
259
+ export function renderEmailSettings() {
260
+ var listEl = document.getElementById('us-email-list');
261
+ if (!listEl) return;
262
+ listEl.innerHTML = '';
263
+
264
+ var accounts = getEmailAccountListCache();
265
+ if (!accounts || accounts.length === 0) {
266
+ return;
267
+ }
268
+
269
+ for (var i = 0; i < accounts.length; i++) {
270
+ var acc = accounts[i];
271
+ var row = document.createElement('div');
272
+ row.className = 'us-integration-email-row';
273
+ row.innerHTML =
274
+ '<div class="us-voice-row-controls"><div class="us-integration-brand">' + providerIcon(acc.provider) + '</div>' +
275
+ '<div class="us-voice-provider-copy"><strong>' + escHtml(acc.email) + '</strong><small>Email · ' + escHtml(({gmail:'Gmail',outlook:'Outlook Mail',yahoo:'Yahoo Mail',custom:'Custom email'})[acc.provider] || 'Custom email') + (acc.label ? ' · ' + escHtml(acc.label) : '') + '</small></div>' +
276
+ '<span class="us-integration-state">Account saved</span><button type="button" class="settings-btn-sm" aria-expanded="false">Manage</button></div>' +
277
+ '<div class="us-email-manage" hidden><p class="settings-hint">This personal mailbox can be used to read, search and send email. Disconnecting keeps your mail at the provider.</p><button type="button" class="settings-btn-sm" data-account-id="' + escHtml(acc.id) + '">Disconnect</button></div>';
278
+ var manageBtn = row.querySelector('[aria-expanded]');
279
+ manageBtn.setAttribute('aria-label', 'Manage ' + acc.email);
280
+ manageBtn.addEventListener('click', function () {
281
+ var panel = this.closest('.us-integration-email-row').querySelector('.us-email-manage');
282
+ panel.hidden = !panel.hidden; this.setAttribute('aria-expanded', String(!panel.hidden));
283
+ });
284
+ var removeBtn = row.querySelector('[data-account-id]');
285
+ removeBtn.addEventListener('click', function () {
286
+ var accountId = this.getAttribute('data-account-id');
287
+ showConfirmDialog('Disconnect this personal mailbox from Clay? Existing mail at the provider is kept.', 'Disconnect', function () {
288
+ var ws=getWs();
289
+ if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'email_account_remove',accountId:accountId}));
290
+ else showToast('Disconnected from Clay. Reconnect and retry.');
291
+ });
292
+ });
293
+
294
+ listEl.appendChild(row);
295
+ }
296
+ refreshIcons();
297
+ }
298
+
299
+
300
+ var PROVIDER_ICON_PATHS = {
301
+ gmail: '/icons/email/gmail.svg',
302
+ outlook: '/icons/email/outlook.svg',
303
+ yahoo: '/icons/email/yahoo.svg',
304
+ };
305
+
306
+ function providerIcon(provider) {
307
+ var src = PROVIDER_ICON_PATHS[provider];
308
+ if (src) {
309
+ return '<img src="' + src + '" class="us-email-provider-icon" alt="' + provider + '">';
310
+ }
311
+ return '<i data-lucide="mail" class="us-email-provider-icon-fallback"></i>';
312
+ }
313
+
314
+ function escHtml(str) {
315
+ var d = document.createElement('div');
316
+ d.textContent = str;
317
+ return d.innerHTML;
318
+ }
@@ -0,0 +1,55 @@
1
+ import { store } from './store.js';
2
+ import { initPersonalThemes } from './user-settings-themes.js';
3
+ import { TERMINAL_FONT_OPTIONS, getTerminalFontFamily, getTerminalFontSize, saveTerminalFont, onTerminalFontChange } from './terminal-prefs.js';
4
+ import { enhanceSettingsSelects } from './settings-select.js';
5
+
6
+ export function initPersonalAppearance() {
7
+ var theme = document.getElementById('us-theme-picker').closest('.settings-card');
8
+ initPersonalThemes(theme);
9
+
10
+ var layout = document.getElementById('us-layout-switcher');
11
+ layout.closest('.settings-card').classList.add('us-layout-group');
12
+ layout.setAttribute('role', 'group'); layout.setAttribute('aria-label', 'Conversation layout');
13
+ layout.querySelectorAll('button').forEach(function (button) {
14
+ var bubble = button.dataset.layout === 'bubble';
15
+ button.querySelector('.layout-option-desc').textContent = bubble ? 'Centered messages in separate bubbles.' : 'Full-width conversation with avatars.';
16
+ var preview = document.createElement('span'); preview.className = 'us-layout-preview us-layout-preview-' + button.dataset.layout; preview.setAttribute('aria-hidden', 'true');
17
+ preview.innerHTML = '<i></i><i></i><i></i>'; button.prepend(preview);
18
+ });
19
+ layout.querySelectorAll('button').forEach(function (button) { button.setAttribute('aria-pressed', button.classList.contains('selected')); });
20
+ addTerminalTypography(theme.closest('.us-section'));
21
+ enhanceSettingsSelects(theme);
22
+ }
23
+
24
+ function addTerminalTypography(section) {
25
+ var content = document.createElement('section'); content.className = 'us-typography settings-card';
26
+ content.innerHTML = '<h3>Terminal typography</h3><p class="settings-hint">Used by existing and new terminals. Also available from the terminal toolbar.</p><div class="us-font-controls"><div><label class="settings-label" for="us-terminal-family">Font</label><select id="us-terminal-family" class="settings-select"></select></div><div><label class="settings-label" for="us-terminal-size">Size</label><div class="us-font-size"><input id="us-terminal-size" class="us-text-input" type="number" min="9" max="32" step="1" aria-label="Terminal font size"><span>px</span></div></div></div><div class="us-font-preview" aria-hidden="true">Aa Bb 0123 · ~/projects/clay</div><p class="settings-status" role="status" id="us-terminal-status"></p><button type="button" class="settings-btn-sm" id="us-terminal-retry" hidden>Retry save</button>';
27
+ section.appendChild(content);
28
+ var family = content.querySelector('select'); var size = content.querySelector('input');
29
+ var preview = content.querySelector('.us-font-preview'); var status = content.querySelector('[role=status]'); var retry = content.querySelector('#us-terminal-retry');
30
+ TERMINAL_FONT_OPTIONS.forEach(function (font) {
31
+ var option = document.createElement('option'); option.value = font.family; option.textContent = font.label; family.appendChild(option);
32
+ });
33
+ function sync() {
34
+ if (store.get('userTerminalDirty')) return;
35
+ var value = getTerminalFontFamily();
36
+ if (!Array.from(family.options).some(function (option) { return option.value === value; })) {
37
+ var option = document.createElement('option'); option.value = value; option.textContent = 'Custom font'; family.appendChild(option);
38
+ }
39
+ family.value = value; size.value = getTerminalFontSize();
40
+ preview.style.fontFamily = value; preview.style.fontSize = size.value + 'px';
41
+ family.dispatchEvent(new Event('settings-sync'));
42
+ }
43
+ async function save() {
44
+ var value = Number(size.value); store.set({ userTerminalDirty: true });
45
+ if (!Number.isInteger(value) || value < 9 || value > 32) { status.textContent = 'Choose a whole size from 9 to 32.'; status.dataset.state = 'error'; return; }
46
+ family.disabled = true; size.disabled = true; retry.hidden = true; status.textContent = 'Saving…'; status.dataset.state = '';
47
+ try {
48
+ await saveTerminalFont(family.value, value); store.set({ userTerminalDirty: false }); sync(); status.textContent = 'Saved.';
49
+ } catch (error) { status.textContent = error.message; status.dataset.state = 'error'; retry.hidden = false; }
50
+ finally { family.disabled = false; size.disabled = false; family.dispatchEvent(new Event('settings-sync')); }
51
+ }
52
+ family.onchange = save; size.onchange = save; retry.onclick = save;
53
+ size.oninput = function () { store.set({ userTerminalDirty: true }); };
54
+ onTerminalFontChange(sync); sync(); enhanceSettingsSelects(content);
55
+ }
@@ -0,0 +1,24 @@
1
+ import { store } from './store.js';
2
+ import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
3
+
4
+ function wrapEditor(content, title, summary) {
5
+ var group=document.createElement('section');group.className='us-config-group';
6
+ content.before(group);
7
+ var heading=document.createElement('div');heading.className='us-editor-heading';
8
+ var label=document.createElement('h3');label.textContent=title;
9
+ var edit=document.createElement('button');edit.type='button';edit.className='settings-btn-sm';edit.textContent='Edit';
10
+ var state=document.createElement('p');state.className='us-editor-summary';state.textContent=summary();
11
+ heading.append(label,edit);group.append(heading,state);
12
+ var help=content.querySelector('.settings-hint');var copy=null;if(help && title!=='Personal allow rules'){copy=help.cloneNode(true);group.appendChild(copy);}
13
+ group.appendChild(content);content.hidden=true;
14
+ store.subscribe(function(next,previous){if(next.userSettingsProfile!==previous.userSettingsProfile)state.textContent=summary();});
15
+ var done=document.createElement('button');done.type='button';done.className='settings-btn-sm';done.textContent='Done';content.appendChild(done);
16
+ edit.onclick=function(){if(copy)copy.hidden=true;content.hidden=false;state.hidden=true;edit.hidden=true;var first=content.querySelector('button,input,textarea,select');if(first)first.focus();};
17
+ done.onclick=function(){if(copy)copy.hidden=false;content.hidden=true;state.hidden=false;edit.hidden=false;state.textContent=summary();edit.focus();};
18
+ return {group:group,state:state,edit:edit,done:done,content:content};
19
+ }
20
+ export function initPersonalEditors() {
21
+ var allow=document.getElementById('us-claude-allow-list');var permission=wrapEditor(allow.closest('.settings-card'),'Personal allow rules',function(){var count=(store.get('userAllowSaved') || []).length;return count+' personal rules. Managed defaults apply separately.';});
22
+ permission.done.textContent='Cancel';permission.done.onclick=function(){function cancel(){allow.value=(store.get('userAllowSaved') || []).join('\n');store.set({userAllowDirty:false});permission.content.hidden=true;permission.state.hidden=false;permission.edit.hidden=false;permission.edit.focus();}if(store.get('userAllowDirty'))showConfirmDialog('Discard unsaved command rules?', 'Discard',cancel);else cancel();};
23
+ store.subscribe(function(state,previous){if(state.userAllowSaved!==previous.userAllowSaved){permission.state.textContent=(state.userAllowSaved || []).length+' personal rules. Managed defaults apply separately.';if(previous.userAllowDirty && !state.userAllowDirty){permission.content.hidden=true;permission.state.hidden=false;permission.edit.hidden=false;permission.edit.focus();}}});
24
+ }
@@ -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
+ }