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
@@ -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
+ }
@@ -0,0 +1,323 @@
1
+ // vendor-usage-overview.js - Top-bar usage overview for every tracked vendor.
2
+ //
3
+ // This is a read-only projection of the owner-scoped `vendor_usage_overview`
4
+ // message. It sits beside the existing active-session usage pill and never
5
+ // touches rejection, warning or scheduling state in app-rate-limit.js.
6
+
7
+ import { store } from './store.js';
8
+ import { getWs } from './ws-ref.js';
9
+ import { iconHtml, refreshIcons } from './icons.js';
10
+ import { overviewVendors, allVendors } from './vendor-usage-model.js';
11
+ import { fillListPopover } from './vendor-usage-list.js';
12
+
13
+ var LIST_KEY = "__all";
14
+
15
+ var groupEl = null;
16
+ var records = {};
17
+ var tickTimer = null;
18
+ var outsideHandler = null;
19
+ var keyHandler = null;
20
+ var blurHandler = null;
21
+ var requestCounter = 0;
22
+ var pendingRequestId = null;
23
+
24
+ // `refresh` asks the server to read account usage for vendors that support it;
25
+ // the server caches, so repeating it is cheap. Replies echo the request id.
26
+ function requestOverview(refresh) {
27
+ var ws = getWs();
28
+ if (!ws || ws.readyState !== 1) return;
29
+ pendingRequestId = "vu-" + (++requestCounter) + "-" + Date.now().toString(36);
30
+ var message = { type: "vendor_usage_get", slug: store.get('currentSlug') || undefined, requestId: pendingRequestId };
31
+ if (refresh) message.refresh = true;
32
+ ws.send(JSON.stringify(message));
33
+ }
34
+
35
+ // A reply is shown only for the project, user and request it was asked for.
36
+ // A delayed reply from before a project or account change is dropped.
37
+ export function handleVendorUsageOverview(msg) {
38
+ if (!msg || msg.error) return;
39
+ var slug = store.get('currentSlug');
40
+ if (slug && msg.slug !== slug) return;
41
+ if ((msg.userId || null) !== (store.get('myUserId') || null)) return;
42
+ if (msg.requestId !== undefined && msg.requestId !== pendingRequestId) return;
43
+ var vendors = Array.isArray(msg.vendors) ? msg.vendors : [];
44
+ store.set({ vendorUsageOverview: { slug: msg.slug, serverNowMs: msg.serverNowMs, receivedAtMs: Date.now(), vendors: vendors } });
45
+ }
46
+
47
+ function el(tag, className, text) {
48
+ var node = document.createElement(tag);
49
+ if (className) node.className = className;
50
+ if (text !== undefined) node.textContent = text;
51
+ return node;
52
+ }
53
+
54
+ function closePopover(restoreFocus) {
55
+ var open = store.get('vendorUsageOpen');
56
+ if (!open) return;
57
+ store.set({ vendorUsageOpen: null });
58
+ var record = records[open];
59
+ if (restoreFocus && record && typeof record.chip.focus === "function") record.chip.focus();
60
+ }
61
+
62
+ function sourceNote(view) {
63
+ var note = view.lastObservedText ? "Latest report " + view.lastObservedText : "Not observed yet in this project";
64
+ if (view.sourceSessionId !== null && view.sourceSessionId !== undefined) note += " from session #" + view.sourceSessionId;
65
+ note += ".";
66
+ if (view.otherSourceCount > 0) note += " " + view.otherSourceCount + " other session" + (view.otherSourceCount === 1 ? "" : "s") + " reported separately and are not combined.";
67
+ return note;
68
+ }
69
+
70
+ function ensurePopover(record, view) {
71
+ if (!record.pop) {
72
+ var pop = el("div", "vendor-usage-popover");
73
+ pop.setAttribute("role", "dialog");
74
+ record.title = el("div", "vendor-usage-popover-title");
75
+ record.rows = el("div", "vendor-usage-rows");
76
+ record.notes = el("div", "vendor-usage-notes");
77
+ pop.appendChild(record.title);
78
+ pop.appendChild(record.rows);
79
+ pop.appendChild(record.notes);
80
+ // The link is created once and only updated, so keyboard focus on it
81
+ // survives refreshes.
82
+ record.link = el("a", "vendor-usage-link");
83
+ record.link.target = "_blank";
84
+ record.link.rel = "noopener";
85
+ record.linkLabel = el("span", "", "Open usage dashboard");
86
+ record.link.appendChild(record.linkLabel);
87
+ record.link.insertAdjacentHTML("beforeend", iconHtml("external-link"));
88
+ pop.appendChild(record.link);
89
+ record.item.appendChild(pop);
90
+ record.pop = pop;
91
+ }
92
+ record.pop.setAttribute("aria-label", view.name + " usage");
93
+ record.title.textContent = view.name;
94
+ record.rows.textContent = "";
95
+ view.rows.forEach(function (row) {
96
+ var line = el("div", "vendor-usage-row" + (row.warning ? " warning" : "") + (row.stale ? " stale" : ""));
97
+ line.appendChild(el("span", "vendor-usage-row-label", row.label));
98
+ var parts = [];
99
+ if (row.source === "account") { if (row.value) parts.push(row.value); } else if (row.percentText) parts.push(row.percentText);
100
+ if (row.resetText) parts.push("resets in " + row.resetText);
101
+ var value = el("span", "vendor-usage-row-value");
102
+ value.appendChild(el("span", "", parts.length ? parts.join(" \u00b7 ") : "Limit window"));
103
+ var origin = row.source === "account" ? "account, " : (row.origin === "history" ? "session record, " : "session, ");
104
+ value.appendChild(el("span", "vendor-usage-row-age", origin + row.observedText + (row.stale ? " (stale)" : "") + (row.origin === "history" ? " (earlier session record)" : "")));
105
+ line.appendChild(value);
106
+ record.rows.appendChild(line);
107
+ });
108
+ record.notes.textContent = "";
109
+ if (view.status) record.notes.appendChild(el("div", "vendor-usage-note", view.status));
110
+ if (view.accountCount > 0 && view.accountSource) record.notes.appendChild(el("div", "vendor-usage-note", "Account figures: " + view.accountSource + ". Units are the vendor's own and are not converted."));
111
+ if (view.observedCount > 0) {
112
+ record.notes.appendChild(el("div", "vendor-usage-note", sourceNote(view)));
113
+ record.notes.appendChild(el("div", "vendor-usage-note", "Session rows come from your own sessions in this project; another account or session may differ."));
114
+ }
115
+ if (view.href) {
116
+ if (record.link.getAttribute("href") !== view.href) record.link.href = view.href;
117
+ record.link.title = view.linkTitle || "";
118
+ record.link.style.display = "";
119
+ } else {
120
+ record.link.style.display = "none";
121
+ }
122
+ }
123
+
124
+ function removePopover(record) {
125
+ if (record.pop) { record.pop.remove(); record.pop = null; }
126
+ }
127
+
128
+ // The popover is absolutely positioned under its chip, right-aligned; a measured
129
+ // shift keeps it inside the viewport.
130
+ function placePopover(pop) {
131
+ if (typeof pop.getBoundingClientRect !== "function") return;
132
+ pop.style.marginRight = "";
133
+ var bounds = pop.getBoundingClientRect();
134
+ var viewport = document.documentElement.clientWidth;
135
+ var shift = bounds.left < 8 ? 8 - bounds.left : Math.min(0, viewport - 8 - bounds.right);
136
+ if (shift) pop.style.marginRight = (-shift) + "px";
137
+ }
138
+
139
+ function shellWindow() { return typeof window !== "undefined" ? window : null; }
140
+
141
+ function bindDismissal(open) {
142
+ if (open && !outsideHandler) {
143
+ outsideHandler = function (event) {
144
+ if (groupEl && !groupEl.contains(event.target)) closePopover(false);
145
+ };
146
+ keyHandler = function (event) {
147
+ if (event.key === "Escape") { closePopover(true); }
148
+ };
149
+ // A click inside a split pane or contextual iframe never reaches this
150
+ // document; focus moving into the frame blurs the shell window instead.
151
+ blurHandler = function () {
152
+ if (document.activeElement && document.activeElement.tagName === "IFRAME") closePopover(false);
153
+ };
154
+ document.addEventListener("pointerdown", outsideHandler, true);
155
+ document.addEventListener("keydown", keyHandler, true);
156
+ if (shellWindow()) shellWindow().addEventListener("blur", blurHandler);
157
+ } else if (!open && outsideHandler) {
158
+ document.removeEventListener("pointerdown", outsideHandler, true);
159
+ document.removeEventListener("keydown", keyHandler, true);
160
+ if (blurHandler && shellWindow()) shellWindow().removeEventListener("blur", blurHandler);
161
+ outsideHandler = null;
162
+ keyHandler = null;
163
+ blurHandler = null;
164
+ }
165
+ }
166
+
167
+ function removeGroup() {
168
+ if (groupEl) { groupEl.remove(); groupEl = null; }
169
+ records = {};
170
+ bindDismissal(false);
171
+ if (tickTimer) { clearInterval(tickTimer); tickTimer = null; }
172
+ }
173
+
174
+ function createRecord(view) {
175
+ var item = el("span", "vendor-usage-item");
176
+ var chip = el("button", "top-bar-pill vendor-usage-chip");
177
+ chip.type = "button";
178
+ chip.setAttribute("data-vendor", view.vendor);
179
+ chip.setAttribute("aria-haspopup", "dialog");
180
+ var record = { item: item, chip: chip, icon: null, text: el("span", "vendor-usage-chip-text"), pop: null };
181
+ chip.appendChild(record.text);
182
+ chip.addEventListener("click", function () {
183
+ var opening = store.get('vendorUsageOpen') !== view.vendor;
184
+ store.set({ vendorUsageOpen: opening ? view.vendor : null });
185
+ if (opening) requestOverview(true);
186
+ });
187
+ item.appendChild(chip);
188
+ return record;
189
+ }
190
+
191
+ function updateChip(record, view, open) {
192
+ record.chip.className = "top-bar-pill vendor-usage-chip " + (view.warning ? "pill-warning" : "pill-dim") + (view.known ? "" : " unknown") + (view.stale ? " stale" : "");
193
+ record.chip.setAttribute("aria-expanded", open ? "true" : "false");
194
+ record.chip.setAttribute("aria-label", view.summary + ". Show details");
195
+ record.chip.title = view.summary;
196
+ if (view.icon) {
197
+ if (!record.icon) {
198
+ record.icon = el("img", "usage-check-vendor-icon");
199
+ record.icon.alt = "";
200
+ record.chip.insertBefore(record.icon, record.text);
201
+ }
202
+ if (record.icon.getAttribute("src") !== view.icon) record.icon.setAttribute("src", view.icon);
203
+ }
204
+ if (record.text.textContent !== view.text) record.text.textContent = view.text;
205
+ }
206
+
207
+ function updateListChip(record, listViews, open) {
208
+ var known = listViews.filter(function (view) { return view.known; }).length;
209
+ var summary = "Usage for " + listViews.length + " vendor" + (listViews.length === 1 ? "" : "s") + ", " + known + " with figures";
210
+ record.chip.className = "top-bar-pill vendor-usage-chip vendor-usage-list-chip pill-dim";
211
+ record.chip.setAttribute("aria-expanded", open ? "true" : "false");
212
+ record.chip.setAttribute("aria-label", summary + ". Show all vendors");
213
+ record.chip.title = summary;
214
+ if (!record.icon) {
215
+ record.icon = el("span", "vendor-usage-list-icon");
216
+ record.icon.insertAdjacentHTML("beforeend", iconHtml("gauge"));
217
+ record.chip.insertBefore(record.icon, record.text);
218
+ }
219
+ var text = known + "/" + listViews.length;
220
+ if (record.text.textContent !== text) record.text.textContent = text;
221
+ }
222
+
223
+ function ensureListPopover(record, listViews) {
224
+ if (!record.pop) {
225
+ var pop = el("div", "vendor-usage-popover vendor-usage-list-popover");
226
+ pop.setAttribute("role", "dialog");
227
+ pop.setAttribute("aria-label", "Usage by vendor");
228
+ record.title = el("div", "vendor-usage-popover-title", "Usage by vendor");
229
+ record.rows = el("div", "vendor-usage-rows");
230
+ pop.appendChild(record.title);
231
+ pop.appendChild(record.rows);
232
+ record.item.appendChild(pop);
233
+ record.pop = pop;
234
+ }
235
+ fillListPopover(record.rows, listViews, el);
236
+ }
237
+
238
+ // Updates stable elements in place. Chips, the popover and its link are never
239
+ // recreated while they stay relevant, so focus survives refreshes.
240
+ export function renderVendorUsageOverview() {
241
+ var actions = document.querySelector("#top-bar .top-bar-actions");
242
+ if (!actions) return;
243
+ var overview = store.get('vendorUsageOverview');
244
+ var info = store.get('vendorInfo');
245
+ var views = overviewVendors(info, overview, Date.now());
246
+ var listViews = allVendors(info, overview, Date.now());
247
+ if (listViews.length === 0) {
248
+ if (store.get('vendorUsageOpen')) store.set({ vendorUsageOpen: null });
249
+ removeGroup();
250
+ return;
251
+ }
252
+ var open = store.get('vendorUsageOpen');
253
+ if (open && open !== LIST_KEY && !views.some(function (view) { return view.vendor === open; })) {
254
+ open = null;
255
+ store.set({ vendorUsageOpen: null });
256
+ }
257
+ if (!groupEl) {
258
+ groupEl = el("div", "vendor-usage-overview");
259
+ groupEl.id = "vendor-usage-overview";
260
+ groupEl.setAttribute("role", "group");
261
+ groupEl.setAttribute("aria-label", "Vendor usage");
262
+ }
263
+ var anchor = document.getElementById("rate-limit-usage-link") || document.getElementById("skip-perms-pill");
264
+ if (groupEl.parentNode !== actions || groupEl.nextSibling !== anchor) actions.insertBefore(groupEl, anchor);
265
+ var present = {};
266
+ views.forEach(function (view, index) {
267
+ present[view.vendor] = true;
268
+ var record = records[view.vendor] || (records[view.vendor] = createRecord(view));
269
+ if (groupEl.children[index] !== record.item) groupEl.insertBefore(record.item, groupEl.children[index] || null);
270
+ updateChip(record, view, open === view.vendor);
271
+ if (open === view.vendor) {
272
+ var created = !record.pop;
273
+ ensurePopover(record, view);
274
+ placePopover(record.pop);
275
+ if (created) bindDismissal(true);
276
+ } else {
277
+ removePopover(record);
278
+ }
279
+ });
280
+ // The list entry is always last and reaches every installed vendor.
281
+ present[LIST_KEY] = true;
282
+ var listRecord = records[LIST_KEY] || (records[LIST_KEY] = createRecord({ vendor: LIST_KEY }));
283
+ if (groupEl.children[views.length] !== listRecord.item) groupEl.insertBefore(listRecord.item, groupEl.children[views.length] || null);
284
+ updateListChip(listRecord, listViews, open === LIST_KEY);
285
+ if (open === LIST_KEY) {
286
+ var listCreated = !listRecord.pop;
287
+ ensureListPopover(listRecord, listViews);
288
+ placePopover(listRecord.pop);
289
+ if (listCreated) bindDismissal(true);
290
+ } else {
291
+ removePopover(listRecord);
292
+ }
293
+ Object.keys(records).forEach(function (vendor) {
294
+ if (present[vendor]) return;
295
+ records[vendor].item.remove();
296
+ delete records[vendor];
297
+ });
298
+ if (!open) bindDismissal(false);
299
+ refreshIcons();
300
+ // Relative times only change slowly; one light timer keeps them honest.
301
+ if (!tickTimer) tickTimer = setInterval(renderVendorUsageOverview, 30000);
302
+ }
303
+
304
+ export function initVendorUsageOverview() {
305
+ store.subscribe(function (state, prev) {
306
+ var identityChanged = state.currentSlug !== prev.currentSlug || state.myUserId !== prev.myUserId;
307
+ if (identityChanged) {
308
+ // Another project or account must start unknown and ignore any reply
309
+ // that was requested before the change.
310
+ pendingRequestId = null;
311
+ if (state.vendorUsageOverview) {
312
+ store.set({ vendorUsageOverview: null, vendorUsageOpen: null });
313
+ if (state.connected) requestOverview(true);
314
+ return;
315
+ }
316
+ }
317
+ if (state.connected && (!prev.connected || identityChanged)) requestOverview(true);
318
+ if (state.vendorUsageOverview !== prev.vendorUsageOverview || state.vendorInfo !== prev.vendorInfo
319
+ || state.currentVendor !== prev.currentVendor || state.vendorUsageOpen !== prev.vendorUsageOpen) {
320
+ renderVendorUsageOverview();
321
+ }
322
+ });
323
+ }
@@ -6,15 +6,15 @@
6
6
  @import url("css/linear.css");
