@slkiser/opencode-quota 4.8.1 → 4.9.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/README.md +17 -18
- package/dist/bin/opencode-quota.d.ts.map +1 -1
- package/dist/bin/opencode-quota.js +5 -3
- package/dist/bin/opencode-quota.js.map +1 -1
- package/dist/lib/accounting-format.d.ts +54 -0
- package/dist/lib/accounting-format.d.ts.map +1 -0
- package/dist/lib/accounting-format.js +271 -0
- package/dist/lib/accounting-format.js.map +1 -0
- package/dist/lib/anthropic.d.ts +2 -0
- package/dist/lib/anthropic.d.ts.map +1 -1
- package/dist/lib/anthropic.js +79 -19
- package/dist/lib/anthropic.js.map +1 -1
- package/dist/lib/cli-show.d.ts.map +1 -1
- package/dist/lib/cli-show.js +1 -0
- package/dist/lib/cli-show.js.map +1 -1
- package/dist/lib/config.d.ts +10 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +35 -3
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/copilot.d.ts +2 -0
- package/dist/lib/copilot.d.ts.map +1 -1
- package/dist/lib/copilot.js +38 -0
- package/dist/lib/copilot.js.map +1 -1
- package/dist/lib/deepseek.d.ts +12 -12
- package/dist/lib/deepseek.d.ts.map +1 -1
- package/dist/lib/deepseek.js +33 -29
- package/dist/lib/deepseek.js.map +1 -1
- package/dist/lib/display-sanitize.d.ts.map +1 -1
- package/dist/lib/display-sanitize.js +76 -22
- package/dist/lib/display-sanitize.js.map +1 -1
- package/dist/lib/entries.d.ts +107 -23
- package/dist/lib/entries.d.ts.map +1 -1
- package/dist/lib/entries.js +37 -1
- package/dist/lib/entries.js.map +1 -1
- package/dist/lib/format.d.ts +1 -0
- package/dist/lib/format.d.ts.map +1 -1
- package/dist/lib/format.js +84 -19
- package/dist/lib/format.js.map +1 -1
- package/dist/lib/glm-coding-plan.d.ts.map +1 -1
- package/dist/lib/glm-coding-plan.js +4 -2
- package/dist/lib/glm-coding-plan.js.map +1 -1
- package/dist/lib/google-agy.d.ts +2 -0
- package/dist/lib/google-agy.d.ts.map +1 -1
- package/dist/lib/google-agy.js +29 -14
- package/dist/lib/google-agy.js.map +1 -1
- package/dist/lib/google-gemini-cli.d.ts +2 -0
- package/dist/lib/google-gemini-cli.d.ts.map +1 -1
- package/dist/lib/google-gemini-cli.js +29 -3
- package/dist/lib/google-gemini-cli.js.map +1 -1
- package/dist/lib/google-token-cache.d.ts +5 -8
- package/dist/lib/google-token-cache.d.ts.map +1 -1
- package/dist/lib/google-token-cache.js +30 -118
- package/dist/lib/google-token-cache.js.map +1 -1
- package/dist/lib/google.d.ts +2 -0
- package/dist/lib/google.d.ts.map +1 -1
- package/dist/lib/google.js +31 -6
- package/dist/lib/google.js.map +1 -1
- package/dist/lib/grouped-entry-normalization.d.ts.map +1 -1
- package/dist/lib/grouped-entry-normalization.js +53 -20
- package/dist/lib/grouped-entry-normalization.js.map +1 -1
- package/dist/lib/minimax-auth.d.ts +1 -1
- package/dist/lib/minimax-auth.js +1 -1
- package/dist/lib/nanogpt.d.ts +5 -5
- package/dist/lib/nanogpt.d.ts.map +1 -1
- package/dist/lib/nanogpt.js +59 -23
- package/dist/lib/nanogpt.js.map +1 -1
- package/dist/lib/openai.d.ts +4 -0
- package/dist/lib/openai.d.ts.map +1 -1
- package/dist/lib/openai.js +22 -3
- package/dist/lib/openai.js.map +1 -1
- package/dist/lib/opencode-go-auth.js +1 -1
- package/dist/lib/opencode-go-auth.js.map +1 -1
- package/dist/lib/opencode-go.d.ts.map +1 -1
- package/dist/lib/opencode-go.js +6 -1
- package/dist/lib/opencode-go.js.map +1 -1
- package/dist/lib/opencode-zen-config.d.ts +0 -1
- package/dist/lib/opencode-zen-config.d.ts.map +1 -1
- package/dist/lib/opencode-zen-config.js +0 -22
- package/dist/lib/opencode-zen-config.js.map +1 -1
- package/dist/lib/opencode-zen.d.ts +3 -0
- package/dist/lib/opencode-zen.d.ts.map +1 -1
- package/dist/lib/opencode-zen.js +18 -1
- package/dist/lib/opencode-zen.js.map +1 -1
- package/dist/lib/openrouter.d.ts +2 -0
- package/dist/lib/openrouter.d.ts.map +1 -1
- package/dist/lib/openrouter.js +10 -0
- package/dist/lib/openrouter.js.map +1 -1
- package/dist/lib/provider-metadata.d.ts +3 -307
- package/dist/lib/provider-metadata.d.ts.map +1 -1
- package/dist/lib/provider-metadata.js +18 -316
- package/dist/lib/provider-metadata.js.map +1 -1
- package/dist/lib/provider-registration.d.ts +325 -0
- package/dist/lib/provider-registration.d.ts.map +1 -0
- package/dist/lib/provider-registration.js +338 -0
- package/dist/lib/provider-registration.js.map +1 -0
- package/dist/lib/quota-accounting-projection.d.ts +7 -0
- package/dist/lib/quota-accounting-projection.d.ts.map +1 -0
- package/dist/lib/quota-accounting-projection.js +239 -0
- package/dist/lib/quota-accounting-projection.js.map +1 -0
- package/dist/lib/quota-command-format.d.ts +3 -2
- package/dist/lib/quota-command-format.d.ts.map +1 -1
- package/dist/lib/quota-command-format.js +35 -9
- package/dist/lib/quota-command-format.js.map +1 -1
- package/dist/lib/quota-dialog-commands.d.ts.map +1 -1
- package/dist/lib/quota-dialog-commands.js +1 -0
- package/dist/lib/quota-dialog-commands.js.map +1 -1
- package/dist/lib/quota-export.d.ts +2 -1
- package/dist/lib/quota-export.d.ts.map +1 -1
- package/dist/lib/quota-export.js +36 -10
- package/dist/lib/quota-export.js.map +1 -1
- package/dist/lib/quota-render-data.d.ts +0 -1
- package/dist/lib/quota-render-data.d.ts.map +1 -1
- package/dist/lib/quota-render-data.js +43 -155
- package/dist/lib/quota-render-data.js.map +1 -1
- package/dist/lib/quota-reset-notifications.d.ts.map +1 -1
- package/dist/lib/quota-reset-notifications.js +3 -0
- package/dist/lib/quota-reset-notifications.js.map +1 -1
- package/dist/lib/quota-state-codec.d.ts +16 -0
- package/dist/lib/quota-state-codec.d.ts.map +1 -0
- package/dist/lib/quota-state-codec.js +409 -0
- package/dist/lib/quota-state-codec.js.map +1 -0
- package/dist/lib/quota-state.d.ts +2 -11
- package/dist/lib/quota-state.d.ts.map +1 -1
- package/dist/lib/quota-state.js +165 -304
- package/dist/lib/quota-state.js.map +1 -1
- package/dist/lib/quota-status.d.ts.map +1 -1
- package/dist/lib/quota-status.js +14 -7
- package/dist/lib/quota-status.js.map +1 -1
- package/dist/lib/quota-telemetry.d.ts.map +1 -1
- package/dist/lib/quota-telemetry.js +7 -3
- package/dist/lib/quota-telemetry.js.map +1 -1
- package/dist/lib/quota-toast-runtime.d.ts +60 -0
- package/dist/lib/quota-toast-runtime.d.ts.map +1 -0
- package/dist/lib/quota-toast-runtime.js +765 -0
- package/dist/lib/quota-toast-runtime.js.map +1 -0
- package/dist/lib/resolved-auth-identity.d.ts +36 -0
- package/dist/lib/resolved-auth-identity.d.ts.map +1 -0
- package/dist/lib/resolved-auth-identity.js +193 -0
- package/dist/lib/resolved-auth-identity.js.map +1 -0
- package/dist/lib/scoped-update-migration.d.ts +101 -0
- package/dist/lib/scoped-update-migration.d.ts.map +1 -0
- package/dist/lib/scoped-update-migration.js +348 -0
- package/dist/lib/scoped-update-migration.js.map +1 -0
- package/dist/lib/scoped-update.d.ts +8 -0
- package/dist/lib/scoped-update.d.ts.map +1 -1
- package/dist/lib/scoped-update.js +263 -50
- package/dist/lib/scoped-update.js.map +1 -1
- package/dist/lib/toast-format-grouped.d.ts +1 -0
- package/dist/lib/toast-format-grouped.d.ts.map +1 -1
- package/dist/lib/toast-format-grouped.js +111 -24
- package/dist/lib/toast-format-grouped.js.map +1 -1
- package/dist/lib/tui-compact-format.d.ts +1 -0
- package/dist/lib/tui-compact-format.d.ts.map +1 -1
- package/dist/lib/tui-compact-format.js +136 -24
- package/dist/lib/tui-compact-format.js.map +1 -1
- package/dist/lib/tui-panel-state.d.ts +2 -0
- package/dist/lib/tui-panel-state.d.ts.map +1 -1
- package/dist/lib/tui-panel-state.js.map +1 -1
- package/dist/lib/tui-runtime.d.ts.map +1 -1
- package/dist/lib/tui-runtime.js +84 -10
- package/dist/lib/tui-runtime.js.map +1 -1
- package/dist/lib/tui-sidebar-format.d.ts +1 -1
- package/dist/lib/tui-sidebar-format.d.ts.map +1 -1
- package/dist/lib/tui-sidebar-format.js +1 -0
- package/dist/lib/tui-sidebar-format.js.map +1 -1
- package/dist/lib/types.d.ts +7 -1
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/types.js +1 -0
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/xai.d.ts +2 -0
- package/dist/lib/xai.d.ts.map +1 -1
- package/dist/lib/xai.js +10 -0
- package/dist/lib/xai.js.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +27 -720
- package/dist/plugin.js.map +1 -1
- package/dist/providers/accounting-decimal.d.ts +2 -0
- package/dist/providers/accounting-decimal.d.ts.map +1 -0
- package/dist/providers/accounting-decimal.js +23 -0
- package/dist/providers/accounting-decimal.js.map +1 -0
- package/dist/providers/cache-policies.d.ts +64 -0
- package/dist/providers/cache-policies.d.ts.map +1 -0
- package/dist/providers/cache-policies.js +197 -0
- package/dist/providers/cache-policies.js.map +1 -0
- package/dist/providers/cursor.d.ts.map +1 -1
- package/dist/providers/cursor.js +73 -58
- package/dist/providers/cursor.js.map +1 -1
- package/dist/providers/deepseek.d.ts +2 -2
- package/dist/providers/deepseek.d.ts.map +1 -1
- package/dist/providers/deepseek.js +64 -33
- package/dist/providers/deepseek.js.map +1 -1
- package/dist/providers/google-agy.js +2 -2
- package/dist/providers/google-agy.js.map +1 -1
- package/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/dist/providers/google-gemini-cli.js +1 -1
- package/dist/providers/google-gemini-cli.js.map +1 -1
- package/dist/providers/kilo.d.ts.map +1 -1
- package/dist/providers/kilo.js +56 -29
- package/dist/providers/kilo.js.map +1 -1
- package/dist/providers/mimo.d.ts.map +1 -1
- package/dist/providers/mimo.js +37 -15
- package/dist/providers/mimo.js.map +1 -1
- package/dist/providers/minimax-coding-plan.d.ts +8 -7
- package/dist/providers/minimax-coding-plan.d.ts.map +1 -1
- package/dist/providers/minimax-coding-plan.js +97 -83
- package/dist/providers/minimax-coding-plan.js.map +1 -1
- package/dist/providers/nanogpt.d.ts.map +1 -1
- package/dist/providers/nanogpt.js +72 -51
- package/dist/providers/nanogpt.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +0 -3
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/opencode-go.d.ts.map +1 -1
- package/dist/providers/opencode-go.js +3 -4
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/opencode-zen.d.ts.map +1 -1
- package/dist/providers/opencode-zen.js +78 -17
- package/dist/providers/opencode-zen.js.map +1 -1
- package/dist/providers/quota-providers.d.ts.map +1 -1
- package/dist/providers/quota-providers.js +31 -23
- package/dist/providers/quota-providers.js.map +1 -1
- package/dist/providers/registry.d.ts +3 -2
- package/dist/providers/registry.d.ts.map +1 -1
- package/dist/providers/registry.js +41 -31
- package/dist/providers/registry.js.map +1 -1
- package/dist/providers/result-helpers.d.ts +10 -5
- package/dist/providers/result-helpers.d.ts.map +1 -1
- package/dist/providers/result-helpers.js +3 -3
- package/dist/providers/result-helpers.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +11 -3
- package/dist/tui.tsx +16 -7
- package/package.json +1 -1
package/dist/plugin.js
CHANGED
|
@@ -7,29 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { isMainThread } from "node:worker_threads";
|
|
9
9
|
import { tool } from "@opencode-ai/plugin";
|
|
10
|
-
import { DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS, isAlibabaModelId, resolveAlibabaCodingPlanAuthCached, } from "./lib/alibaba-auth.js";
|
|
11
|
-
import { getOrFetchWithCacheControl } from "./lib/cache.js";
|
|
12
10
|
import { handled } from "./lib/command-handled.js";
|
|
13
11
|
import { shouldRegisterServerSlashCommands } from "./lib/command-surfaces.js";
|
|
14
|
-
import { createLoadConfigMeta } from "./lib/config.js";
|
|
15
12
|
import { findGitWorktreeRoot, getEffectiveConfigRoot } from "./lib/config-file-utils.js";
|
|
16
|
-
import { isCursorModelId, isCursorProviderId } from "./lib/cursor-pricing.js";
|
|
17
13
|
import { sanitizeDisplayText } from "./lib/display-sanitize.js";
|
|
18
|
-
import { formatQuotaRows } from "./lib/format.js";
|
|
19
|
-
import { BUNDLED_MAINTAINER_ANNOUNCEMENTS, formatMaintainerAnnouncementHomeCountLine, getMaintainerAnnouncementsSummary, } from "./lib/maintainer-announcements.js";
|
|
20
|
-
import { maybeRefreshPricingSnapshot, setPricingSnapshotAutoRefresh, setPricingSnapshotSelection, } from "./lib/modelsdev-pricing.js";
|
|
21
14
|
import { reconcileDetectedProvidersInGlobalConfig } from "./lib/opencode-config-providers.js";
|
|
22
15
|
import { buildQuotaDialogCommandOutput, isQuotaDialogCommand, QUOTA_DIALOG_COMMANDS, } from "./lib/quota-dialog-commands.js";
|
|
23
|
-
import { resolveQuotaFormatStyle } from "./lib/quota-format-style.js";
|
|
24
|
-
import { customQuotaProviderDefinitions, QUOTA_PROVIDERS_AGGREGATE_ID, } from "./lib/quota-providers.js";
|
|
25
|
-
import { collectQuotaRenderData } from "./lib/quota-render-data.js";
|
|
26
|
-
import { formatQuotaResetNotification, observeQuotaResetNotifications, } from "./lib/quota-reset-notifications.js";
|
|
27
|
-
import { createQuotaRuntimeRequestContext, resolveQuotaRuntimeContext, } from "./lib/quota-runtime-context.js";
|
|
28
16
|
import { disposeQuotaTelemetryOwner } from "./lib/quota-telemetry.js";
|
|
29
|
-
import {
|
|
30
|
-
import { inspectTuiConfig } from "./lib/tui-config-diagnostics.js";
|
|
31
|
-
import { DEFAULT_CONFIG } from "./lib/types.js";
|
|
32
|
-
import { getProviders } from "./providers/registry.js";
|
|
17
|
+
import { createQuotaToastRuntime } from "./lib/quota-toast-runtime.js";
|
|
33
18
|
function normalizeDefaultAgent(cfg) {
|
|
34
19
|
if (!cfg?.default_agent || !cfg.agent || cfg.default_agent in cfg.agent)
|
|
35
20
|
return;
|
|
@@ -40,7 +25,6 @@ function normalizeDefaultAgent(cfg) {
|
|
|
40
25
|
cfg.default_agent = matches[0];
|
|
41
26
|
}
|
|
42
27
|
}
|
|
43
|
-
const DEFERRED_QUOTA_REFRESH_DELAYS_MS = [3_000, 15_000, 60_000, 300_000];
|
|
44
28
|
// =============================================================================
|
|
45
29
|
// Plugin Implementation
|
|
46
30
|
// =============================================================================
|
|
@@ -82,105 +66,9 @@ export const QuotaToastPlugin = async ({ client, directory }) => {
|
|
|
82
66
|
}
|
|
83
67
|
}
|
|
84
68
|
}
|
|
85
|
-
// Keep init fast/non-blocking so TUI never hangs. We still want the first
|
|
86
|
-
// toast trigger to work reliably, so we refresh config on-demand.
|
|
87
|
-
let config = DEFAULT_CONFIG;
|
|
88
|
-
let configLoaded = false;
|
|
89
|
-
let configInFlight = null;
|
|
90
69
|
let providerConfigReconcileQueue = Promise.resolve();
|
|
91
|
-
let configMeta = createLoadConfigMeta();
|
|
92
|
-
let runtimeProviders = getProviders();
|
|
93
70
|
// Track last session token error for /quota_status diagnostics
|
|
94
71
|
let lastSessionTokenError;
|
|
95
|
-
const deferredQuotaRefreshes = new Map();
|
|
96
|
-
const detectedProviderIdsByToastCacheKey = new Map();
|
|
97
|
-
const maintainerAnnouncementToastFallback = {
|
|
98
|
-
pending: true,
|
|
99
|
-
inFlight: false,
|
|
100
|
-
};
|
|
101
|
-
function getDeferredQuotaRefreshDelayMs(attempts) {
|
|
102
|
-
const index = Math.min(Math.max(0, attempts), DEFERRED_QUOTA_REFRESH_DELAYS_MS.length - 1);
|
|
103
|
-
return DEFERRED_QUOTA_REFRESH_DELAYS_MS[index];
|
|
104
|
-
}
|
|
105
|
-
function clearDeferredQuotaRefresh(sessionID) {
|
|
106
|
-
const state = deferredQuotaRefreshes.get(sessionID);
|
|
107
|
-
if (state?.timer) {
|
|
108
|
-
clearTimeout(state.timer);
|
|
109
|
-
}
|
|
110
|
-
deferredQuotaRefreshes.delete(sessionID);
|
|
111
|
-
}
|
|
112
|
-
function clearDeferredQuotaRefreshTimer(state) {
|
|
113
|
-
if (!state.timer)
|
|
114
|
-
return;
|
|
115
|
-
clearTimeout(state.timer);
|
|
116
|
-
state.timer = null;
|
|
117
|
-
}
|
|
118
|
-
function scheduleDeferredQuotaRefresh(params) {
|
|
119
|
-
let state = deferredQuotaRefreshes.get(params.sessionID);
|
|
120
|
-
if (!state) {
|
|
121
|
-
state = {
|
|
122
|
-
sessionID: params.sessionID,
|
|
123
|
-
attempts: 0,
|
|
124
|
-
reason: params.reason,
|
|
125
|
-
queuedAtMs: Date.now(),
|
|
126
|
-
timer: null,
|
|
127
|
-
inFlight: false,
|
|
128
|
-
};
|
|
129
|
-
deferredQuotaRefreshes.set(params.sessionID, state);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
if (params.incrementAttempts) {
|
|
133
|
-
state.attempts += 1;
|
|
134
|
-
}
|
|
135
|
-
state.reason = params.reason;
|
|
136
|
-
clearDeferredQuotaRefreshTimer(state);
|
|
137
|
-
}
|
|
138
|
-
const delayMs = getDeferredQuotaRefreshDelayMs(state.attempts);
|
|
139
|
-
state.timer = setTimeout(() => {
|
|
140
|
-
void runDeferredQuotaRefresh(params.sessionID);
|
|
141
|
-
}, delayMs);
|
|
142
|
-
state.timer.unref?.();
|
|
143
|
-
void log("Deferred quota refresh scheduled", {
|
|
144
|
-
sessionID: params.sessionID,
|
|
145
|
-
reason: params.reason,
|
|
146
|
-
attempts: state.attempts,
|
|
147
|
-
delayMs,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
async function runDeferredQuotaRefresh(sessionID) {
|
|
151
|
-
const state = deferredQuotaRefreshes.get(sessionID);
|
|
152
|
-
if (!state || state.inFlight)
|
|
153
|
-
return;
|
|
154
|
-
await showQuotaToast(sessionID, "deferred.retry", { deferredRetry: true });
|
|
155
|
-
}
|
|
156
|
-
function isProviderEnabled(providerId) {
|
|
157
|
-
return config.enabledProviders === "auto" || config.enabledProviders.includes(providerId);
|
|
158
|
-
}
|
|
159
|
-
async function shouldBypassToastCacheForLiveLocalUsage(params) {
|
|
160
|
-
if (isProviderEnabled(QUOTA_PROVIDERS_AGGREGATE_ID) &&
|
|
161
|
-
customQuotaProviderDefinitions(config.quotaProviders).some((definition) => definition.mode === "local-estimate")) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
const currentSession = params.sessionMeta ?? (await getSessionModelMeta(params.sessionID));
|
|
165
|
-
const currentModel = currentSession.modelID;
|
|
166
|
-
if (currentSession.providerID === "qwen-code" || isQwenCodeModelId(currentModel)) {
|
|
167
|
-
const plan = await resolveQwenLocalPlanCached();
|
|
168
|
-
return plan.state === "qwen_free" && isProviderEnabled("qwen-code");
|
|
169
|
-
}
|
|
170
|
-
if (currentSession.providerID === "alibaba-coding-plan" ||
|
|
171
|
-
currentSession.providerID === "alibaba" ||
|
|
172
|
-
isAlibabaModelId(currentModel)) {
|
|
173
|
-
const plan = await resolveAlibabaCodingPlanAuthCached({
|
|
174
|
-
maxAgeMs: DEFAULT_ALIBABA_AUTH_CACHE_MAX_AGE_MS,
|
|
175
|
-
fallbackTier: "lite",
|
|
176
|
-
});
|
|
177
|
-
return plan.state === "configured" && isProviderEnabled("alibaba-coding-plan");
|
|
178
|
-
}
|
|
179
|
-
if (isCursorProviderId(currentSession.providerID) || isCursorModelId(currentModel)) {
|
|
180
|
-
return isProviderEnabled("cursor");
|
|
181
|
-
}
|
|
182
|
-
return false;
|
|
183
|
-
}
|
|
184
72
|
function getPluginRuntimeRootHints() {
|
|
185
73
|
const cwd = directory || process.cwd();
|
|
186
74
|
const workspaceRoot = findGitWorktreeRoot(cwd) ?? cwd;
|
|
@@ -220,174 +108,6 @@ export const QuotaToastPlugin = async ({ client, directory }) => {
|
|
|
220
108
|
}
|
|
221
109
|
handled();
|
|
222
110
|
}
|
|
223
|
-
function triggerMaintainerAnnouncementToastFallback(trigger, detectedProviderIds) {
|
|
224
|
-
if (!maintainerAnnouncementToastFallback.pending ||
|
|
225
|
-
maintainerAnnouncementToastFallback.inFlight) {
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
if (!config.enabled || !config.enableToast) {
|
|
229
|
-
maintainerAnnouncementToastFallback.pending = false;
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
if (!config.maintainerAnnouncements.enabled || !config.maintainerAnnouncements.home) {
|
|
233
|
-
maintainerAnnouncementToastFallback.pending = false;
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
maintainerAnnouncementToastFallback.inFlight = true;
|
|
237
|
-
void (async () => {
|
|
238
|
-
try {
|
|
239
|
-
const summary = getMaintainerAnnouncementsSummary({
|
|
240
|
-
announcements: BUNDLED_MAINTAINER_ANNOUNCEMENTS,
|
|
241
|
-
enabledProviders: detectedProviderIds,
|
|
242
|
-
});
|
|
243
|
-
if (summary.activeCount <= 0) {
|
|
244
|
-
if (summary.futureCount <= 0) {
|
|
245
|
-
maintainerAnnouncementToastFallback.pending = false;
|
|
246
|
-
}
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
const tuiDiagnostics = await inspectTuiConfig({ roots: getPluginRuntimeRootHints() });
|
|
250
|
-
if (tuiDiagnostics.quotaPluginConfigured) {
|
|
251
|
-
maintainerAnnouncementToastFallback.pending = false;
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
const message = formatMaintainerAnnouncementHomeCountLine(summary.activeCount);
|
|
255
|
-
if (!message) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
await typedClient.tui.showToast({
|
|
259
|
-
body: {
|
|
260
|
-
message: sanitizeDisplayText(message),
|
|
261
|
-
variant: "info",
|
|
262
|
-
duration: config.toastDurationMs,
|
|
263
|
-
},
|
|
264
|
-
});
|
|
265
|
-
maintainerAnnouncementToastFallback.pending = false;
|
|
266
|
-
await log("Displayed maintainer announcement fallback toast", { trigger });
|
|
267
|
-
}
|
|
268
|
-
catch (err) {
|
|
269
|
-
await log("Failed to show maintainer announcement fallback toast", {
|
|
270
|
-
trigger,
|
|
271
|
-
error: err instanceof Error ? err.message : String(err),
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
finally {
|
|
275
|
-
maintainerAnnouncementToastFallback.inFlight = false;
|
|
276
|
-
}
|
|
277
|
-
})();
|
|
278
|
-
}
|
|
279
|
-
async function resolvePluginRuntimeContext(params = {}) {
|
|
280
|
-
if (!configLoaded) {
|
|
281
|
-
await refreshConfig();
|
|
282
|
-
}
|
|
283
|
-
return resolveQuotaRuntimeContext({
|
|
284
|
-
client: typedClient,
|
|
285
|
-
roots: getPluginRuntimeRootHints(),
|
|
286
|
-
config,
|
|
287
|
-
configMeta,
|
|
288
|
-
providers: runtimeProviders,
|
|
289
|
-
sessionID: params.sessionID,
|
|
290
|
-
sessionMeta: params.sessionMeta,
|
|
291
|
-
resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID),
|
|
292
|
-
includeSessionMeta: params.includeSessionMeta,
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
async function refreshConfig() {
|
|
296
|
-
if (configInFlight)
|
|
297
|
-
return configInFlight;
|
|
298
|
-
configInFlight = (async () => {
|
|
299
|
-
try {
|
|
300
|
-
const runtime = await resolveQuotaRuntimeContext({
|
|
301
|
-
client: typedClient,
|
|
302
|
-
roots: getPluginRuntimeRootHints(),
|
|
303
|
-
});
|
|
304
|
-
configMeta = runtime.configMeta;
|
|
305
|
-
config = runtime.config;
|
|
306
|
-
runtimeProviders = runtime.providers;
|
|
307
|
-
setPricingSnapshotAutoRefresh(config.pricingSnapshot.autoRefresh);
|
|
308
|
-
setPricingSnapshotSelection(config.pricingSnapshot.source);
|
|
309
|
-
configLoaded = true;
|
|
310
|
-
onFirstConfigLoaded();
|
|
311
|
-
}
|
|
312
|
-
catch {
|
|
313
|
-
// Leave configLoaded=false so we can retry on next trigger.
|
|
314
|
-
config = DEFAULT_CONFIG;
|
|
315
|
-
configMeta = createLoadConfigMeta();
|
|
316
|
-
runtimeProviders = getProviders();
|
|
317
|
-
setPricingSnapshotAutoRefresh(DEFAULT_CONFIG.pricingSnapshot.autoRefresh);
|
|
318
|
-
setPricingSnapshotSelection(DEFAULT_CONFIG.pricingSnapshot.source);
|
|
319
|
-
}
|
|
320
|
-
finally {
|
|
321
|
-
configInFlight = null;
|
|
322
|
-
}
|
|
323
|
-
})();
|
|
324
|
-
return configInFlight;
|
|
325
|
-
}
|
|
326
|
-
async function kickPricingRefresh(params) {
|
|
327
|
-
try {
|
|
328
|
-
const refreshPromise = maybeRefreshPricingSnapshot({
|
|
329
|
-
reason: params.reason,
|
|
330
|
-
snapshotSelection: config.pricingSnapshot.source,
|
|
331
|
-
});
|
|
332
|
-
const guardedRefreshPromise = refreshPromise.catch(() => undefined);
|
|
333
|
-
if (!params.maxWaitMs || params.maxWaitMs <= 0) {
|
|
334
|
-
void guardedRefreshPromise;
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
await Promise.race([
|
|
338
|
-
guardedRefreshPromise,
|
|
339
|
-
new Promise((resolve) => {
|
|
340
|
-
setTimeout(resolve, params.maxWaitMs);
|
|
341
|
-
}),
|
|
342
|
-
]);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
await log("Pricing refresh failed", {
|
|
346
|
-
reason: params.reason,
|
|
347
|
-
error: error instanceof Error ? error.message : String(error),
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
// Deferred init: runs once after the first successful config load.
|
|
352
|
-
// Avoids HTTP calls during plugin construction, which can interfere with
|
|
353
|
-
// other plugins that are still being loaded (see #39).
|
|
354
|
-
let initDone = false;
|
|
355
|
-
function onFirstConfigLoaded() {
|
|
356
|
-
if (initDone)
|
|
357
|
-
return;
|
|
358
|
-
initDone = true;
|
|
359
|
-
if (config.enabled) {
|
|
360
|
-
void kickPricingRefresh({ reason: "init" });
|
|
361
|
-
}
|
|
362
|
-
void typedClient.app
|
|
363
|
-
.log({
|
|
364
|
-
body: {
|
|
365
|
-
service: "quota-toast",
|
|
366
|
-
level: "info",
|
|
367
|
-
message: "plugin initialized",
|
|
368
|
-
extra: {
|
|
369
|
-
configLoaded,
|
|
370
|
-
configSource: configMeta.source,
|
|
371
|
-
configPaths: configMeta.paths,
|
|
372
|
-
enabledProviders: config.enabledProviders,
|
|
373
|
-
minIntervalMs: config.minIntervalMs,
|
|
374
|
-
googleModels: config.googleModels,
|
|
375
|
-
cursorPlan: config.cursorPlan,
|
|
376
|
-
cursorIncludedApiUsd: config.cursorIncludedApiUsd,
|
|
377
|
-
cursorBillingCycleStartDay: config.cursorBillingCycleStartDay,
|
|
378
|
-
pricingSnapshotSource: config.pricingSnapshot.source,
|
|
379
|
-
pricingSnapshotAutoRefresh: config.pricingSnapshot.autoRefresh,
|
|
380
|
-
showOnIdle: config.showOnIdle,
|
|
381
|
-
showOnQuestion: config.showOnQuestion,
|
|
382
|
-
showOnCompact: config.showOnCompact,
|
|
383
|
-
showOnBothFail: config.showOnBothFail,
|
|
384
|
-
},
|
|
385
|
-
},
|
|
386
|
-
})
|
|
387
|
-
.catch(() => { });
|
|
388
|
-
}
|
|
389
|
-
// If disabled in config, it'll be picked up on first trigger; we can't
|
|
390
|
-
// reliably read config synchronously without risking TUI startup.
|
|
391
111
|
/**
|
|
392
112
|
* Log a message (debug level)
|
|
393
113
|
*/
|
|
@@ -477,422 +197,30 @@ export const QuotaToastPlugin = async ({ client, directory }) => {
|
|
|
477
197
|
return {};
|
|
478
198
|
}
|
|
479
199
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const currentProviderID = config.onlyCurrentModel && params.sessionID ? (params.sessionMeta?.providerID ?? "") : "";
|
|
505
|
-
return [
|
|
506
|
-
`sessionID=${params.sessionID}`,
|
|
507
|
-
`enabledProviders=${enabledProviders}`,
|
|
508
|
-
`formatStyle=${formatStyle}`,
|
|
509
|
-
`percentDisplayMode=${config.percentDisplayMode}`,
|
|
510
|
-
`layout=${JSON.stringify(config.layout)}`,
|
|
511
|
-
`showSessionTokens=${config.showSessionTokens ? "yes" : "no"}`,
|
|
512
|
-
`onlyCurrentModel=${config.onlyCurrentModel ? "yes" : "no"}`,
|
|
513
|
-
`currentModel=${currentModel}`,
|
|
514
|
-
`currentProviderID=${currentProviderID}`,
|
|
515
|
-
`anthropicBinaryPath=${config.anthropicBinaryPath}`,
|
|
516
|
-
`googleModels=${googleModels}`,
|
|
517
|
-
`cursorPlan=${config.cursorPlan}`,
|
|
518
|
-
`cursorIncludedApiUsd=${config.cursorIncludedApiUsd ?? ""}`,
|
|
519
|
-
`cursorBillingCycleStartDay=${config.cursorBillingCycleStartDay ?? ""}`,
|
|
520
|
-
].join("|");
|
|
521
|
-
}
|
|
522
|
-
function isProviderFetchFailureOnly(errors) {
|
|
523
|
-
return (errors.length > 0 && errors.every((error) => error.message === "Failed to read quota data"));
|
|
524
|
-
}
|
|
525
|
-
async function fetchQuotaMessageResult(params) {
|
|
526
|
-
// Ensure we have loaded config at least once. If load fails, we keep trying
|
|
527
|
-
// on subsequent triggers and queue a deferred retry for toast paths.
|
|
528
|
-
if (!configLoaded) {
|
|
529
|
-
await refreshConfig();
|
|
530
|
-
}
|
|
531
|
-
if (!configLoaded) {
|
|
532
|
-
return {
|
|
533
|
-
message: config.debug
|
|
534
|
-
? formatDebugInfo({
|
|
535
|
-
trigger: params.trigger,
|
|
536
|
-
reason: "config load failed",
|
|
537
|
-
enabledProviders: config.enabledProviders,
|
|
538
|
-
})
|
|
539
|
-
: null,
|
|
540
|
-
cacheRenderedMessage: false,
|
|
541
|
-
retryable: true,
|
|
542
|
-
retryReason: "config_load_failed",
|
|
543
|
-
hasQuotaRows: false,
|
|
544
|
-
detectedProviderIds: [],
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
if (!config.enabled) {
|
|
548
|
-
return {
|
|
549
|
-
message: config.debug
|
|
550
|
-
? formatDebugInfo({ trigger: params.trigger, reason: "disabled", enabledProviders: [] })
|
|
551
|
-
: null,
|
|
552
|
-
cacheRenderedMessage: false,
|
|
553
|
-
retryable: false,
|
|
554
|
-
hasQuotaRows: false,
|
|
555
|
-
detectedProviderIds: [],
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
if (config.enabledProviders !== "auto" && config.enabledProviders.length === 0) {
|
|
559
|
-
return {
|
|
560
|
-
message: config.debug
|
|
561
|
-
? formatDebugInfo({
|
|
562
|
-
trigger: params.trigger,
|
|
563
|
-
reason: "enabledProviders empty",
|
|
564
|
-
enabledProviders: [],
|
|
565
|
-
})
|
|
566
|
-
: null,
|
|
567
|
-
cacheRenderedMessage: false,
|
|
568
|
-
retryable: false,
|
|
569
|
-
hasQuotaRows: false,
|
|
570
|
-
detectedProviderIds: [],
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
const runtime = await resolvePluginRuntimeContext({
|
|
574
|
-
sessionID: params.sessionID,
|
|
575
|
-
sessionMeta: params.sessionMeta,
|
|
576
|
-
includeSessionMeta: (config) => config.onlyCurrentModel,
|
|
577
|
-
});
|
|
578
|
-
const runtimeConfig = runtime.config;
|
|
579
|
-
const quotaRequestContext = createQuotaRuntimeRequestContext(runtime);
|
|
580
|
-
const quotaResult = await collectQuotaRenderData({
|
|
581
|
-
client: runtime.client,
|
|
582
|
-
resolveRuntimeProviderIds: runtime.resolveRuntimeProviderIds,
|
|
583
|
-
config: runtimeConfig,
|
|
584
|
-
configMeta: runtime.configMeta,
|
|
585
|
-
request: quotaRequestContext,
|
|
586
|
-
surfaceExplicitProviderIssues: true,
|
|
587
|
-
formatStyle: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
|
|
588
|
-
bypassProviderCache: params.bypassProviderCache,
|
|
589
|
-
providers: runtime.providers,
|
|
590
|
-
});
|
|
591
|
-
const { selection, availability, active, providerResults, attemptedAny, hasExplicitProviderIssues, data, } = quotaResult;
|
|
592
|
-
let resetNotification;
|
|
593
|
-
if (runtimeConfig.enableToast &&
|
|
594
|
-
runtimeConfig.resetNotifications.enabled &&
|
|
595
|
-
providerResults.length > 0) {
|
|
596
|
-
try {
|
|
597
|
-
const notices = await observeQuotaResetNotifications({
|
|
598
|
-
providers: providerResults,
|
|
599
|
-
windows: runtimeConfig.resetNotifications.windows,
|
|
600
|
-
});
|
|
601
|
-
resetNotification = formatQuotaResetNotification(notices) ?? undefined;
|
|
602
|
-
}
|
|
603
|
-
catch (error) {
|
|
604
|
-
await log("Failed to observe quota reset transitions", {
|
|
605
|
-
error: error instanceof Error ? error.message : String(error),
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
if (selection?.isAutoMode) {
|
|
610
|
-
await reconcileDetectedProviderConfig(active.map((provider) => provider.id));
|
|
611
|
-
}
|
|
612
|
-
const detectedProviderIds = active.map((provider) => provider.id);
|
|
613
|
-
if (runtimeConfig.showSessionTokens && params.sessionID) {
|
|
614
|
-
lastSessionTokenError = quotaResult.sessionTokenError;
|
|
615
|
-
}
|
|
616
|
-
const currentModel = selection?.currentModel;
|
|
617
|
-
const errors = data?.errors ?? [];
|
|
618
|
-
const hasProviderQuotaRows = Boolean(data?.entries.length);
|
|
619
|
-
const hasQuotaRows = Boolean(hasProviderQuotaRows || data?.sessionTokens);
|
|
620
|
-
const providerFetchFailureOnly = attemptedAny && isProviderFetchFailureOnly(errors);
|
|
621
|
-
const retryableAvailabilityFailure = active.length === 0 && availability.some((item) => !item.ok && item.error === true);
|
|
622
|
-
if (active.length === 0 && !(hasExplicitProviderIssues && errors.length > 0)) {
|
|
623
|
-
const message = runtimeConfig.debug
|
|
624
|
-
? formatDebugInfo({
|
|
625
|
-
trigger: params.trigger,
|
|
626
|
-
reason: "no enabled providers available",
|
|
627
|
-
currentModel,
|
|
628
|
-
enabledProviders: runtimeConfig.enabledProviders,
|
|
629
|
-
availability: availability.map((item) => ({
|
|
630
|
-
id: item.provider.id,
|
|
631
|
-
ok: item.ok,
|
|
632
|
-
})),
|
|
633
|
-
})
|
|
634
|
-
: null;
|
|
635
|
-
const retryableNoProviders = selection?.isAutoMode === true || retryableAvailabilityFailure;
|
|
636
|
-
return {
|
|
637
|
-
message,
|
|
638
|
-
cacheRenderedMessage: false,
|
|
639
|
-
retryable: retryableNoProviders,
|
|
640
|
-
retryReason: retryableNoProviders ? "no_available_providers" : undefined,
|
|
641
|
-
hasQuotaRows: false,
|
|
642
|
-
detectedProviderIds,
|
|
643
|
-
resetNotification,
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
if (hasQuotaRows) {
|
|
647
|
-
const formatted = formatQuotaRows({
|
|
648
|
-
version: "1.0.0",
|
|
649
|
-
layout: runtimeConfig.layout,
|
|
650
|
-
entries: data?.entries ?? [],
|
|
651
|
-
errors: data?.errors ?? [],
|
|
652
|
-
style: resolveQuotaFormatStyle(runtimeConfig.formatStyle),
|
|
653
|
-
percentDisplayMode: runtimeConfig.percentDisplayMode,
|
|
654
|
-
resetTimeDecimals: runtimeConfig.resetTimeDecimals,
|
|
655
|
-
sessionTokens: data?.sessionTokens,
|
|
656
|
-
});
|
|
657
|
-
const retryableMaskedProviderFailure = !hasProviderQuotaRows && providerFetchFailureOnly;
|
|
658
|
-
if (!runtimeConfig.debug) {
|
|
659
|
-
return {
|
|
660
|
-
message: formatted,
|
|
661
|
-
cacheRenderedMessage: true,
|
|
662
|
-
retryable: retryableMaskedProviderFailure,
|
|
663
|
-
retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
|
|
664
|
-
hasQuotaRows: true,
|
|
665
|
-
detectedProviderIds,
|
|
666
|
-
resetNotification,
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
const debugFooter = `\n\n[debug] src=${configMeta.source} providers=${runtimeConfig.enabledProviders === "auto" ? "(auto)" : runtimeConfig.enabledProviders.join(",") || "(none)"} avail=${availability
|
|
670
|
-
.map((item) => `${item.provider.id}:${item.ok ? "ok" : "no"}`)
|
|
671
|
-
.join(" ")}`;
|
|
672
|
-
return {
|
|
673
|
-
message: formatted + debugFooter,
|
|
674
|
-
cacheRenderedMessage: false,
|
|
675
|
-
retryable: retryableMaskedProviderFailure,
|
|
676
|
-
retryReason: retryableMaskedProviderFailure ? "provider_fetch_failed" : undefined,
|
|
677
|
-
hasQuotaRows: true,
|
|
678
|
-
detectedProviderIds,
|
|
679
|
-
resetNotification,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
// Show errors even without entries when:
|
|
683
|
-
// 1. showOnBothFail is enabled and at least one provider attempted (existing behavior)
|
|
684
|
-
// 2. OR we're in explicit mode and have "Not configured"/"Unavailable" errors (new behavior)
|
|
685
|
-
if ((runtimeConfig.showOnBothFail && attemptedAny && errors.length > 0) ||
|
|
686
|
-
hasExplicitProviderIssues) {
|
|
687
|
-
const errorLines = errors.map((error) => `${error.label}: ${error.message}`).join("\n");
|
|
688
|
-
const retryableFetchFailure = !hasExplicitProviderIssues && providerFetchFailureOnly;
|
|
689
|
-
const retryableFailure = retryableFetchFailure || retryableAvailabilityFailure;
|
|
690
|
-
const retryReason = retryableFetchFailure
|
|
691
|
-
? "provider_fetch_failed"
|
|
692
|
-
: retryableAvailabilityFailure
|
|
693
|
-
? "no_available_providers"
|
|
694
|
-
: undefined;
|
|
695
|
-
const message = !runtimeConfig.debug
|
|
696
|
-
? errorLines || "Quota unavailable"
|
|
697
|
-
: (errorLines || "Quota unavailable") +
|
|
698
|
-
"\n\n" +
|
|
699
|
-
formatDebugInfo({
|
|
700
|
-
trigger: params.trigger,
|
|
701
|
-
reason: hasExplicitProviderIssues
|
|
702
|
-
? "providers missing/unavailable"
|
|
703
|
-
: "all providers failed",
|
|
704
|
-
currentModel,
|
|
705
|
-
enabledProviders: runtimeConfig.enabledProviders,
|
|
706
|
-
availability: availability.map((item) => ({
|
|
707
|
-
id: item.provider.id,
|
|
708
|
-
ok: item.ok,
|
|
709
|
-
})),
|
|
710
|
-
});
|
|
711
|
-
return {
|
|
712
|
-
message,
|
|
713
|
-
cacheRenderedMessage: false,
|
|
714
|
-
retryable: retryableFailure,
|
|
715
|
-
retryReason,
|
|
716
|
-
hasQuotaRows: false,
|
|
717
|
-
detectedProviderIds,
|
|
718
|
-
resetNotification,
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
const retryableNoData = providerFetchFailureOnly ||
|
|
722
|
-
(selection?.isAutoMode === true && active.length > 0 && errors.length === 0);
|
|
723
|
-
return {
|
|
724
|
-
message: runtimeConfig.debug
|
|
725
|
-
? formatDebugInfo({
|
|
726
|
-
trigger: params.trigger,
|
|
727
|
-
reason: "no entries",
|
|
728
|
-
currentModel,
|
|
729
|
-
enabledProviders: runtimeConfig.enabledProviders,
|
|
730
|
-
availability: availability.map((item) => ({
|
|
731
|
-
id: item.provider.id,
|
|
732
|
-
ok: item.ok,
|
|
733
|
-
})),
|
|
734
|
-
})
|
|
735
|
-
: null,
|
|
736
|
-
cacheRenderedMessage: false,
|
|
737
|
-
retryable: retryableNoData,
|
|
738
|
-
retryReason: providerFetchFailureOnly
|
|
739
|
-
? "provider_fetch_failed"
|
|
740
|
-
: retryableNoData
|
|
741
|
-
? "no_reportable_data"
|
|
742
|
-
: undefined,
|
|
743
|
-
hasQuotaRows: false,
|
|
744
|
-
detectedProviderIds,
|
|
745
|
-
resetNotification,
|
|
746
|
-
};
|
|
747
|
-
}
|
|
748
|
-
async function reconcileDeferredQuotaRefresh(params) {
|
|
749
|
-
const existing = deferredQuotaRefreshes.get(params.sessionID);
|
|
750
|
-
if (!params.result.retryable) {
|
|
751
|
-
if (existing) {
|
|
752
|
-
clearDeferredQuotaRefresh(params.sessionID);
|
|
753
|
-
await log("Deferred quota refresh cleared", {
|
|
754
|
-
sessionID: params.sessionID,
|
|
755
|
-
trigger: params.trigger,
|
|
756
|
-
reason: params.result.hasQuotaRows ? "quota_rows_available" : "not_retryable",
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (!params.result.retryReason) {
|
|
762
|
-
return;
|
|
763
|
-
}
|
|
764
|
-
scheduleDeferredQuotaRefresh({
|
|
765
|
-
sessionID: params.sessionID,
|
|
766
|
-
reason: params.result.retryReason,
|
|
767
|
-
incrementAttempts: params.consumedDeferredRetry,
|
|
768
|
-
});
|
|
769
|
-
}
|
|
770
|
-
/**
|
|
771
|
-
* Show quota toast for a session
|
|
772
|
-
*/
|
|
773
|
-
async function showQuotaToast(sessionID, trigger, options = {}) {
|
|
774
|
-
if (!configLoaded) {
|
|
775
|
-
await refreshConfig();
|
|
776
|
-
}
|
|
777
|
-
const pendingDeferred = deferredQuotaRefreshes.get(sessionID);
|
|
778
|
-
const consumedDeferredRetry = options.deferredRetry === true || Boolean(pendingDeferred);
|
|
779
|
-
if (pendingDeferred) {
|
|
780
|
-
if (pendingDeferred.inFlight && !options.deferredRetry) {
|
|
781
|
-
await log("Skipping duplicate deferred quota refresh", { sessionID, trigger });
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
pendingDeferred.inFlight = true;
|
|
785
|
-
clearDeferredQuotaRefreshTimer(pendingDeferred);
|
|
786
|
-
}
|
|
787
|
-
try {
|
|
788
|
-
// Check if session is a subagent session
|
|
789
|
-
if (await isSubagentSession(sessionID)) {
|
|
790
|
-
if (consumedDeferredRetry) {
|
|
791
|
-
clearDeferredQuotaRefresh(sessionID);
|
|
792
|
-
}
|
|
793
|
-
await log("Skipping toast for subagent session", { sessionID, trigger });
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
// Get or fetch quota (with caching/throttling).
|
|
797
|
-
// If debug is enabled, bypass caching so the toast reflects current state.
|
|
798
|
-
const sessionMeta = await getSessionModelMeta(sessionID);
|
|
799
|
-
const bypassForLiveLocalUsage = await shouldBypassToastCacheForLiveLocalUsage({
|
|
800
|
-
sessionID,
|
|
801
|
-
sessionMeta,
|
|
802
|
-
});
|
|
803
|
-
const bypassMessageCache = config.debug || consumedDeferredRetry || bypassForLiveLocalUsage;
|
|
804
|
-
const bypassProviderCache = consumedDeferredRetry || bypassForLiveLocalUsage;
|
|
805
|
-
const toastCacheKey = buildToastCacheKey({ sessionID, sessionMeta });
|
|
806
|
-
let fetchResult;
|
|
807
|
-
const fetchForToast = () => fetchQuotaMessageResult({
|
|
808
|
-
trigger,
|
|
809
|
-
sessionID,
|
|
810
|
-
sessionMeta,
|
|
811
|
-
bypassProviderCache,
|
|
812
|
-
});
|
|
813
|
-
const message = bypassMessageCache
|
|
814
|
-
? await (async () => {
|
|
815
|
-
fetchResult = await fetchForToast();
|
|
816
|
-
return fetchResult.message;
|
|
817
|
-
})()
|
|
818
|
-
: await (async () => {
|
|
819
|
-
const fetched = {};
|
|
820
|
-
const cachedMessage = await getOrFetchWithCacheControl(toastCacheKey, async () => {
|
|
821
|
-
const result = await fetchForToast();
|
|
822
|
-
fetched.result = result;
|
|
823
|
-
const cache = Boolean(result.message && result.cacheRenderedMessage && result.hasQuotaRows);
|
|
824
|
-
return { message: result.message, cache };
|
|
825
|
-
}, config.minIntervalMs);
|
|
826
|
-
fetchResult = fetched.result;
|
|
827
|
-
return cachedMessage;
|
|
828
|
-
})();
|
|
829
|
-
if (fetchResult) {
|
|
830
|
-
detectedProviderIdsByToastCacheKey.set(toastCacheKey, [...fetchResult.detectedProviderIds]);
|
|
831
|
-
await reconcileDeferredQuotaRefresh({
|
|
832
|
-
sessionID,
|
|
833
|
-
result: fetchResult,
|
|
834
|
-
consumedDeferredRetry,
|
|
835
|
-
trigger,
|
|
836
|
-
});
|
|
837
|
-
}
|
|
838
|
-
if (options.deferredRetry && fetchResult && !fetchResult.hasQuotaRows) {
|
|
839
|
-
await log("Deferred quota refresh did not produce reportable data", {
|
|
840
|
-
sessionID,
|
|
841
|
-
trigger,
|
|
842
|
-
retryable: fetchResult.retryable,
|
|
843
|
-
retryReason: fetchResult.retryReason,
|
|
844
|
-
});
|
|
845
|
-
return;
|
|
846
|
-
}
|
|
847
|
-
if (!message) {
|
|
848
|
-
await log("No quota message to display", { trigger });
|
|
849
|
-
return;
|
|
850
|
-
}
|
|
851
|
-
if (!config.enableToast) {
|
|
852
|
-
await log("Toast disabled (enableToast=false)", { trigger });
|
|
853
|
-
return;
|
|
854
|
-
}
|
|
855
|
-
// Show toast
|
|
856
|
-
try {
|
|
857
|
-
await typedClient.tui.showToast({
|
|
858
|
-
body: {
|
|
859
|
-
message: sanitizeDisplayText(message),
|
|
860
|
-
variant: "info",
|
|
861
|
-
duration: config.toastDurationMs,
|
|
862
|
-
},
|
|
863
|
-
});
|
|
864
|
-
triggerMaintainerAnnouncementToastFallback(trigger, fetchResult?.detectedProviderIds ??
|
|
865
|
-
detectedProviderIdsByToastCacheKey.get(toastCacheKey) ??
|
|
866
|
-
[]);
|
|
867
|
-
await log("Displayed quota toast", { message, trigger });
|
|
868
|
-
if (fetchResult?.resetNotification) {
|
|
869
|
-
await typedClient.tui.showToast({
|
|
870
|
-
body: {
|
|
871
|
-
title: "Quota available",
|
|
872
|
-
message: sanitizeDisplayText(fetchResult.resetNotification),
|
|
873
|
-
variant: "success",
|
|
874
|
-
duration: config.toastDurationMs,
|
|
875
|
-
},
|
|
876
|
-
});
|
|
877
|
-
await log("Displayed quota reset notification", {
|
|
878
|
-
message: fetchResult.resetNotification,
|
|
879
|
-
trigger,
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
catch (err) {
|
|
884
|
-
await log("Failed to show toast", {
|
|
885
|
-
error: err instanceof Error ? err.message : String(err),
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
finally {
|
|
890
|
-
const state = deferredQuotaRefreshes.get(sessionID);
|
|
891
|
-
if (state && state === pendingDeferred) {
|
|
892
|
-
state.inFlight = false;
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
}
|
|
200
|
+
const quotaToastRuntime = createQuotaToastRuntime({
|
|
201
|
+
client: typedClient,
|
|
202
|
+
roots: getPluginRuntimeRootHints,
|
|
203
|
+
resolveSessionMeta: (sessionID) => getSessionModelMeta(sessionID),
|
|
204
|
+
isSubagentSession,
|
|
205
|
+
reconcileDetectedProviders: reconcileDetectedProviderConfig,
|
|
206
|
+
setSessionTokenError: (error) => {
|
|
207
|
+
lastSessionTokenError = error;
|
|
208
|
+
},
|
|
209
|
+
showToast: (body) => typedClient.tui.showToast({ body }),
|
|
210
|
+
log,
|
|
211
|
+
onInitialized: (extra) => {
|
|
212
|
+
void typedClient.app
|
|
213
|
+
.log({
|
|
214
|
+
body: {
|
|
215
|
+
service: "quota-toast",
|
|
216
|
+
level: "info",
|
|
217
|
+
message: "plugin initialized",
|
|
218
|
+
extra,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
.catch(() => { });
|
|
222
|
+
},
|
|
223
|
+
});
|
|
896
224
|
// Return hook implementations
|
|
897
225
|
return {
|
|
898
226
|
dispose: async () => {
|
|
@@ -964,34 +292,13 @@ export const QuotaToastPlugin = async ({ client, directory }) => {
|
|
|
964
292
|
if (event.type !== "session.idle" && event.type !== "session.compacted") {
|
|
965
293
|
return;
|
|
966
294
|
}
|
|
967
|
-
|
|
968
|
-
await refreshConfig();
|
|
969
|
-
}
|
|
970
|
-
if (!config.enabled) {
|
|
971
|
-
clearDeferredQuotaRefresh(sessionID);
|
|
972
|
-
return;
|
|
973
|
-
}
|
|
974
|
-
if (event.type === "session.idle" && config.showOnIdle) {
|
|
975
|
-
await showQuotaToast(sessionID, "session.idle");
|
|
976
|
-
}
|
|
977
|
-
else if (event.type === "session.compacted" && config.showOnCompact) {
|
|
978
|
-
await showQuotaToast(sessionID, "session.compacted");
|
|
979
|
-
}
|
|
295
|
+
await quotaToastRuntime.handleTrigger({ sessionID, trigger: event.type });
|
|
980
296
|
},
|
|
981
297
|
// Tool execute hook for question tool
|
|
982
298
|
"tool.execute.after": async (input, _output) => {
|
|
983
299
|
if (input.tool !== "question")
|
|
984
300
|
return;
|
|
985
|
-
|
|
986
|
-
await refreshConfig();
|
|
987
|
-
}
|
|
988
|
-
if (!config.enabled) {
|
|
989
|
-
clearDeferredQuotaRefresh(input.sessionID);
|
|
990
|
-
return;
|
|
991
|
-
}
|
|
992
|
-
if (config.showOnQuestion) {
|
|
993
|
-
await showQuotaToast(input.sessionID, "question");
|
|
994
|
-
}
|
|
301
|
+
await quotaToastRuntime.handleTrigger({ sessionID: input.sessionID, trigger: "question" });
|
|
995
302
|
},
|
|
996
303
|
};
|
|
997
304
|
};
|