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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (222) hide show
  1. package/LICENSES/README.md +4 -0
  2. package/LICENSES/dicebear-avatars.md +16 -0
  3. package/bin/cli.js +9 -91
  4. package/lib/daemon-project-access.js +7 -1
  5. package/lib/daemon-settings.js +11 -0
  6. package/lib/daemon.js +10 -37
  7. package/lib/dicebear-avatar.js +62 -0
  8. package/lib/knowledge-db-service.js +175 -0
  9. package/lib/knowledge-db-validation.js +0 -0
  10. package/lib/knowledge-document-index.js +49 -0
  11. package/lib/knowledge-draft-store.js +56 -0
  12. package/lib/knowledge-file-store.js +199 -0
  13. package/lib/knowledge-markdown.js +128 -0
  14. package/lib/linear-store.js +35 -8
  15. package/lib/mate-knowledge-mcp-server.js +29 -6
  16. package/lib/mate-knowledge-service.js +23 -2
  17. package/lib/mate-knowledge-sync.js +36 -21
  18. package/lib/mate-workspace-color.js +4 -0
  19. package/lib/mates.js +18 -0
  20. package/lib/os-users.js +5 -16
  21. package/lib/project-connection.js +36 -9
  22. package/lib/project-debate-proposal.js +6 -0
  23. package/lib/project-debate-workspace.js +43 -0
  24. package/lib/project-debate.js +1 -0
  25. package/lib/project-filesystem.js +21 -39
  26. package/lib/project-knowledge.js +315 -136
  27. package/lib/project-mate-creation-proposal.js +1 -0
  28. package/lib/project-mate-interaction.js +2 -2
  29. package/lib/project-server-settings.js +31 -0
  30. package/lib/project-sessions.js +92 -182
  31. package/lib/project-user-knowledge.js +60 -0
  32. package/lib/project.js +59 -18
  33. package/lib/public/app.js +79 -71
  34. package/lib/public/clay-studio-wordmark-bold.svg +20 -0
  35. package/lib/public/css/clay-workspace.css +105 -0
  36. package/lib/public/css/debates-workbench.css +47 -0
  37. package/lib/public/css/home-debate-live.css +26 -0
  38. package/lib/public/css/home-hub.css +3 -1
  39. package/lib/public/css/home-mate-model-picker.css +15 -131
  40. package/lib/public/css/home-mate-settings.css +168 -16
  41. package/lib/public/css/icon-strip.css +249 -32
  42. package/lib/public/css/input.css +3 -1
  43. package/lib/public/css/mate-conversation.css +176 -0
  44. package/lib/public/css/mate-knowledge-workbench.css +118 -0
  45. package/lib/public/css/mate-workspace-color.css +16 -0
  46. package/lib/public/css/mates.css +28 -28
  47. package/lib/public/css/messages.css +1 -114
  48. package/lib/public/css/mobile-nav.css +16 -16
  49. package/lib/public/css/overlays.css +8 -0
  50. package/lib/public/css/project-logs.css +7 -0
  51. package/lib/public/css/project-mate-creation.css +105 -0
  52. package/lib/public/css/project-settings-modal.css +56 -0
  53. package/lib/public/css/right-workbench.css +3 -3
  54. package/lib/public/css/server-settings-system.css +125 -0
  55. package/lib/public/css/session-folders.css +80 -21
  56. package/lib/public/css/session-github.css +25 -16
  57. package/lib/public/css/settings-ui.css +247 -22
  58. package/lib/public/css/shared-browser.css +2 -0
  59. package/lib/public/css/sidebar.css +183 -107
  60. package/lib/public/css/title-bar.css +156 -1
  61. package/lib/public/css/worker-delegation.css +47 -0
  62. package/lib/public/css/worker-proposal.css +72 -150
  63. package/lib/public/css/you.css +74 -0
  64. package/lib/public/index.html +203 -547
  65. package/lib/public/modules/account-island.js +232 -0
  66. package/lib/public/modules/admin-settings-email.js +197 -0
  67. package/lib/public/modules/admin-settings-invitations.js +183 -0
  68. package/lib/public/modules/admin-settings-projects.js +198 -0
  69. package/lib/public/modules/admin-settings-ui.js +100 -0
  70. package/lib/public/modules/admin-settings-users.js +331 -0
  71. package/lib/public/modules/admin.js +19 -949
  72. package/lib/public/modules/app-cursors.js +19 -43
  73. package/lib/public/modules/app-debate-ui.js +4 -3
  74. package/lib/public/modules/app-dm.js +0 -2
  75. package/lib/public/modules/app-header.js +106 -0
  76. package/lib/public/modules/app-message-router.js +12 -3
  77. package/lib/public/modules/app-messages.js +47 -61
  78. package/lib/public/modules/app-notifications.js +4 -7
  79. package/lib/public/modules/app-projects.js +5 -38
  80. package/lib/public/modules/app-rate-limit.js +70 -12
  81. package/lib/public/modules/app-rendering.js +12 -13
  82. package/lib/public/modules/avatar.js +25 -9
  83. package/lib/public/modules/chat-render-runtime.js +16 -4
  84. package/lib/public/modules/command-palette.js +2 -15
  85. package/lib/public/modules/context-sources.js +27 -18
  86. package/lib/public/modules/cursor-sharing-preference.js +3 -3
  87. package/lib/public/modules/debate-controls.js +299 -0
  88. package/lib/public/modules/debate.js +2 -0
  89. package/lib/public/modules/debates-workbench.js +339 -0
  90. package/lib/public/modules/default-ai.js +88 -65
  91. package/lib/public/modules/header-tui-font.js +10 -20
  92. package/lib/public/modules/home-debate-controls.js +18 -289
  93. package/lib/public/modules/home-mate-avatar-editor.js +228 -0
  94. package/lib/public/modules/home-mate-color-picker.js +66 -0
  95. package/lib/public/modules/home-mate-creation.js +1 -1
  96. package/lib/public/modules/home-mate-instructions.js +90 -0
  97. package/lib/public/modules/home-mate-model-picker.js +80 -271
  98. package/lib/public/modules/home-mate-profile-editor.js +72 -0
  99. package/lib/public/modules/home-mate-settings.js +118 -125
  100. package/lib/public/modules/home-shell.js +3 -0
  101. package/lib/public/modules/home-surface-boot.js +1 -9
  102. package/lib/public/modules/icon-strip-tooltip.js +95 -0
  103. package/lib/public/modules/knowledge-db-edit-state.js +35 -0
  104. package/lib/public/modules/knowledge-db-workbench.js +234 -0
  105. package/lib/public/modules/knowledge-history-state.js +55 -0
  106. package/lib/public/modules/knowledge-markdown-editor.js +57 -0
  107. package/lib/public/modules/knowledge-preview-links.js +70 -0
  108. package/lib/public/modules/knowledge-workbench-shell.js +21 -0
  109. package/lib/public/modules/linear-settings.js +3 -2
  110. package/lib/public/modules/markdown.js +5 -6
  111. package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
  112. package/lib/public/modules/mate-knowledge-workbench.js +500 -0
  113. package/lib/public/modules/mate-knowledge.js +11 -3
  114. package/lib/public/modules/mate-management.js +54 -10
  115. package/lib/public/modules/mate-sidebar.js +11 -1
  116. package/lib/public/modules/mate-workspace-color.js +33 -0
  117. package/lib/public/modules/mention.js +5 -13
  118. package/lib/public/modules/notification-settings.js +81 -0
  119. package/lib/public/modules/notifications.js +44 -28
  120. package/lib/public/modules/personal-settings-dialog.js +51 -0
  121. package/lib/public/modules/profile.js +66 -24
  122. package/lib/public/modules/project-chat-identity.js +79 -0
  123. package/lib/public/modules/project-debate-planning-workspace.js +250 -0
  124. package/lib/public/modules/project-debate-workspace.js +185 -0
  125. package/lib/public/modules/project-header-identity.js +127 -0
  126. package/lib/public/modules/project-logs-render.js +9 -4
  127. package/lib/public/modules/project-mate-creation-workspace.js +414 -0
  128. package/lib/public/modules/project-mate-navigation.js +238 -0
  129. package/lib/public/modules/project-session-navigation.js +29 -0
  130. package/lib/public/modules/project-settings-environment.js +106 -0
  131. package/lib/public/modules/project-settings.js +72 -628
  132. package/lib/public/modules/project-switcher.js +6 -8
  133. package/lib/public/modules/projects-hub-view.js +5 -6
  134. package/lib/public/modules/projects-hub.js +2 -6
  135. package/lib/public/modules/qrcode.js +27 -8
  136. package/lib/public/modules/search-clay-chat.js +8 -11
  137. package/lib/public/modules/server-settings-controls.js +316 -0
  138. package/lib/public/modules/server-settings-environment.js +194 -0
  139. package/lib/public/modules/server-settings-navigation.js +39 -0
  140. package/lib/public/modules/server-settings-state.js +54 -0
  141. package/lib/public/modules/server-settings.js +116 -788
  142. package/lib/public/modules/server-storage-settings.js +84 -0
  143. package/lib/public/modules/session-activity-tooltip.js +12 -0
  144. package/lib/public/modules/session-create-form.js +86 -14
  145. package/lib/public/modules/session-create.js +50 -7
  146. package/lib/public/modules/session-github.js +2 -1
  147. package/lib/public/modules/session-list-visibility.js +41 -0
  148. package/lib/public/modules/session-presence-ui.js +3 -0
  149. package/lib/public/modules/session-title-marquee.js +51 -0
  150. package/lib/public/modules/settings-env-values.js +34 -0
  151. package/lib/public/modules/settings-select.js +121 -0
  152. package/lib/public/modules/shared-browser.js +10 -7
  153. package/lib/public/modules/sidebar-mates.js +3 -50
  154. package/lib/public/modules/sidebar-mobile.js +63 -60
  155. package/lib/public/modules/sidebar-projects.js +84 -97
  156. package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
  157. package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
  158. package/lib/public/modules/sidebar-sessions.js +49 -50
  159. package/lib/public/modules/sidebar.js +11 -13
  160. package/lib/public/modules/stt-picker.js +14 -5
  161. package/lib/public/modules/terminal-prefs.js +22 -0
  162. package/lib/public/modules/theme.js +88 -52
  163. package/lib/public/modules/thinking-lifecycle.js +7 -6
  164. package/lib/public/modules/tool-palette-order.js +4 -3
  165. package/lib/public/modules/tool-palette.js +13 -3
  166. package/lib/public/modules/tools.js +22 -22
  167. package/lib/public/modules/user-settings-account.js +318 -0
  168. package/lib/public/modules/user-settings-appearance.js +55 -0
  169. package/lib/public/modules/user-settings-editors.js +24 -0
  170. package/lib/public/modules/user-settings-integrations.js +104 -0
  171. package/lib/public/modules/user-settings-layout.js +80 -0
  172. package/lib/public/modules/user-settings-preferences.js +48 -0
  173. package/lib/public/modules/user-settings-profile.js +53 -0
  174. package/lib/public/modules/user-settings-themes.js +38 -0
  175. package/lib/public/modules/user-settings-voice.js +75 -0
  176. package/lib/public/modules/user-settings.js +53 -480
  177. package/lib/public/modules/worker-proposal.js +42 -8
  178. package/lib/public/modules/worker-runtime-picker.js +115 -0
  179. package/lib/public/modules/you-activity.js +121 -0
  180. package/lib/public/modules/you.js +251 -0
  181. package/lib/public/style.css +20 -8
  182. package/lib/public/vendor/codemirror/LICENSE +21 -0
  183. package/lib/public/vendor/codemirror/codemirror.css +344 -0
  184. package/lib/public/vendor/codemirror/codemirror.js +9884 -0
  185. package/lib/public/vendor/codemirror/continuelist.js +101 -0
  186. package/lib/public/vendor/codemirror/markdown.js +886 -0
  187. package/lib/public/vendor/codemirror/show-hint.css +37 -0
  188. package/lib/public/vendor/codemirror/show-hint.js +523 -0
  189. package/lib/sdk-bridge.js +15 -3
  190. package/lib/sdk-message-processor.js +16 -4
  191. package/lib/sdk-quota-snapshot.js +37 -0
  192. package/lib/server-admin.js +11 -0
  193. package/lib/server-appearance-settings.js +71 -0
  194. package/lib/server-home-chat-events.js +1 -1
  195. package/lib/server-home-chat.js +25 -3
  196. package/lib/server-home-debate-planning.js +4 -1
  197. package/lib/server-home-debates.js +2 -0
  198. package/lib/server-home-mate-creation.js +4 -1
  199. package/lib/server-linear.js +14 -8
  200. package/lib/server-mate-instructions.js +44 -0
  201. package/lib/server-mates.js +12 -3
  202. package/lib/server-notification-settings.js +67 -0
  203. package/lib/server-palette.js +1 -1
  204. package/lib/server-settings.js +69 -1
  205. package/lib/server-storage.js +56 -0
  206. package/lib/server-user-knowledge.js +119 -0
  207. package/lib/server.js +31 -3
  208. package/lib/session-visibility.js +7 -2
  209. package/lib/sessions.js +34 -9
  210. package/lib/startup-projects.js +40 -0
  211. package/lib/storage-usage.js +127 -0
  212. package/lib/user-knowledge-service.js +167 -0
  213. package/lib/users-permissions.js +7 -0
  214. package/lib/users-preferences.js +0 -74
  215. package/lib/users-project-mate-filter-preferences.js +39 -0
  216. package/lib/users.js +6 -4
  217. package/lib/workspace-query-access.js +4 -1
  218. package/lib/ws-schema.js +52 -4
  219. package/lib/yoke/adapters/codex.js +8 -45
  220. package/lib/yoke/codex-model-catalog.js +40 -0
  221. package/lib/yoke/codex-rate-limits.js +26 -0
  222. package/package.json +12 -3