7
7
  @import url("css/base.css?v=20260828-type1");
8
8
  @import url("css/icon-strip.css?v=20260828-surfaces4");
9
- @import url("css/title-bar.css?v=20260828-project-header1");
9
+ @import url("css/title-bar.css?v=20261018-actions-layer");
10
10
  @import url("css/clay-workspace.css?v=20261010-tint");
11
11
  @import url("css/pane.css");
12
12
  @import url("css/worker-proposal.css");
13
13
  @import url("css/workspace-assignment.css");
14
14
  @import url("css/session-actions.css?v=20260828-contrast1");
15
- @import url("css/sidebar.css?v=20260828-session-list3");
15
+ @import url("css/sidebar.css?v=20261018-quick-appearance");
16
16
  @import url("css/session-folders.css");
17
- @import url("css/overlays.css?v=20260827-brand3");
17
+ @import url("css/overlays.css?v=20261017-usage2");
18
18
  @import url("css/menus.css");
19
19
  @import url("css/messages.css?v=20260828-delegated-muted1");
20
20
  @import url("css/generated-images.css?v=20260829-save-details1");
@@ -22,7 +22,7 @@
22
22
  @import url("css/elicitation-form.css");
23
23
  @import url("css/input.css?v=20261002-stop-inline1");
24
24
  @import url("css/pending-message-queue.css?v=20260914-queue1");
