clay-server 5.2.0-beta.9 → 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 (104) 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/project-session-notes.js +33 -8
  9. package/lib/project-sessions.js +63 -24
  10. package/lib/project-start-protocol.js +29 -0
  11. package/lib/project-vendor-login.js +2 -0
  12. package/lib/project.js +5 -1
  13. package/lib/public/app.js +27 -18
  14. package/lib/public/css/contextual-clay.css +1 -1
  15. package/lib/public/css/filebrowser.css +1 -1
  16. package/lib/public/css/mate-loading-scene.css +21 -0
  17. package/lib/public/css/overlays.css +64 -256
  18. package/lib/public/css/project-mate-creation.css +5 -6
  19. package/lib/public/css/project-start.css +108 -0
  20. package/lib/public/css/projects-hub.css +53 -14
  21. package/lib/public/css/settings-shell.css +103 -0
  22. package/lib/public/css/sidebar.css +7 -0
  23. package/lib/public/css/title-bar.css +83 -0
  24. package/lib/public/index.html +50 -63
  25. package/lib/public/modules/account-transport-settings.js +8 -2
  26. package/lib/public/modules/admin-settings-email.js +7 -2
  27. package/lib/public/modules/admin-settings-invitations.js +2 -2
  28. package/lib/public/modules/admin-settings-projects.js +2 -2
  29. package/lib/public/modules/admin-settings-ui.js +20 -4
  30. package/lib/public/modules/admin-settings-users.js +4 -4
  31. package/lib/public/modules/app-home-hub.js +2 -1
  32. package/lib/public/modules/app-messages.js +10 -2
  33. package/lib/public/modules/app-projects.js +1 -407
  34. package/lib/public/modules/app-rate-limit.js +12 -1
  35. package/lib/public/modules/clay-tool-presentation.js +1 -0
  36. package/lib/public/modules/contextual-clay.js +3 -1
  37. package/lib/public/modules/github-star.js +66 -0
  38. package/lib/public/modules/home-surface-boot.js +0 -1
  39. package/lib/public/modules/mate-loading-scene.js +17 -0
  40. package/lib/public/modules/pane-appearance-bridge.js +59 -0
  41. package/lib/public/modules/profile.js +11 -3
  42. package/lib/public/modules/project-mate-creation-workspace.js +13 -4
  43. package/lib/public/modules/project-session-visibility-settings.js +19 -16
  44. package/lib/public/modules/project-settings-environment.js +12 -1
  45. package/lib/public/modules/project-settings.js +119 -129
  46. package/lib/public/modules/project-start-dialog.js +130 -0
  47. package/lib/public/modules/project-start-folder-state.js +78 -0
  48. package/lib/public/modules/project-start-folder-view.js +190 -0
  49. package/lib/public/modules/project-start-forms.js +66 -0
  50. package/lib/public/modules/project-start-path.js +60 -0
  51. package/lib/public/modules/project-start-state.js +54 -0
  52. package/lib/public/modules/project-start-submit.js +67 -0
  53. package/lib/public/modules/projects-hub-view.js +32 -44
  54. package/lib/public/modules/projects-hub.js +39 -28
  55. package/lib/public/modules/server-settings.js +36 -91
  56. package/lib/public/modules/settings-project-target.js +56 -0
  57. package/lib/public/modules/settings-select.js +1 -1
  58. package/lib/public/modules/settings-shell-registry.js +88 -0
  59. package/lib/public/modules/settings-shell-view.js +104 -0
  60. package/lib/public/modules/settings-shell.js +342 -0
  61. package/lib/public/modules/sidebar-mobile.js +8 -30
  62. package/lib/public/modules/sidebar-projects.js +2 -1
  63. package/lib/public/modules/theme.js +31 -3
  64. package/lib/public/modules/user-settings-integrations.js +2 -1
  65. package/lib/public/modules/user-settings-layout.js +12 -34
  66. package/lib/public/modules/user-settings-profile.js +1 -1
  67. package/lib/public/modules/user-settings.js +18 -85
  68. package/lib/public/modules/vendor-usage-format.js +93 -0
  69. package/lib/public/modules/vendor-usage-list.js +45 -0
  70. package/lib/public/modules/vendor-usage-model.js +215 -0
  71. package/lib/public/modules/vendor-usage-overview.js +323 -0
  72. package/lib/public/style.css +10 -7
  73. package/lib/public/welcome-art/conversation-desk.png +0 -0
  74. package/lib/public/welcome-art/mate-clay.png +0 -0
  75. package/lib/public/welcome-art/project-clay.png +0 -0
  76. package/lib/sdk-bridge.js +6 -1
  77. package/lib/sdk-message-processor.js +9 -1
  78. package/lib/sdk-quota-snapshot.js +20 -2
  79. package/lib/server-github-stars.js +39 -0
  80. package/lib/server-global-ws.js +21 -17
  81. package/lib/server.js +11 -0
  82. package/lib/session-notes-mcp-server.js +26 -4
  83. package/lib/session-notes-writes.js +88 -0
  84. package/lib/session-visibility.js +20 -8
  85. package/lib/sessions.js +12 -2
  86. package/lib/vendor-usage-acp-readers.js +97 -0
  87. package/lib/vendor-usage-capabilities.js +88 -0
  88. package/lib/vendor-usage-http-readers.js +214 -0
  89. package/lib/vendor-usage-metrics.js +47 -0
  90. package/lib/vendor-usage-native-readers.js +106 -0
  91. package/lib/vendor-usage-overview.js +406 -0
  92. package/lib/vendor-usage-service.js +281 -0
  93. package/lib/ws-schema.js +4 -2
  94. package/lib/yoke/adapters/claude.js +12 -0
  95. package/lib/yoke/adapters/codex-query-contract.js +1 -0
  96. package/lib/yoke/adapters/codex.js +45 -12
  97. package/lib/yoke/claude-hook-installer.js +1 -0
  98. package/lib/yoke/codex-dynamic-tool-names.js +80 -0
  99. package/lib/yoke/codex-model-catalog.js +37 -3
  100. package/lib/yoke/muse-services.js +16 -3
  101. package/lib/yoke/providers/claude-model-descriptions.js +22 -2
  102. package/lib/yoke/vendor-registry.js +6 -2
  103. package/package.json +2 -1
  104. package/lib/public/modules/server-settings-navigation.js +0 -39
