clay-server 5.2.0-beta.8 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +0 -4
  2. package/lib/clay-product-control-tools.js +1 -0
  3. package/lib/clay-product-control.js +14 -11
  4. package/lib/clay-vendor-usage.js +44 -0
  5. package/lib/github-star-count.js +147 -0
  6. package/lib/notes-checklist.js +43 -1
  7. package/lib/notes.js +2 -1
  8. package/lib/pane-pending-interactions.js +20 -0
  9. package/lib/pane-session-activation.js +114 -0
  10. package/lib/pane-session-transport.js +26 -0
  11. package/lib/project-session-notes.js +33 -8
  12. package/lib/project-sessions.js +78 -29
  13. package/lib/project-start-protocol.js +29 -0
  14. package/lib/project-vendor-login.js +2 -0
  15. package/lib/project.js +16 -4
  16. package/lib/public/app.js +41 -18
  17. package/lib/public/css/contextual-clay.css +2 -2
  18. package/lib/public/css/filebrowser.css +1 -1
  19. package/lib/public/css/input.css +6 -0
  20. package/lib/public/css/mate-loading-scene.css +21 -0
  21. package/lib/public/css/overlays.css +64 -256
  22. package/lib/public/css/project-mate-creation.css +5 -6
  23. package/lib/public/css/project-start.css +108 -0
  24. package/lib/public/css/projects-hub.css +53 -14
  25. package/lib/public/css/settings-shell.css +103 -0
  26. package/lib/public/css/sidebar.css +7 -0
  27. package/lib/public/css/sticky-notes.css +37 -11
  28. package/lib/public/css/title-bar.css +83 -0
  29. package/lib/public/index.html +50 -63
  30. package/lib/public/modules/account-transport-settings.js +8 -2
  31. package/lib/public/modules/admin-settings-email.js +7 -2
  32. package/lib/public/modules/admin-settings-invitations.js +2 -2
  33. package/lib/public/modules/admin-settings-projects.js +2 -2
  34. package/lib/public/modules/admin-settings-ui.js +20 -4
  35. package/lib/public/modules/admin-settings-users.js +4 -4
  36. package/lib/public/modules/app-connection.js +3 -0
  37. package/lib/public/modules/app-home-hub.js +2 -1
  38. package/lib/public/modules/app-message-router.js +13 -0
  39. package/lib/public/modules/app-messages.js +35 -7
  40. package/lib/public/modules/app-misc.js +3 -1
  41. package/lib/public/modules/app-projects.js +6 -407
  42. package/lib/public/modules/app-rate-limit.js +12 -1
  43. package/lib/public/modules/clay-owner-navigation.js +0 -3
  44. package/lib/public/modules/clay-studio-render.js +3 -1
  45. package/lib/public/modules/clay-tool-presentation.js +1 -0
  46. package/lib/public/modules/command-palette.js +52 -14
  47. package/lib/public/modules/contextual-clay-workspace.js +14 -2
  48. package/lib/public/modules/contextual-clay.js +19 -4
  49. package/lib/public/modules/contextual-mate-context.js +6 -4
  50. package/lib/public/modules/elicitation-form.js +5 -0
  51. package/lib/public/modules/github-star.js +66 -0
  52. package/lib/public/modules/history-replay-batch.js +8 -0
  53. package/lib/public/modules/home-surface-boot.js +0 -1
  54. package/lib/public/modules/input.js +55 -11
  55. package/lib/public/modules/mate-loading-scene.js +17 -0
  56. package/lib/public/modules/message-delivery.js +10 -2
  57. package/lib/public/modules/model-picker.js +2 -0
  58. package/lib/public/modules/pane-activation.js +214 -0
  59. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  60. package/lib/public/modules/pane-binding.js +82 -0
  61. package/lib/public/modules/pane-bridge.js +5 -2
  62. package/lib/public/modules/pane-browser-bridge.js +5 -3
  63. package/lib/public/modules/pane-file-bridge.js +4 -0
  64. package/lib/public/modules/pane-issue-bridge.js +4 -1
  65. package/lib/public/modules/profile.js +11 -3
  66. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  67. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  68. package/lib/public/modules/project-settings-environment.js +12 -1
  69. package/lib/public/modules/project-settings.js +119 -129
  70. package/lib/public/modules/project-start-dialog.js +130 -0
  71. package/lib/public/modules/project-start-folder-state.js +78 -0
  72. package/lib/public/modules/project-start-folder-view.js +190 -0
  73. package/lib/public/modules/project-start-forms.js +66 -0
  74. package/lib/public/modules/project-start-path.js +60 -0
  75. package/lib/public/modules/project-start-state.js +54 -0
  76. package/lib/public/modules/project-start-submit.js +67 -0
  77. package/lib/public/modules/projects-hub-view.js +32 -44
  78. package/lib/public/modules/projects-hub.js +39 -28
  79. package/lib/public/modules/rewind.js +5 -0
  80. package/lib/public/modules/server-settings.js +36 -91
  81. package/lib/public/modules/session-actions.js +5 -2
  82. package/lib/public/modules/session-context-menu.js +5 -0
  83. package/lib/public/modules/session-rich-drafts.js +21 -0
  84. package/lib/public/modules/session-switch-defer.js +67 -0
  85. package/lib/public/modules/settings-project-target.js +56 -0
  86. package/lib/public/modules/settings-select.js +1 -1
  87. package/lib/public/modules/settings-shell-registry.js +88 -0
  88. package/lib/public/modules/settings-shell-view.js +104 -0
  89. package/lib/public/modules/settings-shell.js +342 -0
  90. package/lib/public/modules/sidebar-mobile.js +8 -30
  91. package/lib/public/modules/sidebar-projects.js +2 -1
  92. package/lib/public/modules/split-group-restore.js +68 -0
  93. package/lib/public/modules/split-pair-ui.js +1 -0
  94. package/lib/public/modules/split-pane-reconciler.js +15 -0
  95. package/lib/public/modules/split-pane-renderer.js +87 -6
  96. package/lib/public/modules/split-session-boundary.js +1 -1
  97. package/lib/public/modules/split-view.js +51 -52
  98. package/lib/public/modules/sticky-notes-checklist.js +51 -6
  99. package/lib/public/modules/stt-controller.js +4 -1
  100. package/lib/public/modules/stt-recording.js +1 -1
  101. package/lib/public/modules/theme.js +31 -3
  102. package/lib/public/modules/tools.js +18 -2
  103. package/lib/public/modules/user-settings-integrations.js +2 -1
  104. package/lib/public/modules/user-settings-layout.js +12 -34
  105. package/lib/public/modules/user-settings-profile.js +1 -1
  106. package/lib/public/modules/user-settings.js +18 -85
  107. package/lib/public/modules/vendor-usage-format.js +93 -0
  108. package/lib/public/modules/vendor-usage-list.js +45 -0
  109. package/lib/public/modules/vendor-usage-model.js +215 -0
  110. package/lib/public/modules/vendor-usage-overview.js +323 -0
  111. package/lib/public/style.css +10 -7
  112. package/lib/public/welcome-art/conversation-desk.png +0 -0
  113. package/lib/public/welcome-art/mate-clay.png +0 -0
  114. package/lib/public/welcome-art/project-clay.png +0 -0
  115. package/lib/sdk-bridge.js +6 -1
  116. package/lib/sdk-message-processor.js +9 -1
  117. package/lib/sdk-quota-snapshot.js +20 -2
  118. package/lib/server-github-stars.js +39 -0
  119. package/lib/server-global-ws.js +21 -17
  120. package/lib/server.js +11 -0
  121. package/lib/session-notes-mcp-server.js +26 -4
  122. package/lib/session-notes-writes.js +88 -0
  123. package/lib/session-visibility.js +20 -8
  124. package/lib/sessions.js +27 -10
  125. package/lib/vendor-usage-acp-readers.js +97 -0
  126. package/lib/vendor-usage-capabilities.js +88 -0
  127. package/lib/vendor-usage-http-readers.js +214 -0
  128. package/lib/vendor-usage-metrics.js +47 -0
  129. package/lib/vendor-usage-native-readers.js +106 -0
  130. package/lib/vendor-usage-overview.js +406 -0
  131. package/lib/vendor-usage-service.js +281 -0
  132. package/lib/ws-schema.js +4 -2
  133. package/lib/yoke/adapters/claude.js +12 -0
  134. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  135. package/lib/yoke/adapters/codex.js +45 -12
  136. package/lib/yoke/claude-hook-installer.js +1 -0
  137. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  138. package/lib/yoke/codex-model-catalog.js +37 -3
  139. package/lib/yoke/muse-services.js +16 -3
  140. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  141. package/lib/yoke/vendor-registry.js +6 -2
  142. package/package.json +5 -3
  143. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -222,6 +222,7 @@ var customSet = {}; // ids that came from ~/.clay/themes/