@@ -2,7 +2,8 @@
2
2
 
3
3
  import { getWs } from './ws-ref.js';
4
4
  import { showMateProfilePopover } from './profile.js';
5
- import { showConfirm } from './app-misc.js';
5
+ import { showToast } from './utils.js';
6
+ import { showConfirm, hideConfirm } from './app-misc.js';
6
7
  import { iconHtml, refreshIcons } from './icons.js';
7
8
 
8
9
  var mateMenu = null;
@@ -21,15 +22,58 @@ export function editMateProfile(anchorEl, mate) {
21
22
 
22
23
  export function confirmMateRemoval(anchorEl, mate, onRemoved) {
23
24
  if (!anchorEl || !mate || mate.primary) return;
24
- showConfirm(
25
- mate.builtinKey ? "Remove this mate? You can restore it from the home switcher." : "Delete this mate permanently?",
26
- function () {
27
- if (onRemoved) onRemoved();
28
- send({ type: "mate_delete", mateId: mate.id });
29
- },
30
- mate.builtinKey ? "Remove" : "Delete",
31
- true
32
- );
25
+ var name = (mate.profile || {}).displayName || mate.name || "this Mate";
26
+ var modal = document.getElementById("confirm-modal");
27
+ var panel = modal.querySelector(".confirm-dialog");
28
+ var ok = document.getElementById("confirm-ok");
29
+ var cancel = document.getElementById("confirm-cancel");
30
+ var input;
31
+ function cleanup() {
32
+ modal.classList.remove("mate-delete-confirm");
33
+ panel.removeAttribute("role"); panel.removeAttribute("aria-modal");
34
+ panel.removeAttribute("aria-labelledby"); panel.removeAttribute("aria-describedby");
35
+ var field = modal.querySelector(".mate-delete-challenge"); if (field) field.remove();
36
+ ok.disabled = false;
37
+ document.removeEventListener("keydown", onKey, true);
38
+ if (anchorEl.isConnected) anchorEl.focus({preventScroll:true});
39
+ }
40
+ function onKey(event) {
41
+ if (event.key === "Escape") { event.preventDefault(); event.stopImmediatePropagation(); hideConfirm(); return; }
42
+ if (event.key !== "Tab") return;
43
+ var controls = ok.disabled ? [input,cancel] : [input,cancel,ok];
44
+ var index = controls.indexOf(document.activeElement);
45
+ event.preventDefault(); event.stopImmediatePropagation();
46
+ controls[(index + (event.shiftKey ? controls.length - 1 : 1)) % controls.length].focus();
47
+ }
48
+ showConfirm("", function () {
49
+ if (input.value !== "understand") { cleanup(); return; }
50
+ var ws = getWs();
51
+ if (!ws || ws.readyState !== 1) { cleanup(); showToast("Clay is offline. The Mate was not deleted.", "error"); return; }
52
+ try { ws.send(JSON.stringify({type:"mate_delete",mateId:mate.id})); }
53
+ catch (error) { cleanup(); showToast("Could not send the deletion request. Try again.", "error"); return; }
54
+ cleanup();
55
+ if (onRemoved) onRemoved(true);
56
+ }, mate.builtinKey ? "Remove permanently" : "Delete permanently", true, cleanup);
57
+ modal.classList.add("mate-delete-confirm");
58
+ panel.setAttribute("role","alertdialog"); panel.setAttribute("aria-modal","true");
59
+ panel.setAttribute("aria-labelledby","mate-delete-title"); panel.setAttribute("aria-describedby","mate-delete-warning");
60
+ var text = document.getElementById("confirm-text");
61
+ var title = document.createElement("h3"); title.id = "mate-delete-title"; title.textContent = "Delete " + name + "?";
62
+ var warning = document.createElement("p"); warning.id = "mate-delete-warning";
63
+ warning.textContent = "This permanently deletes the Mate and the data stored in its workspace, including its prompt and files. This cannot be undone.";
64
+ text.appendChild(title); text.appendChild(warning);
65
+ if (mate.builtinKey) {
66
+ var note = document.createElement("p"); note.textContent = "You can add this built-in Mate again, but its deleted data will not be restored."; text.appendChild(note);
67
+ }
68
+ var field = document.createElement("label"); field.className = "mate-delete-challenge";
69
+ field.textContent = 'Type understand to confirm permanent deletion.';
70
+ input = document.createElement("input"); input.type = "text"; input.autocomplete = "off"; input.spellcheck = false;
71
+ input.setAttribute("autocapitalize","none"); input.setAttribute("aria-label","Type understand to confirm permanent deletion");
72
+ field.appendChild(input); text.after(field);
73
+ ok.disabled = true;
74
+ input.addEventListener("input",function () { ok.disabled = input.value !== "understand"; });
75
+ document.addEventListener("keydown",onKey,true);
76
+ input.focus();
33
77
  }
34
78
 
35
79
  export function closeMateManagementMenu() {
@@ -394,6 +394,16 @@ export function handleMateSearchResults(msg) {
394
394
 
395
395
  var expandedDebateGroups = new Set();
396
396
 
397
+ function appendMateSessionTitleLine(el, textSpan) {
398
+ var processingDot = textSpan.querySelector('.session-processing');
399
+ if (processingDot) processingDot.remove();
400
+ var titleLine = document.createElement('span');
401
+ titleLine.className = 'mate-session-title-line';
402
+ titleLine.appendChild(textSpan);
403
+ if (processingDot) titleLine.appendChild(processingDot);
404
+ el.appendChild(titleLine);
405
+ }
406
+
397
407
  export function renderMateSessionList(sessions) {
398
408
  if (sessions) cachedSessions = sessions;
399
409
  if (!listEl) return;
@@ -529,7 +539,7 @@ function renderDebateGroup(children, groupKey) {
529
539
  textHtml += '<span class="mate-debate-name">' + escapeHtml(debateName) + '</span>';
530
540
  textHtml += '<span class="mate-debate-count">' + children.length + '</span>';
531
541
  textSpan.innerHTML = textHtml;
532
- el.appendChild(textSpan);
542
+ appendMateSessionTitleLine(el, textSpan);
533
543
 
534
544
  // Click row -> switch to latest session
535
545
  el.addEventListener("click", (function (id) {
@@ -0,0 +1,33 @@
1
+ // Theme-specific surfaces keep user color choices separate from text and actions.
2
+ export var MATE_WORKSPACE_COLORS = [
3
+ { id: 'mint', label: 'Mint', pigment: '#07e5a3' },
4
+ { id: 'rose', label: 'Rose', pigment: '#ec82ac' },
5
+ { id: 'plum', label: 'Plum', pigment: '#b780b3' },
6
+ { id: 'coral', label: 'Coral', pigment: '#ff7479' },
7
+ { id: 'peach', label: 'Peach', pigment: '#ed986b' },
8
+ { id: 'gold', label: 'Gold', pigment: '#e0b45b' },
9
+ { id: 'sky', label: 'Sky', pigment: '#69b6d4' },
10
+ { id: 'indigo', label: 'Indigo', pigment: '#8582df' },
11
+ { id: 'stone', label: 'Stone', pigment: '#a5a59e' }
12
+ ];
13
+ function mix(pigment, base, amount) {
14
+ var channels = [1, 3, 5].map(function (offset) {
15
+ var a = parseInt(pigment.slice(offset, offset + 2), 16);
16
+ var b = parseInt(base.slice(offset, offset + 2), 16);
17
+ return Math.round(a * amount + b * (1 - amount)).toString(16).padStart(2, '0');
18
+ });
19
+ return '#' + channels.join('');
20
+ }
21
+ export function workspaceColor(mate) {
22
+ var id = mate && mate.builtinKey !== 'clay' && !mate.primary ? mate.workspaceColor : 'mint';
23
+ var color = MATE_WORKSPACE_COLORS.find(function (item) { return item.id === id; }) || MATE_WORKSPACE_COLORS[0];
24
+ return Object.assign({}, color, {
25
+ light: mix(color.pigment, '#f2f2ef', color.id === 'mint' ? 0.03 : 0.08),
26
+ dark: mix(color.pigment, '#232322', color.id === 'mint' ? 0.02 : 0.10)
27
+ });
28
+ }
29
+ export function applyWorkspaceColor(mate) {
30
+ var color = workspaceColor(mate);
31
+ document.body.style.setProperty('--mate-surround-light', color.light);
32
+ document.body.style.setProperty('--mate-surround-dark', color.dark);
33
+ }
@@ -3,6 +3,7 @@ import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
3
3
  import { escapeHtml, copyToClipboard } from './utils.js';
4
4
  import { iconHtml, refreshIcons } from './icons.js';
5
5
  import { store } from './store.js';
6
+ import { getProjectAssistantPlaceholder } from './project-chat-identity.js';
6
7
 
7
8
  var ctx;
8
9
 
@@ -311,10 +312,6 @@ function color_mix_lighten(r, g, b, amount) {
311
312
  .map(function (v) { return v.toString(16).padStart(2, "0"); }).join("");
312
313
  }
313
314
 
314
- // Saved textarea placeholder so we can restore it when the chip is removed.
315
- // Set on first chip show, cleared on chip removal.
316
- var savedInputPlaceholder = null;
317
-
318
315
  function showInputMentionChip(name, color, avatarSrc) {
319
316
  removeInputMentionChip();
320
317
  var textColor = ensureChipContrast(color);
@@ -333,13 +330,9 @@ function showInputMentionChip(name, color, avatarSrc) {
333
330
  inputRow.insertBefore(chip, textareaWrap);
334
331
  }
335
332
 
336
- // Swap the textarea placeholder so the user sees "Ask @{name}..." instead of
337
- // the session-vendor placeholder ("Message Claude Code...") while a mention
338
- // is queued. Save the original once so re-shows don't clobber it.
333
+ // Swap the textarea placeholder so the user sees "Ask @{name}..." while a
334
+ // mention is queued. Restoration is handled only while this prompt owns it.
339
335
  if (ctx && ctx.inputEl) {
340
- if (savedInputPlaceholder === null) {
341
- savedInputPlaceholder = ctx.inputEl.placeholder || "";
342
- }
343
336
  ctx.inputEl.placeholder = "Ask @" + name + "...";
344
337
  }
345
338
 
@@ -353,9 +346,8 @@ function showInputMentionChip(name, color, avatarSrc) {
353
346
  function removeInputMentionChip() {
354
347
  var existing = document.getElementById("input-mention-chip");
355
348
  if (existing) existing.remove();
356
- if (ctx && ctx.inputEl && savedInputPlaceholder !== null) {
357
- ctx.inputEl.placeholder = savedInputPlaceholder;
358
- savedInputPlaceholder = null;
349
+ if (existing && ctx && ctx.inputEl && !ctx.inputEl.disabled && /^Ask @/.test(ctx.inputEl.placeholder || "")) {
350
+ ctx.inputEl.placeholder = getProjectAssistantPlaceholder(store.snap(), store.get('currentVendor'));
359
351
  }
360
352
  }
361
353
 
@@ -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
+ }