@@ -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
+ }
@@ -0,0 +1,215 @@
1
+ // vendor-usage-model.js - Pure projection of the owner-scoped vendor usage
2
+ // overview into what the top bar shows. No DOM, store or network access.
3
+ //
4
+ // Principle: a value is shown only if it was observed. A missing observation is
5
+ // "Unknown", never 0%. Ages are measured from the server's own clock so a
6
+ // skewed browser clock cannot make an old observation look fresh.
7
+
8
+ import { formatMetric, pickHeadline } from "./vendor-usage-format.js";
9
+
10
+ var STALE_AFTER_MS = 15 * 60 * 1000;
11
+ var WARNING_PERCENT = 80;
12
+
13
+ var TYPE_LABELS = {
14
+ five_hour: "5h",
15
+ seven_day: "7d",
16
+ seven_day_opus: "7d Opus",
17
+ seven_day_sonnet: "7d Sonnet",
18
+ overage: "Extra usage",
19
+ seven_day_overage_included: "7d included extra usage",
20
+ };
21
+
22
+ export function estimatedServerNow(overview, nowMs) {
23
+ if (!overview || !Number.isFinite(overview.serverNowMs) || !Number.isFinite(overview.receivedAtMs)) return nowMs;
24
+ return overview.serverNowMs + (nowMs - overview.receivedAtMs);
25
+ }
26
+
27
+ export function ageText(ms) {
28
+ if (!Number.isFinite(ms) || ms < 0) return "";
29
+ var minutes = Math.floor(ms / 60000);
30
+ if (minutes < 1) return "just now";
31
+ if (minutes < 60) return minutes + "m ago";
32
+ var hours = Math.floor(minutes / 60);
33
+ if (hours < 48) return hours + "h ago";
34
+ return Math.floor(hours / 24) + "d ago";
35
+ }
36
+
37
+ export function durationText(ms) {
38
+ if (!Number.isFinite(ms) || ms <= 0) return "";
39
+ var totalMinutes = Math.floor(ms / 60000);
40
+ var days = Math.floor(totalMinutes / 1440);
41
+ var hours = Math.floor((totalMinutes % 1440) / 60);
42
+ var minutes = totalMinutes % 60;
43
+ if (days >= 2) return days + "d " + hours + "h";
44
+ if (totalMinutes >= 60) return Math.floor(totalMinutes / 60) + "h " + minutes + "m";
45
+ return Math.max(1, minutes) + "m";
46
+ }
47
+
48
+ export function windowLabel(entry, vendor) {
49
+ var label;
50
+ if (entry.kind === "reset") {
51
+ label = TYPE_LABELS[entry.rateLimitType] || entry.rateLimitType || "Usage";
52
+ } else {
53
+ var mins = entry.windowDurationMins;
54
+ if (!Number.isFinite(mins) || mins <= 0) label = "Usage";
55
+ else if (mins % 1440 === 0) label = (mins / 1440) + "d";
56
+ else if (mins % 60 === 0) label = (mins / 60) + "h";
57
+ else label = mins + "m";
58
+ }
59
+ // A separate limit bucket (for example a model-specific one) keeps its name.
60
+ if (entry.limitId && entry.limitId !== vendor) label += " (" + entry.limitId + ")";
61
+ return label;
62
+ }
63
+
64
+ function percentOf(entry) {
65
+ return Number.isFinite(entry.usedPercent) ? entry.usedPercent : null;
66
+ }
67
+
68
+ function windowView(entry, vendor, serverNow) {
69
+ var percent = percentOf(entry);
70
+ var resetText = entry.resetsAt ? durationText(entry.resetsAt - serverNow) : "";
71
+ var warn = (percent !== null && percent >= WARNING_PERCENT) || entry.status === "allowed_warning" || entry.status === "rejected";
72
+ var observed = Number.isFinite(entry.observedAtMs) ? serverNow - entry.observedAtMs : null;
73
+ return {
74
+ label: windowLabel(entry, vendor),
75
+ percent: percent,
76
+ percentText: percent === null ? "" : Math.round(percent) + "% used",
77
+ resetText: resetText,
78
+ resetsAt: entry.resetsAt || null,
79
+ warning: warn,
80
+ observedAgeMs: observed,
81
+ observedText: observed === null ? "observation time unknown" : "observed " + ageText(observed),
82
+ origin: entry.origin === "history" ? "history" : "live",
83
+ };
84
+ }
85
+
86
+ var MAX_CHIPS = 3;
87
+
88
+ function capabilityOf(info, entry) {
89
+ if (entry && entry.capability) return entry.capability;
90
+ // An older server without capabilities: only the observed path exists.
91
+ return { mode: info && info.rateLimitTracking === true ? "event" : "unintegrated", observed: info && info.rateLimitTracking === true, units: [] };
92
+ }
93
+
94
+ function accountRows(account, serverNow) {
95
+ if (!account || !Array.isArray(account.metrics)) return [];
96
+ var age = Number.isFinite(account.fetchedAtMs) ? serverNow - account.fetchedAtMs : null;
97
+ var stale = account.stale === true || (age !== null && age >= STALE_AFTER_MS);
98
+ return account.metrics.map(function (metric) {
99
+ var row = formatMetric(metric);
100
+ row.resetText = row.resetsAtMs ? durationText(row.resetsAtMs - serverNow) : "";
101
+ row.expired = !!(row.resetsAtMs && row.resetsAtMs <= serverNow);
102
+ row.stale = stale;
103
+ row.source = "account";
104
+ row.observedAgeMs = age;
105
+ row.observedText = age === null ? "read time unknown" : "read " + ageText(age);
106
+ return row;
107
+ }).filter(function (row) { return !row.expired; });
108
+ }
109
+
110
+ // Why a vendor has no figures, in the vendor's own terms.
111
+ function statusLine(cap, account, hasObserved) {
112
+ if (cap.mode === "unintegrated" || cap.mode === "unsupported") return cap.reason || "Not integrated: no verified usage source.";
113
+ var status = account && account.status;
114
+ if (account && account.fetching) return "Checking account usage\u2026";
115
+ if (status === "refused") return account.message || "Account usage is not available for you.";
116
+ if (status === "no_auth") return "Not signed in, so account usage cannot be read.";
117
+ if (status === "expired_auth") return "Sign-in has expired; run the vendor CLI once to refresh it.";
118
+ if (status === "error") return (account.message || "Account usage could not be read.") + (Number.isFinite(account.retryAtMs) ? " Will retry later." : "");
119
+ if (cap.mode === "query" && (!status || status === "idle")) return "Not checked yet.";
120
+ if (!hasObserved) return cap.mode === "event" ? "Waiting for a report from one of your sessions. Usage is unknown, not zero." : "No usage reported. Usage is unknown, not zero.";
121
+ return "";
122
+ }
123
+
124
+ // Returns the display model for one vendor, or null when it is not installed.
125
+ // The headline value, its label and its age always describe the same row: a
126
+ // stale high row is never presented with the freshness of a different one.
127
+ export function describeVendor(vendor, info, entry, overview, nowMs) {
128
+ if (!info) return null;
129
+ if (entry && entry.installed === false) return null;
130
+ var cap = capabilityOf(info, entry);
131
+ var serverNow = estimatedServerNow(overview, nowMs);
132
+ var name = info.displayName || vendor;
133
+ var meta = info.usageDashboard || null;
134
+ var observed = [];
135
+ if (entry && Array.isArray(entry.windows)) {
136
+ entry.windows.forEach(function (raw) {
137
+ if (raw.resetsAt && raw.resetsAt <= serverNow) return;
138
+ var row = windowView(raw, vendor, serverNow);
139
+ row.source = "session";
140
+ observed.push(row);
141
+ });
142
+ }
143
+ var account = entry && entry.account ? entry.account : null;
144
+ var accounts = accountRows(account, serverNow);
145
+ var lastSeenAge = entry && Number.isFinite(entry.lastObservedAtMs) ? serverNow - entry.lastObservedAtMs : null;
146
+ var chosen = null;
147
+ var fromAccount = accounts.length > 0 && !(accounts[0].stale && observed.length > 0);
148
+ if (fromAccount) {
149
+ chosen = pickHeadline(accounts);
150
+ chosen.percentText = chosen.percent === null ? "" : Math.round(chosen.percent) + "% used";
151
+ } else {
152
+ observed.forEach(function (row) { if (row.percent !== null && (chosen === null || row.percent > chosen.percent)) chosen = row; });
153
+ if (!chosen) observed.forEach(function (row) { if (row.resetText && (chosen === null || row.resetsAt < chosen.resetsAt)) chosen = row; });
154
+ if (!chosen && observed.length) chosen = observed[0];
155
+ if (!chosen && accounts.length) { chosen = pickHeadline(accounts); fromAccount = true; }
156
+ }
157
+ var known = chosen !== null;
158
+ var rows = accounts.concat(observed);
159
+ var warning = rows.some(function (row) { return row.warning; });
160
+ var text = "Unknown";
161
+ var summary = name + " usage unknown";
162
+ var stale = false;
163
+ if (known) {
164
+ var chosenAge = chosen.observedAgeMs;
165
+ var headline = fromAccount ? chosen.headline : (chosen.percent !== null ? Math.round(chosen.percent) + "%" : chosen.resetText ? "resets " + chosen.resetText : "Observed");
166
+ text = headline;
167
+ stale = fromAccount ? chosen.stale === true : (chosenAge !== null && chosenAge >= STALE_AFTER_MS);
168
+ if (chosenAge === null) text += " \u00b7 age unknown";
169
+ else if (stale) text += " \u00b7 " + ageText(chosenAge);
170
+ var pieces = [name + " " + chosen.label + " usage"];
171
+ if (fromAccount) pieces.push(chosen.value);
172
+ else if (chosen.percent !== null) pieces.push(Math.round(chosen.percent) + "% used");
173
+ else if (chosen.resetText) pieces.push("resets in " + chosen.resetText);
174
+ pieces.push(fromAccount ? "account, " + chosen.observedText : chosen.observedText);
175
+ summary = pieces.join(", ");
176
+ }
177
+ var others = entry && Number.isFinite(entry.otherSourceCount) ? entry.otherSourceCount : 0;
178
+ return {
179
+ vendor: vendor, name: name, mode: cap.mode, capability: cap, known: known, warning: warning, stale: stale,
180
+ text: text, summary: summary, rows: rows, accountCount: accounts.length, observedCount: observed.length,
181
+ status: statusLine(cap, account, observed.length > 0 || accounts.length > 0),
182
+ accountSource: cap.source || "",
183
+ sourceSessionId: entry && entry.sourceSessionId !== undefined ? entry.sourceSessionId : null,
184
+ otherSourceCount: others,
185
+ lastObservedText: lastSeenAge === null ? "" : ageText(lastSeenAge),
186
+ href: meta && meta.href || "", linkTitle: meta && meta.title || "", icon: meta && meta.icon || info.avatar || "",
187
+ };
188
+ }
189
+
190
+ // Every installed vendor, in registry order. Nothing is shown until the server
191
+ // has answered for this project and user.
192
+ export function allVendors(vendorInfo, overview, nowMs) {
193
+ if (!overview) return [];
194
+ var infos = vendorInfo || {};
195
+ var byVendor = {};
196
+ (Array.isArray(overview.vendors) ? overview.vendors : []).forEach(function (entry) { byVendor[entry.vendor] = entry; });
197
+ var result = [];
198
+ Object.keys(infos).forEach(function (vendor) {
199
+ // Without a server entry only a tracked vendor is listed (older servers).
200
+ if (!byVendor[vendor] && infos[vendor].rateLimitTracking !== true) return;
201
+ var view = describeVendor(vendor, infos[vendor], byVendor[vendor], overview, nowMs);
202
+ if (view) result.push(view);
203
+ });
204
+ return result;
205
+ }
206
+
207
+ // Compact bar: chips only for vendors that have figures (warnings first, then
208
+ // registry order, at most MAX_CHIPS); everything else lives in the list.
209
+ export function overviewVendors(vendorInfo, overview, nowMs) {
210
+ var all = allVendors(vendorInfo, overview, nowMs);
211
+ var withData = all.filter(function (view) { return view.known; });
212
+ var ordered = withData.filter(function (v) { return v.warning; }).concat(withData.filter(function (v) { return !v.warning; }));
213
+ var shown = ordered.slice(0, MAX_CHIPS);
214
+ return all.filter(function (view) { return shown.indexOf(view) !== -1; });
215
+ }