clay-server 5.1.0-beta.2 → 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 (83) hide show
  1. package/bin/cli.js +9 -91
  2. package/lib/daemon-settings.js +11 -0
  3. package/lib/daemon.js +8 -37
  4. package/lib/linear-store.js +35 -8
  5. package/lib/os-users.js +5 -16
  6. package/lib/project-connection.js +0 -7
  7. package/lib/project-filesystem.js +21 -39
  8. package/lib/project-mate-interaction.js +2 -2
  9. package/lib/project-server-settings.js +31 -0
  10. package/lib/project-sessions.js +57 -184
  11. package/lib/public/app.js +6 -37
  12. package/lib/public/css/input.css +3 -1
  13. package/lib/public/css/project-settings-modal.css +56 -0
  14. package/lib/public/css/server-settings-system.css +125 -0
  15. package/lib/public/css/session-github.css +25 -16
  16. package/lib/public/css/settings-ui.css +247 -22
  17. package/lib/public/css/shared-browser.css +2 -0
  18. package/lib/public/css/title-bar.css +5 -1
  19. package/lib/public/index.html +170 -446
  20. package/lib/public/modules/account-island.js +9 -7
  21. package/lib/public/modules/admin-settings-email.js +197 -0
  22. package/lib/public/modules/admin-settings-invitations.js +183 -0
  23. package/lib/public/modules/admin-settings-projects.js +198 -0
  24. package/lib/public/modules/admin-settings-ui.js +100 -0
  25. package/lib/public/modules/admin-settings-users.js +331 -0
  26. package/lib/public/modules/admin.js +19 -949
  27. package/lib/public/modules/app-dm.js +0 -2
  28. package/lib/public/modules/app-messages.js +13 -52
  29. package/lib/public/modules/app-rate-limit.js +70 -12
  30. package/lib/public/modules/context-sources.js +27 -18
  31. package/lib/public/modules/default-ai.js +14 -7
  32. package/lib/public/modules/header-tui-font.js +10 -20
  33. package/lib/public/modules/linear-settings.js +3 -2
  34. package/lib/public/modules/markdown.js +5 -6
  35. package/lib/public/modules/notification-settings.js +81 -0
  36. package/lib/public/modules/notifications.js +44 -28
  37. package/lib/public/modules/personal-settings-dialog.js +51 -0
  38. package/lib/public/modules/profile.js +15 -6
  39. package/lib/public/modules/project-settings-environment.js +106 -0
  40. package/lib/public/modules/project-settings.js +72 -628
  41. package/lib/public/modules/server-settings-controls.js +316 -0
  42. package/lib/public/modules/server-settings-environment.js +194 -0
  43. package/lib/public/modules/server-settings-navigation.js +39 -0
  44. package/lib/public/modules/server-settings-state.js +54 -0
  45. package/lib/public/modules/server-settings.js +116 -788
  46. package/lib/public/modules/server-storage-settings.js +84 -0
  47. package/lib/public/modules/session-github.js +2 -1
  48. package/lib/public/modules/session-presence-ui.js +1 -1
  49. package/lib/public/modules/settings-env-values.js +34 -0
  50. package/lib/public/modules/settings-select.js +121 -0
  51. package/lib/public/modules/shared-browser.js +10 -7
  52. package/lib/public/modules/stt-picker.js +14 -5
  53. package/lib/public/modules/terminal-prefs.js +22 -0
  54. package/lib/public/modules/theme.js +88 -52
  55. package/lib/public/modules/user-settings-account.js +318 -0
  56. package/lib/public/modules/user-settings-appearance.js +55 -0
  57. package/lib/public/modules/user-settings-editors.js +24 -0
  58. package/lib/public/modules/user-settings-integrations.js +104 -0
  59. package/lib/public/modules/user-settings-layout.js +80 -0
  60. package/lib/public/modules/user-settings-preferences.js +48 -0
  61. package/lib/public/modules/user-settings-profile.js +53 -0
  62. package/lib/public/modules/user-settings-themes.js +38 -0
  63. package/lib/public/modules/user-settings-voice.js +75 -0
  64. package/lib/public/modules/user-settings.js +53 -480
  65. package/lib/public/style.css +4 -0
  66. package/lib/sdk-message-processor.js +16 -4
  67. package/lib/sdk-quota-snapshot.js +37 -0
  68. package/lib/server-appearance-settings.js +71 -0
  69. package/lib/server-linear.js +14 -8
  70. package/lib/server-notification-settings.js +67 -0
  71. package/lib/server-settings.js +4 -0
  72. package/lib/server-storage.js +56 -0
  73. package/lib/server.js +10 -2
  74. package/lib/sessions.js +2 -3
  75. package/lib/startup-projects.js +40 -0
  76. package/lib/storage-usage.js +127 -0
  77. package/lib/users-preferences.js +0 -74
  78. package/lib/users.js +0 -4
  79. package/lib/ws-schema.js +2 -4
  80. package/lib/yoke/adapters/codex.js +8 -45
  81. package/lib/yoke/codex-model-catalog.js +40 -0
  82. package/lib/yoke/codex-rate-limits.js +26 -0
  83. package/package.json +3 -3
