clay-server 4.1.0-beta.9 → 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.
Files changed (111) hide show
  1. package/lib/context-usage.js +47 -0
  2. package/lib/issues-mcp-server.js +1 -1
  3. package/lib/issues-service.js +3 -1
  4. package/lib/loop-registry-occurrence.js +1 -0
  5. package/lib/notes.js +20 -3
  6. package/lib/os-users.js +6 -0
  7. package/lib/project-connection.js +14 -3
  8. package/lib/project-file-path.js +13 -0
  9. package/lib/project-file-watch.js +19 -8
  10. package/lib/project-filesystem.js +10 -16
  11. package/lib/project-http.js +53 -48
  12. package/lib/project-logs-mcp-server.js +8 -8
  13. package/lib/project-logs-schema.js +1 -1
  14. package/lib/project-logs-service.js +46 -13
  15. package/lib/project-logs-store.js +17 -15
  16. package/lib/project-logs.js +70 -0
  17. package/lib/project-loop.js +1 -0
  18. package/lib/project-notifications.js +1 -1
  19. package/lib/project-scheduled-result-store.js +131 -0
  20. package/lib/project-scheduled-task-execution.js +35 -2
  21. package/lib/project-session-notes.js +11 -4
  22. package/lib/project-sessions.js +7 -0
  23. package/lib/project-user-message.js +47 -0
  24. package/lib/project.js +53 -3
  25. package/lib/public/app.js +26 -9
  26. package/lib/public/css/avatar-imprints.css +1 -1
  27. package/lib/public/css/command-palette.css +20 -0
  28. package/lib/public/css/git-panel.css +13 -12
  29. package/lib/public/css/git-placard.css +4 -4
  30. package/lib/public/css/issues.css +78 -31
  31. package/lib/public/css/mobile-nav.css +7 -0
  32. package/lib/public/css/notifications-center.css +3 -3
  33. package/lib/public/css/project-logs.css +15 -0
  34. package/lib/public/css/sidebar.css +7 -0
  35. package/lib/public/css/sticky-notes.css +29 -0
  36. package/lib/public/modules/app-connection.js +9 -1
  37. package/lib/public/modules/app-favicon.js +10 -0
  38. package/lib/public/modules/app-messages.js +43 -12
  39. package/lib/public/modules/app-notifications.js +2 -0
  40. package/lib/public/modules/app-panels.js +77 -23
  41. package/lib/public/modules/app-projects.js +2 -0
  42. package/lib/public/modules/app-rate-limit.js +148 -61
  43. package/lib/public/modules/avatar-imprint.js +25 -70
  44. package/lib/public/modules/avatar.js +12 -4
  45. package/lib/public/modules/chat-render-runtime.js +20 -0
  46. package/lib/public/modules/clay-issue-links.js +23 -2
  47. package/lib/public/modules/clay-log-links.js +11 -1
  48. package/lib/public/modules/context-view-preference.js +143 -0
  49. package/lib/public/modules/default-vendor.js +116 -0
  50. package/lib/public/modules/filebrowser-tabs.js +1 -0
  51. package/lib/public/modules/filebrowser.js +45 -18
  52. package/lib/public/modules/issues-render.js +36 -10
  53. package/lib/public/modules/issues.js +95 -13
  54. package/lib/public/modules/markdown.js +3 -1
  55. package/lib/public/modules/pane-bridge.js +14 -8
  56. package/lib/public/modules/pane-issue-bridge.js +27 -0
  57. package/lib/public/modules/permission-hints.js +13 -0
  58. package/lib/public/modules/profile.js +9 -9
  59. package/lib/public/modules/project-activation.js +49 -0
  60. package/lib/public/modules/project-logs-render.js +13 -3
  61. package/lib/public/modules/project-logs.js +45 -15
  62. package/lib/public/modules/right-workbench.js +44 -0
  63. package/lib/public/modules/scheduled-result-client.js +33 -0
  64. package/lib/public/modules/scheduled-result-display.js +50 -0
  65. package/lib/public/modules/scheduled-result-notification.js +31 -0
  66. package/lib/public/modules/scheduled-task-presentation.js +15 -0
  67. package/lib/public/modules/scheduled-tasks.js +9 -21
  68. package/lib/public/modules/sidebar-mobile.js +51 -3
  69. package/lib/public/modules/sidebar-sessions.js +65 -5
  70. package/lib/public/modules/split-session-boundary.js +28 -0
  71. package/lib/public/modules/split-view.js +42 -7
  72. package/lib/public/modules/sticky-notes-browser.js +130 -18
  73. package/lib/public/modules/sticky-notes.js +2 -3
  74. package/lib/public/modules/terminal.js +4 -4
  75. package/lib/public/modules/thinking-lifecycle.js +4 -1
  76. package/lib/public/modules/tools.js +5 -1
  77. package/lib/scheduled-run-state.js +10 -2
  78. package/lib/scheduler.js +1 -0
  79. package/lib/sdk-bridge.js +97 -61
  80. package/lib/sdk-message-processor.js +44 -13
  81. package/lib/sdk-skill-discovery.js +6 -28
  82. package/lib/sdk-turn-queue.js +14 -0
  83. package/lib/server-context-view.js +53 -0
  84. package/lib/server-default-vendor.js +60 -0
  85. package/lib/server.js +23 -6
  86. package/lib/session-notes-mcp-server.js +3 -5
  87. package/lib/session-pair-factory.js +2 -2
  88. package/lib/session-title-generator.js +89 -0
  89. package/lib/session-title-policy.js +8 -2
  90. package/lib/sessions.js +26 -1
  91. package/lib/user-presence.js +6 -0
  92. package/lib/users-context-view-preferences.js +65 -0
  93. package/lib/users-default-vendor-preferences.js +80 -0
  94. package/lib/users.js +12 -0
  95. package/lib/ws-schema.js +11 -0
  96. package/lib/yoke/acp-agent-profiles.js +15 -2
  97. package/lib/yoke/adapters/acp.js +3 -2
  98. package/lib/yoke/adapters/antigravity.js +2 -2
  99. package/lib/yoke/adapters/claude-worker.js +11 -12
  100. package/lib/yoke/adapters/claude.js +75 -22
  101. package/lib/yoke/adapters/codex.js +216 -29
  102. package/lib/yoke/adapters/kiro.js +6 -5
  103. package/lib/yoke/claude-context-usage.js +57 -0
  104. package/lib/yoke/claude-message-correlation.js +31 -0
  105. package/lib/yoke/claude-model-switch.js +15 -0
  106. package/lib/yoke/index.js +5 -1
  107. package/lib/yoke/mcp-bridge-server.js +18 -5
  108. package/lib/yoke/session-skill-catalog.js +45 -0
  109. package/lib/yoke/skill-discovery.js +60 -20
  110. package/lib/yoke/vendor-registry.js +1 -1
  111. package/package.json +5 -7
