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,81 @@
1
+ import { store } from './store.js';
2
+
3
+ function status(text) { var el = document.getElementById('personal-notification-status'); if (el) el.textContent = text; }
4
+ function apply(values) {
5
+ store.set({ notificationPreferences: values });
6
+ ['alert', 'sound'].forEach(function (key) {
7
+ var source = document.getElementById('notif-toggle-' + key);
8
+ var target = document.getElementById('settings-notif-' + key);
9
+ if (source) source.checked = values[key];
10
+ if (target) target.checked = values[key];
11
+ });
12
+ window.dispatchEvent(new CustomEvent('clay:notification-preferences', { detail: values }));
13
+ }
14
+ export async function loadNotificationSettings() {
15
+ if (store.get('notificationDraft') || store.get('notificationSaving')) return;
16
+ try {
17
+ var response = await fetch('/api/user/notifications');
18
+ var data = await response.json();
19
+ if (!response.ok) throw new Error(data.error);
20
+ if (store.get('notificationDraft') || store.get('notificationSaving')) return;
21
+ apply(data);
22
+ if (!data.configured) {
23
+ var legacyAlert = localStorage.getItem('notif-alert');
24
+ var legacySound = localStorage.getItem('notif-sound');
25
+ if (legacyAlert !== null || legacySound !== null) {
26
+ store.set({ notificationPreferences: { alert: legacyAlert !== '0', sound: legacySound !== '0' } });
27
+ await saveNotificationPreference('alert', legacyAlert !== '0');
28
+ return;
29
+ }
30
+ }
31
+ status('Preferences saved on this server. Browser permission: ' + ('Notification' in window ? Notification.permission : 'unsupported') + '.');
32
+ } catch (error) { status(error.message || 'Notifications unavailable. Reopen Personal settings to retry.'); }
33
+ }
34
+ export async function saveNotificationPreference(key, value) {
35
+ if (store.get('notificationSaving')) return;
36
+ var saved = store.get('notificationDraft') || store.get('notificationPreferences');
37
+ if (!saved) { status('Preferences have not loaded. Reopen Personal settings to retry.'); return; }
38
+ var draft = Object.assign({}, saved); draft[key] = value;
39
+ store.set({ notificationDraft: draft, notificationSaving: true });
40
+ setPending(true);
41
+ status('Saving…');
42
+ try {
43
+ var response = await fetch('/api/user/notifications', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Clay-Settings': '1' }, body: JSON.stringify(draft) });
44
+ var data = await response.json();
45
+ if (!response.ok) throw new Error(data.error);
46
+ store.set({ notificationDraft: null });
47
+ localStorage.removeItem('notif-alert'); localStorage.removeItem('notif-sound');
48
+ apply(data); status('Saved. Browser permissions and push subscriptions are managed separately on this device.');
49
+ } catch (error) { status(error.message || 'Could not save. Your selection is retained; retry below.'); }
50
+ finally { store.set({ notificationSaving: false }); setPending(false); }
51
+ }
52
+ function setPending(pending) {
53
+ ['settings-notif-alert', 'settings-notif-sound', 'notif-toggle-alert', 'notif-toggle-sound'].forEach(function (id) {
54
+ var input = document.getElementById(id); if (input) input.disabled = pending;
55
+ });
56
+ var retry = document.getElementById('notification-save-retry');
57
+ if (retry) retry.hidden = pending || !store.get('notificationDraft');
58
+ }
59
+ export function initNotificationSettings() {
60
+ var retry = document.createElement('button'); retry.id = 'notification-save-retry'; retry.textContent = 'Retry save'; retry.className = 'settings-btn-sm'; retry.hidden = true;
61
+ retry.onclick = function () { var draft = store.get('notificationDraft'); if (draft) saveNotificationPreference('alert', draft.alert); };
62
+ document.getElementById('personal-notification-status').insertAdjacentElement('afterend', retry);
63
+ ['alert', 'sound', 'push'].forEach(function (key) {
64
+ var target = document.getElementById('settings-notif-' + key);
65
+ if (!target) return;
66
+ target.onchange = function () {
67
+ var source = document.getElementById('notif-toggle-' + key);
68
+ if (!source) { target.checked = false; status('Push requires the installed app on this device.'); return; }
69
+ source.checked = target.checked;
70
+ source.dispatchEvent(new Event('change', { bubbles: true }));
71
+ };
72
+ });
73
+ var push = document.getElementById('settings-notif-push');
74
+ push.disabled = !('serviceWorker' in navigator) || !window.isSecureContext || !('PushManager' in window);
75
+ loadNotificationSettings();
76
+ }
77
+ export function updatePushSettings(checked, message, pending) {
78
+ var input = document.getElementById('settings-notif-push');
79
+ if (input) { input.checked = checked; if (pending !== undefined) input.disabled = pending; }
80
+ if (message) status(message);
81
+ }
@@ -1,3 +1,4 @@
1
+ import { initNotificationSettings, saveNotificationPreference, updatePushSettings } from './notification-settings.js';
1
2
  import { initExtensionSettings } from './extension-settings.js';
2
3
  import { copyToClipboard, showToast } from './utils.js';
3
4
  import { iconHtml, refreshIcons } from './icons.js';
@@ -193,9 +194,8 @@ export function initNotifications(_ctx) {
193
194
  if (settingsUpdateCheck) {
194
195
  settingsUpdateCheck.addEventListener("click", function (e) {
195
196
  e.stopPropagation();
196
- if (ctx.ws && ctx.connected) {
197
- ctx.ws.send(JSON.stringify({ type: "check_update" }));
198
- }
197
+ if (!ctx.ws || ctx.ws.readyState !== 1) { showToast("Reconnect before checking for updates."); return; }
198
+ ctx.ws.send(JSON.stringify({ type: "check_update" }));
199
199
  setUpdateBtn("Checking…", true, true);
200
200
  });
201
201
  }
@@ -222,10 +222,14 @@ export function initNotifications(_ctx) {
222
222
  })();
223
223
  var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
224
224
 
225
+ window.addEventListener('clay:notification-preferences', function (event) {
226
+ notifAlertEnabled = event.detail.alert;
227
+ notifSoundEnabled = event.detail.sound;
228
+ });
225
229
  // --- Browser notifications ---
226
230
  notifPermission = ("Notification" in window) ? Notification.permission : "denied";
227
- notifAlertEnabled = localStorage.getItem("notif-alert") !== "0";
228
- notifSoundEnabled = localStorage.getItem("notif-sound") !== "0";
231
+ notifAlertEnabled = true;
232
+ notifSoundEnabled = true;
229
233
 
230
234
  var notifBtn = $("notif-btn");
231
235
  var notifMenu = $("notif-menu");
@@ -234,7 +238,7 @@ export function initNotifications(_ctx) {
234
238
 
235
239
  if (notifAlertEnabled && "Notification" in window && Notification.permission === "denied") {
236
240
  notifAlertEnabled = false;
237
- localStorage.setItem("notif-alert", "0");
241
+
238
242
  }
239
243
  notifToggleAlert.checked = notifAlertEnabled;
240
244
  notifToggleSound.checked = notifSoundEnabled;
@@ -256,23 +260,24 @@ export function initNotifications(_ctx) {
256
260
 
257
261
  notifToggleAlert.addEventListener("change", function () {
258
262
  notifAlertEnabled = notifToggleAlert.checked;
259
- localStorage.setItem("notif-alert", notifAlertEnabled ? "1" : "0");
263
+ saveNotificationPreference("alert", notifAlertEnabled);
260
264
  notifBlockedHint.classList.add("hidden");
261
265
  if (notifAlertEnabled && notifPermission !== "granted") {
262
266
  if ("Notification" in window && Notification.permission === "denied") {
263
267
  notifAlertEnabled = false;
264
268
  notifToggleAlert.checked = false;
265
- localStorage.setItem("notif-alert", "0");
269
+
266
270
  notifBlockedHint.classList.remove("hidden");
267
271
  refreshIcons();
268
272
  return;
269
273
  }
274
+ if (!("Notification" in window)) { notifToggleAlert.checked = false; return; }
270
275
  Notification.requestPermission().then(function (p) {
271
276
  notifPermission = p;
272
277
  if (p !== "granted") {
273
278
  notifAlertEnabled = false;
274
279
  notifToggleAlert.checked = false;
275
- localStorage.setItem("notif-alert", "0");
280
+
276
281
  notifBlockedHint.classList.remove("hidden");
277
282
  refreshIcons();
278
283
  }
@@ -327,7 +332,7 @@ export function initNotifications(_ctx) {
327
332
 
328
333
  notifToggleSound.addEventListener("change", function () {
329
334
  notifSoundEnabled = notifToggleSound.checked;
330
- localStorage.setItem("notif-sound", notifSoundEnabled ? "1" : "0");
335
+ saveNotificationPreference("sound", notifSoundEnabled);
331
336
  });
332
337
 
333
338
  // --- Push notifications toggle ---
@@ -376,14 +381,18 @@ export function initNotifications(_ctx) {
376
381
  if (prevEndpoint && prevEndpoint !== sub.endpoint) {
377
382
  payload.replaceEndpoint = prevEndpoint;
378
383
  }
379
- if (ctx.ws && ctx.ws.readyState === 1) {
380
- ctx.ws.send(JSON.stringify({ type: "push_subscribe", subscription: json, replaceEndpoint: payload.replaceEndpoint || null }));
381
- } else {
382
- fetch(basePath + "api/push-subscribe", {
383
- method: "POST", headers: { "Content-Type": "application/json" },
384
- credentials: "same-origin", body: JSON.stringify(payload),
385
- });
386
- }
384
+ updatePushSettings(true, 'Registering push on this device…', true);
385
+ return fetch('/api/push-subscribe', {
386
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
387
+ credentials: 'same-origin', body: JSON.stringify(payload)
388
+ }).then(function (response) {
389
+ if (!response.ok) throw new Error('Registration failed');
390
+ updatePushSettings(true, 'Push enabled on this device.', false);
391
+ return true;
392
+ }).catch(function () {
393
+ updatePushSettings(false, 'Could not register push with Clay. Enable it again to retry.', false);
394
+ return false;
395
+ });
387
396
  }
388
397
 
389
398
  function subscribePush() {
@@ -398,8 +407,9 @@ export function initNotifications(_ctx) {
398
407
  return reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: key });
399
408
  });
400
409
  }).then(function (sub) {
401
- sendPushSubscription(sub);
402
- localStorage.setItem("notif-push", "1");
410
+ return sendPushSubscription(sub);
411
+ }).then(function (registered) {
412
+ if (!registered) return;
403
413
  hideOnboarding();
404
414
  localStorage.setItem("onboarding-dismissed", "1");
405
415
  // Show a welcome notification so the user knows it works
@@ -411,18 +421,19 @@ export function initNotifications(_ctx) {
411
421
  }).catch(function () {});
412
422
  }).catch(function () {
413
423
  notifTogglePush.checked = false;
414
- localStorage.setItem("notif-push", "0");
424
+ updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
415
425
  notifBlockedHint.classList.remove("hidden");
416
426
  refreshIcons();
417
427
  });
418
428
  }
419
429
 
420
430
  function unsubscribePush() {
421
- if (window._pushSubscription) {
422
- window._pushSubscription.unsubscribe().catch(function () {});
431
+ var subscription = window._pushSubscription;
432
+ updatePushSettings(false, 'Disabling push on this device…', true);
433
+ Promise.resolve(subscription ? subscription.unsubscribe() : true).then(function () {
423
434
  window._pushSubscription = null;
424
- }
425
- localStorage.setItem("notif-push", "0");
435
+ updatePushSettings(false, 'Push is off on this device.', false);
436
+ }).catch(function () { notifTogglePush.checked = true; updatePushSettings(true, 'Could not disable push. Retry on this device.', false); });
426
437
  }
427
438
 
428
439
  notifTogglePush.addEventListener("change", function () {
@@ -434,6 +445,8 @@ export function initNotifications(_ctx) {
434
445
  }
435
446
  });
436
447
 
448
+ initNotificationSettings();
449
+
437
450
  // --- Service Worker registration & push state sync ---
438
451
  (function initServiceWorker() {
439
452
  if (!("serviceWorker" in navigator)) return;
@@ -464,6 +477,7 @@ export function initNotifications(_ctx) {
464
477
 
465
478
  return Promise.all([reg.pushManager.getSubscription(), vapidPromise]).then(function (results) {
466
479
  var sub = results[0];
480
+ var hadSubscription = !!sub;
467
481
  var serverKey = results[1];
468
482
 
469
483
  // If subscription exists but VAPID key changed, unsubscribe and re-subscribe
@@ -479,9 +493,10 @@ export function initNotifications(_ctx) {
479
493
  if (sub) {
480
494
  window._pushSubscription = sub;
481
495
  notifTogglePush.checked = true;
496
+ updatePushSettings(true);
482
497
  sendPushSubscription(sub);
483
498
  hideOnboarding();
484
- } else if (serverKey && localStorage.getItem("notif-push") === "1") {
499
+ } else if (serverKey && hadSubscription) {
485
500
  // Had push enabled but subscription is gone (VAPID key change), re-subscribe
486
501
  var raw = atob(serverKey.replace(/-/g, "+").replace(/_/g, "/"));
487
502
  var key = new Uint8Array(raw.length);
@@ -490,13 +505,14 @@ export function initNotifications(_ctx) {
490
505
  .then(function (newSub) {
491
506
  sendPushSubscription(newSub);
492
507
  notifTogglePush.checked = true;
508
+ updatePushSettings(true);
493
509
  }).catch(function () {
494
510
  notifTogglePush.checked = false;
495
- localStorage.setItem("notif-push", "0");
511
+ updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
496
512
  });
497
513
  } else {
498
514
  notifTogglePush.checked = false;
499
- localStorage.setItem("notif-push", "0");
515
+ updatePushSettings(false, "Push is off on this device. Enable it here to retry.");
500
516
  // Standalone (PWA) without push: redirect to setup for push onboarding
501
517
  // Skip if setup was just completed (setup-done flag)
502
518
  var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
@@ -0,0 +1,51 @@
1
+ import { store } from './store.js';
2
+ import { confirmSettingsAction, isolateSettingsDialog, trapSettingsFocus } from './server-settings-state.js';
3
+ import { enhanceSettingsSelects } from './settings-select.js';
4
+
5
+ // Keep contextual connection forms transactional when opened from settings.
6
+ export function preparePersonalConnectionDialog(overlay, inline) {
7
+ var previous = document.activeElement;
8
+ var restore = inline ? function () {} : isolateSettingsDialog(overlay);
9
+ var dirty = false;
10
+ var dialog = overlay.querySelector('.email-setup-dialog');
11
+ dialog.setAttribute('role', inline ? 'region' : 'dialog');
12
+ if (!inline) dialog.setAttribute('aria-modal', 'true');
13
+ dialog.setAttribute('aria-label', 'Add personal mailbox');
14
+ overlay.querySelectorAll('.email-setup-field').forEach(function (field) {
15
+ var label = field.querySelector('label');
16
+ var input = field.querySelector('input,select');
17
+ if (label && input) label.htmlFor = input.id;
18
+ });
19
+ enhanceSettingsSelects(overlay);
20
+ function markDirty() { dirty = true; if (inline) store.set({personalEmailDirty:true}); }
21
+ overlay.addEventListener('input', markDirty);
22
+ overlay.addEventListener('change', markDirty);
23
+ var unsubscribe = inline ? store.subscribe(function (state, previousState) {
24
+ if (state.myUserId !== previousState.myUserId) close();
25
+ }) : function () {};
26
+ function cleanup() { observer.disconnect(); unsubscribe(); restore(); if (inline) store.set({personalEmailDirty:false}); }
27
+ function close() {
28
+ cleanup();overlay.remove();
29
+ if (previous && previous.isConnected) previous.focus();
30
+ }
31
+ function leave() {
32
+ if (dirty) confirmSettingsAction('Discard this unsaved mailbox connection?', 'Discard', close);
33
+ else close();
34
+ }
35
+ overlay.addEventListener('click', function (event) {
36
+ if (!inline && event.target === overlay || event.target.closest('#email-setup-cancel')) {
37
+ event.preventDefault();event.stopImmediatePropagation();leave();
38
+ }
39
+ }, true);
40
+ overlay.addEventListener('keydown', function (event) {
41
+ if (!inline || event.key === 'Escape') event.stopPropagation();
42
+ if (event.key === 'Escape' && !event.defaultPrevented) {event.preventDefault();leave();}
43
+ if (!inline) trapSettingsFocus(event, overlay);
44
+ });
45
+ var observer = new MutationObserver(function () {
46
+ if (!overlay.isConnected) {cleanup();if(previous && previous.isConnected)previous.focus();}
47
+ });
48
+ observer.observe(inline ? overlay.parentElement : document.body,{childList:true});
49
+ (overlay.querySelector('#email-setup-address') || overlay.querySelector('button')).focus();
50
+ return close;
51
+ }
@@ -1,3 +1,4 @@
1
+ import { setChatLayout } from './theme.js';
1
2
  // User profile module — profile popovers for people and Mates
2
3
  // Stores profile server-side in ~/.clay/profile.json
3
4
  // Generated identities use deterministic local pixel identicons.
@@ -7,7 +8,6 @@ import { avatarUrl, mateAvatarUrl, mateMarkUrl, userAvatarSeed } from './avatar.
7
8
  import { store } from './store.js';
8
9
  import { applyTerminalFont } from './terminal-prefs.js';
9
10
 
10
- var ctx;
11
11
  var profile = { name: '', lang: 'en-US', avatarStyle: 'imprint', avatarSeed: '', avatarColor: '#5857fc', avatarCustom: '' };
12
12
  var profileUsername = '';
13
13
  var popoverEl = null;
@@ -87,6 +87,16 @@ export function applyToIsland() {
87
87
  document.dispatchEvent(new CustomEvent('clay:profile-updated'));
88
88
  }
89
89
 
90
+ export async function saveProfileUpdates(updates) {
91
+ var next = Object.assign({}, profile, updates);
92
+ var response = await fetch('/api/profile', {method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(next)});
93
+ var result = await response.json();
94
+ if (!response.ok || result.error) throw new Error(result.error || 'Could not save your profile. Your draft is retained.');
95
+ profile = next;
96
+ applyToIsland();
97
+ return result;
98
+ }
99
+
90
100
  export function updateProfileName(name) {
91
101
  profile.name = String(name || '').trim();
92
102
  applyToIsland();
@@ -528,8 +538,7 @@ function escapeAttr(str) {
528
538
  }
529
539
 
530
540
  // --- Init ---
531
- export function initProfile(_ctx) {
532
- ctx = _ctx;
541
+ export function initProfile() {
533
542
 
534
543
  var island = document.getElementById('user-island');
535
544
  if (!island) return;
@@ -556,13 +565,13 @@ export function initProfile(_ctx) {
556
565
  // when the user opened the Settings modal (user-settings.js's
557
566
  // populateAccount), so a hard refresh would drop persisted picks back
558
567
  // to defaults until the modal was visited.
568
+ if(data.chatLayout)setChatLayout(data.chatLayout);
559
569
  if (data.terminalFont && typeof data.terminalFont === "object") {
560
570
  applyTerminalFont(data.terminalFont.family, data.terminalFont.size);
561
571
  }
562
572
 
563
- // Apply the project-only Mate DM shortcut preference at boot. Home and
564
- // the rest of the Mate capability surface remain available.
565
- var matesOn = data.matesEnabled === true;
573
+ // Keep project Mate DM shortcuts available for legacy profiles too.
574
+ var matesOn = true;
566
575
  store.set({ projectMateDmsEnabled: matesOn });
567
576
  if (document.body) document.body.classList.toggle('project-mate-dms-disabled', !matesOn);
568
577
  }).catch(function(err) {
@@ -0,0 +1,106 @@
1
+ import { refreshIcons } from './icons.js';
2
+ import { store } from './store.js';
3
+ import { getWs } from './ws-ref.js';
4
+ import { parseEnvString, serializeEnvValue, looksLikeEnv } from './settings-env-values.js';
5
+
6
+ function state() { return store.get('projectEnvironmentEditor') || {}; }
7
+ function update(values) { store.set({ projectEnvironmentEditor: Object.assign({}, state(), values) }); }
8
+ function status(text) { document.getElementById('ps-env-save-status').textContent = text; }
9
+ function send(message) {
10
+ var ws = getWs();
11
+ if (!ws || ws.readyState !== 1) { status('Disconnected. Your draft is kept. Reconnect and try again.'); return false; }
12
+ ws.send(JSON.stringify(message)); return true;
13
+ }
14
+ export function projectEnvironmentDirty() { return state().slug === store.get("projectSettingsSlug") && !!(state().dirty || state().saving); }
15
+ export function openProjectEnvironment(slug) {
16
+ if (state().slug === slug && (state().loaded || state().loading || state().dirty)) return;
17
+ var cached = Object.assign({}, store.get('projectEnvironmentDrafts') || {});
18
+ if (state().slug && state().dirty) cached[state().slug] = state();
19
+ store.set({ projectEnvironmentDrafts: cached });
20
+ if (cached[slug]) { store.set({ projectEnvironmentEditor: cached[slug] }); render(); status('Unsaved changes'); return; }
21
+ update({ slug: slug, saved: '', rows: [], dirty: false, loaded: false, loading: false, editing: false, saving: false });
22
+ render(); status('Loading…');
23
+ var requestId = crypto.randomUUID();
24
+ update({ requestId: requestId });
25
+ if (send({ type: 'get_project_env', slug: slug, requestId: requestId })) update({ loading: true });
26
+ }
27
+ export function handleProjectEnv(msg) {
28
+ if (!state().loading || (msg.slug !== state().slug || msg.requestId !== state().requestId)) return;
29
+ update({ loading: false });
30
+ if (msg.error) { status(msg.error); return; }
31
+ update({ loaded: true, saved: msg.envrc || '', rows: parseEnvString(msg.envrc || '') });
32
+ render(); status('');
33
+ }
34
+ function changeRows(rows) { update({ rows: rows, dirty: true }); document.getElementById('ps-env-save').disabled = false; status('Unsaved changes'); }
35
+ function render() {
36
+ var editor = state();
37
+ var list = document.getElementById('ps-env-list'); list.replaceChildren();
38
+ (editor.rows || []).forEach(function (entry, index) {
39
+ var row = document.createElement('div'); row.className = 'ps-env-row';
40
+ var key = document.createElement('input'); key.className = 'ps-env-key'; key.value = entry.key; key.placeholder = 'KEY'; key.setAttribute('aria-label', 'Variable name');
41
+ var value = document.createElement('input'); value.className = 'ps-env-val'; value.type = 'password'; value.value = entry.value; value.placeholder = 'Value'; value.setAttribute('aria-label', 'Variable value');
42
+ [key, value].forEach(function (input) { input.autocomplete = 'off'; input.spellcheck = false; input.disabled = !!editor.saving; });
43
+ function changed() {
44
+ var rows = state().rows.slice(); rows[index] = { key: key.value, value: value.value }; changeRows(rows);
45
+ }
46
+ key.addEventListener('input', changed); value.addEventListener('input', changed);
47
+ key.addEventListener('paste', function (event) {
48
+ var text = event.clipboardData.getData('text');
49
+ if (!looksLikeEnv(text)) return;
50
+ event.preventDefault(); var rows = state().rows.slice();
51
+ rows.splice.apply(rows, [index, 1].concat(parseEnvString(text))); changeRows(rows); render();
52
+ });
53
+ var remove = document.createElement('button'); remove.className = 'ps-env-del'; remove.textContent = '×'; remove.setAttribute('aria-label', 'Remove variable'); remove.disabled = !!editor.saving;
54
+ remove.onclick = function () { var rows = state().rows.slice(); rows.splice(index, 1); changeRows(rows); render(); };
55
+ remove.innerHTML = '<i data-lucide="x"></i>';
56
+ var reveal = document.createElement('button'); reveal.type = 'button'; reveal.className = 'settings-btn-sm'; reveal.textContent = 'Show';
57
+ reveal.setAttribute('aria-label', 'Show variable value'); reveal.setAttribute('aria-pressed', 'false');
58
+ reveal.onclick = function () { var visible = value.type === 'password'; value.type = visible ? 'text' : 'password'; reveal.textContent = visible ? 'Hide' : 'Show'; reveal.setAttribute('aria-pressed', String(visible)); };
59
+ row.append(key, value, reveal, remove); list.appendChild(row);
60
+ });
61
+ document.getElementById('ps-env-add-btn').disabled = !editor.loaded || !!editor.saving;
62
+ document.getElementById('ps-env-save').disabled = !editor.loaded || !editor.dirty || !!editor.saving;
63
+ refreshIcons();
64
+ }
65
+ function clearCachedDraft() {
66
+ var cached = Object.assign({}, store.get('projectEnvironmentDrafts') || {}); delete cached[state().slug]; store.set({ projectEnvironmentDrafts: cached });
67
+ }
68
+ export function discardProjectEnvironment() {
69
+ clearCachedDraft();
70
+ update({ rows: parseEnvString(state().saved || ''), editing: false, dirty: false }); render(); status(''); document.getElementById('ps-env-add-btn').focus();
71
+ }
72
+ export function handleProjectEnvSaved(msg) {
73
+ if (!state().saving || (msg.slug !== state().slug || msg.requestId !== state().requestId)) return;
74
+ update({ saving: false });
75
+ if (!msg.ok) { render(); status(msg.error || 'Save failed. Your draft is kept.'); return; }
76
+ clearCachedDraft();
77
+ update({ saved: state().submitted, dirty: false, editing: false });
78
+ render(); document.getElementById('ps-env-add-btn').focus(); status('Saved. ' + (msg.timing || 'Applies to newly created coding-agent processes.'));
79
+ }
80
+ function save() {
81
+ var names = {};
82
+ var rows = state().rows || [];
83
+ for (var i = 0; i < rows.length; i++) {
84
+ var key = rows[i].key.trim();
85
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) { status('Enter a valid name for every variable.'); return; }
86
+ if (Object.prototype.hasOwnProperty.call(names, key)) { status('Each variable needs a unique name.'); return; }
87
+ names[key] = true;
88
+ }
89
+ var envrc = rows.map(function (row) { return 'export ' + row.key.trim() + '=' + serializeEnvValue(row.value); }).join('\n');
90
+ var requestId = crypto.randomUUID();
91
+ update({ requestId: requestId });
92
+ if (!send({ type: 'set_project_env', slug: state().slug, envrc: envrc, requestId: requestId })) return;
93
+ update({ submitted: envrc, saving: true }); render(); status('Saving…');
94
+ }
95
+ export function initProjectEnvironment() {
96
+ document.getElementById('ps-env-add-btn').onclick = function () { changeRows((state().rows || []).concat([{ key: '', value: '' }])); render(); document.querySelector('#ps-env-list .ps-env-row:last-child input').focus(); };
97
+ document.getElementById('ps-env-save').onclick = save;
98
+ store.subscribe(function (next, previous) {
99
+ if (next.myUserId !== previous.myUserId) {
100
+ store.set({ projectEnvironmentEditor: {}, projectEnvironmentDrafts: {} }); render(); status(''); return;
101
+ }
102
+ if (previous.connected && !next.connected && (state().saving || state().loading)) {
103
+ update({ saving: false, loading: false }); render(); status('Disconnected. Your draft is kept. Reconnect and try again.');
104
+ }
105
+ });
106
+ }