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

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 (231) 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/builtin-mates.js +7 -7
  5. package/lib/daemon-project-access.js +7 -1
  6. package/lib/daemon-settings.js +11 -0
  7. package/lib/daemon.js +10 -37
  8. package/lib/dicebear-avatar.js +62 -0
  9. package/lib/knowledge-db-service.js +175 -0
  10. package/lib/knowledge-db-validation.js +0 -0
  11. package/lib/knowledge-document-index.js +49 -0
  12. package/lib/knowledge-draft-store.js +56 -0
  13. package/lib/knowledge-file-store.js +199 -0
  14. package/lib/knowledge-markdown.js +128 -0
  15. package/lib/linear-store.js +35 -8
  16. package/lib/mate-identity-file.js +39 -0
  17. package/lib/mate-knowledge-mcp-server.js +29 -6
  18. package/lib/mate-knowledge-service.js +23 -2
  19. package/lib/mate-knowledge-sync.js +37 -22
  20. package/lib/mate-ready-creation.js +2 -1
  21. package/lib/mate-workspace-color.js +4 -0
  22. package/lib/mates-identity.js +4 -4
  23. package/lib/mates-prompts.js +4 -3
  24. package/lib/mates.js +30 -11
  25. package/lib/os-users.js +5 -16
  26. package/lib/project-connection.js +36 -9
  27. package/lib/project-debate-proposal.js +6 -0
  28. package/lib/project-debate-workspace.js +43 -0
  29. package/lib/project-debate.js +4 -2
  30. package/lib/project-filesystem.js +21 -39
  31. package/lib/project-knowledge.js +315 -136
  32. package/lib/project-mate-creation-proposal.js +1 -0
  33. package/lib/project-mate-interaction.js +7 -6
  34. package/lib/project-server-settings.js +31 -0
  35. package/lib/project-sessions.js +92 -182
  36. package/lib/project-user-knowledge.js +60 -0
  37. package/lib/project.js +62 -21
  38. package/lib/public/app.js +79 -71
  39. package/lib/public/clay-studio-wordmark-bold.svg +20 -0
  40. package/lib/public/css/clay-workspace.css +105 -0
  41. package/lib/public/css/debates-workbench.css +47 -0
  42. package/lib/public/css/home-debate-live.css +26 -0
  43. package/lib/public/css/home-hub.css +3 -1
  44. package/lib/public/css/home-mate-model-picker.css +15 -131
  45. package/lib/public/css/home-mate-settings.css +168 -16
  46. package/lib/public/css/icon-strip.css +249 -32
  47. package/lib/public/css/input.css +3 -1
  48. package/lib/public/css/mate-conversation.css +176 -0
  49. package/lib/public/css/mate-knowledge-workbench.css +118 -0
  50. package/lib/public/css/mate-workspace-color.css +16 -0
  51. package/lib/public/css/mates.css +28 -28
  52. package/lib/public/css/menus.css +3 -0
  53. package/lib/public/css/messages.css +1 -114
  54. package/lib/public/css/mobile-nav.css +16 -16
  55. package/lib/public/css/overlays.css +15 -0
  56. package/lib/public/css/project-logs.css +7 -0
  57. package/lib/public/css/project-mate-creation.css +105 -0
  58. package/lib/public/css/project-settings-modal.css +56 -0
  59. package/lib/public/css/right-workbench.css +3 -3
  60. package/lib/public/css/server-settings-system.css +125 -0
  61. package/lib/public/css/session-folders.css +80 -21
  62. package/lib/public/css/session-github.css +25 -16
  63. package/lib/public/css/settings-ui.css +247 -22
  64. package/lib/public/css/shared-browser.css +7 -3
  65. package/lib/public/css/sidebar.css +183 -107
  66. package/lib/public/css/title-bar.css +156 -1
  67. package/lib/public/css/worker-delegation.css +47 -0
  68. package/lib/public/css/worker-proposal.css +72 -150
  69. package/lib/public/css/you.css +74 -0
  70. package/lib/public/index.html +206 -548
  71. package/lib/public/modules/account-island.js +232 -0
  72. package/lib/public/modules/admin-settings-email.js +197 -0
  73. package/lib/public/modules/admin-settings-invitations.js +183 -0
  74. package/lib/public/modules/admin-settings-projects.js +198 -0
  75. package/lib/public/modules/admin-settings-ui.js +100 -0
  76. package/lib/public/modules/admin-settings-users.js +331 -0
  77. package/lib/public/modules/admin.js +19 -949
  78. package/lib/public/modules/app-cursors.js +19 -43
  79. package/lib/public/modules/app-debate-ui.js +4 -3
  80. package/lib/public/modules/app-dm.js +0 -2
  81. package/lib/public/modules/app-header.js +106 -0
  82. package/lib/public/modules/app-message-router.js +12 -3
  83. package/lib/public/modules/app-messages.js +47 -61
  84. package/lib/public/modules/app-notifications.js +4 -7
  85. package/lib/public/modules/app-projects.js +5 -38
  86. package/lib/public/modules/app-rate-limit.js +70 -12
  87. package/lib/public/modules/app-rendering.js +12 -13
  88. package/lib/public/modules/avatar.js +30 -9
  89. package/lib/public/modules/chat-render-runtime.js +16 -4
  90. package/lib/public/modules/command-palette.js +2 -15
  91. package/lib/public/modules/context-sources.js +27 -18
  92. package/lib/public/modules/cursor-sharing-preference.js +3 -3
  93. package/lib/public/modules/debate-controls.js +299 -0
  94. package/lib/public/modules/debate.js +2 -0
  95. package/lib/public/modules/debates-workbench.js +339 -0
  96. package/lib/public/modules/default-ai.js +88 -65
  97. package/lib/public/modules/header-tui-font.js +10 -20
  98. package/lib/public/modules/home-debate-controls.js +18 -289
  99. package/lib/public/modules/home-mate-avatar-editor.js +228 -0
  100. package/lib/public/modules/home-mate-color-picker.js +66 -0
  101. package/lib/public/modules/home-mate-creation.js +1 -1
  102. package/lib/public/modules/home-mate-instructions.js +90 -0
  103. package/lib/public/modules/home-mate-model-picker.js +80 -271
  104. package/lib/public/modules/home-mate-profile-editor.js +72 -0
  105. package/lib/public/modules/home-mate-settings.js +118 -125
  106. package/lib/public/modules/home-shell.js +3 -0
  107. package/lib/public/modules/home-surface-boot.js +1 -9
  108. package/lib/public/modules/icon-strip-tooltip.js +95 -0
  109. package/lib/public/modules/knowledge-db-edit-state.js +35 -0
  110. package/lib/public/modules/knowledge-db-workbench.js +234 -0
  111. package/lib/public/modules/knowledge-history-state.js +55 -0
  112. package/lib/public/modules/knowledge-markdown-editor.js +57 -0
  113. package/lib/public/modules/knowledge-preview-links.js +70 -0
  114. package/lib/public/modules/knowledge-workbench-shell.js +21 -0
  115. package/lib/public/modules/linear-settings.js +3 -2
  116. package/lib/public/modules/markdown.js +5 -6
  117. package/lib/public/modules/mate-knowledge-workbench-state.js +174 -0
  118. package/lib/public/modules/mate-knowledge-workbench.js +500 -0
  119. package/lib/public/modules/mate-knowledge.js +11 -3
  120. package/lib/public/modules/mate-management.js +54 -10
  121. package/lib/public/modules/mate-sidebar.js +11 -1
  122. package/lib/public/modules/mate-workspace-color.js +33 -0
  123. package/lib/public/modules/mention.js +5 -13
  124. package/lib/public/modules/notification-settings.js +81 -0
  125. package/lib/public/modules/notifications.js +44 -28
  126. package/lib/public/modules/personal-settings-dialog.js +51 -0
  127. package/lib/public/modules/profile.js +66 -24
  128. package/lib/public/modules/project-chat-identity.js +79 -0
  129. package/lib/public/modules/project-debate-planning-workspace.js +250 -0
  130. package/lib/public/modules/project-debate-workspace.js +185 -0
  131. package/lib/public/modules/project-header-identity.js +127 -0
  132. package/lib/public/modules/project-logs-render.js +9 -4
  133. package/lib/public/modules/project-mate-creation-workspace.js +414 -0
  134. package/lib/public/modules/project-mate-navigation.js +238 -0
  135. package/lib/public/modules/project-session-navigation.js +29 -0
  136. package/lib/public/modules/project-settings-environment.js +106 -0
  137. package/lib/public/modules/project-settings.js +72 -628
  138. package/lib/public/modules/project-switcher.js +6 -8
  139. package/lib/public/modules/projects-hub-view.js +5 -6
  140. package/lib/public/modules/projects-hub.js +2 -6
  141. package/lib/public/modules/qrcode.js +27 -8
  142. package/lib/public/modules/search-clay-chat.js +8 -11
  143. package/lib/public/modules/server-settings-controls.js +316 -0
  144. package/lib/public/modules/server-settings-environment.js +194 -0
  145. package/lib/public/modules/server-settings-navigation.js +39 -0
  146. package/lib/public/modules/server-settings-state.js +54 -0
  147. package/lib/public/modules/server-settings.js +116 -788
  148. package/lib/public/modules/server-storage-settings.js +84 -0
  149. package/lib/public/modules/session-activity-tooltip.js +12 -0
  150. package/lib/public/modules/session-create-form.js +86 -14
  151. package/lib/public/modules/session-create.js +50 -7
  152. package/lib/public/modules/session-github.js +2 -1
  153. package/lib/public/modules/session-list-visibility.js +41 -0
  154. package/lib/public/modules/session-presence-ui.js +3 -0
  155. package/lib/public/modules/session-title-marquee.js +51 -0
  156. package/lib/public/modules/settings-env-values.js +34 -0
  157. package/lib/public/modules/settings-select.js +121 -0
  158. package/lib/public/modules/shared-browser.js +22 -12
  159. package/lib/public/modules/sidebar-mates.js +3 -50
  160. package/lib/public/modules/sidebar-mobile.js +63 -60
  161. package/lib/public/modules/sidebar-projects.js +84 -97
  162. package/lib/public/modules/sidebar-session-folders-ui.js +2 -2
  163. package/lib/public/modules/sidebar-session-hierarchy.js +149 -70
  164. package/lib/public/modules/sidebar-sessions.js +49 -50
  165. package/lib/public/modules/sidebar.js +11 -13
  166. package/lib/public/modules/stt-picker.js +14 -5
  167. package/lib/public/modules/terminal-prefs.js +22 -0
  168. package/lib/public/modules/theme.js +88 -52
  169. package/lib/public/modules/thinking-lifecycle.js +7 -6
  170. package/lib/public/modules/tool-palette-order.js +4 -3
  171. package/lib/public/modules/tool-palette.js +13 -3
  172. package/lib/public/modules/tools.js +22 -22
  173. package/lib/public/modules/user-settings-account.js +318 -0
  174. package/lib/public/modules/user-settings-appearance.js +55 -0
  175. package/lib/public/modules/user-settings-editors.js +24 -0
  176. package/lib/public/modules/user-settings-integrations.js +104 -0
  177. package/lib/public/modules/user-settings-layout.js +80 -0
  178. package/lib/public/modules/user-settings-preferences.js +48 -0
  179. package/lib/public/modules/user-settings-profile.js +53 -0
  180. package/lib/public/modules/user-settings-themes.js +38 -0
  181. package/lib/public/modules/user-settings-voice.js +75 -0
  182. package/lib/public/modules/user-settings.js +53 -480
  183. package/lib/public/modules/worker-proposal.js +42 -8
  184. package/lib/public/modules/worker-runtime-picker.js +115 -0
  185. package/lib/public/modules/you-activity.js +121 -0
  186. package/lib/public/modules/you.js +251 -0
  187. package/lib/public/style.css +20 -8
  188. package/lib/public/vendor/codemirror/LICENSE +21 -0
  189. package/lib/public/vendor/codemirror/codemirror.css +344 -0
  190. package/lib/public/vendor/codemirror/codemirror.js +9884 -0
  191. package/lib/public/vendor/codemirror/continuelist.js +101 -0
  192. package/lib/public/vendor/codemirror/markdown.js +886 -0
  193. package/lib/public/vendor/codemirror/show-hint.css +37 -0
  194. package/lib/public/vendor/codemirror/show-hint.js +523 -0
  195. package/lib/sdk-bridge.js +15 -3
  196. package/lib/sdk-message-processor.js +16 -4
  197. package/lib/sdk-quota-snapshot.js +37 -0
  198. package/lib/server-admin.js +11 -0
  199. package/lib/server-appearance-settings.js +71 -0
  200. package/lib/server-home-chat-events.js +1 -1
  201. package/lib/server-home-chat.js +25 -3
  202. package/lib/server-home-debate-planning.js +4 -1
  203. package/lib/server-home-debates.js +2 -0
  204. package/lib/server-home-mate-creation.js +4 -1
  205. package/lib/server-linear.js +14 -8
  206. package/lib/server-mate-instructions.js +44 -0
  207. package/lib/server-mates.js +14 -4
  208. package/lib/server-notification-settings.js +67 -0
  209. package/lib/server-palette.js +1 -1
  210. package/lib/server-settings.js +69 -1
  211. package/lib/server-storage.js +57 -0
  212. package/lib/server-user-knowledge.js +119 -0
  213. package/lib/server.js +31 -3
  214. package/lib/session-visibility.js +7 -2
  215. package/lib/sessions.js +35 -10
  216. package/lib/startup-projects.js +40 -0
  217. package/lib/storage-native-walk.js +85 -0
  218. package/lib/storage-usage.js +144 -0
  219. package/lib/user-knowledge-service.js +167 -0
  220. package/lib/users-permissions.js +7 -0
  221. package/lib/users-preferences.js +0 -74
  222. package/lib/users-project-mate-filter-preferences.js +39 -0
  223. package/lib/users.js +6 -4
  224. package/lib/workspace-query-access.js +4 -1
  225. package/lib/ws-schema.js +52 -4
  226. package/lib/yoke/acp-process-manager.js +2 -2
  227. package/lib/yoke/adapters/codex.js +12 -52
  228. package/lib/yoke/codex-app-server.js +2 -2
  229. package/lib/yoke/codex-model-catalog.js +52 -0
  230. package/lib/yoke/codex-rate-limits.js +26 -0
  231. package/package.json +12 -3
