clay-server 4.1.0-beta.13 → 4.1.0-beta.15
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/lib/issues-mcp-server.js +1 -1
- package/lib/project-file-path.js +13 -0
- package/lib/project-file-watch.js +19 -8
- package/lib/project-filesystem.js +10 -16
- package/lib/project-http.js +19 -11
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project.js +3 -2
- package/lib/public/app.js +15 -9
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/modules/app-connection.js +3 -0
- package/lib/public/modules/app-messages.js +7 -5
- package/lib/public/modules/app-panels.js +47 -12
- package/lib/public/modules/app-rate-limit.js +148 -61
- package/lib/public/modules/default-vendor.js +116 -0
- package/lib/public/modules/filebrowser-tabs.js +1 -0
- package/lib/public/modules/filebrowser.js +45 -18
- package/lib/public/modules/issues.js +4 -6
- package/lib/public/modules/project-logs.js +4 -9
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-tasks.js +4 -5
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/sticky-notes-browser.js +4 -13
- package/lib/public/modules/terminal.js +4 -4
- package/lib/sdk-message-processor.js +11 -2
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +4 -0
- package/lib/session-notes-mcp-server.js +2 -4
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +6 -0
- package/lib/ws-schema.js +3 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/codex.js +56 -13
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,7 @@ import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelP
|
|
|
10
10
|
|
|
11
11
|
// --- Module-owned state (not in store) ---
|
|
12
12
|
var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
|
|
13
|
-
var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
13
|
+
var contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
14
14
|
var ctxPopoverEl = null;
|
|
15
15
|
var ctxHoverTimer = null;
|
|
16
16
|
var statusRefreshTimer = null;
|
|
@@ -718,9 +718,18 @@ export function contextPctClass(pct) {
|
|
|
718
718
|
|
|
719
719
|
export function updateContextPanel() {
|
|
720
720
|
if (!contextUsedEl) return;
|
|
721
|
-
// Context
|
|
722
|
-
|
|
723
|
-
|
|
721
|
+
// Context occupancy is separate from generic per-turn accounting. Codex
|
|
722
|
+
// only supplies currentInput from a verified last-turn snapshot; rich
|
|
723
|
+
// context usage is more authoritative when available.
|
|
724
|
+
var rich = store.get('richContextUsage');
|
|
725
|
+
var hasRich = rich && (
|
|
726
|
+
(typeof rich.totalTokens === "number" && typeof rich.maxTokens === "number" && rich.maxTokens > 0) ||
|
|
727
|
+
(typeof rich.input_tokens === "number" && typeof rich.contextWindow === "number" && rich.contextWindow > 0)
|
|
728
|
+
);
|
|
729
|
+
var hasCurrent = typeof contextData.currentInput === "number";
|
|
730
|
+
var hasContext = !!(hasRich || hasCurrent);
|
|
731
|
+
var used = hasRich ? (typeof rich.totalTokens === "number" ? rich.totalTokens : rich.input_tokens) : (hasCurrent ? contextData.currentInput : 0);
|
|
732
|
+
var win = hasRich ? (typeof rich.maxTokens === "number" ? rich.maxTokens : rich.contextWindow) : (hasCurrent ? contextData.contextWindow : 0);
|
|
724
733
|
var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
|
|
725
734
|
var cls = contextPctClass(pct);
|
|
726
735
|
reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
|
|
@@ -734,7 +743,7 @@ export function updateContextPanel() {
|
|
|
734
743
|
contextMiniFill.className = "context-mini-fill" + cls;
|
|
735
744
|
}
|
|
736
745
|
if (contextMiniLabel) {
|
|
737
|
-
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "
|
|
746
|
+
contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "-");
|
|
738
747
|
}
|
|
739
748
|
// Header bar. A split group has no meaningful group-level context usage:
|
|
740
749
|
// the two members have separate windows, and the parent shell's own numbers
|
|
@@ -742,9 +751,9 @@ export function updateContextPanel() {
|
|
|
742
751
|
// chip instead, so hide the title-bar gauge while a split is open.
|
|
743
752
|
var splitOpen = !!store.get('splitPanes');
|
|
744
753
|
var headerCtxEl = store.get('headerContextEl');
|
|
745
|
-
if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
|
|
746
|
-
if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
|
|
747
|
-
if (
|
|
754
|
+
if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen || !hasContext);
|
|
755
|
+
if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen || !hasContext);
|
|
756
|
+
if (hasContext && !splitOpen) {
|
|
748
757
|
var statusArea = document.querySelector(".title-bar-content .status");
|
|
749
758
|
var hCtxEl = store.get('headerContextEl');
|
|
750
759
|
if (statusArea && !hCtxEl) {
|
|
@@ -808,7 +817,7 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
808
817
|
// when available -- result.usage.input_tokens sums all API calls in a turn,
|
|
809
818
|
// inflating context usage when tools are involved.
|
|
810
819
|
// Falls back to the summed value for setups that don't emit message_start.
|
|
811
|
-
if (lastStreamInputTokens) {
|
|
820
|
+
if (typeof lastStreamInputTokens === "number") {
|
|
812
821
|
contextData.input = lastStreamInputTokens;
|
|
813
822
|
} else {
|
|
814
823
|
contextData.input = (usage.input_tokens || usage.inputTokens || 0)
|
|
@@ -817,6 +826,11 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
817
826
|
contextData.output = usage.output_tokens || usage.outputTokens || 0;
|
|
818
827
|
contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
|
|
819
828
|
contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
|
|
829
|
+
if (store.get('currentVendor') === "codex") {
|
|
830
|
+
contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : null;
|
|
831
|
+
} else {
|
|
832
|
+
contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : contextData.input;
|
|
833
|
+
}
|
|
820
834
|
}
|
|
821
835
|
contextData.turns++;
|
|
822
836
|
if (modelUsage) {
|
|
@@ -830,6 +844,21 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
|
|
|
830
844
|
contextData.model = displayModel;
|
|
831
845
|
contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
|
|
832
846
|
if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
|
|
847
|
+
if (store.get('currentVendor') === "codex") {
|
|
848
|
+
var snapshot = mu.contextSnapshot;
|
|
849
|
+
if (snapshot) store.set({ richContextUsage: null });
|
|
850
|
+
if (snapshot && snapshot.valid === true
|
|
851
|
+
&& typeof snapshot.inputTokens === "number"
|
|
852
|
+
&& typeof snapshot.contextWindow === "number"
|
|
853
|
+
&& snapshot.contextWindow > 0) {
|
|
854
|
+
contextData.currentInput = snapshot.inputTokens;
|
|
855
|
+
contextData.contextWindow = snapshot.contextWindow;
|
|
856
|
+
} else {
|
|
857
|
+
// An explicit invalid marker is authoritative: do not retain an
|
|
858
|
+
// occupancy value from the previous live turn or history replay.
|
|
859
|
+
contextData.currentInput = null;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
833
862
|
|
|
834
863
|
// thinkingTokens and costBasis are per-model and optional: absent on
|
|
835
864
|
// older CLI builds and, for a resumed session, on the turns that ran
|
|
@@ -874,11 +903,17 @@ export function applyContextView(view) {
|
|
|
874
903
|
}
|
|
875
904
|
|
|
876
905
|
export function resetContextData() {
|
|
877
|
-
contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
906
|
+
contextData = { contextWindow: 0, currentInput: null, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0, thinking: null, costBasis: null };
|
|
878
907
|
store.set({ richContextUsage: null });
|
|
879
908
|
// hideCtxPopover + updateContextPanel handled by store subscriber
|
|
880
909
|
}
|
|
881
910
|
|
|
911
|
+
export function markContextUnavailable() {
|
|
912
|
+
contextData.currentInput = null;
|
|
913
|
+
store.set({ richContextUsage: null });
|
|
914
|
+
updateContextPanel();
|
|
915
|
+
}
|
|
916
|
+
|
|
882
917
|
export function resetContext() {
|
|
883
918
|
resetContextData();
|
|
884
919
|
// Keep view state, just reset data
|
|
@@ -952,8 +987,8 @@ export function renderCtxPopover() {
|
|
|
952
987
|
if (!ctxPopoverEl || !richContextUsage) return;
|
|
953
988
|
var d = richContextUsage;
|
|
954
989
|
var cats = d.categories || [];
|
|
955
|
-
var total = d.totalTokens || 0;
|
|
956
|
-
var max = d.maxTokens || 0;
|
|
990
|
+
var total = d.totalTokens != null ? d.totalTokens : (d.input_tokens || 0);
|
|
991
|
+
var max = d.maxTokens != null ? d.maxTokens : (d.contextWindow || 0);
|
|
957
992
|
var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
|
|
958
993
|
|
|
959
994
|
var html = "";
|
|
@@ -13,6 +13,7 @@ var rateLimitResetTimer = null;
|
|
|
13
13
|
var rateLimitUsageEl = null;
|
|
14
14
|
var rateLimitResetState = {};
|
|
15
15
|
var rateLimitTickTimer = null;
|
|
16
|
+
var rateLimitPopoverTimers = [];
|
|
16
17
|
var fastModeIndicatorEl = null;
|
|
17
18
|
|
|
18
19
|
// --- Internal helpers ---
|
|
@@ -25,7 +26,7 @@ function getVendorUsageMeta(vendor) {
|
|
|
25
26
|
codex: {
|
|
26
27
|
icon: "/codex-avatar.png",
|
|
27
28
|
alt: "Codex",
|
|
28
|
-
href: "https://chatgpt.com/
|
|
29
|
+
href: "https://chatgpt.com/codex/settings/usage",
|
|
29
30
|
title: "Check usage on ChatGPT",
|
|
30
31
|
},
|
|
31
32
|
claude: {
|
|
@@ -35,7 +36,7 @@ function getVendorUsageMeta(vendor) {
|
|
|
35
36
|
title: "Check usage on claude.ai",
|
|
36
37
|
},
|
|
37
38
|
};
|
|
38
|
-
return fallbacks[vendor] ||
|
|
39
|
+
return fallbacks[vendor] || null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
function vendorTracksRateLimits(vendor) {
|
|
@@ -51,6 +52,37 @@ function vendorSupportsScheduledMessages(vendor) {
|
|
|
51
52
|
return scheduledMessageVendors.indexOf(vendor) !== -1;
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
function rateLimitEventAppliesToPane(msg, state) {
|
|
56
|
+
var activeSessionId = state.activeSessionId;
|
|
57
|
+
var activeVendor = state.currentVendor || "claude";
|
|
58
|
+
if (!msg) return false;
|
|
59
|
+
if (msg.sessionId != null && String(msg.sessionId) !== String(activeSessionId)) return false;
|
|
60
|
+
if (msg.vendor) return msg.vendor === activeVendor;
|
|
61
|
+
// Legacy Claude records predate vendor/session stamps. Never project them
|
|
62
|
+
// into a non-Claude pane, where they would look like Claude usage.
|
|
63
|
+
return activeVendor === "claude";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function rateLimitStateKey(sessionId, vendor) {
|
|
67
|
+
return String(store.get('currentSlug') || "") + "\u0000" +
|
|
68
|
+
String(store.get('myUserId') || "") + "\u0000" +
|
|
69
|
+
String(sessionId == null ? "" : sessionId) + "\u0000" + String(vendor || "claude");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getActiveRateLimitState() {
|
|
73
|
+
var state = store.get('rateLimitState') || {};
|
|
74
|
+
var key = rateLimitStateKey(store.get('activeSessionId'), store.get('currentVendor'));
|
|
75
|
+
return state[key] || {};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function setActiveRateLimitState(next) {
|
|
79
|
+
var state = Object.assign({}, store.get('rateLimitState') || {});
|
|
80
|
+
var key = rateLimitStateKey(store.get('activeSessionId'), store.get('currentVendor'));
|
|
81
|
+
state[key] = next;
|
|
82
|
+
store.set({ rateLimitState: state });
|
|
83
|
+
rateLimitResetState = next;
|
|
84
|
+
}
|
|
85
|
+
|
|
54
86
|
function rateLimitTypeLabel(type) {
|
|
55
87
|
if (!type) return "Usage";
|
|
56
88
|
var labels = {
|
|
@@ -108,13 +140,15 @@ function updateRateLimitIndicator(msg) {
|
|
|
108
140
|
var isRejected = msg.status === "rejected";
|
|
109
141
|
var pillClass = "header-rate-limit" + (isRejected ? " rejected" : " warning");
|
|
110
142
|
var label = isRejected ? "Rate limited" : "Rate warning";
|
|
143
|
+
var meta = getVendorUsageMeta(store.get('currentVendor') || "claude");
|
|
144
|
+
var link = meta
|
|
145
|
+
? '<a href="' + meta.href + '" target="_blank" rel="noopener" class="rate-limit-link" title="' + meta.title + '">' + iconHtml("external-link") + "</a>"
|
|
146
|
+
: '<span class="rate-limit-link unavailable" title="Usage information is unavailable for this vendor">' + iconHtml("external-link") + "</span>";
|
|
111
147
|
rateLimitIndicatorEl.innerHTML =
|
|
112
148
|
'<span class="' + pillClass + '">' +
|
|
113
149
|
iconHtml("alert-triangle") +
|
|
114
150
|
'<span class="header-pill-text">' + label + "</span>" +
|
|
115
|
-
|
|
116
|
-
iconHtml("external-link") +
|
|
117
|
-
"</a>" +
|
|
151
|
+
link +
|
|
118
152
|
"</span>";
|
|
119
153
|
refreshIcons();
|
|
120
154
|
}
|
|
@@ -131,10 +165,12 @@ function showRateLimitPopover(text, isRejected) {
|
|
|
131
165
|
rateLimitIndicatorEl.appendChild(pop);
|
|
132
166
|
|
|
133
167
|
// Auto-dismiss after 5s
|
|
134
|
-
setTimeout(function () {
|
|
168
|
+
var fadeTimer = setTimeout(function () {
|
|
135
169
|
pop.classList.add("fade-out");
|
|
136
|
-
setTimeout(function () { if (pop.parentNode) pop.remove(); }, 300);
|
|
170
|
+
var removeTimer = setTimeout(function () { if (pop.parentNode) pop.remove(); }, 300);
|
|
171
|
+
rateLimitPopoverTimers.push(removeTimer);
|
|
137
172
|
}, 5000);
|
|
173
|
+
rateLimitPopoverTimers.push(fadeTimer);
|
|
138
174
|
}
|
|
139
175
|
|
|
140
176
|
function clearRateLimitIndicator() {
|
|
@@ -168,22 +204,81 @@ function tickRateLimitUsage() {
|
|
|
168
204
|
if (!rateLimitUsageEl) return;
|
|
169
205
|
var parts = [];
|
|
170
206
|
var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
207
|
+
var nextState = Object.assign({}, rateLimitResetState);
|
|
208
|
+
var changed = false;
|
|
171
209
|
for (var i = 0; i < types.length; i++) {
|
|
172
210
|
var entry = rateLimitResetState[types[i]];
|
|
173
211
|
if (!entry || !entry.resetsAt) continue;
|
|
174
212
|
var timeStr = formatResetTime(entry.resetsAt);
|
|
175
|
-
if (!timeStr) { delete
|
|
213
|
+
if (!timeStr) { delete nextState[types[i]]; changed = true; continue; }
|
|
176
214
|
parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
|
|
177
215
|
}
|
|
216
|
+
if (changed) setActiveRateLimitState(nextState);
|
|
178
217
|
if (parts.length === 0) {
|
|
179
|
-
|
|
180
|
-
|
|
218
|
+
renderRateLimitUsage();
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
renderRateLimitUsage();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function renderRateLimitUsage() {
|
|
225
|
+
var activeVendor = store.get('currentVendor') || "claude";
|
|
226
|
+
if (!vendorTracksRateLimits(activeVendor)) {
|
|
227
|
+
if (rateLimitUsageEl) {
|
|
228
|
+
rateLimitUsageEl.remove();
|
|
229
|
+
rateLimitUsageEl = null;
|
|
230
|
+
}
|
|
181
231
|
if (rateLimitTickTimer) { clearInterval(rateLimitTickTimer); rateLimitTickTimer = null; }
|
|
182
232
|
return;
|
|
183
233
|
}
|
|
184
|
-
|
|
185
|
-
|
|
234
|
+
|
|
235
|
+
rateLimitResetState = getActiveRateLimitState();
|
|
236
|
+
var topBarActions = document.querySelector("#top-bar .top-bar-actions");
|
|
237
|
+
if (!topBarActions) return;
|
|
238
|
+
if (!rateLimitUsageEl) {
|
|
239
|
+
rateLimitUsageEl = document.createElement("a");
|
|
240
|
+
rateLimitUsageEl.id = "rate-limit-usage-link";
|
|
241
|
+
rateLimitUsageEl.className = "top-bar-pill pill-dim usage-check-link";
|
|
242
|
+
rateLimitUsageEl.target = "_blank";
|
|
243
|
+
rateLimitUsageEl.rel = "noopener";
|
|
244
|
+
var ref = document.getElementById("skip-perms-pill");
|
|
245
|
+
topBarActions.insertBefore(rateLimitUsageEl, ref);
|
|
246
|
+
}
|
|
247
|
+
rateLimitUsageEl.style.display = "";
|
|
248
|
+
|
|
249
|
+
var parts = [];
|
|
250
|
+
var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
251
|
+
for (var i = 0; i < types.length; i++) {
|
|
252
|
+
var entry = rateLimitResetState[types[i]];
|
|
253
|
+
if (!entry || !entry.resetsAt) continue;
|
|
254
|
+
var timeStr = formatResetTime(entry.resetsAt);
|
|
255
|
+
if (!timeStr) continue;
|
|
256
|
+
parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
|
|
260
|
+
var meta = getVendorUsageMeta(activeVendor);
|
|
261
|
+
if (meta) {
|
|
262
|
+
rateLimitUsageEl.href = meta.href;
|
|
263
|
+
rateLimitUsageEl.title = meta.title;
|
|
264
|
+
rateLimitUsageEl.removeAttribute("aria-disabled");
|
|
265
|
+
rateLimitUsageEl.innerHTML =
|
|
266
|
+
'<img src="' + meta.icon + '" class="usage-check-vendor-icon" alt="' + meta.alt + '">' +
|
|
267
|
+
'<span>' + label + '</span>' + iconHtml("external-link");
|
|
268
|
+
} else {
|
|
269
|
+
rateLimitUsageEl.removeAttribute("href");
|
|
270
|
+
rateLimitUsageEl.setAttribute("aria-disabled", "true");
|
|
271
|
+
rateLimitUsageEl.title = "Usage information is unavailable for this vendor";
|
|
272
|
+
rateLimitUsageEl.innerHTML = '<span>' + (parts.length > 0 ? label : "Usage unavailable") + '</span>';
|
|
273
|
+
}
|
|
186
274
|
refreshIcons();
|
|
275
|
+
|
|
276
|
+
if (parts.length > 0 && !rateLimitTickTimer) {
|
|
277
|
+
rateLimitTickTimer = setInterval(tickRateLimitUsage, 30000);
|
|
278
|
+
} else if (parts.length === 0 && rateLimitTickTimer) {
|
|
279
|
+
clearInterval(rateLimitTickTimer);
|
|
280
|
+
rateLimitTickTimer = null;
|
|
281
|
+
}
|
|
187
282
|
}
|
|
188
283
|
|
|
189
284
|
// --- Exported functions ---
|
|
@@ -193,15 +288,32 @@ export function initRateLimit() {
|
|
|
193
288
|
if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
|
|
194
289
|
if (!vendorSupportsScheduledMessages(state.currentVendor)) clearScheduleDelay();
|
|
195
290
|
}
|
|
196
|
-
if (state.currentVendor !== prev.currentVendor || state.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
291
|
+
if (state.currentVendor !== prev.currentVendor || state.activeSessionId !== prev.activeSessionId
|
|
292
|
+
|| state.currentSlug !== prev.currentSlug || state.myUserId !== prev.myUserId) {
|
|
293
|
+
resetRateLimitState();
|
|
294
|
+
}
|
|
295
|
+
if (state.vendorInfo !== prev.vendorInfo && state.currentVendor === prev.currentVendor && state.activeSessionId === prev.activeSessionId
|
|
296
|
+
&& state.currentSlug === prev.currentSlug && state.myUserId === prev.myUserId) {
|
|
297
|
+
renderRateLimitUsage();
|
|
200
298
|
}
|
|
201
299
|
});
|
|
202
300
|
}
|
|
203
301
|
|
|
204
302
|
export function handleRateLimitEvent(msg) {
|
|
303
|
+
if (!rateLimitEventAppliesToPane(msg, store.snap())) return;
|
|
304
|
+
if (msg.resetsAt && msg.resetsAt <= Date.now()) {
|
|
305
|
+
var expiredState = Object.assign({}, getActiveRateLimitState());
|
|
306
|
+
if (msg.rateLimitType) delete expiredState[msg.rateLimitType];
|
|
307
|
+
setActiveRateLimitState(expiredState);
|
|
308
|
+
clearRateLimitIndicator();
|
|
309
|
+
renderRateLimitUsage();
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
if (msg.rateLimitType && msg.resetsAt) {
|
|
313
|
+
var eventState = Object.assign({}, getActiveRateLimitState());
|
|
314
|
+
eventState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
|
|
315
|
+
setActiveRateLimitState(eventState);
|
|
316
|
+
}
|
|
205
317
|
var isRejected = msg.status === "rejected";
|
|
206
318
|
var typeLabel = rateLimitTypeLabel(msg.rateLimitType);
|
|
207
319
|
var popoverText = "";
|
|
@@ -226,8 +338,12 @@ export function handleRateLimitEvent(msg) {
|
|
|
226
338
|
rateLimitResetTimer = setTimeout(function () {
|
|
227
339
|
rateLimitResetsAt = null;
|
|
228
340
|
rateLimitResetTimer = null;
|
|
341
|
+
var currentState = Object.assign({}, getActiveRateLimitState());
|
|
342
|
+
delete currentState[msg.rateLimitType];
|
|
343
|
+
setActiveRateLimitState(currentState);
|
|
229
344
|
// Clear schedule mode when rate limit resets
|
|
230
345
|
clearScheduleDelay();
|
|
346
|
+
renderRateLimitUsage();
|
|
231
347
|
}, msg.resetsAt - Date.now() + 1000);
|
|
232
348
|
} else {
|
|
233
349
|
var pct = msg.utilization ? Math.round(msg.utilization * 100) : null;
|
|
@@ -239,58 +355,19 @@ export function handleRateLimitEvent(msg) {
|
|
|
239
355
|
}
|
|
240
356
|
|
|
241
357
|
export function updateRateLimitUsage(msg) {
|
|
358
|
+
if (!rateLimitEventAppliesToPane(msg, store.snap())) return;
|
|
242
359
|
var activeVendor = store.get('currentVendor') || "claude";
|
|
243
360
|
if (!vendorTracksRateLimits(activeVendor)) {
|
|
244
|
-
|
|
361
|
+
renderRateLimitUsage();
|
|
245
362
|
return;
|
|
246
363
|
}
|
|
247
364
|
if (msg.rateLimitType && msg.resetsAt) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
var topBarActions = document.querySelector("#top-bar .top-bar-actions");
|
|
252
|
-
if (!topBarActions) return;
|
|
253
|
-
|
|
254
|
-
if (!rateLimitUsageEl) {
|
|
255
|
-
rateLimitUsageEl = document.createElement("a");
|
|
256
|
-
rateLimitUsageEl.id = "rate-limit-usage-link";
|
|
257
|
-
rateLimitUsageEl.className = "top-bar-pill pill-dim usage-check-link";
|
|
258
|
-
rateLimitUsageEl.target = "_blank";
|
|
259
|
-
rateLimitUsageEl.rel = "noopener";
|
|
260
|
-
var ref = document.getElementById("skip-perms-pill");
|
|
261
|
-
topBarActions.insertBefore(rateLimitUsageEl, ref);
|
|
262
|
-
}
|
|
263
|
-
rateLimitUsageEl.style.display = "";
|
|
264
|
-
|
|
265
|
-
// Build label from available reset times
|
|
266
|
-
var parts = [];
|
|
267
|
-
var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
268
|
-
for (var i = 0; i < types.length; i++) {
|
|
269
|
-
var entry = rateLimitResetState[types[i]];
|
|
270
|
-
if (!entry || !entry.resetsAt) continue;
|
|
271
|
-
var timeStr = formatResetTime(entry.resetsAt);
|
|
272
|
-
if (!timeStr) continue;
|
|
273
|
-
parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
|
|
365
|
+
var nextState = Object.assign({}, getActiveRateLimitState());
|
|
366
|
+
nextState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
|
|
367
|
+
setActiveRateLimitState(nextState);
|
|
274
368
|
}
|
|
275
369
|
|
|
276
|
-
|
|
277
|
-
var vendor = activeVendor;
|
|
278
|
-
var meta = getVendorUsageMeta(vendor);
|
|
279
|
-
rateLimitUsageEl.href = meta.href;
|
|
280
|
-
rateLimitUsageEl.title = meta.title;
|
|
281
|
-
rateLimitUsageEl.innerHTML =
|
|
282
|
-
'<img src="' + meta.icon + '" class="usage-check-vendor-icon" alt="' + meta.alt + '">' +
|
|
283
|
-
'<span>' + label + '</span>' +
|
|
284
|
-
iconHtml("external-link");
|
|
285
|
-
refreshIcons();
|
|
286
|
-
|
|
287
|
-
// Start or stop live countdown tick
|
|
288
|
-
if (parts.length > 0 && !rateLimitTickTimer) {
|
|
289
|
-
rateLimitTickTimer = setInterval(tickRateLimitUsage, 30000);
|
|
290
|
-
} else if (parts.length === 0 && rateLimitTickTimer) {
|
|
291
|
-
clearInterval(rateLimitTickTimer);
|
|
292
|
-
rateLimitTickTimer = null;
|
|
293
|
-
}
|
|
370
|
+
renderRateLimitUsage();
|
|
294
371
|
}
|
|
295
372
|
|
|
296
373
|
export function handleFastModeState(state) {
|
|
@@ -322,6 +399,16 @@ export function handleFastModeState(state) {
|
|
|
322
399
|
|
|
323
400
|
export function resetRateLimitState() {
|
|
324
401
|
clearRateLimitIndicator();
|
|
402
|
+
rateLimitResetsAt = null;
|
|
403
|
+
if (rateLimitResetTimer) { clearTimeout(rateLimitResetTimer); rateLimitResetTimer = null; }
|
|
404
|
+
clearScheduleDelay();
|
|
405
|
+
for (var i = 0; i < rateLimitPopoverTimers.length; i++) clearTimeout(rateLimitPopoverTimers[i]);
|
|
406
|
+
rateLimitPopoverTimers = [];
|
|
407
|
+
if (rateLimitTickTimer) { clearInterval(rateLimitTickTimer); rateLimitTickTimer = null; }
|
|
325
408
|
if (rateLimitCountdownTimer) { clearInterval(rateLimitCountdownTimer); rateLimitCountdownTimer = null; }
|
|
409
|
+
rateLimitResetState = getActiveRateLimitState();
|
|
410
|
+
renderRateLimitUsage();
|
|
326
411
|
if (fastModeIndicatorEl) { fastModeIndicatorEl.remove(); fastModeIndicatorEl = null; }
|
|
327
412
|
}
|
|
413
|
+
|
|
414
|
+
export { rateLimitEventAppliesToPane, getVendorUsageMeta };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { store } from './store.js';
|
|
2
|
+
import { getWs } from './ws-ref.js';
|
|
3
|
+
import { firstInstalledVendor } from './vendor-priority.js';
|
|
4
|
+
|
|
5
|
+
function send(message) {
|
|
6
|
+
var ws = getWs();
|
|
7
|
+
if (!ws || ws.readyState !== 1) return false;
|
|
8
|
+
ws.send(JSON.stringify(message));
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
var sequence = 0;
|
|
13
|
+
|
|
14
|
+
function nextRequestId(prefix) {
|
|
15
|
+
sequence += 1;
|
|
16
|
+
return prefix + Date.now() + "-" + sequence;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function emptyState(accountId, projectSlug) {
|
|
20
|
+
return { loading: false, saving: false, getRequestId: null, saveRequestId: null, preference: null, preferencePresent: false, installedVendors: [], accountId: accountId || null, projectSlug: projectSlug || null, serverEpoch: null, canonicalRevision: 0, error: "" };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requestDefaultVendor() {
|
|
24
|
+
var id = nextRequestId("default-vendor-");
|
|
25
|
+
var state = store.get("defaultVendorState") || {};
|
|
26
|
+
store.set({ defaultVendorState: Object.assign({}, state, { loading: true, getRequestId: id, error: "" }) });
|
|
27
|
+
if (!send({ type: "default_vendor_get", requestId: id })) {
|
|
28
|
+
store.set({ defaultVendorState: Object.assign({}, store.get("defaultVendorState") || {}, { loading: false, getRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function beginDefaultVendorConnection() {
|
|
35
|
+
var state = store.get("defaultVendorState") || {};
|
|
36
|
+
store.set({ defaultVendorState: Object.assign({}, state, {
|
|
37
|
+
loading: false,
|
|
38
|
+
saving: false,
|
|
39
|
+
getRequestId: null,
|
|
40
|
+
saveRequestId: null,
|
|
41
|
+
preference: null,
|
|
42
|
+
preferencePresent: false,
|
|
43
|
+
installedVendors: [],
|
|
44
|
+
accountId: store.get("myUserId") || "default",
|
|
45
|
+
projectSlug: store.get("currentSlug") || null,
|
|
46
|
+
serverEpoch: null,
|
|
47
|
+
canonicalRevision: 0,
|
|
48
|
+
error: "",
|
|
49
|
+
}) });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function saveDefaultVendor(vendor) {
|
|
53
|
+
var state = store.get("defaultVendorState") || {};
|
|
54
|
+
if (vendor !== "" && (state.installedVendors || []).indexOf(vendor) === -1) return false;
|
|
55
|
+
var id = nextRequestId("default-vendor-save-");
|
|
56
|
+
store.set({ defaultVendorState: Object.assign({}, state, { saving: true, saveRequestId: id, error: "" }) });
|
|
57
|
+
if (!send({ type: "default_vendor_set", requestId: id, vendor: vendor })) {
|
|
58
|
+
store.set({ defaultVendorState: Object.assign({}, store.get("defaultVendorState") || {}, { saving: false, saveRequestId: null, error: "Clay is offline. Reconnect and try again." }) });
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handleDefaultVendorMessage(msg) {
|
|
65
|
+
if (!msg || msg.type !== "default_vendor_state") return false;
|
|
66
|
+
var state = store.get("defaultVendorState") || {};
|
|
67
|
+
var accountId = store.get("myUserId") || "default";
|
|
68
|
+
var projectSlug = store.get("currentSlug") || null;
|
|
69
|
+
var isGetReply = !!msg.requestId && msg.requestId === state.getRequestId;
|
|
70
|
+
var isSaveReply = !!msg.requestId && msg.requestId === state.saveRequestId;
|
|
71
|
+
var isCorrelatedAccountFailure = (isGetReply || isSaveReply) && msg.accountId === null && msg.accountAvailable === false;
|
|
72
|
+
if ((!isCorrelatedAccountFailure && msg.accountId !== accountId) || (msg.accountId === null && !isCorrelatedAccountFailure)) return true;
|
|
73
|
+
if (Object.prototype.hasOwnProperty.call(msg, "projectSlug") && msg.projectSlug !== projectSlug) return true;
|
|
74
|
+
if (msg.serverEpoch && state.serverEpoch && msg.serverEpoch !== state.serverEpoch && (!msg.requestId || (msg.requestId !== state.getRequestId && msg.requestId !== state.saveRequestId))) return true;
|
|
75
|
+
if (msg.canonicalRevision !== undefined && msg.canonicalRevision < (state.canonicalRevision || 0) && msg.serverEpoch === state.serverEpoch) return true;
|
|
76
|
+
if (msg.requestId && !isGetReply && !isSaveReply) return true;
|
|
77
|
+
if (msg.requestId && !isSaveReply && msg.ready === false && state.saving) return true;
|
|
78
|
+
var next = Object.assign({}, state, {
|
|
79
|
+
accountId: msg.accountId || accountId,
|
|
80
|
+
projectSlug: projectSlug,
|
|
81
|
+
serverEpoch: msg.serverEpoch || state.serverEpoch || null,
|
|
82
|
+
canonicalRevision: typeof msg.canonicalRevision === "number" ? msg.canonicalRevision : state.canonicalRevision || 0,
|
|
83
|
+
installedVendors: Array.isArray(msg.installedVendors) ? msg.installedVendors : state.installedVendors || [],
|
|
84
|
+
loading: isGetReply ? false : state.loading,
|
|
85
|
+
saving: isSaveReply ? false : state.saving,
|
|
86
|
+
getRequestId: isGetReply ? null : state.getRequestId,
|
|
87
|
+
saveRequestId: isSaveReply ? null : state.saveRequestId,
|
|
88
|
+
error: msg.error || ""
|
|
89
|
+
});
|
|
90
|
+
if (Object.prototype.hasOwnProperty.call(msg, "preference")) {
|
|
91
|
+
next.preference = msg.preference || null;
|
|
92
|
+
next.preferencePresent = msg.preferencePresent === true;
|
|
93
|
+
}
|
|
94
|
+
store.set({ defaultVendorState: next });
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
store.subscribe(function (state, previous) {
|
|
99
|
+
if (state.myUserId !== previous.myUserId || state.currentSlug !== previous.currentSlug) {
|
|
100
|
+
var accountId = state.myUserId || "default";
|
|
101
|
+
store.set({ defaultVendorState: emptyState(accountId, state.currentSlug || null) });
|
|
102
|
+
if (state.connected) requestDefaultVendor();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export function resolvePreferredVendor(installedVendors) {
|
|
107
|
+
var installed = Array.isArray(installedVendors) ? installedVendors : [];
|
|
108
|
+
var state = store.get("defaultVendorState") || {};
|
|
109
|
+
if (state.preference && installed.indexOf(state.preference) !== -1) return state.preference;
|
|
110
|
+
return firstInstalledVendor(installed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function defaultVendorUnavailable(installedVendors) {
|
|
114
|
+
var state = store.get("defaultVendorState") || {};
|
|
115
|
+
return !!state.preference && (installedVendors || []).indexOf(state.preference) === -1;
|
|
116
|
+
}
|
|
@@ -101,4 +101,5 @@ export function closeFileViewerTab(path) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function focusedFileViewerTab() { return focusedPath; }
|
|
104
|
+
export function focusedFileViewerTabData() { return focusedPath && tabs.has(focusedPath) ? tabs.get(focusedPath) : null; }
|
|
104
105
|
export function clearFileViewerTabs() { tabs.clear(); focusedPath = null; previewPath = null; render(); }
|