222
222
  var themesLoaded = false;
223
223
  var currentThemeId = DEFAULT_DARK_THEME_ID;
224
224
  var changeCallbacks = [];
225
+ var externalThemeId = null; // set by the parent shell for pane iframes
225
226
  var STORAGE_KEY = "clay-theme";
226
227
  var MODE_KEY = "clay-mode"; // "light" | "dark" | null (system)
227
228
  var SKIN_KEY = "clay-skin"; // theme id within current variant pair
@@ -274,6 +275,23 @@ export function onThemeChange(fn) {
274
275
  changeCallbacks.push(fn);
275
276
  }
276
277
 
278
+ // The appearance this frame currently shows, for the parent shell to hand to
279
+ // its pane iframes. Never persisted from here.
280
+ export function getAppearanceSnapshot() {
281
+ var pref = store.get('appearanceDraft') || store.get('appearancePreference') || {};
282
+ return { themeId: currentThemeId, userId: store.get('myUserId') || null,
283
+ preference: { configured: !!pref.configured || !!pref.mode || !!pref.theme, theme: pref.theme || null, mode: pref.mode || null,
284
+ lightTheme: pref.lightTheme || null, darkTheme: pref.darkTheme || null } };
285
+ }
286
+
287
+ // A pane iframe mirrors the parent's live appearance. The server remains the
288
+ // only persisted source, so this applies the view without saving anything.
289
+ export function applyExternalAppearance(snapshot) {
290
+ externalThemeId = snapshot.themeId;
291
+ store.set({ appearancePreference: snapshot.preference, appearanceDraft: null });
292
+ applyTheme(snapshot.themeId);
293
+ }
294
+
277
295
  export function getThemes() {
278
296
  // Return a copy
279
297
  var all = {};
@@ -367,7 +385,7 @@ function loadThemes() {
367
385
 
368
386
  // Always apply the current theme now that real data is loaded
369
387
  // (before this, only defaultExactVars was used as fallback)
370
- applyTheme(currentThemeId);
388
+ applyTheme(externalThemeId && getTheme(externalThemeId) ? externalThemeId : currentThemeId);
371
389
  }).catch(function () {
372
390
  // API unavailable — keep the embedded Clay Studio pair.
373
391
  themes[DEFAULT_DARK_THEME_ID] = defaultDarkFallback;
@@ -482,8 +500,13 @@ function updateToggleIcon() {
482
500
  // User settings toggle
483
501
  var usToggle = document.getElementById("us-theme-toggle");
484
502
  if (usToggle) usToggle.checked = isLight;
485
- // User island button is now a static "skins / themes" trigger; no
486
- // icon swap on mode change.
503
+ var quickToggle = document.getElementById("appearance-mode-toggle");
504
+ if (quickToggle) {
505
+ var label = isLight ? "Switch to dark mode" : "Switch to light mode";
506
+ quickToggle.setAttribute("aria-label", label);
507
+ quickToggle.title = label;
508
+ quickToggle.innerHTML = '<i data-lucide="' + (isLight ? "moon" : "sun") + '" class="cursor-share-icon" aria-hidden="true"></i>';
509
+ }
487
510
  refreshIcons();
488
511
  }
489
512
 
@@ -649,6 +672,9 @@ export function initTheme() {
649
672
  }
650
673
  }
651
674
 
675
+ var quickToggle = document.getElementById("appearance-mode-toggle");
676
+ if (quickToggle) quickToggle.addEventListener("click", toggleDarkMode);
677
+
652
678
  // Set initial toggle icon
653
679
  updateToggleIcon();
654
680
 
@@ -711,6 +737,8 @@ async function loadAppearance() {
711
737
  if (!response.ok) throw new Error('Appearance preferences unavailable.');
712
738
  var data = await response.json();
713
739
  if(store.get('appearanceSaveRequest'))return;
740
+ // A pane that already mirrors its parent's live view is newer than this GET, which may predate a pending save.
741
+ if(externalThemeId)return;
714
742
  if (data.configured) {store.set({appearancePreference:data});applyTheme(themeIdForMode(data.mode || getSystemMode()));}
715
743
  else {
716
744
  var legacy = null;try {legacy = localStorage.getItem(STORAGE_KEY);}catch(error){}
@@ -9,6 +9,7 @@ import { openFile } from './filebrowser.js';
9
9
  import { mateAvatarUrl } from './avatar.js';
10
10
  import { getChatLayout } from './theme.js';
11
11
  import { store } from './store.js';
12
+ import { currentPaneReview } from './pane-binding.js';
12
13
  import { applyPermissionHints } from './permission-hints.js';
13
14
  import { VENDOR_NAMES } from './app-rendering.js';
14
15
  import { startThinkingSegment, appendThinkingText, stopThinkingSegment, resetThinkingTurn, clearThinkingState, saveThinkingState, restoreThinkingState, updateThinkingTokenEstimate } from './thinking-lifecycle.js';
@@ -394,12 +395,13 @@ export function renderAskUserQuestion(toolId, input) {
394
395
  skipBtn.className = "ask-user-skip";
395
396
  skipBtn.textContent = "Skip";
396
397
  skipBtn.addEventListener("click", function () {
398
+ if (!currentPaneReview(container)) return;
397
399
  if (container.classList.contains("answered")) return;
398
400
  container.classList.add("answered");
399
401
  markAskUserRequestAnswered(toolId);
400
402
  enableMainInput();
401
403
  if (ctx.ws && ctx.connected) {
402
- ctx.ws.send(JSON.stringify({ type: "stop" }));
404
+ ctx.ws.send(JSON.stringify({ type: "stop", paneBinding: container.paneBinding || undefined }));
403
405
  }
404
406
  });
405
407
  actions.appendChild(skipBtn);
@@ -419,6 +421,7 @@ export function renderAskUserQuestion(toolId, input) {
419
421
  container.appendChild(mateContentWrap);
420
422
  }
421
423
 
424
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
422
425
  ctx.addToMessages(container);
423
426
  disableMainInput();
424
427
  ctx.setActivity(null);
@@ -436,6 +439,7 @@ export function enableMainInput() {
436
439
  }
437
440
 
438
441
  function submitAskUserAnswer(container, toolId, questions, answers, multiSelections) {
442
+ if (!currentPaneReview(container)) return;
439
443
  if (container.classList.contains("answered") || container.dataset.askUserSubmitted === "true") return;
440
444
 
441
445
  var result = {};
@@ -461,6 +465,7 @@ function submitAskUserAnswer(container, toolId, questions, answers, multiSelecti
461
465
  if (ctx.ws && ctx.connected) {
462
466
  ctx.ws.send(JSON.stringify({
463
467
  type: "ask_user_response",
468
+ paneBinding: container.paneBinding || undefined,
464
469
  toolId: toolId,
465
470
  answers: result,
466
471
  }));
@@ -655,6 +660,7 @@ function renderFormalPermission(requestId, toolName, toolInput, decisionReason)
655
660
  container.appendChild(header);
656
661
  container.appendChild(body);
657
662
  container.appendChild(actions);
663
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
658
664
  ctx.addToMessages(container);
659
665
 
660
666
  pendingPermissions[requestId] = container;
@@ -762,6 +768,7 @@ function renderPlanPermission(requestId) {
762
768
  container.appendChild(body);
763
769
  container.appendChild(actions);
764
770
  container.appendChild(feedbackRow);
771
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
765
772
  ctx.addToMessages(container);
766
773
 
767
774
  pendingPermissions[requestId] = container;
@@ -769,10 +776,11 @@ function renderPlanPermission(requestId) {
769
776
  ctx.setActivity(null);
770
777
  maybeScrollToBottom();
771
778
  // Focus the feedback input after render
772
- setTimeout(function () { feedbackInput.focus(); }, 50);
779
+ setTimeout(function () { if (currentPaneReview(container)) feedbackInput.focus(); }, 50);
773
780
  }
774
781
 
775
782
  function sendPlanResponse(container, requestId, decision, feedback) {
783
+ if (!currentPaneReview(container)) return;
776
784
  if (container.classList.contains("resolved")) return;
777
785
  container.classList.add("resolved");
778
786
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
@@ -800,6 +808,7 @@ function sendPlanResponse(container, requestId, decision, feedback) {
800
808
  if (ctx.ws && ctx.connected) {
801
809
  var payload = {
802
810
  type: "permission_response",
811
+ paneBinding: container.paneBinding || undefined,
803
812
  requestId: requestId,
804
813
  decision: decision,
805
814
  };
@@ -954,6 +963,7 @@ function renderConversationalPermission(requestId, toolName, toolInput, mateId,
954
963
 
955
964
  container.appendChild(content);
956
965
 
966
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
957
967
  ctx.addToMessages(container);
958
968
  pendingPermissions[requestId] = container;
959
969
  refreshIcons();
@@ -962,6 +972,7 @@ function renderConversationalPermission(requestId, toolName, toolInput, mateId,
962
972
  }
963
973
 
964
974
  function sendPermissionResponse(container, requestId, decision) {
975
+ if (!currentPaneReview(container)) return;
965
976
  if (container.classList.contains("resolved")) return;
966
977
  container.classList.add("resolved");
967
978
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
@@ -979,6 +990,7 @@ function sendPermissionResponse(container, requestId, decision) {
979
990
  if (ctx.ws && ctx.connected) {
980
991
  ctx.ws.send(JSON.stringify({
981
992
  type: "permission_response",
993
+ paneBinding: container.paneBinding || undefined,
982
994
  requestId: requestId,
983
995
  decision: decision,
984
996
  }));
@@ -1053,6 +1065,7 @@ export function renderElicitationRequest(msg) {
1053
1065
  var container = buildElicitationCard(msg, function () {
1054
1066
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
1055
1067
  });
1068
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
1056
1069
  ctx.addToMessages(container);
1057
1070
  pendingElicitations[msg.requestId] = container;
1058
1071
  refreshIcons();
@@ -1079,6 +1092,7 @@ export function markElicitationError(requestId, error, expired) {
1079
1092
  var pendingUserDialogs = {};
1080
1093
 
1081
1094
  function sendUserDialogResponse(container, requestId, behavior, result) {
1095
+ if (!currentPaneReview(container)) return;
1082
1096
  if (container.classList.contains("resolved")) return;
1083
1097
  container.classList.add("resolved");
1084
1098
  if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
@@ -1094,6 +1108,7 @@ function sendUserDialogResponse(container, requestId, behavior, result) {
1094
1108
  if (ctx.ws && ctx.connected) {
1095
1109
  var msg = {
1096
1110
  type: "user_dialog_response",
1111
+ paneBinding: container.paneBinding || undefined,
1097
1112
  requestId: requestId,
1098
1113
  behavior: isCancel ? "cancelled" : "completed",
1099
1114
  };
@@ -1185,6 +1200,7 @@ export function renderUserDialogRequest(msg) {
1185
1200
  container.appendChild(header);
1186
1201
  container.appendChild(body);
1187
1202
  container.appendChild(actions);
1203
+ container.paneBinding = store.get("panePendingBinding") || store.get("paneCommittedBinding") || null;
1188
1204
  ctx.addToMessages(container);
1189
1205
 
1190
1206
  pendingUserDialogs[msg.requestId] = container;
@@ -54,7 +54,8 @@ export function initPersonalIntegrations() {
54
54
  if (!store.get('personalLinearDirty')) { action(); return; }
55
55
  confirmSettingsAction('Discard this unsaved connection draft?', 'Discard', action);
56
56
  }
57
- form.oninput = function () { store.set({personalLinearDirty: !!key.value || name.value !== store.get('personalLinearOriginalName')}); };
57
+ // Only an open editor holds a draft; autofill or restore into the hidden form is not an edit.
58
+ form.oninput = function () { if (!store.get('personalLinearEditing')) return; store.set({personalLinearDirty: !!key.value || name.value !== store.get('personalLinearOriginalName')}); };
58
59
  async function save(disconnect) {
59
60
  if (store.get('personalLinearSaving')) return;
60
61
  var actor = store.get('myUserId'), generation = store.get('personalLinearGeneration');
@@ -4,15 +4,9 @@ import { store } from './store.js';
4
4
  import { closeSpeechPicker } from './stt-picker.js';
5
5
  import { loadSpeechSettings } from './stt-settings.js';
6
6
  import { mountDefaultAi, unmountDefaultAi, requestDefaultAi } from './default-ai.js';
7
- import { confirmSettingsAction as showConfirmDialog } from './server-settings-state.js';
7
+ import { resolvePage } from './settings-shell-registry.js';
8
8
 
9
- export var USER_SETTINGS_PAGES = [
10
- ['us-account', 'Account'], ['us-appearance', 'Appearance'], ['us-chat', 'Conversation'],
11
- ['us-notifications', 'Notifications'], ['us-ai', 'AI preferences'], ['us-integrations', 'Integrations']
12
- ];
13
- export function resolveUserSettingsPage(id) {
14
- return ({'us-mates':'us-chat','us-experimental':'us-chat','us-connections':'us-integrations','us-email':'us-integrations','us-logout':'us-account'})[id] || id;
15
- }
9
+ export function resolveUserSettingsPage(id) { return resolvePage('personal', id); }
16
10
  function section(id) { return document.querySelector('#user-settings .us-section[data-section="' + id + '"]'); }
17
11
  function heading(target, text, copy) {
18
12
  var title = document.createElement('h3'); title.textContent = text; target.appendChild(title);
@@ -20,15 +14,6 @@ function heading(target, text, copy) {
20
14
  }
21
15
  export function buildUserSettingsLayout() {
22
16
  var root = document.getElementById('user-settings');
23
- root.querySelector('.us-modal').setAttribute('role','dialog');root.querySelector('.us-modal').setAttribute('aria-modal','true');root.querySelector('.us-modal').setAttribute('aria-label','User settings');
24
- var nav = root.querySelector('.us-modal-nav-items'); nav.replaceChildren();
25
- var dropdown = document.getElementById('user-settings-nav-dropdown'); dropdown.replaceChildren();
26
- USER_SETTINGS_PAGES.forEach(function (page) {
27
- var button = document.createElement('button'); button.className = 'us-nav-item'; button.dataset.section = page[0]; button.textContent = page[1]; button.type = 'button'; nav.appendChild(button);
28
- var option = document.createElement('option'); option.value = page[0]; option.textContent = page[1]; dropdown.appendChild(option);
29
- });
30
- dropdown.setAttribute('aria-label', 'Personal settings page');
31
- root.querySelector('.us-modal-nav').setAttribute('aria-label', 'Personal settings pages');
32
17
  var notifications=section('us-notifications');
33
18
  var push=document.getElementById('settings-notif-push').closest('label');
34
19
  heading(notifications,'This device','Browser permission and push delivery are configured separately on each browser.');notifications.appendChild(push);
@@ -54,36 +39,29 @@ export function buildUserSettingsLayout() {
54
39
  document.getElementById('us-logout-btn').closest('.settings-card').classList.remove('settings-card-danger');
55
40
  document.getElementById('us-pin-set-btn').parentElement.querySelector('.settings-hint').textContent = 'Change your personal account PIN. Server sign-in policy is managed separately by an administrator.';
56
41
  root.querySelectorAll('.layout-option-icon').forEach(function (el) {el.remove();});
57
- var back = document.createElement('button');back.type = 'button';back.className = 'settings-btn-sm us-back';back.textContent = '← Settings';root.querySelector('.us-modal-content').prepend(back);
58
- back.onclick = function () {root.classList.add('us-show-navigation');nav.querySelector('.active').focus();};
59
- nav.addEventListener('keydown', function (event) {
60
- if (['ArrowDown','ArrowUp','Home','End'].indexOf(event.key) < 0) return;
61
- var items = Array.from(nav.querySelectorAll('button'));var index = items.indexOf(document.activeElement);if(index < 0)return;
62
- event.preventDefault();index = event.key === 'Home' ? 0 : event.key === 'End' ? items.length-1 : (index+(event.key === 'ArrowDown'?1:-1)+items.length)%items.length;items[index].focus();
63
- });
64
42
  }
65
43
  export function loadUserSettingsExtras(page) {
66
- document.getElementById('user-settings').classList.remove('us-show-navigation');
67
44
  if (page === 'us-integrations') loadPersonalIntegrations();
68
45
  if (page === 'us-chat') loadSpeechSettings();
69
46
  if (page === 'us-ai') { mountDefaultAi(document.getElementById('us-default-ai')); requestDefaultAi(); var ws=getWs();if(ws && ws.readyState===1)ws.send(JSON.stringify({type:'get_claude_allow_list'})); }
70
47
  }
71
48
  export function finishUserSettingsExtras() {closeSpeechPicker();unmountDefaultAi(document.getElementById('us-default-ai'));}
72
- export function guardUserSettingsLeave(done, destination) {
73
- var active = document.querySelector('#user-settings .us-section.active');
74
- var page = active && active.dataset.section;
75
- if (destination && resolveUserSettingsPage(destination) === page) {done();return;}
76
- function leaving(id) {return !destination || page === id;}
49
+ // Returns the draft prompt for leaving one personal page (or every page when
50
+ // pageId is omitted); the shell shows it with the shared confirmation.
51
+ export function personalLeavePrompt(pageId) {
52
+ function leaving(id) {return !pageId || pageId === id;}
77
53
  var pin = document.getElementById('us-pin-form');
78
54
  var pinDirty = pin && !pin.classList.contains('hidden') && Array.from(pin.querySelectorAll('input')).some(function (input) {return !!input.value;});
55
+ // A value left in the hidden key field (browser autofill or form restore) is
56
+ // not an edit; only an open connection editor can hold a draft key.
79
57
  var key = document.getElementById('us-linear-key');
80
- if(store.get('speechPopup'))return;
81
- var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || key && key.value) ||
58
+ var keyDraft = key && key.value && store.get('personalLinearEditing') && !(key.form && key.form.hidden);
59
+ var dirty = leaving('us-integrations') && (store.get('personalEmailDirty') || store.get('personalLinearDirty') || keyDraft) ||
82
60
  leaving('us-chat') && store.get('userVoiceDirty') ||
83
61
  leaving('us-notifications') && store.get('notificationDraft') ||
84
62
  leaving('us-appearance') && (store.get('appearanceDraft') || store.get('userTerminalDirty')) ||
85
63
  leaving('us-account') && (store.get('userProfileDirty') || pinDirty) ||
86
64
  leaving('us-ai') && (store.get('defaultAiDraftDirty') || store.get('userAllowDirty'));
87
- if (!dirty) {done();return;}
88
- showConfirmDialog('Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', 'Keep draft and leave', done);
65
+ if (!dirty) return null;
66
+ return { message: 'Your unsaved changes will remain in this tab. Leave this editor and keep the draft?', action: 'Keep draft and leave' };
89
67
  }
@@ -55,7 +55,7 @@ export function initPersonalProfile() {
55
55
  URL.revokeObjectURL(url); status.textContent = 'Uploading picture…';
56
56
  uploadAvatarBlob(blob).then(function () { status.textContent = 'Picture updated.'; populateAccount(); avatar.focus(); })
57
57
  .catch(function (error) { status.textContent = error.message; status.dataset.state = 'error'; });
58
- });
58
+ }, document.querySelector('#user-settings .us-section[data-section="us-account"]'));
59
59
  };
60
60
  image.onerror = function () { URL.revokeObjectURL(url); status.textContent = 'Could not read that image. Choose another file.'; };
61
61
  image.src = url; file.value = '';
@@ -1,15 +1,15 @@
1
1
  import { initPersonalAppearance } from './user-settings-appearance.js';
2
2
  import { initAccountSessions, resetAccountSessions } from './user-settings-sessions.js';
3
3
  import { initPersonalVoice } from './user-settings-voice.js';
4
- import { isolateSettingsDialog } from './server-settings-state.js';
4
+ import { registerSettingsScope, openSettings, closeSettings, isSettingsShellOpen, activeSettingsScope } from './settings-shell.js';
5
5
  import { initPersonalProfile } from './user-settings-profile.js';
6
6
  import { initPersonalEditors } from './user-settings-editors.js';
7
7
  import { initPersonalPreferences } from './user-settings-preferences.js';
8
8
  import { showPinForm, hidePinForm, populateAccount, savePin, renderEmailSettings } from './user-settings-account.js';
9
- import { buildUserSettingsLayout, resolveUserSettingsPage, loadUserSettingsExtras, finishUserSettingsExtras, guardUserSettingsLeave } from './user-settings-layout.js';
9
+ import { buildUserSettingsLayout, resolveUserSettingsPage, loadUserSettingsExtras, finishUserSettingsExtras, personalLeavePrompt } from './user-settings-layout.js';
10
10
  import { enhanceSettingsSelects } from './settings-select.js';
11
11
  import { loadNotificationSettings } from './notification-settings.js';
12
- // user-settings.js — Modal dialog for user settings
12
+ // user-settings.js — Personal settings pane of the unified settings shell
13
13
  // Account management and logout
14
14
 
15
15
  import { refreshIcons } from './icons.js';
@@ -18,10 +18,6 @@ import { store } from './store.js';
18
18
  import { initCapsulePreference } from './capsule-preference.js';
19
19
 
20
20
  var settingsEl = null;
21
- var openBtn = null;
22
- var closeBtn = null;
23
- var backdrop = null;
24
- var navItems = null;
25
21
  var sections = null;
26
22
  var pinForm = null;
27
23
  var pinSetBtn = null;
@@ -32,61 +28,20 @@ export function initUserSettings() {
32
28
  buildUserSettingsLayout();
33
29
  initAccountSessions();
34
30
  settingsEl = document.getElementById('user-settings');
35
- openBtn = document.getElementById('user-settings-btn');
36
- closeBtn = document.getElementById('user-settings-close');
37
- backdrop = document.getElementById('user-settings-backdrop');
38
-
39
- if (!settingsEl || !openBtn) return;
31
+ if (!settingsEl) return;
40
32
 
41
33
  initCapsulePreference();
42
34
 
43
-
44
- navItems = settingsEl.querySelectorAll('.us-nav-item');
45
35
  sections = settingsEl.querySelectorAll('.us-section');
46
- pinForm = document.getElementById('us-pin-form');
47
-
48
- openBtn.addEventListener('click', function () {
49
- openUserSettings();
36
+ registerSettingsScope('personal', {
37
+ pane: settingsEl,
38
+ enter: function () { populateAccount(); },
39
+ activate: function (page) { switchSection(page); },
40
+ leave: function () { resetAccountSessions(); finishUserSettingsExtras(); },
41
+ busy: function () { return store.get('speechPopup') ? 'Finish or close the open voice settings editor first.' : ''; },
42
+ leavePrompt: function (kind, pageId) { return personalLeavePrompt(kind === 'page' ? pageId : undefined); }
50
43
  });
51
-
52
- if (closeBtn) {
53
- closeBtn.addEventListener('click', function () {
54
- closeUserSettings();
55
- });
56
- }
57
-
58
- if (backdrop) {
59
- backdrop.addEventListener('click', function () {
60
- closeUserSettings();
61
- });
62
- }
63
-
64
- document.addEventListener('keydown', function (e) {
65
- if (e.key === 'Tab' && isUserSettingsOpen() && settingsEl.contains(document.activeElement)) {
66
- var items = Array.from(settingsEl.querySelectorAll('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href]')).filter(function (el) {return el.getClientRects().length && el.tabIndex !== -1;});
67
- var first=items[0],last=items[items.length-1];
68
- if(e.shiftKey && document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey && document.activeElement===last){e.preventDefault();first.focus();}
69
- }
70
- if (e.key === 'Escape' && isUserSettingsOpen() && !e.defaultPrevented && !store.get('speechPopup') && !document.querySelector('.admin-modal-overlay, #settings-confirm, .avatar-positioner-overlay, #email-setup-modal')) {
71
- e.preventDefault();closeUserSettings();
72
- }
73
- });
74
-
75
- for (var i = 0; i < navItems.length; i++) {
76
- navItems[i].addEventListener('click', function () {
77
- var section = this.dataset.section;
78
- guardUserSettingsLeave(function () { switchSection(section); }, section);
79
- });
80
- }
81
-
82
- // Mobile nav dropdown
83
- var navDropdown = document.getElementById('user-settings-nav-dropdown');
84
- if (navDropdown) {
85
- navDropdown.addEventListener('change', function () {
86
- var value = this.value; guardUserSettingsLeave(function () {switchSection(value);}, value);
87
- this.value = settingsEl.querySelector('.us-section.active').dataset.section;
88
- });
89
- }
44
+ pinForm = document.getElementById('us-pin-form');
90
45
 
91
46
  // PIN save button
92
47
  var pinInput = document.getElementById('us-pin-input');
@@ -190,51 +145,29 @@ export function initUserSettings() {
190
145
  });
191
146
  }
192
147
 
193
- // Optional `initialSection` lets callers deep-link directly into a specific
194
- // settings tab (e.g. the disabled-Mates picker button passing 'us-mates').
195
- // Falls back to Account so existing callers and the icon click are unchanged.
148
+ // Optional `initialSection` deep-links into a personal page (e.g. the
149
+ // disabled-Mates picker passes 'us-mates'). Defaults to Account.
196
150
  export function openUserSettings(initialSection) {
197
- if(!isUserSettingsOpen())store.set({ userSettingsReturnFocus: document.activeElement, userSettingsRestore: isolateSettingsDialog(settingsEl) });
198
-
199
- settingsEl.classList.remove('hidden');
200
- openBtn.classList.add('active');
201
- refreshIcons(settingsEl);
202
- populateAccount();
203
- switchSection(initialSection || 'us-account');
204
- enhanceSettingsSelects(settingsEl);
205
- var title=settingsEl.querySelector(".us-section.active h2"); if(title){title.tabIndex=-1;title.focus();}
151
+ return openSettings({ scope: 'personal', page: initialSection || 'us-account' });
206
152
  }
207
153
 
208
154
  export function closeUserSettings() {
209
- guardUserSettingsLeave(function () {
210
- resetAccountSessions();
211
- finishUserSettingsExtras();
212
- settingsEl.classList.add('hidden');
213
- var restore=store.get('userSettingsRestore');if(restore)restore();store.set({userSettingsRestore:null});
214
- openBtn.classList.remove('active');
215
- var focus = store.get('userSettingsReturnFocus'); if (focus && focus.isConnected) focus.focus();
216
- });
155
+ if (isUserSettingsOpen()) closeSettings();
217
156
  }
218
157
 
219
158
  export function isUserSettingsOpen() {
220
- return settingsEl && !settingsEl.classList.contains('hidden');
159
+ return isSettingsShellOpen() && activeSettingsScope() === 'personal';
221
160
  }
222
161
 
223
162
  function switchSection(sectionName) {
224
163
  sectionName = resolveUserSettingsPage(sectionName);
225
164
  loadUserSettingsExtras(sectionName);
226
- for (var i = 0; i < navItems.length; i++) {
227
- navItems[i].classList.toggle('active', navItems[i].dataset.section === sectionName);
228
- if(navItems[i].dataset.section===sectionName)navItems[i].setAttribute('aria-current','page');else navItems[i].removeAttribute('aria-current');
229
- }
230
165
  for (var j = 0; j < sections.length; j++) {
231
166
  sections[j].classList.toggle('active', sections[j].dataset.section === sectionName);
232
167
  }
233
- var navDropdown = document.getElementById('user-settings-nav-dropdown');
234
- if (navDropdown) navDropdown.value = sectionName;
235
168
  if (sectionName === 'us-notifications') loadNotificationSettings();
236
169
  if (sectionName === 'us-integrations') renderEmailSettings();
237
- var heading=settingsEl.querySelector('.us-section.active h2');if(heading){heading.tabIndex=-1;heading.focus();}
170
+ enhanceSettingsSelects(settingsEl); refreshIcons(settingsEl);
238
171
  }
239
172
 
240
173
  function stopProp(e) {
@@ -0,0 +1,93 @@
1
+ // vendor-usage-format.js - Pure formatting of account usage metrics.
2
+ //
3
+ // Each unit is shown in its own terms. Counts, credits, currency and unnamed
4
+ // amounts are never turned into a percentage, and a missing field is omitted,
5
+ // never shown as 0.
6
+
7
+ var WARNING_PERCENT = 80;
8
+
9
+ function finite(value) {
10
+ return typeof value === "number" && Number.isFinite(value);
11
+ }
12
+
13
+ function plain(value) {
14
+ if (!finite(value)) return "";
15
+ var rounded = Math.abs(value) >= 100 ? Math.round(value) : Math.round(value * 100) / 100;
16
+ return rounded.toLocaleString("en-US");
17
+ }
18
+
19
+ function money(value, code) {
20
+ if (!finite(value)) return "";
21
+ try {
22
+ return new Intl.NumberFormat("en-US", { style: "currency", currency: code }).format(value);
23
+ } catch (e) {
24
+ return plain(value) + " " + code;
25
+ }
26
+ }
27
+
28
+ function windowText(mins) {
29
+ if (!finite(mins) || mins <= 0) return "";
30
+ if (mins % 1440 === 0) return (mins / 1440) + "d";
31
+ if (mins % 60 === 0) return (mins / 60) + "h";
32
+ return mins + "m";
33
+ }
34
+
35
+ // One metric -> { label, value, headline, percent, warning, unit }.
36
+ // `value` is the full detail text; `headline` is the short chip text.
37
+ export function formatMetric(metric) {
38
+ var label = metric.label || "Usage";
39
+ var win = windowText(metric.windowMins);
40
+ if (win && label.indexOf(win) === -1) label += " (" + win + ")";
41
+ var out = { id: metric.id, unit: metric.unit, label: label, value: "", headline: "", percent: null, warning: false, resetsAtMs: finite(metric.resetsAtMs) ? metric.resetsAtMs : null };
42
+ if (metric.unit === "percent") {
43
+ out.percent = metric.usedPercent;
44
+ out.value = Math.round(metric.usedPercent) + "% used";
45
+ out.headline = Math.round(metric.usedPercent) + "%";
46
+ out.warning = metric.usedPercent >= WARNING_PERCENT;
47
+ } else if (metric.unit === "count" || metric.unit === "amount") {
48
+ var unitWord = metric.unit === "count" ? "" : " (unit not stated)";
49
+ if (finite(metric.used) && finite(metric.limit)) {
50
+ out.value = plain(metric.used) + " of " + plain(metric.limit) + " used" + unitWord;
51
+ out.headline = plain(metric.used) + "/" + plain(metric.limit);
52
+ out.warning = metric.limit > 0 && metric.used / metric.limit * 100 >= WARNING_PERCENT;
53
+ } else if (finite(metric.used)) {
54
+ out.value = plain(metric.used) + " used" + unitWord;
55
+ out.headline = plain(metric.used);
56
+ } else {
57
+ out.value = plain(metric.remaining) + " left" + unitWord;
58
+ out.headline = plain(metric.remaining) + " left";
59
+ }
60
+ if (finite(metric.remaining) && finite(metric.used) && finite(metric.limit)) out.value += ", " + plain(metric.remaining) + " left";
61
+ } else if (metric.unit === "credits") {
62
+ if (metric.unlimited) { out.headline = "Unlimited"; out.value = "Unlimited credits"; }
63
+ else if (finite(metric.used) && finite(metric.limit)) {
64
+ out.headline = plain(metric.used) + "/" + plain(metric.limit);
65
+ out.value = plain(metric.used) + " of " + plain(metric.limit) + " credits used";
66
+ out.warning = metric.limit > 0 && metric.used / metric.limit * 100 >= WARNING_PERCENT;
67
+ } else if (finite(metric.used)) { out.headline = plain(metric.used); out.value = plain(metric.used) + " credits used"; }
68
+ else { out.headline = plain(metric.balance); out.value = plain(metric.balance) + " credits"; }
69
+ } else if (metric.unit === "currency") {
70
+ var code = metric.currencyCode;
71
+ if (finite(metric.used) && finite(metric.limit)) {
72
+ out.value = money(metric.used, code) + " of " + money(metric.limit, code) + " used";
73
+ out.headline = money(metric.used, code);
74
+ } else if (finite(metric.used)) {
75
+ out.value = money(metric.used, code) + " used";
76
+ out.headline = money(metric.used, code);
77
+ } else {
78
+ out.value = money(metric.balance, code) + " balance";
79
+ out.headline = money(metric.balance, code);
80
+ }
81
+ }
82
+ return out;
83
+ }
84
+
85
+ // The metric that best summarises a vendor: the highest percent when any
86
+ // metric is a percent, otherwise the first reported.
87
+ export function pickHeadline(formatted) {
88
+ var best = null;
89
+ formatted.forEach(function (item) {
90
+ if (item.percent !== null && (best === null || best.percent === null || item.percent > best.percent)) best = item;
91
+ });
92
+ return best || formatted[0] || null;
93
+ }
@@ -0,0 +1,45 @@
1
+ // vendor-usage-list.js - Compact "all vendors" list for the top-bar usage
2
+ // overview. One two-line row per installed vendor: name, where its figure
3
+ // comes from, the figure, then either the metrics or the honest reason there
4
+ // is none. Presentational only; views come from vendor-usage-model.js.
5
+
6
+ var MAX_DETAIL_METRICS = 3;
7
+
8
+ function sourceLabel(view) {
9
+ if (view.mode === "unintegrated" || view.mode === "unsupported") return "Not integrated";
10
+ if (view.accountCount > 0) return "Account";
11
+ if (view.observedCount > 0) return "Session";
12
+ return view.mode === "query" ? "Account" : "Session";
13
+ }
14
+
15
+ function detailText(view) {
16
+ if ((view.mode === "unintegrated" || view.mode === "unsupported") && view.capability && view.capability.reason) return view.capability.reason;
17
+ if (!view.known) return view.status || "Usage is unknown, not zero.";
18
+ var parts = view.rows.slice(0, MAX_DETAIL_METRICS).map(function (row) {
19
+ var value = row.source === "account" ? row.value : (row.percentText || (row.resetText ? "resets in " + row.resetText : ""));
20
+ return row.label + ": " + value;
21
+ });
22
+ if (view.rows.length > MAX_DETAIL_METRICS) parts.push("+" + (view.rows.length - MAX_DETAIL_METRICS) + " more");
23
+ return parts.join(" · ");
24
+ }
25
+
26
+ export function fillListPopover(container, views, el) {
27
+ container.textContent = "";
28
+ views.forEach(function (view) {
29
+ var row = el("div", "vendor-usage-list-row" + (view.warning ? " warning" : "") + (view.known ? "" : " unknown") + (view.stale ? " stale" : ""));
30
+ row.setAttribute("data-vendor", view.vendor);
31
+ var top = el("div", "vendor-usage-list-top");
32
+ if (view.icon) {
33
+ var icon = el("img", "usage-check-vendor-icon");
34
+ icon.alt = "";
35
+ icon.setAttribute("src", view.icon);
36
+ top.appendChild(icon);
37
+ }
38
+ top.appendChild(el("span", "vendor-usage-list-name", view.name));
39
+ top.appendChild(el("span", "vendor-usage-list-source", sourceLabel(view)));
40
+ top.appendChild(el("span", "vendor-usage-list-value", view.known ? view.text : (view.mode === "unintegrated" || view.mode === "unsupported" ? "—" : "Unknown")));
41
+ row.appendChild(top);
42
+ row.appendChild(el("div", "vendor-usage-list-detail", detailText(view)));
43
+ container.appendChild(row);
44
+ });
45
+ }