clay-server 4.1.0-beta.8 → 4.1.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.
- package/lib/context-usage.js +47 -0
- package/lib/issues-mcp-server.js +1 -1
- package/lib/issues-service.js +3 -1
- package/lib/loop-registry-occurrence.js +1 -0
- package/lib/notes.js +20 -3
- package/lib/os-users.js +6 -0
- package/lib/project-connection.js +14 -3
- 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 +53 -48
- package/lib/project-logs-mcp-server.js +8 -8
- package/lib/project-logs-schema.js +1 -1
- package/lib/project-logs-service.js +46 -13
- package/lib/project-logs-store.js +17 -15
- package/lib/project-logs.js +70 -0
- package/lib/project-loop.js +1 -0
- package/lib/project-notifications.js +1 -1
- package/lib/project-pending-message-queue.js +71 -7
- package/lib/project-scheduled-messages.js +235 -52
- package/lib/project-scheduled-result-store.js +131 -0
- package/lib/project-scheduled-task-execution.js +35 -2
- package/lib/project-session-notes.js +11 -4
- package/lib/project-sessions.js +7 -0
- package/lib/project-user-message.js +72 -5
- package/lib/project.js +60 -5
- package/lib/public/app.js +27 -12
- package/lib/public/css/avatar-imprints.css +1 -1
- package/lib/public/css/command-palette.css +20 -0
- package/lib/public/css/git-panel.css +13 -12
- package/lib/public/css/git-placard.css +4 -4
- package/lib/public/css/input.css +0 -109
- package/lib/public/css/issues.css +78 -31
- package/lib/public/css/mobile-nav.css +7 -0
- package/lib/public/css/notifications-center.css +3 -3
- package/lib/public/css/pending-message-queue.css +3 -0
- package/lib/public/css/project-logs.css +15 -0
- package/lib/public/css/sidebar.css +7 -0
- package/lib/public/css/sticky-notes.css +29 -0
- package/lib/public/modules/app-connection.js +9 -1
- package/lib/public/modules/app-favicon.js +10 -0
- package/lib/public/modules/app-messages.js +49 -37
- package/lib/public/modules/app-notifications.js +2 -0
- package/lib/public/modules/app-panels.js +77 -23
- package/lib/public/modules/app-projects.js +3 -0
- package/lib/public/modules/app-rate-limit.js +148 -243
- package/lib/public/modules/avatar-imprint.js +25 -70
- package/lib/public/modules/avatar.js +12 -4
- package/lib/public/modules/chat-render-runtime.js +20 -2
- package/lib/public/modules/clay-issue-links.js +23 -2
- package/lib/public/modules/clay-log-links.js +11 -1
- package/lib/public/modules/context-view-preference.js +143 -0
- 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/input.js +81 -20
- package/lib/public/modules/issues-render.js +36 -10
- package/lib/public/modules/issues.js +95 -13
- package/lib/public/modules/markdown.js +3 -1
- package/lib/public/modules/pane-bridge.js +14 -8
- package/lib/public/modules/pane-issue-bridge.js +27 -0
- package/lib/public/modules/pending-message-queue.js +43 -1
- package/lib/public/modules/permission-hints.js +13 -0
- package/lib/public/modules/profile.js +9 -9
- package/lib/public/modules/project-activation.js +49 -0
- package/lib/public/modules/project-logs-render.js +13 -3
- package/lib/public/modules/project-logs.js +45 -15
- package/lib/public/modules/right-workbench.js +44 -0
- package/lib/public/modules/scheduled-message-state.js +12 -36
- package/lib/public/modules/scheduled-result-client.js +33 -0
- package/lib/public/modules/scheduled-result-display.js +50 -0
- package/lib/public/modules/scheduled-result-notification.js +31 -0
- package/lib/public/modules/scheduled-task-presentation.js +15 -0
- package/lib/public/modules/scheduled-tasks.js +9 -21
- package/lib/public/modules/sidebar-mobile.js +51 -3
- package/lib/public/modules/sidebar-sessions.js +65 -5
- package/lib/public/modules/split-session-boundary.js +28 -0
- package/lib/public/modules/split-view.js +42 -7
- package/lib/public/modules/sticky-notes-browser.js +130 -18
- package/lib/public/modules/sticky-notes.js +2 -3
- package/lib/public/modules/terminal.js +4 -4
- package/lib/public/modules/thinking-lifecycle.js +4 -1
- package/lib/public/modules/tools.js +5 -1
- package/lib/scheduled-run-state.js +10 -2
- package/lib/scheduler.js +1 -0
- package/lib/sdk-bridge.js +97 -61
- package/lib/sdk-message-processor.js +44 -13
- package/lib/sdk-skill-discovery.js +6 -28
- package/lib/sdk-turn-queue.js +14 -0
- package/lib/server-context-view.js +53 -0
- package/lib/server-default-vendor.js +60 -0
- package/lib/server.js +23 -6
- package/lib/session-notes-mcp-server.js +3 -5
- package/lib/session-pair-factory.js +2 -2
- package/lib/session-pair-mcp-server.js +5 -4
- package/lib/session-title-generator.js +89 -0
- package/lib/session-title-policy.js +8 -2
- package/lib/sessions.js +26 -1
- package/lib/user-presence.js +6 -0
- package/lib/users-context-view-preferences.js +65 -0
- package/lib/users-default-vendor-preferences.js +80 -0
- package/lib/users.js +12 -0
- package/lib/ws-schema.js +11 -0
- package/lib/yoke/acp-agent-profiles.js +15 -2
- package/lib/yoke/adapters/acp.js +3 -2
- package/lib/yoke/adapters/antigravity.js +2 -2
- package/lib/yoke/adapters/claude-worker.js +11 -12
- package/lib/yoke/adapters/claude.js +75 -22
- package/lib/yoke/adapters/codex.js +216 -29
- package/lib/yoke/adapters/kiro.js +6 -5
- package/lib/yoke/claude-context-usage.js +57 -0
- package/lib/yoke/claude-message-correlation.js +31 -0
- package/lib/yoke/claude-model-switch.js +15 -0
- package/lib/yoke/index.js +5 -1
- package/lib/yoke/mcp-bridge-server.js +18 -5
- package/lib/yoke/session-skill-catalog.js +45 -0
- package/lib/yoke/skill-discovery.js +60 -20
- package/lib/yoke/vendor-registry.js +1 -1
- package/package.json +5 -7
|
@@ -3,10 +3,6 @@
|
|
|
3
3
|
|
|
4
4
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
5
5
|
import { store } from './store.js';
|
|
6
|
-
import { scheduledMessageControl } from './scheduled-message-state.js';
|
|
7
|
-
import { getWs } from './ws-ref.js';
|
|
8
|
-
import { addToMessages, scrollToBottom } from './app-rendering.js';
|
|
9
|
-
import { userAvatarUrl } from './avatar.js';
|
|
10
6
|
import { setScheduleDelayMs, clearScheduleDelay } from './input.js';
|
|
11
7
|
|
|
12
8
|
// --- Module-owned state ---
|
|
@@ -17,8 +13,7 @@ var rateLimitResetTimer = null;
|
|
|
17
13
|
var rateLimitUsageEl = null;
|
|
18
14
|
var rateLimitResetState = {};
|
|
19
15
|
var rateLimitTickTimer = null;
|
|
20
|
-
var
|
|
21
|
-
var scheduledCountdownTimer = null;
|
|
16
|
+
var rateLimitPopoverTimers = [];
|
|
22
17
|
var fastModeIndicatorEl = null;
|
|
23
18
|
|
|
24
19
|
// --- Internal helpers ---
|
|
@@ -31,7 +26,7 @@ function getVendorUsageMeta(vendor) {
|
|
|
31
26
|
codex: {
|
|
32
27
|
icon: "/codex-avatar.png",
|
|
33
28
|
alt: "Codex",
|
|
34
|
-
href: "https://chatgpt.com/
|
|
29
|
+
href: "https://chatgpt.com/codex/settings/usage",
|
|
35
30
|
title: "Check usage on ChatGPT",
|
|
36
31
|
},
|
|
37
32
|
claude: {
|
|
@@ -41,7 +36,7 @@ function getVendorUsageMeta(vendor) {
|
|
|
41
36
|
title: "Check usage on claude.ai",
|
|
42
37
|
},
|
|
43
38
|
};
|
|
44
|
-
return fallbacks[vendor] ||
|
|
39
|
+
return fallbacks[vendor] || null;
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
function vendorTracksRateLimits(vendor) {
|
|
@@ -57,6 +52,37 @@ function vendorSupportsScheduledMessages(vendor) {
|
|
|
57
52
|
return scheduledMessageVendors.indexOf(vendor) !== -1;
|
|
58
53
|
}
|
|
59
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
|
+
|
|
60
86
|
function rateLimitTypeLabel(type) {
|
|
61
87
|
if (!type) return "Usage";
|
|
62
88
|
var labels = {
|
|
@@ -114,13 +140,15 @@ function updateRateLimitIndicator(msg) {
|
|
|
114
140
|
var isRejected = msg.status === "rejected";
|
|
115
141
|
var pillClass = "header-rate-limit" + (isRejected ? " rejected" : " warning");
|
|
116
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>";
|
|
117
147
|
rateLimitIndicatorEl.innerHTML =
|
|
118
148
|
'<span class="' + pillClass + '">' +
|
|
119
149
|
iconHtml("alert-triangle") +
|
|
120
150
|
'<span class="header-pill-text">' + label + "</span>" +
|
|
121
|
-
|
|
122
|
-
iconHtml("external-link") +
|
|
123
|
-
"</a>" +
|
|
151
|
+
link +
|
|
124
152
|
"</span>";
|
|
125
153
|
refreshIcons();
|
|
126
154
|
}
|
|
@@ -137,10 +165,12 @@ function showRateLimitPopover(text, isRejected) {
|
|
|
137
165
|
rateLimitIndicatorEl.appendChild(pop);
|
|
138
166
|
|
|
139
167
|
// Auto-dismiss after 5s
|
|
140
|
-
setTimeout(function () {
|
|
168
|
+
var fadeTimer = setTimeout(function () {
|
|
141
169
|
pop.classList.add("fade-out");
|
|
142
|
-
setTimeout(function () { if (pop.parentNode) pop.remove(); }, 300);
|
|
170
|
+
var removeTimer = setTimeout(function () { if (pop.parentNode) pop.remove(); }, 300);
|
|
171
|
+
rateLimitPopoverTimers.push(removeTimer);
|
|
143
172
|
}, 5000);
|
|
173
|
+
rateLimitPopoverTimers.push(fadeTimer);
|
|
144
174
|
}
|
|
145
175
|
|
|
146
176
|
function clearRateLimitIndicator() {
|
|
@@ -174,22 +204,81 @@ function tickRateLimitUsage() {
|
|
|
174
204
|
if (!rateLimitUsageEl) return;
|
|
175
205
|
var parts = [];
|
|
176
206
|
var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
207
|
+
var nextState = Object.assign({}, rateLimitResetState);
|
|
208
|
+
var changed = false;
|
|
177
209
|
for (var i = 0; i < types.length; i++) {
|
|
178
210
|
var entry = rateLimitResetState[types[i]];
|
|
179
211
|
if (!entry || !entry.resetsAt) continue;
|
|
180
212
|
var timeStr = formatResetTime(entry.resetsAt);
|
|
181
|
-
if (!timeStr) { delete
|
|
213
|
+
if (!timeStr) { delete nextState[types[i]]; changed = true; continue; }
|
|
182
214
|
parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
|
|
183
215
|
}
|
|
216
|
+
if (changed) setActiveRateLimitState(nextState);
|
|
184
217
|
if (parts.length === 0) {
|
|
185
|
-
|
|
186
|
-
|
|
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
|
+
}
|
|
187
231
|
if (rateLimitTickTimer) { clearInterval(rateLimitTickTimer); rateLimitTickTimer = null; }
|
|
188
232
|
return;
|
|
189
233
|
}
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
}
|
|
192
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
|
+
}
|
|
193
282
|
}
|
|
194
283
|
|
|
195
284
|
// --- Exported functions ---
|
|
@@ -199,15 +288,32 @@ export function initRateLimit() {
|
|
|
199
288
|
if (state.currentVendor !== prev.currentVendor && state.currentVendor) {
|
|
200
289
|
if (!vendorSupportsScheduledMessages(state.currentVendor)) clearScheduleDelay();
|
|
201
290
|
}
|
|
202
|
-
if (state.currentVendor !== prev.currentVendor || state.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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();
|
|
206
298
|
}
|
|
207
299
|
});
|
|
208
300
|
}
|
|
209
301
|
|
|
210
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
|
+
}
|
|
211
317
|
var isRejected = msg.status === "rejected";
|
|
212
318
|
var typeLabel = rateLimitTypeLabel(msg.rateLimitType);
|
|
213
319
|
var popoverText = "";
|
|
@@ -232,8 +338,12 @@ export function handleRateLimitEvent(msg) {
|
|
|
232
338
|
rateLimitResetTimer = setTimeout(function () {
|
|
233
339
|
rateLimitResetsAt = null;
|
|
234
340
|
rateLimitResetTimer = null;
|
|
341
|
+
var currentState = Object.assign({}, getActiveRateLimitState());
|
|
342
|
+
delete currentState[msg.rateLimitType];
|
|
343
|
+
setActiveRateLimitState(currentState);
|
|
235
344
|
// Clear schedule mode when rate limit resets
|
|
236
345
|
clearScheduleDelay();
|
|
346
|
+
renderRateLimitUsage();
|
|
237
347
|
}, msg.resetsAt - Date.now() + 1000);
|
|
238
348
|
} else {
|
|
239
349
|
var pct = msg.utilization ? Math.round(msg.utilization * 100) : null;
|
|
@@ -245,230 +355,19 @@ export function handleRateLimitEvent(msg) {
|
|
|
245
355
|
}
|
|
246
356
|
|
|
247
357
|
export function updateRateLimitUsage(msg) {
|
|
358
|
+
if (!rateLimitEventAppliesToPane(msg, store.snap())) return;
|
|
248
359
|
var activeVendor = store.get('currentVendor') || "claude";
|
|
249
360
|
if (!vendorTracksRateLimits(activeVendor)) {
|
|
250
|
-
|
|
361
|
+
renderRateLimitUsage();
|
|
251
362
|
return;
|
|
252
363
|
}
|
|
253
364
|
if (msg.rateLimitType && msg.resetsAt) {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
var topBarActions = document.querySelector("#top-bar .top-bar-actions");
|
|
258
|
-
if (!topBarActions) return;
|
|
259
|
-
|
|
260
|
-
if (!rateLimitUsageEl) {
|
|
261
|
-
rateLimitUsageEl = document.createElement("a");
|
|
262
|
-
rateLimitUsageEl.id = "rate-limit-usage-link";
|
|
263
|
-
rateLimitUsageEl.className = "top-bar-pill pill-dim usage-check-link";
|
|
264
|
-
rateLimitUsageEl.target = "_blank";
|
|
265
|
-
rateLimitUsageEl.rel = "noopener";
|
|
266
|
-
var ref = document.getElementById("skip-perms-pill");
|
|
267
|
-
topBarActions.insertBefore(rateLimitUsageEl, ref);
|
|
365
|
+
var nextState = Object.assign({}, getActiveRateLimitState());
|
|
366
|
+
nextState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
|
|
367
|
+
setActiveRateLimitState(nextState);
|
|
268
368
|
}
|
|
269
|
-
rateLimitUsageEl.style.display = "";
|
|
270
369
|
|
|
271
|
-
|
|
272
|
-
var parts = [];
|
|
273
|
-
var types = ["five_hour", "seven_day", "seven_day_opus", "seven_day_sonnet"];
|
|
274
|
-
for (var i = 0; i < types.length; i++) {
|
|
275
|
-
var entry = rateLimitResetState[types[i]];
|
|
276
|
-
if (!entry || !entry.resetsAt) continue;
|
|
277
|
-
var timeStr = formatResetTime(entry.resetsAt);
|
|
278
|
-
if (!timeStr) continue;
|
|
279
|
-
parts.push(rateLimitTypeShortLabel(types[i]) + " resets " + timeStr);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
|
|
283
|
-
var vendor = activeVendor;
|
|
284
|
-
var meta = getVendorUsageMeta(vendor);
|
|
285
|
-
rateLimitUsageEl.href = meta.href;
|
|
286
|
-
rateLimitUsageEl.title = meta.title;
|
|
287
|
-
rateLimitUsageEl.innerHTML =
|
|
288
|
-
'<img src="' + meta.icon + '" class="usage-check-vendor-icon" alt="' + meta.alt + '">' +
|
|
289
|
-
'<span>' + label + '</span>' +
|
|
290
|
-
iconHtml("external-link");
|
|
291
|
-
refreshIcons();
|
|
292
|
-
|
|
293
|
-
// Start or stop live countdown tick
|
|
294
|
-
if (parts.length > 0 && !rateLimitTickTimer) {
|
|
295
|
-
rateLimitTickTimer = setInterval(tickRateLimitUsage, 30000);
|
|
296
|
-
} else if (parts.length === 0 && rateLimitTickTimer) {
|
|
297
|
-
clearInterval(rateLimitTickTimer);
|
|
298
|
-
rateLimitTickTimer = null;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
export function addScheduledMessageBubble(text, resetsAt, correlation) {
|
|
303
|
-
removeScheduledMessageBubble();
|
|
304
|
-
var isChannel = document.body.classList.contains("wide-view");
|
|
305
|
-
var wrap = document.createElement("div");
|
|
306
|
-
wrap.className = "msg-user scheduled-msg-wrap";
|
|
307
|
-
wrap.id = "scheduled-msg-bubble";
|
|
308
|
-
|
|
309
|
-
var countdownEl;
|
|
310
|
-
var cancelBtn;
|
|
311
|
-
|
|
312
|
-
if (isChannel) {
|
|
313
|
-
// Channel mode: avatar + header with scheduled badge + message
|
|
314
|
-
var _me = store.get('cachedAllUsers').find(function (u) { return u.id === store.get('myUserId'); });
|
|
315
|
-
var _myName = document.body.dataset.myDisplayName || (_me && (_me.displayName || _me.username)) || "Me";
|
|
316
|
-
|
|
317
|
-
var avi = document.createElement("img");
|
|
318
|
-
avi.className = "dm-bubble-avatar dm-bubble-avatar-me";
|
|
319
|
-
avi.src = document.body.dataset.myAvatarUrl || userAvatarUrl(_me || { id: store.get('myUserId') }, 36);
|
|
320
|
-
wrap.appendChild(avi);
|
|
321
|
-
|
|
322
|
-
var content = document.createElement("div");
|
|
323
|
-
content.className = "dm-bubble-content";
|
|
324
|
-
|
|
325
|
-
var header = document.createElement("div");
|
|
326
|
-
header.className = "dm-bubble-header";
|
|
327
|
-
|
|
328
|
-
var nameSpan = document.createElement("span");
|
|
329
|
-
nameSpan.className = "dm-bubble-name";
|
|
330
|
-
nameSpan.textContent = _myName;
|
|
331
|
-
header.appendChild(nameSpan);
|
|
332
|
-
|
|
333
|
-
var badge = document.createElement("span");
|
|
334
|
-
badge.className = "scheduled-msg-badge";
|
|
335
|
-
badge.innerHTML = iconHtml("clock");
|
|
336
|
-
countdownEl = document.createElement("span");
|
|
337
|
-
countdownEl.className = "scheduled-msg-countdown";
|
|
338
|
-
badge.appendChild(countdownEl);
|
|
339
|
-
header.appendChild(badge);
|
|
340
|
-
|
|
341
|
-
var actions = document.createElement("span");
|
|
342
|
-
actions.className = "scheduled-msg-actions";
|
|
343
|
-
|
|
344
|
-
var sendNowBtn = document.createElement("button");
|
|
345
|
-
sendNowBtn.className = "scheduled-msg-send-now";
|
|
346
|
-
sendNowBtn.textContent = "Send now";
|
|
347
|
-
sendNowBtn.addEventListener("click", function () {
|
|
348
|
-
var ws = getWs();
|
|
349
|
-
if (ws && ws.readyState === 1) {
|
|
350
|
-
ws.send(JSON.stringify(scheduledMessageControl("send_scheduled_now", correlation)));
|
|
351
|
-
}
|
|
352
|
-
});
|
|
353
|
-
actions.appendChild(sendNowBtn);
|
|
354
|
-
|
|
355
|
-
var sep = document.createElement("span");
|
|
356
|
-
sep.className = "scheduled-msg-sep";
|
|
357
|
-
sep.textContent = "\u00b7";
|
|
358
|
-
actions.appendChild(sep);
|
|
359
|
-
|
|
360
|
-
cancelBtn = document.createElement("button");
|
|
361
|
-
cancelBtn.className = "scheduled-msg-cancel";
|
|
362
|
-
cancelBtn.textContent = "Cancel";
|
|
363
|
-
cancelBtn.addEventListener("click", function () {
|
|
364
|
-
var ws = getWs();
|
|
365
|
-
if (ws && ws.readyState === 1) {
|
|
366
|
-
ws.send(JSON.stringify(scheduledMessageControl("cancel_scheduled_message", correlation)));
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
actions.appendChild(cancelBtn);
|
|
370
|
-
|
|
371
|
-
header.appendChild(actions);
|
|
372
|
-
|
|
373
|
-
content.appendChild(header);
|
|
374
|
-
|
|
375
|
-
var bubble = document.createElement("div");
|
|
376
|
-
bubble.className = "bubble scheduled-msg-bubble";
|
|
377
|
-
var textEl = document.createElement("span");
|
|
378
|
-
textEl.textContent = text;
|
|
379
|
-
bubble.appendChild(textEl);
|
|
380
|
-
content.appendChild(bubble);
|
|
381
|
-
|
|
382
|
-
wrap.appendChild(content);
|
|
383
|
-
} else {
|
|
384
|
-
// Bubble mode: original layout
|
|
385
|
-
var bubble = document.createElement("div");
|
|
386
|
-
bubble.className = "bubble scheduled-msg-bubble";
|
|
387
|
-
|
|
388
|
-
var textEl = document.createElement("span");
|
|
389
|
-
textEl.textContent = text;
|
|
390
|
-
bubble.appendChild(textEl);
|
|
391
|
-
|
|
392
|
-
var metaEl = document.createElement("div");
|
|
393
|
-
metaEl.className = "scheduled-msg-meta";
|
|
394
|
-
|
|
395
|
-
var clockIcon = document.createElement("span");
|
|
396
|
-
clockIcon.className = "scheduled-msg-icon";
|
|
397
|
-
clockIcon.innerHTML = iconHtml("clock");
|
|
398
|
-
metaEl.appendChild(clockIcon);
|
|
399
|
-
|
|
400
|
-
countdownEl = document.createElement("span");
|
|
401
|
-
countdownEl.className = "scheduled-msg-countdown";
|
|
402
|
-
metaEl.appendChild(countdownEl);
|
|
403
|
-
|
|
404
|
-
var sendNowBtn2 = document.createElement("button");
|
|
405
|
-
sendNowBtn2.className = "scheduled-msg-send-now";
|
|
406
|
-
sendNowBtn2.textContent = "Send now";
|
|
407
|
-
sendNowBtn2.addEventListener("click", function () {
|
|
408
|
-
var ws = getWs();
|
|
409
|
-
if (ws && ws.readyState === 1) {
|
|
410
|
-
ws.send(JSON.stringify(scheduledMessageControl("send_scheduled_now", correlation)));
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
metaEl.appendChild(sendNowBtn2);
|
|
414
|
-
|
|
415
|
-
var sep2 = document.createElement("span");
|
|
416
|
-
sep2.className = "scheduled-msg-sep";
|
|
417
|
-
sep2.textContent = "\u00b7";
|
|
418
|
-
metaEl.appendChild(sep2);
|
|
419
|
-
|
|
420
|
-
cancelBtn = document.createElement("button");
|
|
421
|
-
cancelBtn.className = "scheduled-msg-cancel";
|
|
422
|
-
cancelBtn.textContent = "Cancel";
|
|
423
|
-
cancelBtn.addEventListener("click", function () {
|
|
424
|
-
var ws = getWs();
|
|
425
|
-
if (ws && ws.readyState === 1) {
|
|
426
|
-
ws.send(JSON.stringify(scheduledMessageControl("cancel_scheduled_message", correlation)));
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
metaEl.appendChild(cancelBtn);
|
|
430
|
-
|
|
431
|
-
wrap.appendChild(bubble);
|
|
432
|
-
wrap.appendChild(metaEl);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
addToMessages(wrap);
|
|
436
|
-
scheduledMsgEl = wrap;
|
|
437
|
-
scrollToBottom();
|
|
438
|
-
|
|
439
|
-
// Start countdown
|
|
440
|
-
function updateCountdown() {
|
|
441
|
-
var remaining = resetsAt - Date.now();
|
|
442
|
-
if (remaining <= 0) {
|
|
443
|
-
countdownEl.textContent = "Sending...";
|
|
444
|
-
if (scheduledCountdownTimer) { clearInterval(scheduledCountdownTimer); scheduledCountdownTimer = null; }
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
var hrs = Math.floor(remaining / 3600000);
|
|
448
|
-
var mins = Math.floor((remaining % 3600000) / 60000);
|
|
449
|
-
var secs = Math.floor((remaining % 60000) / 1000);
|
|
450
|
-
var timeStr = "";
|
|
451
|
-
if (hrs > 0) timeStr += hrs + "h ";
|
|
452
|
-
if (mins > 0 || hrs > 0) timeStr += mins + "m ";
|
|
453
|
-
timeStr += secs + "s";
|
|
454
|
-
|
|
455
|
-
var sendDate = new Date(resetsAt);
|
|
456
|
-
var absTime = sendDate.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
457
|
-
countdownEl.textContent = "Sends at " + absTime + " (" + timeStr + ")";
|
|
458
|
-
}
|
|
459
|
-
updateCountdown();
|
|
460
|
-
scheduledCountdownTimer = setInterval(updateCountdown, 1000);
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function removeScheduledMessageBubble() {
|
|
464
|
-
if (scheduledMsgEl) {
|
|
465
|
-
scheduledMsgEl.remove();
|
|
466
|
-
scheduledMsgEl = null;
|
|
467
|
-
}
|
|
468
|
-
if (scheduledCountdownTimer) {
|
|
469
|
-
clearInterval(scheduledCountdownTimer);
|
|
470
|
-
scheduledCountdownTimer = null;
|
|
471
|
-
}
|
|
370
|
+
renderRateLimitUsage();
|
|
472
371
|
}
|
|
473
372
|
|
|
474
373
|
export function handleFastModeState(state) {
|
|
@@ -498,12 +397,18 @@ export function handleFastModeState(state) {
|
|
|
498
397
|
refreshIcons();
|
|
499
398
|
}
|
|
500
399
|
|
|
501
|
-
export function getScheduledMsgEl() { return scheduledMsgEl; }
|
|
502
|
-
|
|
503
400
|
export function resetRateLimitState() {
|
|
504
401
|
clearRateLimitIndicator();
|
|
505
|
-
|
|
506
|
-
|
|
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; }
|
|
507
408
|
if (rateLimitCountdownTimer) { clearInterval(rateLimitCountdownTimer); rateLimitCountdownTimer = null; }
|
|
409
|
+
rateLimitResetState = getActiveRateLimitState();
|
|
410
|
+
renderRateLimitUsage();
|
|
508
411
|
if (fastModeIndicatorEl) { fastModeIndicatorEl.remove(); fastModeIndicatorEl = null; }
|
|
509
412
|
}
|
|
413
|
+
|
|
414
|
+
export { rateLimitEventAppliesToPane, getVendorUsageMeta };
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Local pixel identities: deterministic SVG avatars derived from a stable seed.
|
|
2
2
|
|
|
3
3
|
var PAPER = "#f1efe8";
|
|
4
4
|
var INK = "#1b1b1a";
|
|
5
|
-
var LIGHT_BASE = "#e7e5de";
|
|
6
|
-
var LIGHT_FIELD = "#333330";
|
|
7
5
|
var MUTED_INDIGO = "#72778f";
|
|
6
|
+
var IDENTICON_PALETTE = ["#333330", MUTED_INDIGO, "#6f8382", "#887b91", "#8a806a"];
|
|
8
7
|
var MATE_MARK_PALETTE = [
|
|
9
8
|
{ background: "#6f7489", foreground: PAPER },
|
|
10
9
|
{ background: "#78816c", foreground: PAPER },
|
|
@@ -24,25 +23,6 @@ function hashString(value) {
|
|
|
24
23
|
return hash >>> 0;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
function makeRandom(seed) {
|
|
28
|
-
var state = seed >>> 0;
|
|
29
|
-
return function nextRandom() {
|
|
30
|
-
state += 0x6d2b79f5;
|
|
31
|
-
var value = state;
|
|
32
|
-
value = Math.imul(value ^ (value >>> 15), value | 1);
|
|
33
|
-
value ^= value + Math.imul(value ^ (value >>> 7), value | 61);
|
|
34
|
-
return ((value ^ (value >>> 14)) >>> 0) / 4294967296;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function between(random, min, max) {
|
|
39
|
-
return min + random() * (max - min);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function number(value) {
|
|
43
|
-
return Math.round(value * 100) / 100;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
26
|
function xmlText(value) {
|
|
47
27
|
return String(value).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
48
28
|
}
|
|
@@ -54,62 +34,37 @@ function legacyIdentity(style, seed) {
|
|
|
54
34
|
return "legacy:" + sourceStyle + ":" + sourceSeed;
|
|
55
35
|
}
|
|
56
36
|
|
|
57
|
-
function curvePath(y, bow, endOffset) {
|
|
58
|
-
return "M -28 " + number(y) +
|
|
59
|
-
" C 17 " + number(y + bow) +
|
|
60
|
-
", 71 " + number(y - bow) +
|
|
61
|
-
", 124 " + number(y + endOffset);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function cutPath(random) {
|
|
65
|
-
var start = between(random, 37, 52);
|
|
66
|
-
var first = between(random, 14, 84);
|
|
67
|
-
var second = between(random, 12, 86);
|
|
68
|
-
var end = between(random, 40, 64);
|
|
69
|
-
return "M -22 " + number(start) + " C 21 " + number(first) + ", 73 " + number(second) + ", 118 " + number(end);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
37
|
export function imprintSvg(options) {
|
|
73
38
|
var settings = options || {};
|
|
74
39
|
var size = Math.max(12, Math.min(256, Number(settings.size) || 64));
|
|
75
40
|
var identity = legacyIdentity(settings.style, settings.seed);
|
|
76
41
|
var hash = hashString(identity);
|
|
77
|
-
var
|
|
78
|
-
var
|
|
79
|
-
var
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
var firstBow = between(random, -34, 34);
|
|
89
|
-
var secondBow = between(random, -32, 32);
|
|
90
|
-
var firstPath = curvePath(firstY, firstBow, 6);
|
|
91
|
-
var secondPath = curvePath(secondY, -secondBow, -5);
|
|
92
|
-
var contours = "";
|
|
42
|
+
var cells = identiconCells(hash);
|
|
43
|
+
var foreground = IDENTICON_PALETTE[hash % IDENTICON_PALETTE.length];
|
|
44
|
+
var pixels = "";
|
|
45
|
+
for (var i = 0; i < cells.length; i++) {
|
|
46
|
+
pixels += '<rect x="' + (cells[i][0] + 1) + '" y="' + (cells[i][1] + 1) + '" width="1" height="1"/>';
|
|
47
|
+
}
|
|
48
|
+
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + size +
|
|
49
|
+
'" viewBox="0 0 7 7" shape-rendering="crispEdges"><rect width="7" height="7" fill="' + PAPER +
|
|
50
|
+
'"/><g fill="' + foreground + '">' + pixels + '</g><rect x="0.5" y="0.5" width="6" height="6" fill="none" stroke="' +
|
|
51
|
+
INK + '" stroke-opacity="0.14" stroke-width="0.08"/></svg>';
|
|
52
|
+
}
|
|
93
53
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
54
|
+
// Generate the left three columns and mirror them into a compact 5x5 grid.
|
|
55
|
+
export function identiconCells(hash) {
|
|
56
|
+
var value = hash >>> 0;
|
|
57
|
+
var cells = [];
|
|
58
|
+
for (var row = 0; row < 5; row++) {
|
|
59
|
+
for (var column = 0; column < 3; column++) {
|
|
60
|
+
value = Math.imul(value ^ (value >>> 13), 16777619) >>> 0;
|
|
61
|
+
if ((value & 1) || (row === 2 && column === 1)) {
|
|
62
|
+
cells.push([column, row]);
|
|
63
|
+
if (column < 2) cells.push([4 - column, row]);
|
|
64
|
+
}
|
|
99
65
|
}
|
|
100
66
|
}
|
|
101
|
-
|
|
102
|
-
var clipId = "imprint-" + hash.toString(16);
|
|
103
|
-
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + size + '" height="' + size +
|
|
104
|
-
'" viewBox="0 0 96 96"><defs><clipPath id="' + clipId +
|
|
105
|
-
'"><rect width="96" height="96" rx="24"/></clipPath></defs><g clip-path="url(#' + clipId +
|
|
106
|
-
')"><rect width="96" height="96" fill="' + base + '"/><g transform="rotate(' + rotation +
|
|
107
|
-
' 48 48)"><path d="' + firstPath + '" fill="none" stroke="' + field + '" stroke-width="' +
|
|
108
|
-
number(bandWidth) + '" stroke-linecap="square"/><path d="' + secondPath + '" fill="none" stroke="' +
|
|
109
|
-
accent + '" stroke-width="' + number(secondaryWidth) + '" stroke-linecap="square"/><path d="' +
|
|
110
|
-
cutPath(random) + '" fill="none" stroke="' + base + '" stroke-width="' + number(cutWidth) +
|
|
111
|
-
'" stroke-linecap="square"/>' + contours + '</g></g><rect x="0.5" y="0.5" width="95" height="95" rx="23.5" fill="none" stroke="' +
|
|
112
|
-
(darkBase ? "#000000" : INK) + '" stroke-opacity="0.16"/></svg>';
|
|
67
|
+
return cells;
|
|
113
68
|
}
|
|
114
69
|
|
|
115
70
|
export function imprintDataUrl(options) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Centralized
|
|
1
|
+
// Centralized local avatar URL builder for people and Mates.
|
|
2
2
|
import { imprintDataUrl, mateMarkDataUrl } from './avatar-imprint.js';
|
|
3
3
|
|
|
4
4
|
var imprintCache = new Map();
|
|
5
5
|
var mateMarkCache = new Map();
|
|
6
6
|
|
|
7
7
|
// Legacy style remains part of the identity hash so existing selections map
|
|
8
|
-
// deterministically into the
|
|
8
|
+
// deterministically into the local pixel identity system.
|
|
9
9
|
export function avatarUrl(style, seed, size) {
|
|
10
10
|
var cacheKey = [style || 'imprint', seed || 'anonymous', size || 64].join('|');
|
|
11
11
|
if (imprintCache.has(cacheKey)) return imprintCache.get(cacheKey);
|
|
@@ -15,12 +15,20 @@ export function avatarUrl(style, seed, size) {
|
|
|
15
15
|
return url;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// Use the same bounded account identity for fresh fallbacks across every surface.
|
|
19
|
+
export function userAvatarSeed(user) {
|
|
20
|
+
var profile = user && user.profile ? user.profile : user;
|
|
21
|
+
var savedSeed = profile && profile.avatarSeed || user && user.avatarSeed;
|
|
22
|
+
if (savedSeed) return String(savedSeed);
|
|
23
|
+
var identity = profile && (profile.username || profile.id || profile.userId) || user && (user.username || user.id || user.userId) || 'anonymous';
|
|
24
|
+
return String(identity).substring(0, 30);
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
// Build avatar URL for a user object, preferring custom avatar if set.
|
|
19
28
|
export function userAvatarUrl(user, size) {
|
|
20
29
|
if (user && user.avatarCustom) return user.avatarCustom;
|
|
21
30
|
var style = (user && user.avatarStyle) || 'imprint';
|
|
22
|
-
|
|
23
|
-
return avatarUrl(style, seed, size);
|
|
31
|
+
return avatarUrl(style, userAvatarSeed(user), size);
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
export function mateMarkUrl(seed, size) {
|