clay-server 5.2.0-beta.10 → 5.2.0-beta.11
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.
- package/README.md +0 -4
- package/lib/clay-product-control-tools.js +1 -0
- package/lib/clay-product-control.js +14 -11
- package/lib/clay-vendor-usage.js +44 -0
- package/lib/github-star-count.js +147 -0
- package/lib/notes-checklist.js +43 -1
- package/lib/notes.js +2 -1
- package/lib/project-session-notes.js +33 -8
- package/lib/project-sessions.js +63 -24
- package/lib/project-start-protocol.js +29 -0
- package/lib/project-vendor-login.js +2 -0
- package/lib/project.js +3 -0
- package/lib/public/app.js +25 -17
- package/lib/public/css/contextual-clay.css +1 -1
- package/lib/public/css/filebrowser.css +1 -1
- package/lib/public/css/mate-loading-scene.css +21 -0
- package/lib/public/css/overlays.css +64 -256
- package/lib/public/css/project-mate-creation.css +5 -6
- package/lib/public/css/project-start.css +108 -0
- package/lib/public/css/projects-hub.css +53 -14
- package/lib/public/css/settings-shell.css +103 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/title-bar.css +79 -0
- package/lib/public/index.html +50 -63
- package/lib/public/modules/admin-settings-invitations.js +2 -2
- package/lib/public/modules/admin-settings-projects.js +2 -2
- package/lib/public/modules/admin-settings-ui.js +20 -4
- package/lib/public/modules/admin-settings-users.js +4 -4
- package/lib/public/modules/app-home-hub.js +2 -1
- package/lib/public/modules/app-messages.js +10 -2
- package/lib/public/modules/app-projects.js +1 -407
- package/lib/public/modules/app-rate-limit.js +12 -1
- package/lib/public/modules/clay-tool-presentation.js +1 -0
- package/lib/public/modules/contextual-clay.js +3 -1
- package/lib/public/modules/github-star.js +66 -0
- package/lib/public/modules/mate-loading-scene.js +17 -0
- package/lib/public/modules/profile.js +11 -3
- package/lib/public/modules/project-mate-creation-workspace.js +13 -4
- package/lib/public/modules/project-session-visibility-settings.js +19 -16
- package/lib/public/modules/project-settings.js +119 -129
- package/lib/public/modules/project-start-dialog.js +130 -0
- package/lib/public/modules/project-start-folder-state.js +78 -0
- package/lib/public/modules/project-start-folder-view.js +190 -0
- package/lib/public/modules/project-start-forms.js +66 -0
- package/lib/public/modules/project-start-path.js +60 -0
- package/lib/public/modules/project-start-state.js +54 -0
- package/lib/public/modules/project-start-submit.js +67 -0
- package/lib/public/modules/projects-hub-view.js +33 -14
- package/lib/public/modules/projects-hub.js +10 -6
- package/lib/public/modules/server-settings.js +36 -91
- package/lib/public/modules/settings-project-target.js +56 -0
- package/lib/public/modules/settings-select.js +1 -1
- package/lib/public/modules/settings-shell-registry.js +88 -0
- package/lib/public/modules/settings-shell-view.js +104 -0
- package/lib/public/modules/settings-shell.js +342 -0
- package/lib/public/modules/sidebar-mobile.js +8 -30
- package/lib/public/modules/sidebar-projects.js +2 -1
- package/lib/public/modules/theme.js +10 -2
- package/lib/public/modules/user-settings-layout.js +8 -33
- package/lib/public/modules/user-settings-profile.js +1 -1
- package/lib/public/modules/user-settings.js +18 -85
- package/lib/public/modules/vendor-usage-format.js +93 -0
- package/lib/public/modules/vendor-usage-list.js +45 -0
- package/lib/public/modules/vendor-usage-model.js +215 -0
- package/lib/public/modules/vendor-usage-overview.js +312 -0
- package/lib/public/style.css +10 -7
- package/lib/public/welcome-art/conversation-desk.png +0 -0
- package/lib/public/welcome-art/mate-clay.png +0 -0
- package/lib/public/welcome-art/project-clay.png +0 -0
- package/lib/sdk-bridge.js +6 -1
- package/lib/sdk-message-processor.js +9 -1
- package/lib/sdk-quota-snapshot.js +20 -2
- package/lib/server-github-stars.js +39 -0
- package/lib/server-global-ws.js +21 -17
- package/lib/server.js +4 -0
- package/lib/session-notes-mcp-server.js +26 -4
- package/lib/session-notes-writes.js +88 -0
- package/lib/session-visibility.js +20 -8
- package/lib/sessions.js +2 -0
- package/lib/vendor-usage-acp-readers.js +97 -0
- package/lib/vendor-usage-capabilities.js +88 -0
- package/lib/vendor-usage-http-readers.js +214 -0
- package/lib/vendor-usage-metrics.js +47 -0
- package/lib/vendor-usage-native-readers.js +106 -0
- package/lib/vendor-usage-overview.js +406 -0
- package/lib/vendor-usage-service.js +281 -0
- package/lib/ws-schema.js +4 -2
- package/lib/yoke/adapters/claude.js +12 -0
- package/lib/yoke/adapters/codex-query-contract.js +1 -0
- package/lib/yoke/adapters/codex.js +45 -12
- package/lib/yoke/claude-hook-installer.js +1 -0
- package/lib/yoke/codex-dynamic-tool-names.js +80 -0
- package/lib/yoke/codex-model-catalog.js +37 -3
- package/lib/yoke/muse-services.js +16 -3
- package/lib/yoke/providers/claude-model-descriptions.js +22 -2
- package/lib/yoke/vendor-registry.js +6 -2
- package/package.json +2 -1
- package/lib/public/modules/server-settings-navigation.js +0 -39
|
@@ -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 {
|
|
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,
|
|
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 —
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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`
|
|
194
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|