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
@@ -80,7 +80,6 @@ export function openDm(targetUserId, opts) {
80
80
  if (mates[i] && mates[i].id === targetUserId) isMateTarget = true;
81
81
  }
82
82
  if (isMateTarget) {
83
- if (store.get('projectMateDmsEnabled') === false) return;
84
83
  showHomeHub();
85
84
  openHomeChat(targetUserId);
86
85
  return;
@@ -152,7 +151,6 @@ function showMateOnboarding(callback) {
152
151
  export function enterDmMode(key, targetUser, messages) {
153
152
  console.log("[DEBUG enterDmMode] key=" + key, "isMate=" + (targetUser && targetUser.isMate), "messages=" + (messages ? messages.length : 0));
154
153
  if (targetUser && targetUser.isMate) {
155
- if (store.get('projectMateDmsEnabled') === false) return;
156
154
  showHomeHub();
157
155
  openHomeChat(targetUser.id);
158
156
  return;
@@ -1,3 +1,5 @@
1
+ import { handlePersonalSettingsMessage } from './user-settings-preferences.js';
2
+ import { handleServerUpdate } from './server-settings-controls.js';
1
3
  import { handleYouMessage } from './you.js';
2
4
  import { handleYouActivity } from './you-activity.js';
3
5
  import { handleProjectDebateMessage } from './project-debate-workspace.js';
@@ -53,9 +55,10 @@ import { showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundE
53
55
  import { handleFsList, handleFsRead, handleFileChanged, handleDirChanged, handleFileHistory, handleGitDiff, handleFileAt, refreshIfOpen, getPendingNavigate, handleFsSearch, presentMarkdownEdit } from './filebrowser.js';
54
56
  import { beginMarkdownTurn, finishMarkdownTurn, markdownPathFromToolInput } from './markdown-live-edit.js';
55
57
  import { forwardPaneMarkdownPresentation } from './pane-bridge.js';
56
- import { isProjectSettingsOpen, handleInstructionsRead, handleInstructionsWrite, handleProjectEnv, handleProjectEnvSaved, handleProjectSharedEnv, handleProjectSharedEnvSaved, handleProjectOwnerChanged } from './project-settings.js';
58
+ import { handleProjectOwnerChanged } from './project-settings.js';
59
+ import { handleProjectEnv, handleProjectEnvSaved } from './project-settings-environment.js';
57
60
  import { handleProjectSessionVisibilityDefault, refreshProjectSessionVisibilitySettings } from './project-session-visibility-settings.js';
58
- import { updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleInheritGroupsChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleGlobalClaudeMdRead, handleGlobalClaudeMdWrite } from './server-settings.js';
61
+ import { updateSettingsStats, updateDaemonConfig, handleSetPinResult, handleKeepAwakeChanged, handleAutoContinueChanged, handleRestartResult, handleShutdownResult, handleSharedEnv, handleSharedEnvSaved, handleServerSettingResult } from './server-settings.js';
59
62
  import { handleTermList, handleTermCreated, sendTerminalCommand, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed } from './terminal.js';
60
63
  import { attachTuiView, detachTuiView, setTuiSuspendedView, tuiHandleTermOutput, tuiHandleTermResized, tuiHandleTermExited, tuiHandleTermClosed } from './session-tui-view.js';
61
64
  import { handleTuiTranscriptState } from './tui-grab.js';
@@ -115,6 +118,8 @@ var connectOverlay = document.getElementById("connect-overlay");
115
118
  initMessageDeliveryUi();
116
119
 
117
120
  export function processMessage(msg) {
121
+ if (["update_available", "up_to_date", "update_started", "update_check_failed"].indexOf(msg.type) !== -1) handleServerUpdate(msg);
122
+ if (/^(set_pin|set_keep_awake|set_inherit_groups|set_image_retention|set_update_channel|restart_server|shutdown_server)_result$/.test(msg.type)) handleServerSettingResult(msg);
118
123
  if (handleProjectDebateMessage(msg)) return;
119
124
  if (handleMcpConnectionMessage(msg)) return;
120
125
  if (handleSharedBrowserMessage(msg)) return;
@@ -881,11 +886,8 @@ export function processMessage(msg) {
881
886
  } else if (_prevSid) {
882
887
  delete store.get('sessionDrafts')[_prevSid];
883
888
  }
884
- // runtimeMode/runtimeTerminalId take precedence over the session's
885
- // born mode so the user's current claudeOpenMode preference applies
886
- // to existing sessions too (born-GUI viewed under TUI pref runs
887
- // claude --resume in xterm; born-TUI viewed under GUI pref was
888
- // in-place converted to GUI by the server before this message).
889
+ // Live legacy terminals keep their runtime presentation. Idle legacy
890
+ // sessions are hydrated into chat by the server before this message.
889
891
  var _effectiveMode = msg.runtimeMode || msg.mode || "gui";
890
892
  var _effectiveTerminalId = (typeof msg.runtimeTerminalId === "number")
891
893
  ? msg.runtimeTerminalId
@@ -1572,15 +1574,10 @@ export function processMessage(msg) {
1572
1574
  break;
1573
1575
 
1574
1576
  case "fs_read_result":
1575
- if (msg.path === "CLAUDE.md" && isProjectSettingsOpen()) {
1576
- handleInstructionsRead(msg);
1577
- } else {
1578
- handleFsRead(msg);
1579
- }
1577
+ handleFsRead(msg);
1580
1578
  break;
1581
1579
 
1582
1580
  case "fs_write_result":
1583
- handleInstructionsWrite(msg);
1584
1581
  break;
1585
1582
 
1586
1583
  case "project_env_result":
@@ -1591,22 +1588,12 @@ export function processMessage(msg) {
1591
1588
  handleProjectEnvSaved(msg);
1592
1589
  break;
1593
1590
 
1594
- case "global_claude_md_result":
1595
- handleGlobalClaudeMdRead(msg);
1596
- break;
1597
-
1598
- case "write_global_claude_md_result":
1599
- handleGlobalClaudeMdWrite(msg);
1600
- break;
1601
-
1602
1591
  case "shared_env_result":
1603
1592
  handleSharedEnv(msg);
1604
- handleProjectSharedEnv(msg);
1605
1593
  break;
1606
1594
 
1607
1595
  case "set_shared_env_result":
1608
1596
  handleSharedEnvSaved(msg);
1609
- handleProjectSharedEnvSaved(msg);
1610
1597
  break;
1611
1598
 
1612
1599
  case "fs_file_changed":
@@ -2187,30 +2174,16 @@ export function processMessage(msg) {
2187
2174
  handleKeepAwakeChanged(msg);
2188
2175
  break;
2189
2176
 
2190
- case "set_inherit_groups_result":
2191
- case "inherit_groups_changed":
2192
- handleInheritGroupsChanged(msg);
2193
- break;
2194
-
2195
2177
  case "set_auto_continue_result":
2196
2178
  case "auto_continue_changed":
2197
2179
  handleAutoContinueChanged(msg);
2198
2180
  break;
2199
2181
 
2200
- case "set_claude_open_mode_result":
2201
- case "claude_open_mode_changed":
2202
- if (msg.claudeOpenMode === "tui" || msg.claudeOpenMode === "gui") {
2203
- store.set({ claudeOpenMode: msg.claudeOpenMode });
2204
- // Reflect into the user-settings toggle if it's already in the DOM.
2205
- var _comToggle = document.getElementById("us-claude-open-mode");
2206
- if (_comToggle) _comToggle.checked = msg.claudeOpenMode === "tui";
2207
- }
2208
- break;
2209
-
2210
2182
  case "claude_allow_list":
2183
+ handlePersonalSettingsMessage(msg);
2211
2184
  // Initial state for the user-settings allow-list editor.
2212
2185
  var _alTa = document.getElementById("us-claude-allow-list");
2213
- if (_alTa && Array.isArray(msg.user)) {
2186
+ if (_alTa && Array.isArray(msg.user) && !store.get("userAllowDirty")) {
2214
2187
  _alTa.value = msg.user.join("\n");
2215
2188
  }
2216
2189
  var _alManaged = document.getElementById("us-claude-allow-managed");
@@ -2226,19 +2199,7 @@ export function processMessage(msg) {
2226
2199
  break;
2227
2200
 
2228
2201
  case "set_claude_user_allow_list_result":
2229
- var _alStatus = document.getElementById("us-claude-allow-status");
2230
- if (_alStatus) {
2231
- if (msg.ok) {
2232
- _alStatus.textContent = "Saved (applies on next claude invocation)";
2233
- _alStatus.classList.remove("error");
2234
- setTimeout(function () {
2235
- if (_alStatus.textContent.indexOf("Saved") === 0) _alStatus.textContent = "";
2236
- }, 4000);
2237
- } else {
2238
- _alStatus.textContent = "Save failed: " + (msg.error || "unknown");
2239
- _alStatus.classList.add("error");
2240
- }
2241
- }
2202
+ handlePersonalSettingsMessage(msg);
2242
2203
  break;
2243
2204
 
2244
2205
  case "restart_server_result":
@@ -83,6 +83,22 @@ function setActiveRateLimitState(next) {
83
83
  rateLimitResetState = next;
84
84
  }
85
85
 
86
+ function extraUsageDescription(msg) {
87
+ if (msg.vendor && msg.vendor !== "claude") return "";
88
+ var reason = msg.overageDisabledReason;
89
+ if (reason === "out_of_credits") return "Extra usage has no credits remaining";
90
+ if (reason === "fetch_error") return "Extra usage availability could not be checked";
91
+ if (reason && reason !== "unknown") return "Extra usage is unavailable for this account";
92
+ if (msg.overageStatus === "rejected") {
93
+ var reset = formatResetTime(msg.overageResetsAt);
94
+ return "Extra usage limit reached" + (reset ? "; resets in " + reset : "");
95
+ }
96
+ if (msg.isUsingOverage) return "Extra usage is currently being used";
97
+ if (msg.overageEnabled === false) return "Extra usage is turned off";
98
+ if (msg.overageEnabled === true) return "Extra usage is enabled";
99
+ return "";
100
+ }
101
+
86
102
  function rateLimitTypeLabel(type) {
87
103
  if (!type) return "Usage";
88
104
  var labels = {
@@ -90,7 +106,8 @@ function rateLimitTypeLabel(type) {
90
106
  "seven_day": "7-day",
91
107
  "seven_day_opus": "7-day Opus",
92
108
  "seven_day_sonnet": "7-day Sonnet",
93
- "overage": "Overage",
109
+ "overage": "Extra usage",
110
+ "seven_day_overage_included": "7-day included extra usage",
94
111
  };
95
112
  return labels[type] || type;
96
113
  }
@@ -163,6 +180,12 @@ function showRateLimitPopover(text, isRejected) {
163
180
  pop.className = "rate-limit-popover" + (isRejected ? " rejected" : "");
164
181
  pop.textContent = text;
165
182
  rateLimitIndicatorEl.appendChild(pop);
183
+ if (typeof pop.getBoundingClientRect === "function") {
184
+ var bounds = pop.getBoundingClientRect();
185
+ var viewportWidth = document.documentElement.clientWidth;
186
+ var shift = bounds.left < 16 ? 16 - bounds.left : Math.min(0, viewportWidth - 16 - bounds.right);
187
+ pop.style.marginLeft = shift + "px";
188
+ }
166
189
 
167
190
  // Auto-dismiss after 5s
168
191
  var fadeTimer = setTimeout(function () {
@@ -182,9 +205,7 @@ function clearRateLimitIndicator() {
182
205
 
183
206
  function formatResetTime(resetsAt) {
184
207
  if (!resetsAt) return "";
185
- var d = new Date(resetsAt);
186
- var now = new Date();
187
- var diff = resetsAt - now.getTime();
208
+ var diff = resetsAt - Date.now();
188
209
  if (diff <= 0) return "";
189
210
  var hrs = Math.floor(diff / 3600000);
190
211
  var mins = Math.floor((diff % 3600000) / 60000);
@@ -200,10 +221,31 @@ function rateLimitTypeShortLabel(type) {
200
221
  return type || "";
201
222
  }
202
223
 
224
+ function quotaWindowLabel(entry) {
225
+ var mins = entry.windowDurationMins;
226
+ if (!Number.isFinite(mins) || mins <= 0) return "Usage";
227
+ if (mins % 1440 === 0) return (mins / 1440) + "d";
228
+ if (mins % 60 === 0) return (mins / 60) + "h";
229
+ return mins + "m";
230
+ }
231
+
232
+ function applyQuotaSnapshot(msg) {
233
+ var next = Object.assign({}, getActiveRateLimitState());
234
+ Object.keys(next).forEach(function (key) {
235
+ // Retire the old inferred Codex windows as soon as real metadata arrives.
236
+ if (!next[key].source || next[key].limitId === msg.limitId) delete next[key];
237
+ });
238
+ msg.quotaWindows.forEach(function (window) {
239
+ if (window.resetsAt && window.resetsAt <= Date.now()) return;
240
+ var key = JSON.stringify([msg.limitId, window.key]);
241
+ next[key] = Object.assign({}, window, { source: "account_snapshot", limitId: msg.limitId });
242
+ });
243
+ setActiveRateLimitState(next);
244
+ }
245
+
203
246
  function tickRateLimitUsage() {
204
247
  if (!rateLimitUsageEl) return;
205
- var parts = [];
206
- var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
248
+ var types = Object.keys(rateLimitResetState);
207
249
  var nextState = Object.assign({}, rateLimitResetState);
208
250
  var changed = false;
209
251
  for (var i = 0; i < types.length; i++) {
@@ -211,13 +253,8 @@ function tickRateLimitUsage() {
211
253
  if (!entry || !entry.resetsAt) continue;
212
254
  var timeStr = formatResetTime(entry.resetsAt);
213
255
  if (!timeStr) { delete nextState[types[i]]; changed = true; continue; }
214
- parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
215
256
  }
216
257
  if (changed) setActiveRateLimitState(nextState);
217
- if (parts.length === 0) {
218
- renderRateLimitUsage();
219
- return;
220
- }
221
258
  renderRateLimitUsage();
222
259
  }
223
260
 
@@ -247,9 +284,19 @@ function renderRateLimitUsage() {
247
284
  rateLimitUsageEl.style.display = "";
248
285
 
249
286
  var parts = [];
250
- var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
287
+ var types = Object.keys(rateLimitResetState);
288
+ var hasWarning = false;
251
289
  for (var i = 0; i < types.length; i++) {
252
290
  var entry = rateLimitResetState[types[i]];
291
+ if (entry && entry.source === "account_snapshot") {
292
+ if (entry.resetsAt && entry.resetsAt <= Date.now()) continue;
293
+ var quotaText = quotaWindowLabel(entry);
294
+ if (Number.isFinite(entry.usedPercent)) quotaText += " " + Math.round(entry.usedPercent) + "% used";
295
+ if (entry.resetsAt) quotaText += " · resets " + formatResetTime(entry.resetsAt);
296
+ if (entry.usedPercent >= 80) hasWarning = true;
297
+ parts.push(quotaText);
298
+ continue;
299
+ }
253
300
  if (!entry || !entry.resetsAt) continue;
254
301
  var timeStr = formatResetTime(entry.resetsAt);
255
302
  if (!timeStr) continue;
@@ -257,6 +304,7 @@ function renderRateLimitUsage() {
257
304
  }
258
305
 
259
306
  var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
307
+ rateLimitUsageEl.className = "top-bar-pill usage-check-link " + (hasWarning ? "pill-warning" : "pill-dim");
260
308
  var meta = getVendorUsageMeta(activeVendor);
261
309
  if (meta) {
262
310
  rateLimitUsageEl.href = meta.href;
@@ -301,6 +349,8 @@ export function initRateLimit() {
301
349
 
302
350
  export function handleRateLimitEvent(msg) {
303
351
  if (!rateLimitEventAppliesToPane(msg, store.snap())) return;
352
+ // Old Codex history contains quota snapshots misrecorded as rejections.
353
+ if (msg.vendor === "codex" && !msg.source) return;
304
354
  if (msg.resetsAt && msg.resetsAt <= Date.now()) {
305
355
  var expiredState = Object.assign({}, getActiveRateLimitState());
306
356
  if (msg.rateLimitType) delete expiredState[msg.rateLimitType];
@@ -351,11 +401,19 @@ export function handleRateLimitEvent(msg) {
351
401
  updateRateLimitIndicator(msg);
352
402
  }
353
403
 
404
+ var extraUsage = extraUsageDescription(msg);
405
+ if (extraUsage) popoverText += ". " + extraUsage;
354
406
  showRateLimitPopover(popoverText, isRejected);
355
407
  }
356
408
 
357
409
  export function updateRateLimitUsage(msg) {
358
410
  if (!rateLimitEventAppliesToPane(msg, store.snap())) return;
411
+ if (msg.vendor === "codex" && msg.source === "account_snapshot" && Array.isArray(msg.quotaWindows)) {
412
+ applyQuotaSnapshot(msg);
413
+ renderRateLimitUsage();
414
+ return;
415
+ }
416
+ if (msg.vendor === "codex" && !msg.source) return;
359
417
  var activeVendor = store.get('currentVendor') || "claude";
360
418
  if (!vendorTracksRateLimits(activeVendor)) {
361
419
  renderRateLimitUsage();
@@ -1,3 +1,5 @@
1
+ import { showToast } from './utils.js';
2
+ import { preparePersonalConnectionDialog } from './personal-settings-dialog.js';
1
3
  // Context Sources — attach terminal output, browser tabs, and email accounts as context for Claude
2
4
 
3
5
  var ctx = null;
@@ -433,25 +435,32 @@ export function getEmailAccountListCache() {
433
435
  return emailAccountList;
434
436
  }
435
437
 
436
- export function showEmailSetupModal() {
438
+ export function showEmailSetupModal(provider, host) {
439
+ var providerNames = { gmail: "Gmail", outlook: "Outlook Mail", yahoo: "Yahoo Mail", custom: "Custom email" };
440
+ if (!Object.hasOwn(providerNames, provider)) provider = null;
437
441
  // Remove any existing modal
438
442
  var existing = document.getElementById("email-setup-modal");
439
- if (existing) existing.remove();
443
+ if (existing) {existing.querySelector("input:not([type=hidden]),button").focus();return;}
440
444
 
441
445
  var overlay = document.createElement("div");
442
446
  overlay.id = "email-setup-modal";
443
- overlay.className = "email-setup-overlay";
444
-
445
- var providerOptions = '<option value="gmail">Gmail</option><option value="outlook">Outlook</option><option value="yahoo">Yahoo</option><option value="custom">Custom</option>';
446
-
447
+ overlay.className = host ? "email-setup-inline" : "email-setup-overlay";
448
+
449
+ if (!provider) {
450
+ var picker = document.createElement('div'); picker.className = 'email-setup-dialog';
451
+ picker.innerHTML = '<h3>Choose an email provider</h3><div class="us-integration-tiles"></div><button id="email-setup-cancel" type="button" class="email-setup-btn">Cancel</button>';
452
+ Object.keys(providerNames).forEach(function (id) {
453
+ var button = document.createElement('button'); button.type = 'button'; button.className = 'email-setup-btn'; button.textContent = providerNames[id];
454
+ button.onclick = function () { closePicker(); showEmailSetupModal(id, host); };
455
+ picker.querySelector('.us-integration-tiles').appendChild(button);
456
+ });
457
+ overlay.appendChild(picker); (host || document.body).appendChild(overlay); var closePicker = preparePersonalConnectionDialog(overlay, !!host); return;
458
+ }
447
459
  var modal = document.createElement("div");
448
460
  modal.className = "email-setup-dialog";
449
461
  modal.innerHTML =
450
- '<h3 class="email-setup-title">Add Email Account</h3>' +
451
- '<div class="email-setup-field">' +
452
- '<label class="email-setup-label">Provider</label>' +
453
- '<select id="email-setup-provider" class="email-setup-input">' + providerOptions + '</select>' +
454
- '</div>' +
462
+ '<h3 class="email-setup-title">Add ' + providerNames[provider] + ' account</h3>' +
463
+ '<input type="hidden" id="email-setup-provider" value="' + provider + '">' +
455
464
  '<div class="email-setup-field">' +
456
465
  '<label class="email-setup-label">Email Address</label>' +
457
466
  '<input id="email-setup-address" type="email" placeholder="you@example.com" class="email-setup-input" />' +
@@ -488,11 +497,12 @@ export function showEmailSetupModal() {
488
497
  '</div>';
489
498
 
490
499
  overlay.appendChild(modal);
491
- document.body.appendChild(overlay);
500
+ (host || document.body).appendChild(overlay);
501
+ preparePersonalConnectionDialog(overlay, !!host);
492
502
 
493
503
  // Event: close on overlay click
494
504
  overlay.addEventListener("click", function (e) {
495
- if (e.target === overlay) overlay.remove();
505
+ if (!host && e.target === overlay) overlay.remove();
496
506
  });
497
507
 
498
508
  // Event: cancel button
@@ -547,9 +557,7 @@ export function showEmailSetupModal() {
547
557
  }
548
558
  }
549
559
 
550
- providerSelect.addEventListener("change", function () {
551
- applyProviderMeta(providerSelect.value);
552
- });
560
+ applyProviderMeta(provider);
553
561
 
554
562
  // Reset test state
555
563
  _emailTestPassed = false;
@@ -564,7 +572,7 @@ export function showEmailSetupModal() {
564
572
  }
565
573
  document.getElementById("email-setup-address").addEventListener("input", onInputChange);
566
574
  document.getElementById("email-setup-password").addEventListener("input", onInputChange);
567
- providerSelect.addEventListener("change", onInputChange);
575
+
568
576
 
569
577
  function runTest() {
570
578
  var statusEl = document.getElementById("email-setup-status");
@@ -701,7 +709,8 @@ export function handleEmailAddResult(msg) {
701
709
 
702
710
  // Handle email_account_remove_result from server
703
711
  export function handleEmailRemoveResult(msg) {
704
- // Account removed; list will be refreshed via email_accounts_list message
712
+ showToast(msg.ok ? 'Personal mailbox disconnected.' : msg.error || 'Could not disconnect mailbox. Retry from Email settings.');
713
+ // The acknowledged list arrives separately through email_accounts_list.
705
714
  }
706
715
 
707
716
  function getSourceLabel(id) {
@@ -1,3 +1,5 @@
1
+ import { confirmSettingsAction } from './server-settings-state.js';
2
+ import { enhanceSettingsSelects } from './settings-select.js';
1
3
  // User-island Default AI picker. Canonical state and the unsaved draft are separate.
2
4
 
3
5
  import { store } from './store.js';
@@ -135,6 +137,7 @@ function renderInlineSummary() {
135
137
  summary.appendChild(icon);
136
138
  popover.appendChild(summary);
137
139
  summary.addEventListener('click', function () { store.set({ defaultAiInlineEditing: true }); });
140
+ if(state.error)addStatus(state.error,true);
138
141
  refreshIcons();
139
142
  }
140
143
 
@@ -157,6 +160,16 @@ function render() {
157
160
  } else {
158
161
  popover.innerHTML = '<div class="default-ai-title">Default AI</div><div class="default-ai-copy">Used for new Clay interviews and built-in AI assistance.</div>';
159
162
  }
163
+ if (inlineTarget) {
164
+ var cancel = popover.querySelector(".default-ai-cancel");
165
+ cancel.addEventListener("click", function () {
166
+ function discard() {
167
+ var canonical = inlineCurrentSelection(store.get("defaultAiState") || {}, store.get("defaultAiDraft") || {});
168
+ store.set({ defaultAiDraft: Object.assign({}, canonical), defaultAiDraftDirty: false, defaultAiInlineEditing: false });
169
+ }
170
+ if(store.get("defaultAiDraftDirty"))confirmSettingsAction("Discard unsaved AI defaults?", "Discard", discard);else discard();
171
+ });
172
+ }
160
173
  if (state.loading && !vendors.length) {
161
174
  addStatus("Loading available AI providers…", false);
162
175
  position();
@@ -228,14 +241,8 @@ function render() {
228
241
  }
229
242
  render();
230
243
  });
231
- if (inlineTarget) {
232
- var cancel = popover.querySelector(".default-ai-cancel");
233
- cancel.addEventListener("click", function () {
234
- var canonical = inlineCurrentSelection(store.get("defaultAiState") || {}, store.get("defaultAiDraft") || {});
235
- store.set({ defaultAiDraft: Object.assign({}, canonical), defaultAiDraftDirty: false, defaultAiInlineEditing: false });
236
- });
237
- }
238
244
  popover.appendChild(save);
245
+ enhanceSettingsSelects(popover);
239
246
  if (state.error) addStatus(state.error, true);
240
247
  position();
241
248
  refreshIcons();
@@ -7,23 +7,14 @@
7
7
  // PUT /api/user/terminal-font.
8
8
 
9
9
  import {
10
- applyTerminalFont,
10
+ saveTerminalFont,
11
+ TERMINAL_FONT_OPTIONS,
11
12
  getTerminalFontFamily,
12
13
  getTerminalFontSize,
13
14
  onTerminalFontChange,
14
15
  } from './terminal-prefs.js';
15
16
 
16
- var FONT_OPTIONS = [
17
- { label: "SF Mono", family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace" },
18
- { label: "JetBrains Mono", family: "'JetBrains Mono', 'SF Mono', Menlo, monospace" },
19
- { label: "Fira Code", family: "'Fira Code', 'SF Mono', Menlo, monospace" },
20
- { label: "Cascadia Code", family: "'Cascadia Code', 'SF Mono', Menlo, monospace" },
21
- { label: "IBM Plex Mono", family: "'IBM Plex Mono', 'SF Mono', Menlo, monospace" },
22
- { label: "Source Code Pro", family: "'Source Code Pro', 'SF Mono', Menlo, monospace" },
23
- { label: "Roboto Mono", family: "'Roboto Mono', 'SF Mono', Menlo, monospace" },
24
- { label: "D2 Coding", family: "'D2 coding', 'D2Coding', 'SF Mono', Menlo, monospace" },
25
- { label: "System mono", family: "ui-monospace, monospace" },
26
- ];
17
+ var FONT_OPTIONS = TERMINAL_FONT_OPTIONS;
27
18
 
28
19
  var MIN_SIZE = 9;
29
20
  var MAX_SIZE = 32;
@@ -48,11 +39,12 @@ function escapeHtml(s) {
48
39
  }
49
40
 
50
41
  function persistTermFont(family, size) {
51
- fetch('/api/user/terminal-font', {
52
- method: 'PUT',
53
- headers: { 'Content-Type': 'application/json' },
54
- body: JSON.stringify({ family: family, size: size }),
55
- }).catch(function () {});
42
+ var status = popoverEl.querySelector('[role=status]');
43
+ status.textContent = 'Saving…';
44
+ popoverEl.querySelectorAll('button').forEach(function (button) {button.disabled = true;});
45
+ saveTerminalFont(family, size).then(function () {status.textContent = 'Saved.';})
46
+ .catch(function (error) {status.textContent = error.message;})
47
+ .finally(function () {popoverEl.querySelectorAll('button').forEach(function (button) {button.disabled = false;});syncSize();});
56
48
  }
57
49
 
58
50
  function syncSize() {
@@ -96,7 +88,6 @@ function buildPopover() {
96
88
  (function (family) {
97
89
  item.addEventListener('click', function (e) {
98
90
  e.stopPropagation();
99
- applyTerminalFont(family, undefined);
100
91
  persistTermFont(family, undefined);
101
92
  });
102
93
  })(opt.family);
@@ -127,16 +118,15 @@ function buildPopover() {
127
118
  sizeDecEl.addEventListener('click', function (e) {
128
119
  e.stopPropagation();
129
120
  var next = Math.max(MIN_SIZE, getTerminalFontSize() - 1);
130
- applyTerminalFont(undefined, next);
131
121
  persistTermFont(undefined, next);
132
122
  });
133
123
  sizeIncEl.addEventListener('click', function (e) {
134
124
  e.stopPropagation();
135
125
  var next = Math.min(MAX_SIZE, getTerminalFontSize() + 1);
136
- applyTerminalFont(undefined, next);
137
126
  persistTermFont(undefined, next);
138
127
  });
139
128
 
129
+ var status = document.createElement('p');status.setAttribute('role','status');status.className='settings-status';popoverEl.appendChild(status);
140
130
  document.body.appendChild(popoverEl);
141
131
  return popoverEl;
142
132
  }
@@ -40,13 +40,14 @@ export function initLinearSettings() {
40
40
  try {
41
41
  var response = await fetch('/api/linear/connection', { method: disconnect ? 'DELETE' : 'PUT',
42
42
  headers: { 'Content-Type': 'application/json' }, body: disconnect ? undefined : JSON.stringify({ key: input.value }) });
43
- clearLinearKeyInput();
44
43
  if (store.get('myUserId') !== actor) return;
45
44
  var data = await response.json();
46
45
  if (!response.ok) throw new Error(data.error || 'Could not update Linear connection.');
46
+ clearLinearKeyInput();
47
47
  store.set({ linearConnection: data, githubWorkRefresh: null });
48
+ form.hidden = true;
48
49
  setLinearStatus(data.connected ? 'Connected to ' + data.workspace.name : 'Disconnected', data.connected ? 'connected' : '');
49
- } catch (error) { clearLinearKeyInput(); setLinearStatus(error.message, 'error'); }
50
+ } catch (error) { setLinearStatus(error.message, 'error'); }
50
51
  finally {
51
52
  store.set({ linearConnectionSaving: false });
52
53
  buttons.forEach(function (button) { button.disabled = false; });
@@ -32,14 +32,12 @@ marked.use({
32
32
  }
33
33
  });
34
34
 
35
- // Initialize mermaid
36
- mermaid.initialize({
37
- startOnLoad: false,
38
- theme: "dark",
39
- themeVariables: getMermaidThemeVars()
40
- });
35
+ // Theme and Markdown import each other. Read theme state only after module
36
+ // initialization, when a theme is applied or the first diagram is rendered.
37
+ var mermaidInitialized = false;
41
38
 
42
39
  export function updateMermaidTheme(vars) {
40
+ mermaidInitialized = true;
43
41
  mermaid.initialize({
44
42
  startOnLoad: false,
45
43
  theme: "dark",
@@ -183,6 +181,7 @@ export function renderMermaidBlocks(el) {
183
181
  container.className = "mermaid-diagram";
184
182
 
185
183
  try {
184
+ if (!mermaidInitialized) updateMermaidTheme(getMermaidThemeVars());
186
185
  mermaid.render(id, source.trim()).then(function (result) {
187
186
  container.innerHTML = result.svg;
188
187
  container.addEventListener("click", function () {
@@ -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
+ }