@@ -12,7 +12,6 @@ import { dismissOverlayPanels, closeSidebar, updatePageTitle, spawnDustParticles
12
12
  import { showConfirm } from './app-misc.js';
13
13
  import { getUpcomingSchedules } from './scheduler.js';
14
14
  import { refreshMobileChatSheet } from './sidebar-mobile.js';
15
- import { VENDOR_AVATARS, VENDOR_NAMES } from './app-rendering.js';
16
15
  import { openGroup, openWorkerWithDriver, separateGroup } from './split-view.js';
17
16
  import { groupedSessionIds } from './split-group-helpers.js';
18
17
  import { resolvePreferredVendor } from './default-vendor.js';
@@ -24,6 +23,7 @@ import { renderFolderToolbar, captureFolderInputs, folderToolbarNeedsRender } fr
24
23
  import { captureFolderCollapseAnimations } from './session-folder-collapse.js';
25
24
  import { getListSearchQuery, getListSearchMatchIds, applyListSearchResults, filterItemsBySearch } from './session-list-search.js';
26
25
  import { expectCreatedSession } from './session-create.js';
26
+ import { isMateWorkspace } from './project-mate-navigation.js';
27
27
  import { describeSessionDelete } from './session-delete-scope.js';
28
28
  import { renderSessionPresence, setSessionPresence } from './session-presence-ui.js';
29
29
  import {
@@ -31,6 +31,9 @@ import {
31
31
  renderDesktopDriverHierarchy,
32
32
  renderDesktopOrphanHierarchy
33
33
  } from './sidebar-session-hierarchy.js';
34
+ import { ordinarySessionListSessions } from './session-list-visibility.js';
35
+ import { formatSessionActivity } from './session-activity-tooltip.js';
36
+ import { bindSessionTitleMarquee } from './session-title-marquee.js';
34
37
  import { summarizeSidebarItems } from './sidebar-session-counts.js';
35
38
  import {
36
39
  cancelSessionInlineRename,
@@ -272,7 +275,7 @@ function showSessionCtxMenu(anchorBtn, sessionId, title, cliSid, sessionData) {
272
275
 
273
276
 
274
277
  // Session visibility toggle (only the session owner can change)
275
- if (store.get('isMultiUserMode') && sessionData && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
278
+ if (!isMateWorkspace(store.snap()) && store.get('isMultiUserMode') && sessionData && !sessionData.isMate && sessionData.ownerId && sessionData.ownerId === store.get('myUserId')) {
276
279
  var currentVis = (sessionData && sessionData.sessionVisibility) || "private";
277
280
  var isPrivate = currentVis === "private";
278
281
  var visItem = document.createElement("button");
@@ -514,6 +517,17 @@ function isSessionVisibleBySearch(sessionId) {
514
517
  return getListSearchMatchIds().has(sessionId);
515
518
  }
516
519
 
520
+ function appendSessionTitleLine(el, textSpan) {
521
+ var processingDot = textSpan.querySelector('.session-processing');
522
+ if (processingDot) processingDot.remove();
523
+ var titleLine = document.createElement('span');
524
+ titleLine.className = 'session-item-title-line';
525
+ titleLine.appendChild(textSpan);
526
+ if (processingDot) titleLine.appendChild(processingDot);
527
+ el.appendChild(titleLine);
528
+ return titleLine;
529
+ }
530
+
517
531
  // --- Loop child / run / group rendering ---
518
532
 
519
533
  function renderLoopChild(s) {
@@ -539,7 +553,7 @@ function renderLoopChild(s) {
539
553
  textHtml += '<span class="session-loop-role-badge' + roleCls + '">' + roleName + iterSuffix + '</span>';
540
554
  }
541
555
  textSpan.innerHTML = textHtml;
542
- el.appendChild(textSpan);
556
+ appendSessionTitleLine(el, textSpan);
543
557
  appendSessionCloseButton(el, s);
544
558
 
545
559
  el.addEventListener("click", (function (id) {
@@ -665,7 +679,7 @@ function renderLoopGroup(loopId, children, groupKey) {
665
679
  textHtml += '<span class="session-loop-count' + countClass + '">' + countLabel + '</span>';
666
680
  }
667
681
  textSpan.innerHTML = textHtml;
668
- el.appendChild(textSpan);
682
+ appendSessionTitleLine(el, textSpan);
669
683
 
670
684
  // More button (ellipsis)
671
685
  var moreBtn = document.createElement("button");
@@ -768,7 +782,7 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
768
782
  textHtml += '<span class="session-loop-run-time">' + escapeHtml(timeLabel) + '</span>';
769
783
  textHtml += '<span class="session-loop-count' + (isRalph ? "" : " scheduled") + '">' + sessions.length + '</span>';
770
784
  textSpan.innerHTML = textHtml;
771
- el.appendChild(textSpan);
785
+ appendSessionTitleLine(el, textSpan);
772
786
 
773
787
  // Click row -> switch to latest session of this run
774
788
  el.addEventListener("click", (function (id) {
@@ -799,26 +813,24 @@ function renderLoopRun(parentGk, startedAtKey, sessions, isRalph) {
799
813
 
800
814
  function renderSessionItem(s, options) {
801
815
  var itemOptions = options || {};
816
+ var displaySession = s;
817
+ if (itemOptions.promotedActive || itemOptions.promotedProcessing || itemOptions.promotedUnread !== undefined) {
818
+ displaySession = Object.assign({}, s, {
819
+ active: !!(s.active || itemOptions.promotedActive),
820
+ isProcessing: !!(s.isProcessing || itemOptions.promotedProcessing),
821
+ unread: (s.unread || 0) + (itemOptions.promotedUnread || 0),
822
+ });
823
+ }
802
824
  var el = document.createElement("div");
803
825
  var isMatch = getListSearchMatchIds() !== null && getListSearchMatchIds().has(s.id);
804
- el.className = "session-item" + (s.active ? " active" : "") + (isMatch ? " search-match" : "");
826
+ el.className = "session-item" + (displaySession.active ? " active" : "") + (isMatch ? " search-match" : "");
805
827
  if (itemOptions.worker) el.classList.add("session-worker-item");
806
828
  el.dataset.sessionId = s.id;
807
829
 
808
- // Keep agent identity separate from the title so each row has a stable
809
- // visual anchor instead of mixing a tiny inline logo into the text.
810
- var itemVendor = s.vendor || "claude";
811
- var vendorIcon = document.createElement("img");
812
- vendorIcon.className = "session-vendor-icon session-vendor-mark";
813
- vendorIcon.src = VENDOR_AVATARS[itemVendor] || VENDOR_AVATARS.claude;
814
- vendorIcon.alt = "";
815
- vendorIcon.title = VENDOR_NAMES[itemVendor] || itemVendor;
816
- el.appendChild(vendorIcon);
817
-
818
830
  var textSpan = document.createElement("span");
819
831
  textSpan.className = "session-item-text";
820
832
  var textHtml = "";
821
- if (s.isProcessing) {
833
+ if (displaySession.isProcessing) {
822
834
  textHtml += '<span class="session-processing"></span>';
823
835
  }
824
836
  if (s.backgroundTaskCount) {
@@ -832,8 +844,12 @@ function renderSessionItem(s, options) {
832
844
  }
833
845
  textHtml += '<span class="session-item-title">' + highlightMatch(s.title || "New Session", getListSearchQuery()) + '</span>';
834
846
  textSpan.innerHTML = textHtml;
835
- el.appendChild(textSpan);
836
- appendGithubWork(el, s, textSpan);
847
+ textSpan.setAttribute("aria-label", s.title || "New Session");
848
+ var titleEl = textSpan.querySelector(".session-item-title");
849
+ var activityTitle = formatSessionActivity(s.lastActivity);
850
+ if (titleEl && activityTitle) titleEl.title = activityTitle;
851
+ var titleLine = appendSessionTitleLine(el, textSpan);
852
+ appendGithubWork(el, s, titleLine);
837
853
 
838
854
  if (itemOptions.worker) {
839
855
  var workerLabel = document.createElement("span");
@@ -844,13 +860,6 @@ function renderSessionItem(s, options) {
844
860
 
845
861
  var trailing = document.createElement("span");
846
862
  trailing.className = "session-row-trailing";
847
- var ageText = relativeTime(s.lastActivity || 0);
848
- if (ageText) {
849
- var age = document.createElement("span");
850
- age.className = "session-item-age";
851
- age.textContent = ageText;
852
- trailing.appendChild(age);
853
- }
854
863
 
855
864
  // Right-click / long-press: context menu
856
865
  el.addEventListener("contextmenu", (function(id, title, cliSid, anchor, sData) {
@@ -865,8 +874,8 @@ function renderSessionItem(s, options) {
865
874
  var unreadBadge = document.createElement("span");
866
875
  unreadBadge.className = "session-unread-badge";
867
876
  unreadBadge.dataset.sessionId = s.id;
868
- if (s.unread > 0) {
869
- unreadBadge.textContent = s.unread > 99 ? "99+" : String(s.unread);
877
+ if (displaySession.unread > 0) {
878
+ unreadBadge.textContent = displaySession.unread > 99 ? "99+" : String(displaySession.unread);
870
879
  unreadBadge.classList.add("has-unread");
871
880
  }
872
881
  el.appendChild(unreadBadge);
@@ -877,6 +886,7 @@ function renderSessionItem(s, options) {
877
886
  if (isFavoriteSession(s)) el.classList.add("is-favorite");
878
887
  }
879
888
  appendSessionCloseButton(actions, s);
889
+ el.style.setProperty("--session-actions-width", (actions.childNodes.length * 18 + Math.max(0, actions.childNodes.length - 1) * 4 + 21) + "px");
880
890
  if (actions.childNodes.length) trailing.appendChild(actions);
881
891
  if (trailing.childNodes.length) el.appendChild(trailing);
882
892
 
@@ -898,6 +908,7 @@ function renderSessionItem(s, options) {
898
908
 
899
909
  // Presence avatars (multi-user)
900
910
  renderSessionPresence(el, String(s.id));
911
+ bindSessionTitleMarquee(el, textSpan.querySelector(".session-item-title"), actions);
901
912
  if (!itemOptions.worker) setupFolderDrag(el, s);
902
913
 
903
914
  return el;
@@ -911,14 +922,7 @@ function renderSplitGroupItem(group, members) {
911
922
 
912
923
  var text = document.createElement("span");
913
924
  text.className = "session-item-text split-group-text";
914
- var html = '<span class="split-group-glyph" aria-hidden="true">' + iconHtml("columns-2") + '</span><span class="split-group-vendors">';
915
- for (var i = 0; i < members.length; i++) {
916
- var vendor = members[i].vendor || "claude";
917
- html += '<img class="session-vendor-icon split-group-vendor" src="' +
918
- (VENDOR_AVATARS[vendor] || VENDOR_AVATARS.claude) + '" alt="" title="' +
919
- escapeHtml(VENDOR_NAMES[vendor] || vendor) + '">';
920
- }
921
- html += "</span>";
925
+ var html = '<span class="split-group-glyph" aria-hidden="true">' + iconHtml("columns-2") + '</span>';
922
926
  var splitProcessing = false;
923
927
  var splitBackgroundTaskCount = 0;
924
928
  for (var si = 0; si < members.length; si++) {
@@ -929,7 +933,7 @@ function renderSplitGroupItem(group, members) {
929
933
  if (splitBackgroundTaskCount) html += '<span class="session-background-task-count">' + splitBackgroundTaskCount + '</span>';
930
934
  html += '<span class="split-group-name">' + highlightMatch(group.name || "Split", getListSearchQuery()) + "</span>";
931
935
  text.innerHTML = html;
932
- el.appendChild(text);
936
+ appendSessionTitleLine(el, text);
933
937
 
934
938
  var unread = 0;
935
939
  for (var ui = 0; ui < members.length; ui++) unread += members[ui].unread || 0;
@@ -938,7 +942,6 @@ function renderSplitGroupItem(group, members) {
938
942
  badge.dataset.splitGroupId = group.id;
939
943
  if (unread > 0) badge.textContent = unread > 99 ? "99+" : String(unread);
940
944
  el.appendChild(badge);
941
-
942
945
  el.addEventListener("click", function () {
943
946
  if (openGroup(group)) {
944
947
  dismissOverlayPanels();
@@ -994,22 +997,23 @@ export function renderSessionList(sessions) {
994
997
 
995
998
  // Partition: loop sessions vs normal sessions
996
999
  // Group by loopId + date so all runs of the same task on the same day are merged
1000
+ var visibleSessions = ordinarySessionListSessions(cachedSessions);
997
1001
  var splitGroups = store.get('splitGroups') || [];
998
1002
  var groupedIds = groupedSessionIds(splitGroups);
999
- var hierarchy = prepareSidebarHierarchy(cachedSessions);
1003
+ var hierarchy = prepareSidebarHierarchy(visibleSessions);
1000
1004
  var hierarchyByDriver = hierarchy.byDriver;
1001
1005
  var hierarchySessionIds = hierarchy.sessionIds;
1002
1006
  hierarchySessionIds.forEach(function (id) { groupedIds.delete(id); });
1003
1007
  var sessionsById = new Map();
1004
- for (var si = 0; si < cachedSessions.length; si++) sessionsById.set(cachedSessions[si].id, cachedSessions[si]);
1008
+ for (var si = 0; si < visibleSessions.length; si++) sessionsById.set(visibleSessions[si].id, visibleSessions[si]);
1005
1009
  var loopGroups = {}; // groupKey -> [sessions]
1006
1010
  var normalSessions = [];
1007
- for (var i = 0; i < cachedSessions.length; i++) {
1008
- var s = cachedSessions[i];
1011
+ for (var i = 0; i < visibleSessions.length; i++) {
1012
+ var s = visibleSessions[i];
1009
1013
  if (s.sessionRole === "worker") continue;
1010
1014
  if (groupedIds.has(s.id)) continue;
1011
- if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph" && s.loop.source !== "debate") {
1012
- // Task crafting sessions live in the scheduler calendar, not the main list (except debate)
1015
+ if (s.loop && s.loop.loopId && s.loop.role === "crafting" && s.loop.source !== "ralph") {
1016
+ // Task crafting sessions live in the scheduler calendar, not the main list.
1013
1017
  continue;
1014
1018
  } else if (s.loop && s.loop.loopId) {
1015
1019
  var startedAt = s.loop.startedAt || 0;
@@ -1029,11 +1033,6 @@ export function renderSessionList(sessions) {
1029
1033
  if (driverRoot) items.push({ type: "driver-hierarchy", root: driverRoot, data: driverRoot.driver, lastActivity: driverRoot.lastActivity });
1030
1034
  else items.push({ type: "session", data: normalSessions[j], lastActivity: normalSessions[j].lastActivity || 0 });
1031
1035
  }
1032
- if (hierarchy.orphans.length) {
1033
- var orphanActivity = 0;
1034
- for (var oi = 0; oi < hierarchy.orphans.length; oi++) orphanActivity = Math.max(orphanActivity, hierarchy.orphans[oi].lastActivity || 0);
1035
- items.push({ type: "orphan-workers", workers: hierarchy.orphans, lastActivity: orphanActivity });
1036
- }
1037
1036
  for (var sg = 0; sg < splitGroups.length; sg++) {
1038
1037
  var splitGroup = splitGroups[sg];
1039
1038
  var splitMembers = [];
@@ -1083,7 +1082,7 @@ export function renderSessionList(sessions) {
1083
1082
  if (it.type === "loop") return renderLoopGroup(it.loopId, it.children, it.groupKey) || document.createElement("div");
1084
1083
  if (it.type === "split-group") return renderSplitGroupItem(it.group, it.members);
1085
1084
  if (it.type === "driver-hierarchy") return renderDesktopDriverHierarchy(it.root, renderSessionItem, rerender, matchIds);
1086
- if (it.type === "orphan-workers") return renderDesktopOrphanHierarchy(it.workers, renderSessionItem, rerender, matchIds);
1085
+ if (it.type === "orphan-workers") return document.createElement("div");
1087
1086
  return renderSessionItem(it.data);
1088
1087
  },
1089
1088
  };
@@ -62,6 +62,14 @@ export function closeSidebar() {
62
62
  ctx.sidebarOverlay.classList.remove("visible");
63
63
  }
64
64
 
65
+ export function toggleSidebarCollapse() {
66
+ if (!ctx) return;
67
+ var layout = ctx.$("layout");
68
+ var collapsed = layout.classList.toggle("sidebar-collapsed");
69
+ try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
70
+ setTimeout(function () { syncResizeHandles(); }, 210);
71
+ }
72
+
65
73
  export function initSidebar(_ctx) {
66
74
  ctx = _ctx;
67
75
 
@@ -87,13 +95,6 @@ export function initSidebar(_ctx) {
87
95
  ctx.sidebarOverlay.addEventListener("click", closeSidebar);
88
96
 
89
97
  // --- Desktop sidebar collapse/expand ---
90
- function toggleSidebarCollapse() {
91
- var layout = ctx.$("layout");
92
- var collapsed = layout.classList.toggle("sidebar-collapsed");
93
- try { localStorage.setItem("sidebar-collapsed", collapsed ? "1" : ""); } catch (e) {}
94
- setTimeout(function () { syncUserIslandWidth(); syncResizeHandle(); }, 210);
95
- }
96
-
97
98
  if (ctx.sidebarToggleBtn) ctx.sidebarToggleBtn.addEventListener("click", toggleSidebarCollapse);
98
99
  if (ctx.sidebarExpandBtn) ctx.sidebarExpandBtn.addEventListener("click", toggleSidebarCollapse);
99
100
  var mateSidebarToggle = document.getElementById("mate-sidebar-toggle-btn");
@@ -245,12 +246,9 @@ export function initSidebar(_ctx) {
245
246
 
246
247
  function syncUserIslandWidth() {
247
248
  if (!userIsland) return;
248
- var mateSidebarColumn = document.getElementById("mate-sidebar-column");
249
- var isMateDM = document.body.classList.contains("mate-dm-active");
250
- var col = (isMateDM && mateSidebarColumn && !mateSidebarColumn.classList.contains("hidden")) ? mateSidebarColumn : sidebarColumn;
251
- if (!col) return;
252
- var rect = col.getBoundingClientRect();
253
- userIsland.style.width = (rect.right - 8 - 8) + "px";
249
+ var iconStrip = document.getElementById("icon-strip");
250
+ var stripWidth = iconStrip ? iconStrip.getBoundingClientRect().width : 0;
251
+ userIsland.style.width = (stripWidth > 0 ? stripWidth : 72) + "px";
254
252
  }
255
253
 
256
254
  // --- Sidebar resize handle ---
@@ -1,3 +1,5 @@
1
+ import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
2
+ import { enhanceSettingsSelects } from './settings-select.js';
1
3
  import { store } from './store.js';
2
4
  import { escapeHtml } from './utils.js';
3
5
  import { iconHtml, refreshIcons } from './icons.js';
@@ -5,7 +7,10 @@ import { speechLanguages, voiceModel, loadSpeechSettings, saveSpeechSettings, sa
5
7
  import { stopSTT } from './stt-controller.js';
6
8
  import { browserSpeechInfo, compareSpeechModels } from './stt-browser.js';
7
9
 
8
- export function closeSpeechPicker() {
10
+ export function closeSpeechPicker(force) {
11
+ if(document.querySelector('.admin-modal-overlay, #settings-confirm') && !force)return;
12
+ var input = document.querySelector("#speech-api-key");
13
+ if (!force && input && input.value) {showConfirmDialog("Discard the unsaved voice API key?", "Discard", function () {closeSpeechPicker(true);});return;}
9
14
  var state = store.get('speechPopup');
10
15
  store.set({ speechPopup: null });
11
16
  if (state && state.anchor.isConnected) { state.anchor.setAttribute('aria-expanded', 'false'); state.anchor.focus(); }
@@ -59,8 +64,10 @@ function render() {
59
64
  popup.style.left = Math.max(8, Math.min(bounds.right - 324, window.innerWidth - 332)) + 'px';
60
65
  popup.style.bottom = Math.max(8, window.innerHeight - bounds.top + 8) + 'px';
61
66
  popup.style.maxHeight = Math.max(150, bounds.top - 16) + 'px';
67
+ if (state.anchor.closest('#user-settings')) {popup.style.bottom = 'auto';popup.style.top = 'max(16px, calc(50dvh - 260px))';popup.style.maxHeight = 'calc(100dvh - 48px)';}
62
68
  popup.addEventListener('click', function (event) {
63
69
  var button = event.target.closest('button'); if (!button || button.disabled) return;
70
+ if (popup.querySelector("#speech-api-key") && popup.querySelector("#speech-api-key").value && (button.dataset.mode || button.dataset.provider || button.dataset.model)) {showConfirmDialog('Discard the unsaved voice API key?', 'Discard', function () {form.elements.key.value = '';button.click();});return;}
64
71
  if (button.dataset.mode) patch({ mode: button.dataset.mode, provider: null, keyModel: null });
65
72
  else if (button.dataset.provider) {
66
73
  var keyModel = button.parentElement.querySelector('[data-model]').dataset.model;
@@ -80,7 +87,7 @@ function render() {
80
87
  var form = popup.querySelector('form');
81
88
  if (form && previousProvider === state.provider) form.elements.key.value = unsavedKey;
82
89
  if (form) form.addEventListener('submit', function (event) {
83
- event.preventDefault(); var key = form.elements.key.value; form.elements.key.value = '';
90
+ event.preventDefault(); var key = form.elements.key.value;
84
91
  save({ provider: state.provider, key: key }, false);
85
92
  });
86
93
  var select = popup.querySelector('select');
@@ -89,6 +96,7 @@ function render() {
89
96
  try { await saveSpeechLanguage(code); if (store.get('speechPopup')) patch({ busy: false, error: '' }); }
90
97
  catch (error) { if (store.get('speechPopup')) patch({ busy: false, error: error.message }); }
91
98
  });
99
+ enhanceSettingsSelects(popup);
92
100
  var focus = Array.from(popup.querySelectorAll('[data-focus]')).find(function (el) { return el.dataset.focus === focusKey && !el.disabled; });
93
101
  (form && previousProvider !== state.provider ? form.elements.key : (focus || popup.querySelector('input,button'))).focus();
94
102
  }
@@ -108,14 +116,15 @@ export function initSpeechPicker() {
108
116
  });
109
117
  document.addEventListener('pointerdown', function (event) {
110
118
  var state = store.get('speechPopup'); var popup = document.getElementById('speech-picker');
119
+ if(event.target.closest('.admin-modal-overlay, #settings-confirm'))return;
111
120
  if (state && popup && !popup.contains(event.target) && !state.anchor.contains(event.target)) closeSpeechPicker();
112
121
  });
113
122
  document.addEventListener('keydown', function (event) {
114
- if (!store.get('speechPopup')) return;
115
- if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); closeSpeechPicker(); }
123
+ if (!store.get('speechPopup') || document.querySelector('.admin-modal-overlay, #settings-confirm')) return;
124
+ if (event.key === 'Escape') { if(document.activeElement.closest('.settings-picker') && document.activeElement.closest('.settings-picker').querySelector('[aria-expanded=true]'))return; event.preventDefault(); event.stopPropagation(); closeSpeechPicker(); }
116
125
  if (event.key === 'Tab') {
117
126
  var popup = document.getElementById('speech-picker');
118
- var items = popup.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),a[href]');
127
+ var items = Array.from(popup.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),a[href]')).filter(function(el){return el.getClientRects().length && el.tabIndex!==-1;});
119
128
  var first = items[0]; var last = items[items.length - 1];
120
129
  if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
121
130
  else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
@@ -1,3 +1,15 @@
1
+ export var TERMINAL_FONT_OPTIONS = [
2
+ { label: "SF Mono", family: "'SF Mono', Menlo, Monaco, 'Courier New', monospace" },
3
+ { label: "JetBrains Mono", family: "'JetBrains Mono', 'SF Mono', Menlo, monospace" },
4
+ { label: "Fira Code", family: "'Fira Code', 'SF Mono', Menlo, monospace" },
5
+ { label: "Cascadia Code", family: "'Cascadia Code', 'SF Mono', Menlo, monospace" },
6
+ { label: "IBM Plex Mono", family: "'IBM Plex Mono', 'SF Mono', Menlo, monospace" },
7
+ { label: "Source Code Pro", family: "'Source Code Pro', 'SF Mono', Menlo, monospace" },
8
+ { label: "Roboto Mono", family: "'Roboto Mono', 'SF Mono', Menlo, monospace" },
9
+ { label: "D2 Coding", family: "'D2 coding', 'D2Coding', 'SF Mono', Menlo, monospace" },
10
+ { label: "System mono", family: "ui-monospace, monospace" },
11
+ ];
12
+
1
13
  // terminal-prefs.js
2
14
  //
3
15
  // Single source of truth for terminal font preferences (family + size).
@@ -98,3 +110,13 @@ export function applyTerminalFont(family, size) {
98
110
  export function onTerminalFontChange(fn) {
99
111
  if (typeof fn === "function") listeners.push(fn);
100
112
  }
113
+
114
+ // Publish only acknowledged values, shared by settings and the terminal toolbar.
115
+ export async function saveTerminalFont(family, size) {
116
+ var response = await fetch('/api/user/terminal-font', {
117
+ method: 'PUT', headers: {'Content-Type':'application/json'},
118
+ body: JSON.stringify({family: family, size: size})
119
+ });
120
+ if (!response.ok) throw new Error('Could not save terminal typography. Try again.');
121
+ applyTerminalFont(family, size);
122
+ }
@@ -1,3 +1,4 @@
1
+ import { store } from './store.js';
1
2
  import { refreshIcons } from './icons.js';
2
3
  import { setTerminalTheme } from './terminal.js';
3
4
  import { setTuiSessionTheme } from './session-tui-view.js';
@@ -315,19 +316,7 @@ export function applyTheme(themeId, fromPicker) {
315
316
  var mermaidVars = computeMermaidVars(theme);
316
317
  try { updateMermaidTheme(mermaidVars); } catch (e) {}
317
318
 
318
- try {
319
- localStorage.setItem(STORAGE_KEY, themeId);
320
- localStorage.setItem(STORAGE_KEY + "-vars", JSON.stringify(vars));
321
- localStorage.setItem(STORAGE_KEY + "-variant", theme.variant || "dark");
322
- } catch (e) {}
323
-
324
- // When picked from skin selector, save as skin preference and sync mode
325
- if (fromPicker) {
326
- try {
327
- localStorage.setItem(SKIN_KEY, themeId);
328
- localStorage.setItem(MODE_KEY, isLight ? "light" : "dark");
329
- } catch (e) {}
330
- }
319
+ if (fromPicker) saveAppearance(themeId, isLight ? 'light' : 'dark');
331
320
 
332
321
  updateToggleIcon();
333
322
 
@@ -417,7 +406,7 @@ function getSystemMode() {
417
406
  // Returns the effective mode: user override or system
418
407
  function getEffectiveMode() {
419
408
  var saved = null;
420
- try { saved = localStorage.getItem(MODE_KEY); } catch (e) {}
409
+ saved = (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode;
421
410
  if (saved === "light" || saved === "dark") return saved;
422
411
  return getSystemMode();
423
412
  }
@@ -426,7 +415,10 @@ function getEffectiveMode() {
426
415
  // If user has a custom skin selected, find its dark/light counterpart
427
416
  function themeIdForMode(mode) {
428
417
  var skin = null;
429
- try { skin = localStorage.getItem(SKIN_KEY); } catch (e) {}
418
+ var preference = store.get('appearanceDraft') || store.get('appearancePreference') || {};
419
+ var paired = preference[mode + 'Theme'];
420
+ if (paired && getTheme(paired) && getTheme(paired).variant === mode) return paired;
421
+ skin = preference.theme;
430
422
 
431
423
  // Default Clay Studio skin pair.
432
424
  if (!skin) {
@@ -457,24 +449,12 @@ function themeIdForMode(mode) {
457
449
 
458
450
  // --- Wide view ---
459
451
 
460
- export function isWideView() {
461
- try {
462
- var v = localStorage.getItem(WIDE_KEY);
463
- return v === null ? true : v !== "bubble"; // default: Channel (wide)
464
- } catch (e) { return true; }
465
- }
466
-
467
- export function getChatLayout() {
468
- try {
469
- var v = localStorage.getItem(WIDE_KEY);
470
- return v === "bubble" ? "bubble" : "channel";
471
- } catch (e) { return "channel"; }
472
- }
473
-
452
+ export function isWideView() { return getChatLayout() === 'channel'; }
453
+ export function getChatLayout() { return store.get('personalChatLayout') || 'channel'; }
474
454
  export function setChatLayout(layout) {
475
- var val = (layout === "bubble") ? "bubble" : "channel";
476
- try { localStorage.setItem(WIDE_KEY, val); } catch (e) {}
477
- document.body.classList.toggle("wide-view", val === "channel");
455
+ var val = layout === 'bubble' ? 'bubble' : 'channel';
456
+ store.set({ personalChatLayout: val });
457
+ document.body.classList.toggle('wide-view', val === 'channel');
478
458
  }
479
459
 
480
460
  export function setWideView(enabled) {
@@ -485,9 +465,10 @@ export function setWideView(enabled) {
485
465
  export function toggleDarkMode() {
486
466
  var current = getEffectiveMode();
487
467
  var next = current === "dark" ? "light" : "dark";
488
- try { localStorage.setItem(MODE_KEY, next); } catch (e) {}
468
+
489
469
  var tid = themeIdForMode(next);
490
470
  applyTheme(tid);
471
+ saveAppearance(tid, next);
491
472
  updateToggleIcon();
492
473
  }
493
474
 
@@ -633,26 +614,11 @@ function rebuildPicker() {
633
614
 
634
615
  // --- Init ---
635
616
  export function initTheme() {
636
- // Preserve an explicitly selected skin. Previously auto-applied Ayu Light
637
- // and Dracula values have no SKIN_KEY, so they migrate to the Clay pair.
638
- var saved = null;
639
- var savedSkin = null;
640
- try { saved = localStorage.getItem(STORAGE_KEY); } catch (e) {}
641
- try { savedSkin = localStorage.getItem(SKIN_KEY); } catch (e) {}
642
-
643
- if (saved && savedSkin) {
644
- currentThemeId = saved;
645
- } else {
646
- // No explicit skin — use the Clay Studio pair in the system mode.
647
- var mode = getSystemMode();
648
- currentThemeId = mode === "light" ? DEFAULT_LIGHT_THEME_ID : DEFAULT_DARK_THEME_ID;
649
- }
650
-
651
- // Apply wide view state from localStorage
652
- document.body.classList.toggle("wide-view", isWideView());
617
+ currentThemeId = getSystemMode() === 'light' ? DEFAULT_LIGHT_THEME_ID : DEFAULT_DARK_THEME_ID;
618
+ document.body.classList.toggle('wide-view', isWideView());
653
619
 
654
620
  // Load all themes from server, then apply properly
655
- loadThemes();
621
+ loadThemes().then(loadAppearance);
656
622
 
657
623
  // The theme picker is mounted in User Settings by user-settings.js.
658
624
 
@@ -661,7 +627,7 @@ export function initTheme() {
661
627
  var mq = window.matchMedia("(prefers-color-scheme: dark)");
662
628
  var handler = function () {
663
629
  var userMode = null;
664
- try { userMode = localStorage.getItem(MODE_KEY); } catch (e) {}
630
+ userMode = (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode;
665
631
  if (!userMode) {
666
632
  // No manual override — follow system
667
633
  var sysMode = getSystemMode();
@@ -680,3 +646,73 @@ export function initTheme() {
680
646
  updateToggleIcon();
681
647
 
682
648
  }
649
+
650
+ function appearanceStatus(text, error) {
651
+ store.set({ appearanceSaveStatus: text, appearanceSaveError: !!error });
652
+ var status = document.getElementById('us-appearance-status');
653
+ if (status) { status.textContent = text; status.dataset.state = error ? 'error' : ''; }
654
+ }
655
+ export function getAppearanceChoices() {
656
+ return { mode: (store.get('appearanceDraft') || store.get('appearancePreference') || {}).mode || 'system', lightTheme: themeIdForMode('light'), darkTheme: themeIdForMode('dark') };
657
+ }
658
+ export function retryAppearanceSave() {
659
+ var draft = store.get('appearanceDraft');
660
+ if (draft) saveAppearance(draft.theme, draft.mode, draft);
661
+ }
662
+ export function selectAppearanceMode(mode) {
663
+ var effective = mode === 'system' ? getSystemMode() : mode;
664
+ var id = themeIdForMode(effective);
665
+ saveAppearance(id, mode === 'system' ? null : mode);
666
+ applyTheme(id);
667
+ }
668
+ export function selectAppearanceTheme(mode, id) {
669
+ if (!getTheme(id) || getTheme(id).variant !== mode) return;
670
+ var effective = getEffectiveMode();
671
+ var current = store.get('appearanceDraft') || store.get('appearancePreference') || {};
672
+ var change = {}; change[mode + 'Theme'] = id;
673
+ saveAppearance(mode === effective ? id : currentThemeId, current.mode || null, change);
674
+ if (mode === effective) applyTheme(id);
675
+ }
676
+ function saveAppearance(theme, mode, overrides) {
677
+ var values = Object.assign({}, store.get('appearanceDraft') || store.get('appearancePreference') || {}, {theme:theme, mode:mode}, overrides || {});
678
+ delete values.configured;
679
+ var selected = getTheme(theme);
680
+ if (selected) values[selected.variant + 'Theme'] = theme;
681
+ store.set({appearanceDraft:values});
682
+ var queue = store.get('appearanceSaveQueue') || Promise.resolve();
683
+ var next = queue.catch(function () {}).then(function () {return persistAppearance(values);});
684
+ store.set({appearanceSaveQueue:next});
685
+ return next;
686
+ }
687
+ async function persistAppearance(values) {
688
+ var request = (store.get('appearanceSaveRequest') || 0) + 1;
689
+ store.set({ appearanceSaveRequest: request });
690
+ appearanceStatus('Saving appearance…');
691
+ try {
692
+ var response = await fetch('/api/user/appearance', {method:'PUT',headers:{'Content-Type':'application/json','X-Clay-Settings':'1'},body:JSON.stringify(values)});
693
+ if (!response.ok) throw new Error('Could not save appearance. Your preview remains. Retry to save it.');
694
+ if (store.get('appearanceSaveRequest') !== request) return;
695
+ var saved = await response.json();
696
+ store.set({ appearancePreference: saved, appearanceDraft: store.get('appearanceDraft') === values ? null : store.get('appearanceDraft') });
697
+ [STORAGE_KEY,SKIN_KEY,MODE_KEY,STORAGE_KEY+'-vars',STORAGE_KEY+'-variant'].forEach(function (key) {try {localStorage.removeItem(key);}catch(error){}});
698
+ appearanceStatus('Saved. Applied to this view; other devices use it when they reload.');
699
+ } catch(error) {if(store.get('appearanceSaveRequest') === request) appearanceStatus(error.message,true);}
700
+ }
701
+ async function loadAppearance() {
702
+ try {
703
+ var response = await fetch('/api/user/appearance');
704
+ if (!response.ok) throw new Error('Appearance preferences unavailable.');
705
+ var data = await response.json();
706
+ if(store.get('appearanceSaveRequest'))return;
707
+ if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
708
+ else {
709
+ var legacy = null;try {legacy = localStorage.getItem(STORAGE_KEY);}catch(error){}
710
+ if(legacy && getTheme(legacy))store.set({legacyAppearanceAvailable:legacy});
711
+ }
712
+ } catch(error) {appearanceStatus(error.message,true);}
713
+ }
714
+
715
+ export function importLegacyAppearance() {
716
+ var legacy=store.get('legacyAppearanceAvailable');
717
+ if(legacy) {applyTheme(legacy,true);store.set({legacyAppearanceAvailable:null});}
718
+ }
@@ -3,6 +3,7 @@ import { iconHtml, refreshIcons } from './icons.js';
3
3
  import { store } from './store.js';
4
4
  import { addToMessages, keepActiveThinkingAtTail, scrollToBottom } from './chat-render-runtime.js';
5
5
  import { prepareThinkingView, bindThinkingView, updateThinkingView, beginThinkingView, setThinkingViewLive, finishThinkingView } from './thinking-view.js';
6
+ import { resolveProjectAssistantIdentity, applyProjectAssistantIdentity } from './project-chat-identity.js';
6
7
 
7
8
  function getThinkingState() {
8
9
  return store.get('thinkingState') || null;
@@ -19,8 +20,7 @@ function updateThinkingState(state, patch) {
19
20
  }
20
21
 
21
22
  function isMateThinking() {
22
- var target = store.get('dmTargetUser');
23
- return !!(store.get('dmMode') && target && target.isMate);
23
+ return resolveProjectAssistantIdentity(store.snap(), store.get('currentVendor')).kind === 'mate';
24
24
  }
25
25
 
26
26
  function maybeScrollToBottom() {
@@ -30,10 +30,10 @@ function maybeScrollToBottom() {
30
30
  function createThinkingElement() {
31
31
  var el = document.createElement("div");
32
32
  el.className = "thinking-item";
33
- if (isMateThinking()) {
34
- var target = store.get('dmTargetUser');
35
- var mateName = (target && target.displayName) || "Mate";
36
- var mateAvatar = document.body.dataset.mateAvatarUrl || "";
33
+ var identity = resolveProjectAssistantIdentity(store.snap(), store.get('currentVendor'));
34
+ if (identity.kind === 'mate') {
35
+ var mateName = identity.name;
36
+ var mateAvatar = identity.avatarUrl;
37
37
  el.classList.add("mate-thinking");
38
38
  el.innerHTML =
39
39
  '<img class="dm-bubble-avatar dm-bubble-avatar-mate" src="' + escapeHtml(mateAvatar) + '" alt="">' +
@@ -48,6 +48,7 @@ function createThinkingElement() {
48
48
  '</div>' +
49
49
  '<div class="thinking-content"></div>' +
50
50
  '</div>';
51
+ applyProjectAssistantIdentity(el, identity);
51
52
  } else {
52
53
  el.innerHTML =
53
54
  '<div class="thinking-header">' +