25
- @import url("css/filebrowser.css?v=20260829-terminal-canvas1");
25
+ @import url("css/filebrowser.css?v=20261018-browser-header1");
26
26
  @import url("css/git-panel.css");
27
27
  @import url("css/git-placard.css");
28
28
  @import url("css/diff.css");
@@ -61,14 +61,15 @@
61
61
  @import url("css/home-mate-settings.css");
62
62
  @import url("css/mate-avatar-gallery.css");
63
63
  @import url("css/home-mate-model-picker.css");
64
- @import url("css/project-mate-creation.css");
64
+ @import url("css/project-mate-creation.css?v=20261018-clay-choices");
65
65
  @import url("css/home-debate-live.css");
66
66
  @import url("css/notifications-center.css");
67
67
  @import url("css/tui-attention.css?v=20260829-terminal-canvas1");
68
68
  @import url("css/pwa-mobile.css?v=20260824f");
69
69
  @import url("css/avatar-imprints.css?v=20260829-imprints1");
70
70
 
71
- @import url("css/projects-hub.css");
71
+ @import url("css/projects-hub.css?v=20261018-welcome-art");
72
+ @import url("css/project-start.css");
72
73
 
73
74
  @import url("./css/shared-browser.css");
74
75
 
@@ -88,7 +89,9 @@
88
89
  @import url("css/server-settings-system.css");