@@ -1,19 +1,21 @@
1
- import { initLinearSettings, loadLinearConnection, clearLinearKeyInput } from './linear-settings.js';
1
+ import { initPersonalAppearance } from './user-settings-appearance.js';
2
+ import { initPersonalVoice } from './user-settings-voice.js';
3
+ import { isolateSettingsDialog } from './server-settings-state.js';
4
+ import { initPersonalProfile } from './user-settings-profile.js';
5
+ import { initPersonalEditors } from './user-settings-editors.js';
6
+ import { initPersonalPreferences } from './user-settings-preferences.js';
7
+ import { showPinForm, hidePinForm, populateAccount, savePin, renderEmailSettings } from './user-settings-account.js';
8
+ import { buildUserSettingsLayout, resolveUserSettingsPage, loadUserSettingsExtras, finishUserSettingsExtras, guardUserSettingsLeave } from './user-settings-layout.js';
9
+ import { enhanceSettingsSelects } from './settings-select.js';
10
+ import { loadNotificationSettings } from './notification-settings.js';
2
11
  // user-settings.js — Modal dialog for user settings
3
12
  // Account management and logout
4
13
 
5
14
  import { refreshIcons } from './icons.js';
6
- import { showToast } from './utils.js';
7
- import { getChatLayout, mountThemePicker, setChatLayout } from './theme.js';
8
- import { applyTerminalFont } from './terminal-prefs.js';
9
- import { showEmailSetupModal, getEmailAccountListCache } from './context-sources.js';
10
- import { saveSpeechLanguage, speechLanguages } from './stt-settings.js';
11
- import { userAvatarUrl } from './avatar.js';
15
+ import { showEmailSetupModal } from './context-sources.js';
12
16
  import { store } from './store.js';
13
- import { getWs } from './ws-ref.js';
14
- import { initCapsulePreference, syncCapsulePreference } from './capsule-preference.js';
17
+ import { initCapsulePreference } from './capsule-preference.js';
15
18
 
16
- var ctx = null;
17
19
  var settingsEl = null;
18
20
  var openBtn = null;
19
21
  var closeBtn = null;
@@ -23,17 +25,10 @@ var sections = null;
23
25
  var pinForm = null;
24
26
  var pinSetBtn = null;
25
27
  var pinCancelBtn = null;
26
- var pinEnabled = false;
27
- var currentProfile = null;
28
28
 
29
29
 