@@ -5,15 +5,17 @@ import { refreshIcons } from "./icons.js";
5
5
  import { escapeHtml, showToast } from "./utils.js";
6
6
  import { store } from './store.js';
7
7
  import { getWs } from './ws-ref.js';
8
+ import { getContextView as getStoredContextView, getEffectiveContextView, setContextView as setStoredContextView } from './context-view-preference.js';
8
9
  import { reportPaneContext } from './pane-bridge.js';
9
10
  import { setupModelPicker, renderModelPicker, requestVendorModels, prepareModelPickerOpen, modelDisplayName } from './model-picker.js';
10
11
 
11
12
  // --- Module-owned state (not in store) ---
12
13
  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 };
14
+ 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
15
  var ctxPopoverEl = null;
15
16
  var ctxHoverTimer = null;
16
17
  var statusRefreshTimer = null;
18
+ var lastAppliedContextView = null;
17
19
 
18
20
  // --- DOM refs ---
19
21
  var configChipWrap = null;
@@ -374,6 +376,15 @@ export function initPanels() {
374
376
 
375
377
  // --- Reactive UI sync ---
376
378
  store.subscribe(function (state, prev) {
379
+ var effectiveContextView = getEffectiveContextView();
380
+ if (effectiveContextView !== lastAppliedContextView) {
381
+ lastAppliedContextView = effectiveContextView;
382
+ applyContextView(effectiveContextView);
383
+ }
384
+ if (state.contextViewPreferenceState && state.contextViewPreferenceState.error &&
385
+ state.contextViewPreferenceState.error !== (prev.contextViewPreferenceState && prev.contextViewPreferenceState.error)) {
386
+ showToast(state.contextViewPreferenceState.error, "error");
387
+ }
377
388
  // richContextUsage changed -> update popover + panel
378
389
  if (state.richContextUsage !== prev.richContextUsage) {
379
390
  if (state.richContextUsage) {
@@ -540,7 +551,6 @@ export function initPanels() {
540
551
  if (contextPanelClose) {
541
552
  contextPanelClose.addEventListener("click", function () {
542
553
  setContextView("off");
543
- applyContextView("off");
544
554
  });
545
555
  }
546
556
 
@@ -553,7 +563,8 @@ export function initPanels() {
553
563
  }
554
564
 
555
565
  // Restore context view on load
556
- applyContextView(getContextView());
566
+ lastAppliedContextView = getEffectiveContextView();
567
+ applyContextView(lastAppliedContextView);
557
568
  }
558
569
 
559
570
  // --- Config chip ---
@@ -718,9 +729,18 @@ export function contextPctClass(pct) {
718
729
 
719
730
  export function updateContextPanel() {
720
731
  if (!contextUsedEl) return;
721
- // Context window usage = input tokens only (includes cache read/write)
722
- var used = contextData.input;
723
- var win = contextData.contextWindow;
732
+ // Context occupancy is separate from generic per-turn accounting. Codex
733
+ // only supplies currentInput from a verified last-turn snapshot; rich
734
+ // context usage is more authoritative when available.
735
+ var rich = store.get('richContextUsage');
736
+ var hasRich = rich && (
737
+ (typeof rich.totalTokens === "number" && typeof rich.maxTokens === "number" && rich.maxTokens > 0) ||
738
+ (typeof rich.input_tokens === "number" && typeof rich.contextWindow === "number" && rich.contextWindow > 0)
739
+ );
740
+ var hasCurrent = typeof contextData.currentInput === "number";
741
+ var hasContext = !!(hasRich || hasCurrent);
742
+ var used = hasRich ? (typeof rich.totalTokens === "number" ? rich.totalTokens : rich.input_tokens) : (hasCurrent ? contextData.currentInput : 0);
743
+ var win = hasRich ? (typeof rich.maxTokens === "number" ? rich.maxTokens : rich.contextWindow) : (hasCurrent ? contextData.contextWindow : 0);
724
744
  var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
725
745
  var cls = contextPctClass(pct);
726
746
  reportPaneContext({ pct: pct, used: used, win: win, cls: cls, model: contextData.model, cost: contextData.cost });
@@ -734,7 +754,7 @@ export function updateContextPanel() {
734
754
  contextMiniFill.className = "context-mini-fill" + cls;
735
755
  }
736
756
  if (contextMiniLabel) {
737
- contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
757
+ contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "-");
738
758
  }
739
759
  // Header bar. A split group has no meaningful group-level context usage:
740
760
  // the two members have separate windows, and the parent shell's own numbers
@@ -742,9 +762,9 @@ export function updateContextPanel() {
742
762
  // chip instead, so hide the title-bar gauge while a split is open.
743
763
  var splitOpen = !!store.get('splitPanes');
744
764
  var headerCtxEl = store.get('headerContextEl');
745
- if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen);
746
- if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen);
747
- if (pct > 0 && !splitOpen) {
765
+ if (headerCtxEl) headerCtxEl.classList.toggle("hidden", splitOpen || !hasContext);
766
+ if (contextMini) contextMini.classList.toggle("split-hidden", splitOpen || !hasContext);
767
+ if (hasContext && !splitOpen) {
748
768
  var statusArea = document.querySelector(".title-bar-content .status");
749
769
  var hCtxEl = store.get('headerContextEl');
750
770
  if (statusArea && !hCtxEl) {
@@ -808,7 +828,7 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
808
828
  // when available -- result.usage.input_tokens sums all API calls in a turn,
809
829
  // inflating context usage when tools are involved.
810
830
  // Falls back to the summed value for setups that don't emit message_start.
811
- if (lastStreamInputTokens) {
831
+ if (typeof lastStreamInputTokens === "number") {
812
832
  contextData.input = lastStreamInputTokens;
813
833
  } else {
814
834
  contextData.input = (usage.input_tokens || usage.inputTokens || 0)
@@ -817,6 +837,11 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
817
837
  contextData.output = usage.output_tokens || usage.outputTokens || 0;
818
838
  contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
819
839
  contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
840
+ if (store.get('currentVendor') === "codex") {
841
+ contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : null;
842
+ } else {
843
+ contextData.currentInput = typeof lastStreamInputTokens === "number" ? lastStreamInputTokens : contextData.input;
844
+ }
820
845
  }
821
846
  contextData.turns++;
822
847
  if (modelUsage) {
@@ -830,6 +855,21 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
830
855
  contextData.model = displayModel;
831
856
  contextData.contextWindow = resolveContextWindow(displayModel, mu.contextWindow);
832
857
  if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
858
+ if (store.get('currentVendor') === "codex") {
859
+ var snapshot = mu.contextSnapshot;
860
+ if (snapshot) store.set({ richContextUsage: null });
861
+ if (snapshot && snapshot.valid === true
862
+ && typeof snapshot.inputTokens === "number"
863
+ && typeof snapshot.contextWindow === "number"
864
+ && snapshot.contextWindow > 0) {
865
+ contextData.currentInput = snapshot.inputTokens;
866
+ contextData.contextWindow = snapshot.contextWindow;
867
+ } else {
868
+ // An explicit invalid marker is authoritative: do not retain an
869
+ // occupancy value from the previous live turn or history replay.
870
+ contextData.currentInput = null;
871
+ }
872
+ }
833
873
 
834
874
  // thinkingTokens and costBasis are per-model and optional: absent on
835
875
  // older CLI builds and, for a resumed session, on the turns that ran
@@ -860,50 +900,56 @@ export function accumulateContext(cost, usage, modelUsage, lastStreamInputTokens
860
900
 
861
901
  // contextView: "off" | "mini" | "panel"
862
902
  export function getContextView() {
863
- try { return localStorage.getItem("clay-context-view") || "off"; } catch (e) { return "off"; }
903
+ return getStoredContextView();
864
904
  }
865
905
 
866
906
  export function setContextView(v) {
867
- try { localStorage.setItem("clay-context-view", v); } catch (e) {}
907
+ if (store.get("paneMode") === true) {
908
+ store.set({ contextViewOverride: v });
909
+ return true;
910
+ }
911
+ return setStoredContextView(v);
868
912
  }
869
913
 
870
914
  export function applyContextView(view) {
871
915
  if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
872
916
  if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
873
- if (view === "panel") refreshIcons();
917
+ if (view === "panel") { refreshIcons(); requestContextDetails(); }
874
918
  }
875
919
 
876
920
  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 };
921
+ 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
922
  store.set({ richContextUsage: null });
879
923
  // hideCtxPopover + updateContextPanel handled by store subscriber
880
924
  }
881
925
 
926
+ export function markContextUnavailable() {
927
+ contextData.currentInput = null;
928
+ store.set({ richContextUsage: null });
929
+ updateContextPanel();
930
+ }
931
+
882
932
  export function resetContext() {
883
933
  resetContextData();
884
934
  // Keep view state, just reset data
885
- applyContextView(getContextView());
935
+ applyContextView(getEffectiveContextView());
886
936
  }
887
937
 
888
938
  export function minimizeContext() {
889
939
  setContextView("mini");
890
- applyContextView("mini");
891
940
  }
892
941
 
893
942
  export function expandContext() {
894
943
  setContextView("panel");
895
- applyContextView("panel");
896
944
  }
897
945
 
898
946
  export function toggleContextPanel() {
899
947
  if (!contextPanel) return;
900
- var view = getContextView();
948
+ var view = getEffectiveContextView();
901
949
  if (view === "panel") {
902
950
  setContextView("mini");
903
- applyContextView("mini");
904
951
  } else {
905
952
  setContextView("panel");
906
- applyContextView("panel");
907
953
  }
908
954
  }
909
955
 
@@ -922,9 +968,17 @@ export function ensureCtxPopover() {
922
968
  });
923
969
  }
924
970
 
971
+ function requestContextDetails() {
972
+ var ws = getWs();
973
+ if (!store.get("replayingHistory") && ws && ws.readyState === 1) {
974
+ ws.send(JSON.stringify({ type: "context_usage_request", sessionId: store.get("activeSessionId") }));
975
+ }
976
+ }
977
+
925
978
  export function showCtxPopover() {
926
979
  var s = store.snap();
927
980
  if (!s.headerContextEl || !s.richContextUsage) return;
981
+ requestContextDetails();
928
982
  if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
929
983
  ensureCtxPopover();
930
984
  s.headerContextEl.appendChild(ctxPopoverEl);
@@ -944,8 +998,8 @@ export function renderCtxPopover() {
944
998
  if (!ctxPopoverEl || !richContextUsage) return;
945
999
  var d = richContextUsage;
946
1000
  var cats = d.categories || [];
947
- var total = d.totalTokens || 0;
948
- var max = d.maxTokens || 0;
1001
+ var total = d.totalTokens != null ? d.totalTokens : (d.input_tokens || 0);
1002
+ var max = d.maxTokens != null ? d.maxTokens : (d.contextWindow || 0);
949
1003
  var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
950
1004
 
951
1005
  var html = "";
@@ -39,6 +39,7 @@ import { closeProjectSettings } from './project-settings.js';
39
39
  import { chooseProjectAfterRemoval } from './project-removal-target.js';
40
40
  import { isProjectActivated, isProjectActivationPending, isProjectContextConnected, projectWsPath } from './project-activation.js';
41
41
  import { rememberHomePrimarySurface } from './home-surface.js';
42
+ import { clearProjectSplitState } from './split-session-boundary.js';
42
43
 
43
44
  // --- Module-owned state ---
44
45
  var cachedProjects = [];
@@ -447,6 +448,7 @@ export function switchProject(slug) {
447
448
  }
448
449
  if (homeVisible) store.set({ pendingHomeProjectSlug: slug });
449
450
  if (isProjectActivationPending(store.snap(), slug, ws)) return;
451
+ if (store.get('currentSlug') !== slug) clearProjectSplitState();
450
452
  resetFileBrowser();
451
453
  if (typeof closeScheduledTasks === "function") closeScheduledTasks();
452
454
  closeNotesBrowser();
@@ -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/admin/usage",
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] || fallbacks.claude;
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
- '<a href="https://claude.ai/settings/usage" target="_blank" rel="noopener" class="rate-limit-link">' +
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 rateLimitResetState[types[i]]; continue; }
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
- rateLimitUsageEl.innerHTML = iconHtml("activity") + '<span>Check usage</span>' + iconHtml("external-link");
180
- refreshIcons();
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
- var label = parts.join(" · ");
185
- rateLimitUsageEl.innerHTML = iconHtml("activity") + '<span>' + label + '</span>' + iconHtml("external-link");
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.vendorInfo !== prev.vendorInfo) {
197
- if (rateLimitUsageEl) {
198
- rateLimitUsageEl.style.display = vendorTracksRateLimits(state.currentVendor || "claude") ? "" : "none";
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
- if (rateLimitUsageEl) rateLimitUsageEl.style.display = "none";
361
+ renderRateLimitUsage();
245
362
  return;
246
363
  }
247
364
  if (msg.rateLimitType && msg.resetsAt) {
248
- rateLimitResetState[msg.rateLimitType] = { resetsAt: msg.resetsAt, status: msg.status };
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
- var label = parts.length > 0 ? parts.join(" · ") : "Check usage";
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 };