89
90
 
90
91
  @import url("css/project-settings-modal.css");
92
+ @import url("css/settings-shell.css");
91
93
 
92
94
  @import url("css/clay-tool-activity.css");
93
95
 
94
- @import url("css/contextual-clay.css");
96
+ @import url("css/contextual-clay.css?v=20261018-loading-scene");
97
+ @import url("css/mate-loading-scene.css");
package/lib/sdk-bridge.js CHANGED
@@ -731,6 +731,7 @@ function createSDKBridge(opts) {
731
731
  "mcp__clay-workspace__read_workspace_receipt": true,
732
732
  "mcp__clay-workspace__inspect_workspace_change": true,
733
733
  "mcp__clay-workspace__control_project_workspace": true,
734
+ "mcp__clay-workspace__read_vendor_usage": true,
734
735
  "mcp__clay-workspace__read_project_records": true,
735
736
  "mcp__clay-workspace__inspect_project_record_change": true,
736
737
  "mcp__clay-workspace__control_project_records": true,
@@ -1734,6 +1735,9 @@ function createSDKBridge(opts) {
1734
1735
  var hasSharedBrowserCatalog = sessionToolDefs.some(function (definition) { return definition.name === "shared_browser"; });
1735
1736
  var recoverBrowserTools = needsCatalogRecovery("browser", 6) && hasSharedBrowserCatalog;
1736
1737
  var recoverIssueTools = needsCatalogRecovery("issues", 1) && hasIssueCatalog;
1738
+ var notesCatalogVersion = require("./session-notes-mcp-server").CATALOG_VERSION;
1739
+ var hasNotesCatalog = sessionToolDefs.some(function (definition) { return definition.name === "write_note"; });
1740
+ var recoverNoteTools = needsCatalogRecovery("notes", notesCatalogVersion) && hasNotesCatalog;
1737
1741
  function queryContextAvailable() {
1738
1742
  if (queryRetired()) return false;
1739
1743
  if (Number(session._sdkQueryGeneration || 0) !== queryGeneration) return false;
@@ -1929,7 +1933,7 @@ function createSDKBridge(opts) {
1929
1933
  dynamicTools: sessionToolDefs,
1930
1934
  // Conversation-scoped catalogs persist definitions. A stale authorized Driver
1931
1935
  // gets this exact-query bridge; its handlers still reauthorize live.
1932
- sessionMcpServer: (recoverMateSchedules || recoverIssueTools || recoverQuestionTools || recoverBrowserTools || recoverLinearTools || recoverYouTools || recoverExternalTools) ? getSessionMcpServer(session, queryGeneration) : undefined,
1936
+ sessionMcpServer: (recoverMateSchedules || recoverIssueTools || recoverQuestionTools || recoverBrowserTools || recoverLinearTools || recoverYouTools || recoverExternalTools || recoverNoteTools) ? getSessionMcpServer(session, queryGeneration) : undefined,
1933
1937
  onRecoveryAdmission: function(waiting) {
1934
1938
  if (!queryContextAvailable()) return;
1935
1939
  if (waiting) session._providerRecoveryWaiting = queryGeneration;
@@ -2177,6 +2181,7 @@ function createSDKBridge(opts) {
2177
2181
  if (hasLinearCatalog) markCatalog("linear", 1);
2178
2182
  if (hasSharedBrowserCatalog) markCatalog("browser", 6);
2179
2183
  if (hasIssueCatalog) markCatalog("issues", 1);
2184
+ if (hasNotesCatalog) markCatalog("notes", notesCatalogVersion);
2180
2185
  }
2181
2186
  session.streamPromise = processQueryStream(session, catalogCandidate, queryContract).catch(function(err) {
2182
2187
  });