30
- export function initUserSettings(appCtx) {
31
- window.addEventListener('clay:speech-language-saved', function (event) {
32
- var select = document.getElementById('us-lang-select');
33
- if (select) select.value = event.detail;
34
- });
35
-
36
- ctx = appCtx;
30
+ export function initUserSettings() {
31
+ buildUserSettingsLayout();
37
32
  settingsEl = document.getElementById('user-settings');
38
33
  openBtn = document.getElementById('user-settings-btn');
39
34
  closeBtn = document.getElementById('user-settings-close');
@@ -42,7 +37,7 @@ export function initUserSettings(appCtx) {
42
37
  if (!settingsEl || !openBtn) return;
43
38
 
44
39
  initCapsulePreference();
45
- initLinearSettings();
40
+
46
41
 
47
42
  navItems = settingsEl.querySelectorAll('.us-nav-item');
48
43
  sections = settingsEl.querySelectorAll('.us-section');
@@ -65,15 +60,20 @@ export function initUserSettings(appCtx) {
65
60
  }
66
61
 
67
62
  document.addEventListener('keydown', function (e) {
68
- if (e.key === 'Escape' && isUserSettingsOpen()) {
69
- closeUserSettings();
63
+ if (e.key === 'Tab' && isUserSettingsOpen() && settingsEl.contains(document.activeElement)) {
64
+ var items = Array.from(settingsEl.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href]')).filter(function (el) {return el.getClientRects().length && el.tabIndex !== -1;});
65
+ var first=items[0],last=items[items.length-1];
66
+ if(e.shiftKey && document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();}
67
+ }
68
+ if (e.key === 'Escape' && isUserSettingsOpen() && !e.defaultPrevented && !store.get('speechPopup') && !document.querySelector('.admin-modal-overlay, #settings-confirm, .avatar-positioner-overlay, #email-setup-modal')) {
69
+ e.preventDefault();closeUserSettings();
70
70
  }
71
71
  });
72
72
 
73
73
  for (var i = 0; i < navItems.length; i++) {
74
74
  navItems[i].addEventListener('click', function () {
75
75
  var section = this.dataset.section;
76
- switchSection(section);
76
+ guardUserSettingsLeave(function () { switchSection(section); });
77
77
  });
78
78
  }
79
79
 
@@ -81,7 +81,7 @@ export function initUserSettings(appCtx) {
81
81
  var navDropdown = document.getElementById('user-settings-nav-dropdown');
82
82
  if (navDropdown) {
83
83
  navDropdown.addEventListener('change', function () {
84
- switchSection(this.value);
84
+ var value = this.value; guardUserSettingsLeave(function () {switchSection(value);});
85
85
  });
86
86
  }
87
87
 
@@ -96,7 +96,7 @@ export function initUserSettings(appCtx) {
96
96
  function validatePin() {
97
97
  var validNew = /^\d{6}$/.test(pinInput.value);
98
98
  var validConfirm = /^\d{6}$/.test(pinConfirmInput.value);
99
- var validCurrent = !pinEnabled || (pinCurrentInput && /^\d{6}$/.test(pinCurrentInput.value));
99
+ var validCurrent = !store.get('userPinEnabled') || (pinCurrentInput && /^\d{6}$/.test(pinCurrentInput.value));
100
100
  pinSave.disabled = !(validNew && validConfirm && validCurrent && pinInput.value === pinConfirmInput.value);
101
101
  }
102
102
  pinInput.addEventListener('input', validatePin);
@@ -158,122 +158,15 @@ export function initUserSettings(appCtx) {
158
158
  });
159
159
  }
160
160
 
161
- var langSelect = document.getElementById('us-lang-select');
162
- if (langSelect) {
163
- langSelect.replaceChildren();
164
- speechLanguages.forEach(function (language) {
165
- var option = document.createElement('option'); option.value = language[0]; option.textContent = language[1]; langSelect.appendChild(option);
166
- });
167
- langSelect.addEventListener('change', function () {
168
- var select = this; select.disabled = true;
169
- saveSpeechLanguage(select.value).catch(function () {
170
- select.value = store.get('speechLang') || 'en-US'; showToast('Could not save voice language');
171
- }).finally(function () { select.disabled = false; });
172
- });
173
- }
174
-
175
- // Auto-continue toggle
176
- var autoContinueToggle = document.getElementById('us-auto-continue');
177
- if (autoContinueToggle) {
178
- autoContinueToggle.addEventListener('change', function () {
179
- fetch('/api/user/auto-continue', {
180
- method: 'PUT',
181
- headers: { 'Content-Type': 'application/json' },
182
- body: JSON.stringify({ enabled: this.checked }),
183
- }).then(function (r) { return r.json(); }).then(function (data) {
184
- if (data.ok) showToast(data.autoContinueOnRateLimit ? 'Auto-continue on' : 'Auto-continue off');
185
- }).catch(function () {});
186
- });
187
- }
188
-
189
- // Claude open mode toggle: GUI (default) vs TUI. The server persists the
190
- // pref per user; the WS broadcast `claude_open_mode_changed` keeps the
191
- // store and other tabs in sync.
192
- var claudeOpenModeToggle = document.getElementById('us-claude-open-mode');
193
- if (claudeOpenModeToggle) {
194
- claudeOpenModeToggle.checked = store.get('claudeOpenMode') === 'tui';
195
- claudeOpenModeToggle.addEventListener('change', function () {
196
- var want = this.checked ? 'tui' : 'gui';
197
- var ws = getWs();
198
- if (ws && ws.readyState === 1) {
199
- ws.send(JSON.stringify({ type: 'set_claude_open_mode', value: want }));
200
- showToast(want === 'tui' ? 'Claude opens as terminal' : 'Claude opens as chat');
201
- }
202
- });
203
- }
204
-
205
- // Claude auto-approve allow-list: textarea-based editor that round-trips
206
- // through the server (which writes ~/.claude/settings.json).
207
- var allowTextarea = document.getElementById('us-claude-allow-list');
208
- var allowSaveBtn = document.getElementById('us-claude-allow-save');
209
- var allowStatus = document.getElementById('us-claude-allow-status');
210
- var allowManagedEl = document.getElementById('us-claude-allow-managed');
211
- if (allowTextarea && allowSaveBtn) {
212
- // Fetch current state on open. The response (`claude_allow_list`)
213
- // populates both the textarea and the managed-defaults readout.
214
- var ws0 = getWs();
215
- if (ws0 && ws0.readyState === 1) {
216
- ws0.send(JSON.stringify({ type: 'get_claude_allow_list' }));
217
- }
218
- allowSaveBtn.addEventListener('click', function () {
219
- var lines = allowTextarea.value.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
220
- var ws = getWs();
221
- if (ws && ws.readyState === 1) {
222
- ws.send(JSON.stringify({ type: 'set_claude_user_allow_list', patterns: lines }));
223
- if (allowStatus) {
224
- allowStatus.textContent = 'Saving...';
225
- allowStatus.classList.remove('error');
226
- }
227
- }
228
- });
229
- }
230
-
231
- // This legacy preference now controls only project-local Mate DM entry
232
- // points. Home, mentions, creation, debates, and Mate data stay available.
233
- var matesEnabledToggle = document.getElementById('us-mates-enabled');
234
- if (matesEnabledToggle) {
235
- matesEnabledToggle.addEventListener('change', function () {
236
- var want = !!this.checked;
237
- applyProjectMateDmsClass(want);
238
- store.set({ projectMateDmsEnabled: want });
239
- fetch('/api/user/mates-enabled', {
240
- method: 'PUT',
241
- headers: { 'Content-Type': 'application/json' },
242
- body: JSON.stringify({ enabled: want }),
243
- }).then(function (r) { return r.json(); }).then(function (data) {
244
- if (data.ok) showToast(data.matesEnabled ? 'Mate DMs shown in projects' : 'Mate DMs hidden in projects');
245
- }).catch(function () {});
246
- });
247
- }
248
-
249
- mountThemePicker(document.getElementById('us-theme-picker'));
161
+ initPersonalPreferences();
162
+ initPersonalAppearance();
163
+ initPersonalVoice();
250
164
 
251
165
  // Terminal font picker lives in the title bar (visible only while a
252
166
  // TUI session is active). See lib/public/modules/header-tui-font.js.
253
167
  // We still seed terminal-prefs from /api/profile below so the
254
168
  // controls populate correctly when they appear.
255
169
 
256
- // Layout switcher (Bubble / Channel)
257
- var layoutSwitcher = document.getElementById('us-layout-switcher');
258
- if (layoutSwitcher) {
259
- var layoutBtns = layoutSwitcher.querySelectorAll('.layout-option');
260
- for (var li = 0; li < layoutBtns.length; li++) {
261
- layoutBtns[li].addEventListener('click', function () {
262
- var layout = this.dataset.layout;
263
- setChatLayout(layout);
264
- for (var lj = 0; lj < layoutBtns.length; lj++) {
265
- layoutBtns[lj].classList.toggle('selected', layoutBtns[lj].dataset.layout === layout);
266
- }
267
- // Save to server
268
- fetch('/api/user/chat-layout', {
269
- method: 'PUT',
270
- headers: { 'Content-Type': 'application/json' },
271
- body: JSON.stringify({ layout: layout }),
272
- }).catch(function () {});
273
- });
274
- }
275
- }
276
-
277
170
  // Logout button
278
171
  var logoutBtn = document.getElementById('us-logout-btn');
279
172
  if (logoutBtn) {
@@ -286,32 +179,37 @@ export function initUserSettings(appCtx) {
286
179
  });
287
180
  }
288
181
 
289
- // Email: Add Account button
290
- var emailAddBtn = document.getElementById('us-email-add');
291
- if (emailAddBtn) {
292
- emailAddBtn.addEventListener('click', function () {
293
- showEmailSetupModal();
294
- });
295
- }
182
+ initPersonalEditors();
183
+ initPersonalProfile();
184
+
185
+ document.querySelectorAll('[data-email-provider]').forEach(function (button) {
186
+ button.addEventListener('click', function () { showEmailSetupModal(button.dataset.emailProvider, document.getElementById("us-integration-setup")); });
187
+ });
296
188
  }
297
189
 
298
190
  // Optional `initialSection` lets callers deep-link directly into a specific
299
191
  // settings tab (e.g. the disabled-Mates picker button passing 'us-mates').
300
192
  // Falls back to Account so existing callers and the icon click are unchanged.
301
193
  export function openUserSettings(initialSection) {
302
- loadLinearConnection();
194
+ if(!isUserSettingsOpen())store.set({ userSettingsReturnFocus: document.activeElement, userSettingsRestore: isolateSettingsDialog(settingsEl) });
195
+
303
196
  settingsEl.classList.remove('hidden');
304
197
  openBtn.classList.add('active');
305
198
  refreshIcons(settingsEl);
306
199
  populateAccount();
307
- hidePinForm();
308
200
  switchSection(initialSection || 'us-account');
201
+ enhanceSettingsSelects(settingsEl);
202
+ var title=settingsEl.querySelector(".us-section.active h2"); if(title){title.tabIndex=-1;title.focus();}
309
203
  }
310
204
 
311
205
  export function closeUserSettings() {
312
- clearLinearKeyInput();
313
- settingsEl.classList.add('hidden');
314
- openBtn.classList.remove('active');
206
+ guardUserSettingsLeave(function () {
207
+ finishUserSettingsExtras();
208
+ settingsEl.classList.add('hidden');
209
+ var restore=store.get('userSettingsRestore');if(restore)restore();store.set({userSettingsRestore:null});
210
+ openBtn.classList.remove('active');
211
+ var focus = store.get('userSettingsReturnFocus'); if (focus && focus.isConnected) focus.focus();
212
+ });
315
213
  }
316
214
 
317
215
  export function isUserSettingsOpen() {
@@ -319,356 +217,31 @@ export function isUserSettingsOpen() {
319
217
  }
320
218
 
321
219
  function switchSection(sectionName) {
220
+ sectionName = resolveUserSettingsPage(sectionName);
221
+ loadUserSettingsExtras(sectionName);
322
222
  for (var i = 0; i < navItems.length; i++) {
323
223
  navItems[i].classList.toggle('active', navItems[i].dataset.section === sectionName);
224
+ if(navItems[i].dataset.section===sectionName)navItems[i].setAttribute('aria-current','page');else navItems[i].removeAttribute('aria-current');
324
225
  }
325
226
  for (var j = 0; j < sections.length; j++) {
326
227
  sections[j].classList.toggle('active', sections[j].dataset.section === sectionName);
327
228
  }
328
229
  var navDropdown = document.getElementById('user-settings-nav-dropdown');
329
230
  if (navDropdown) navDropdown.value = sectionName;
330
- if (sectionName === 'us-email') renderEmailSettings();
231
+ if (sectionName === 'us-notifications') loadNotificationSettings();
232
+ if (sectionName === 'us-integrations') renderEmailSettings();
233
+ var heading=settingsEl.querySelector('.us-section.active h2');if(heading){heading.tabIndex=-1;heading.focus();}
331
234
  }
332
235
 
333
236
  function stopProp(e) {
334
237
  e.stopPropagation();
335
238
  }
336
239
 
337
- // Project-only Mate DM gate. The persisted server field retains its legacy
338
- // name for migration compatibility, but it no longer disables Mate features.
339
- function applyProjectMateDmsClass(enabled) {
340
- if (!document.body) return;
341
- document.body.classList.toggle('project-mate-dms-disabled', !enabled);
342
- }
343
-
344
240
  export function isProjectMateDmsEnabled() {
345
241
  // Default to true until the profile arrives so boot does not briefly hide
346
242
  // a project shortcut that the persisted preference enables.
347
- return store.get('projectMateDmsEnabled') !== false;
348
- }
349
-
350
- function showPinForm() {
351
- if (!pinForm) return;
352
- pinForm.classList.remove('hidden');
353
- var pinCurrentRow = document.getElementById('us-pin-current-row');
354
- var pinInput = document.getElementById('us-pin-input');
355
- var pinConfirmInput = document.getElementById('us-pin-confirm-input');
356
- var pinCurrentInput = document.getElementById('us-pin-current-input');
357
- var pinSave = document.getElementById('us-pin-save');
358
- var pinMsg = document.getElementById('us-pin-msg');
359
- var pinFormTitle = document.getElementById('us-pin-form-title');
360
- if (pinFormTitle) {
361
- pinFormTitle.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
362
- }
363
- if (pinCurrentRow) {
364
- pinCurrentRow.classList.toggle('hidden', !pinEnabled);
365
- }
366
- if (pinInput) {
367
- pinInput.value = '';
368
- }
369
- if (pinConfirmInput) pinConfirmInput.value = '';
370
- if (pinCurrentInput) pinCurrentInput.value = '';
371
- if (pinSave) pinSave.disabled = true;
372
- if (pinSave) pinSave.textContent = 'Save';
373
- if (pinMsg) {
374
- pinMsg.textContent = '';
375
- pinMsg.className = 'settings-hint settings-pin-error hidden';
376
- }
377
- if (pinEnabled && pinCurrentInput) {
378
- pinCurrentInput.focus();
379
- } else if (pinInput) {
380
- pinInput.focus();
381
- }
243
+ return true;
382
244
  }
383
245
 
384
- function hidePinForm() {
385
- if (!pinForm) return;
386
- pinForm.classList.add('hidden');
387
- var pinCurrentRow = document.getElementById('us-pin-current-row');
388
- var pinInput = document.getElementById('us-pin-input');
389
- var pinConfirmInput = document.getElementById('us-pin-confirm-input');
390
- var pinCurrentInput = document.getElementById('us-pin-current-input');
391
- var pinSave = document.getElementById('us-pin-save');
392
- var pinMsg = document.getElementById('us-pin-msg');
393
- if (pinCurrentRow) {
394
- pinCurrentRow.classList.remove('hidden');
395
- }
396
- if (pinInput) pinInput.value = '';
397
- if (pinConfirmInput) pinConfirmInput.value = '';
398
- if (pinCurrentInput) pinCurrentInput.value = '';
399
- if (pinSave) pinSave.disabled = true;
400
- if (pinSave) pinSave.textContent = 'Save';
401
- if (pinMsg) {
402
- pinMsg.textContent = '';
403
- pinMsg.className = 'settings-hint settings-pin-error hidden';
404
- }
405
- }
406
-
407
- // --- Account population ---
408
246
 
409
- function populateAccount() {
410
- fetch('/api/profile').then(function (r) {
411
- if (!r.ok) return null;
412
- return r.json();
413
- }).then(function (data) {
414
- if (!data) return;
415
- currentProfile = data;
416
- var displayName = data.name || data.displayName || data.username || 'Clay User';
417
- var avatarImg = document.getElementById('us-account-avatar-img');
418
- var avatarFallback = document.getElementById('us-account-avatar-fallback');
419
- var accountName = document.getElementById('us-account-name');
420
- var accountSubline = document.getElementById('us-account-subline');
421
- var langSelect = document.getElementById('us-lang-select');
422
- var pinSetBtnEl = document.getElementById('us-pin-set-btn');
423
- var pinFormTitle = document.getElementById('us-pin-form-title');
424
- if (accountName) {
425
- accountName.textContent = displayName;
426
- }
427
- if (accountSubline) {
428
- var parts = [];
429
- if (data.username) parts.push('@' + data.username);
430
- if (data.role) parts.push(data.role.charAt(0).toUpperCase() + data.role.slice(1));
431
- accountSubline.textContent = parts.length ? parts.join(' · ') : 'Local profile';
432
- }
433
- pinEnabled = !!data.pinEnabled;
434
- if (pinSetBtnEl) {
435
- pinSetBtnEl.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
436
- }
437
- if (pinFormTitle) {
438
- pinFormTitle.textContent = pinEnabled ? 'Change PIN' : 'Set PIN';
439
- }
440
- if (langSelect) {
441
- langSelect.value = store.get('speechLang') || 'en-US';
442
- }
443
- if (avatarImg && avatarFallback) {
444
- var avatarUrl = getCurrentUserAvatarSrc(data);
445
- avatarImg.alt = displayName;
446
- avatarImg.classList.remove('hidden');
447
- avatarFallback.classList.remove('hidden');
448
- avatarFallback.textContent = avatarInitials(displayName);
449
- avatarImg.onload = function () {
450
- avatarFallback.classList.add('hidden');
451
- };
452
- avatarImg.onerror = function () {
453
- avatarFallback.classList.remove('hidden');
454
- };
455
- avatarImg.src = avatarUrl;
456
- if (avatarImg.complete && avatarImg.naturalWidth > 0) {
457
- avatarFallback.classList.add('hidden');
458
- }
459
- }
460
- // Hide account section in single-user mode (no username)
461
- var accountNav = settingsEl.querySelector('[data-section="us-account"]');
462
- if (accountNav) accountNav.style.display = data.username ? '' : 'none';
463
- // Terminal font: seed in-memory prefs from server. Header-mounted
464
- // controls (header-tui-font.js) listen to applyTerminalFont and
465
- // sync their UI - we don't touch any DOM here.
466
- if (data.terminalFont && typeof data.terminalFont === "object") {
467
- applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
468
- }
469
- // Auto-continue toggle
470
- var acToggle = document.getElementById('us-auto-continue');
471
- if (acToggle) acToggle.checked = !!data.autoContinueOnRateLimit;
472
- // Theme picker self-syncs active selection via theme.js's applyTheme
473
- // change callbacks (updatePickerActive). Nothing to do here.
474
- // Layout switcher: sync from server response
475
- // Sync mate onboarding state from server
476
- if (data.mateOnboardingShown) {
477
- try { localStorage.setItem("clay-mate-onboarding-shown", "1"); } catch (e) {}
478
- }
479
- // Project Mate DM shortcuts remain opt-in for legacy profiles.
480
- var matesOn = data.matesEnabled === true;
481
- var matesToggle = document.getElementById('us-mates-enabled');
482
- if (matesToggle) matesToggle.checked = matesOn;
483
- store.set({ projectMateDmsEnabled: matesOn });
484
- applyProjectMateDmsClass(matesOn);
485
- syncCapsulePreference(data.capsulesEnabled === true);
486
- if (data.chatLayout) {
487
- setChatLayout(data.chatLayout); // update local cache + CSS
488
- }
489
- var lSwitcher = document.getElementById('us-layout-switcher');
490
- if (lSwitcher) {
491
- var currentLayout = getChatLayout();
492
- var lBtns = lSwitcher.querySelectorAll('.layout-option');
493
- for (var li = 0; li < lBtns.length; li++) {
494
- lBtns[li].classList.toggle('selected', lBtns[li].dataset.layout === currentLayout);
495
- }
496
- }
497
- }).catch(function () {});
498
- }
499
-
500
- function saveProfileChange(updates) {
501
- if (!currentProfile) return;
502
- var nextProfile = {};
503
- var payload = {};
504
- var key;
505
- for (key in currentProfile) {
506
- if (Object.prototype.hasOwnProperty.call(currentProfile, key)) {
507
- nextProfile[key] = currentProfile[key];
508
- }
509
- }
510
- for (key in updates) {
511
- if (Object.prototype.hasOwnProperty.call(updates, key)) {
512
- nextProfile[key] = updates[key];
513
- }
514
- }
515
- currentProfile = nextProfile;
516
- payload.name = nextProfile.name;
517
- payload.lang = nextProfile.lang;
518
- payload.avatarColor = nextProfile.avatarColor;
519
- payload.avatarStyle = nextProfile.avatarStyle;
520
- payload.avatarSeed = nextProfile.avatarSeed;
521
- payload.avatarCustom = nextProfile.avatarCustom;
522
- fetch('/api/profile', {
523
- method: 'PUT',
524
- headers: { 'Content-Type': 'application/json' },
525
- body: JSON.stringify(payload),
526
- }).catch(function () {});
527
- }
528
-
529
- function avatarInitials(value) {
530
- var text = (value || '').trim();
531
- if (!text) return '?';
532
- var parts = text.split(/\s+/);
533
- var first = parts[0].charAt(0);
534
- var second = parts.length > 1 ? parts[1].charAt(0) : parts[0].charAt(1);
535
- var out = first + (second || '');
536
- return out.toUpperCase();
537
- }
538
-
539
- function getCurrentUserAvatarSrc(data) {
540
- var islandAvatar = document.querySelector('.user-island-avatar img');
541
- if (islandAvatar && islandAvatar.src) return islandAvatar.src;
542
- if (data && data.avatarCustom) return data.avatarCustom;
543
- return userAvatarUrl(data, 96);
544
- }
545
-
546
- function savePin(pin) {
547
- var pinInput = document.getElementById('us-pin-input');
548
- var pinConfirmInput = document.getElementById('us-pin-confirm-input');
549
- var pinCurrentInput = document.getElementById('us-pin-current-input');
550
- var pinSave = document.getElementById('us-pin-save');
551
- var pinMsg = document.getElementById('us-pin-msg');
552
- var currentPin = pinCurrentInput ? pinCurrentInput.value.trim() : '';
553
- var confirmPin = pinConfirmInput ? pinConfirmInput.value.trim() : '';
554
- var newPin = pinInput ? pinInput.value.trim() : '';
555
-
556
- if (!/^\d{6}$/.test(newPin)) {
557
- if (pinMsg) {
558
- pinMsg.textContent = 'PIN must be exactly 6 digits.';
559
- pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
560
- pinMsg.classList.remove('hidden');
561
- }
562
- return;
563
- }
564
- if (confirmPin !== newPin) {
565
- if (pinMsg) {
566
- pinMsg.textContent = 'PINs do not match.';
567
- pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
568
- pinMsg.classList.remove('hidden');
569
- }
570
- return;
571
- }
572
- if (pinEnabled && !/^\d{6}$/.test(currentPin)) {
573
- if (pinMsg) {
574
- pinMsg.textContent = 'Current PIN is required.';
575
- pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
576
- pinMsg.classList.remove('hidden');
577
- }
578
- return;
579
- }
580
-
581
- pinSave.disabled = true;
582
- pinSave.textContent = 'Saving\u2026';
583
-
584
- fetch('/api/user/pin', {
585
- method: 'PUT',
586
- headers: { 'Content-Type': 'application/json' },
587
- body: JSON.stringify({ currentPin: currentPin, newPin: newPin }),
588
- }).then(function (r) { return r.json(); }).then(function (data) {
589
- if (data.ok) {
590
- showToast('PIN changed');
591
- hidePinForm();
592
- } else {
593
- pinSave.disabled = false;
594
- pinSave.textContent = 'Save';
595
- if (pinMsg) {
596
- pinMsg.textContent = data.error || 'Could not change your PIN. Please try again.';
597
- pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
598
- pinMsg.classList.remove('hidden');
599
- }
600
- }
601
- }).catch(function () {
602
- pinSave.disabled = false;
603
- pinSave.textContent = 'Save';
604
- if (pinMsg) {
605
- pinMsg.textContent = 'Connection lost. Check your network and try again.';
606
- pinMsg.className = 'settings-hint settings-pin-error us-pin-msg-err';
607
- pinMsg.classList.remove('hidden');
608
- }
609
- });
610
- }
611
-
612
- // --- Email settings ---
613
-
614
- function renderEmailSettings() {
615
- var listEl = document.getElementById('us-email-list');
616
- if (!listEl) return;
617
- listEl.innerHTML = '';
618
-
619
- var accounts = getEmailAccountListCache();
620
- if (!accounts || accounts.length === 0) {
621
- var empty = document.createElement('div');
622
- empty.className = 'settings-field';
623
- empty.innerHTML = '<div class="us-email-empty">No email accounts connected yet.</div>';
624
- listEl.appendChild(empty);
625
- return;
626
- }
627
-
628
- for (var i = 0; i < accounts.length; i++) {
629
- var acc = accounts[i];
630
- var row = document.createElement('div');
631
- row.className = 'us-email-row';
632
- row.innerHTML =
633
- '<div class="us-email-icon">' + providerIcon(acc.provider) + '</div>' +
634
- '<div class="us-email-info">' +
635
- '<div class="us-email-addr">' + escHtml(acc.email) + '</div>' +
636
- '<div class="us-email-provider">' + escHtml(acc.label || acc.provider || 'Custom') + '</div>' +
637
- '</div>' +
638
- '<button class="us-email-remove-btn" data-account-id="' + escHtml(acc.id) + '">Remove</button>';
639
-
640
- var removeBtn = row.querySelector('button');
641
- removeBtn.addEventListener('click', function () {
642
- var accountId = this.getAttribute('data-account-id');
643
- if (ctx && ctx.ws && ctx.connected) {
644
- ctx.ws.send(JSON.stringify({ type: 'email_account_remove', accountId: accountId }));
645
- }
646
- });
647
-
648
- listEl.appendChild(row);
649
- }
650
- }
651
-
652
- export function refreshEmailSettings() {
653
- if (isUserSettingsOpen()) renderEmailSettings();
654
- }
655
-
656
- var PROVIDER_ICON_PATHS = {
657
- gmail: '/icons/email/gmail.svg',
658
- outlook: '/icons/email/outlook.svg',
659
- yahoo: '/icons/email/yahoo.svg',
660
- };
661
-
662
- function providerIcon(provider) {
663
- var src = PROVIDER_ICON_PATHS[provider];
664
- if (src) {
665
- return '<img src="' + src + '" class="us-email-provider-icon" alt="' + provider + '">';
666
- }
667
- return '<i data-lucide="mail" class="us-email-provider-icon-fallback"></i>';
668
- }
669
-
670
- function escHtml(str) {
671
- var d = document.createElement('div');
672
- d.textContent = str;
673
- return d.innerHTML;
674
- }
247
+ export function refreshEmailSettings() { if (isUserSettingsOpen()) renderEmailSettings(); }
@@ -82,3 +82,7 @@
82
82
  @import "css/you.css";
83
83
 
84
84
  @import url("css/mate-workspace-color.css");
85
+
86
+ @import url("css/server-settings-system.css");
87
+
88
+ @import url("css/project-settings-modal.css");