From a0aa52a3f4f6da4a60535ced9f31a2be72d72b7a Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 09:07:06 -0700 Subject: [PATCH 01/76] feat(usage): add a Usage tab with provider limits and usage history --- packages/types/src/history.ts | 19 + packages/types/src/providers/vscode-llm.ts | 58 ++ packages/types/src/vscode-extension-host.ts | 6 +- .../taskMetadata.usageByModel.spec.ts | 82 ++ .../__tests__/usageByModel.spec.ts | 113 ++ src/core/task-persistence/taskMetadata.ts | 5 + src/core/task-persistence/usageByModel.ts | 51 + src/core/task/Task.ts | 5 + ...bviewMessageHandler.providerQuotas.spec.ts | 215 ++++ src/core/webview/webviewMessageHandler.ts | 45 + .../__tests__/copilotQuota.spec.ts | 204 ++++ .../__tests__/providerQuotas.spec.ts | 970 ++++++++++++++++++ .../__tests__/quotaUtils.spec.ts | 109 ++ src/services/language-models/copilotQuota.ts | 132 +++ .../language-models/providerQuotas.ts | 673 ++++++++++++ src/services/language-models/quotaUtils.ts | 66 ++ webview-ui/playwright/gallery/stories.tsx | 12 + .../components/settings/ProviderLimits.tsx | 186 ++++ .../src/components/settings/SettingsView.tsx | 7 + .../src/components/settings/UsageSettings.tsx | 441 ++++++++ .../__tests__/ProviderLimits.spec.tsx | 255 +++++ .../__tests__/UsageSettings.branches.spec.tsx | 276 +++++ .../__tests__/UsageSettings.midnight.spec.tsx | 103 ++ .../settings/__tests__/UsageSettings.spec.tsx | 263 +++++ .../UsageSettings.visual.fixture.tsx | 143 +++ .../__tests__/UsageSettings.visual.tsx | 59 ++ webview-ui/src/i18n/locales/ca/settings.json | 54 +- webview-ui/src/i18n/locales/de/settings.json | 54 +- webview-ui/src/i18n/locales/en/settings.json | 54 +- webview-ui/src/i18n/locales/es/settings.json | 54 +- webview-ui/src/i18n/locales/fr/settings.json | 54 +- webview-ui/src/i18n/locales/hi/settings.json | 54 +- webview-ui/src/i18n/locales/id/settings.json | 54 +- webview-ui/src/i18n/locales/it/settings.json | 54 +- webview-ui/src/i18n/locales/ja/settings.json | 54 +- webview-ui/src/i18n/locales/ko/settings.json | 54 +- webview-ui/src/i18n/locales/nl/settings.json | 54 +- webview-ui/src/i18n/locales/pl/settings.json | 54 +- .../src/i18n/locales/pt-BR/settings.json | 54 +- webview-ui/src/i18n/locales/ru/settings.json | 54 +- webview-ui/src/i18n/locales/tr/settings.json | 54 +- webview-ui/src/i18n/locales/vi/settings.json | 54 +- .../src/i18n/locales/zh-CN/settings.json | 54 +- .../src/i18n/locales/zh-TW/settings.json | 54 +- .../src/utils/__tests__/providerLabel.spec.ts | 14 + .../src/utils/__tests__/usage.dst.spec.ts | 109 ++ webview-ui/src/utils/__tests__/usage.spec.ts | 484 +++++++++ webview-ui/src/utils/providerLabel.ts | 6 + webview-ui/src/utils/usage.ts | 340 ++++++ 49 files changed, 6404 insertions(+), 19 deletions(-) create mode 100644 src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts create mode 100644 src/core/task-persistence/__tests__/usageByModel.spec.ts create mode 100644 src/core/task-persistence/usageByModel.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts create mode 100644 src/services/language-models/__tests__/copilotQuota.spec.ts create mode 100644 src/services/language-models/__tests__/providerQuotas.spec.ts create mode 100644 src/services/language-models/__tests__/quotaUtils.spec.ts create mode 100644 src/services/language-models/copilotQuota.ts create mode 100644 src/services/language-models/providerQuotas.ts create mode 100644 src/services/language-models/quotaUtils.ts create mode 100644 webview-ui/src/components/settings/ProviderLimits.tsx create mode 100644 webview-ui/src/components/settings/UsageSettings.tsx create mode 100644 webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx create mode 100644 webview-ui/src/utils/__tests__/providerLabel.spec.ts create mode 100644 webview-ui/src/utils/__tests__/usage.dst.spec.ts create mode 100644 webview-ui/src/utils/__tests__/usage.spec.ts create mode 100644 webview-ui/src/utils/providerLabel.ts create mode 100644 webview-ui/src/utils/usage.ts diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 131194212e..21cef89e47 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -26,6 +26,23 @@ export const pendingTaskActionSchema = z.discriminatedUnion("kind", [ export type PendingTaskAction = z.infer +/** + * Token/cost totals for one provider+model within a task. A task can span + * several (the user may switch profiles mid-task), so history keeps a list. + */ +export const usageByModelEntrySchema = z.object({ + apiProvider: z.string().optional(), + modelId: z.string().optional(), + requests: z.number(), + tokensIn: z.number(), + tokensOut: z.number(), + cacheReads: z.number().optional(), + cacheWrites: z.number().optional(), + cost: z.number(), +}) + +export type UsageByModelEntry = z.infer + export const historyItemSchema = z.object({ id: z.string(), rootTaskId: z.string().optional(), @@ -39,6 +56,8 @@ export const historyItemSchema = z.object({ cacheWrites: z.number().optional(), cacheReads: z.number().optional(), totalCost: z.number(), + /** Per provider+model breakdown, recorded as requests happen. Absent on tasks created before this was tracked. */ + usageByModel: z.array(usageByModelEntrySchema).optional(), size: z.number().optional(), workspace: z.string().optional(), mode: z.string().optional(), diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index b6ba122d53..91e74e54fd 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -11,8 +11,66 @@ export const vsCodeLmModelsMessageTypes = [ "githubCopilotManageAccount", "githubCopilotSignInResult", "githubCopilotModels", + "requestProviderQuotas", + "cancelProviderQuotas", + "providerQuotas", ] as const +/** + * One limit/balance a provider reports (e.g. Copilot premium credits for the + * month, an OpenRouter key limit, a DeepSeek prepaid balance). + */ +export interface ProviderQuotaWindow { + /** Stable key, e.g. "premium", "key_limit", "balance". */ + id: string + /** Pre-formatted primary figure, e.g. "39,389.3 / 140,000 used", "$12.40 left". */ + valueLabel: string + /** 0–100 when the provider reports a bounded allowance. */ + usedPercent?: number + /** Epoch ms. */ + resetAt?: number +} + +export type ProviderQuota = + | { + providerId: string + ok: true + plan?: string + windows: ProviderQuotaWindow[] + fetchedAt: number + } + | { + providerId: string + ok: false + reason: "signedOut" | "noCopilot" | "noKey" | "unsupported" | "error" + message?: string + fetchedAt: number + } + +/** + * The signed-in account's GitHub Copilot premium-request allowance, as reported by + * `GET /copilot_internal/user` (the same source Copilot Chat's own usage panel reads). + */ +export interface GitHubCopilotQuota { + /** e.g. "business", "individual". */ + plan?: string + unlimited: boolean + /** Total credits for the period; undefined when unlimited or not reported. */ + entitlement?: number + /** Credits used this period, `entitlement - quota_remaining` (one decimal, like GitHub's panel). */ + used?: number + /** 0–100. */ + usedPercent?: number + /** Epoch ms when the allowance resets. */ + resetAt?: number + /** Epoch ms when this snapshot was fetched. */ + fetchedAt: number +} + +export type GitHubCopilotQuotaResult = + | { ok: true; quota: GitHubCopilotQuota } + | { ok: false; reason: "signedOut" | "noCopilot" | "error"; message?: string } + export const vsCodeLmModelsMessageTypeSchema = z.enum(vsCodeLmModelsMessageTypes) export const VsCodeLmModelsMessageType = vsCodeLmModelsMessageTypeSchema.enum diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index dc407325d9..3870bf33d4 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -16,7 +16,7 @@ import { RouterModelsMessageType, type ModelInfo, type ModelRecord, type RouterM import { LmStudioModelsMessageType } from "./providers/lm-studio.js" import { OllamaModelsMessageType } from "./providers/ollama.js" import { OpenAiModelsMessageType } from "./providers/openai.js" -import { VsCodeLmModelsMessageType } from "./providers/vscode-llm.js" +import { VsCodeLmModelsMessageType, type ProviderQuota } from "./providers/vscode-llm.js" import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js" import type { SkillMetadata } from "./skills.js" import type { RuleMetadata } from "./rules.js" @@ -50,6 +50,7 @@ export interface ExtensionMessage { | typeof VsCodeLmModelsMessageType.vsCodeLmModels | typeof VsCodeLmModelsMessageType.githubCopilotModels | typeof VsCodeLmModelsMessageType.githubCopilotSignInResult + | typeof VsCodeLmModelsMessageType.providerQuotas | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" @@ -158,6 +159,7 @@ export interface ExtensionMessage { modelInfo?: ModelInfo }[] githubCopilotAccount?: string | null + providerQuotas?: ProviderQuota[] mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] @@ -516,6 +518,8 @@ export interface WebviewMessage { | typeof VsCodeLmModelsMessageType.githubCopilotSignIn | typeof VsCodeLmModelsMessageType.githubCopilotReconnect | typeof VsCodeLmModelsMessageType.githubCopilotManageAccount + | typeof VsCodeLmModelsMessageType.requestProviderQuotas + | typeof VsCodeLmModelsMessageType.cancelProviderQuotas | "openImage" | "saveImage" | "openFile" diff --git a/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts new file mode 100644 index 0000000000..a16e27259d --- /dev/null +++ b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type ClineMessage } from "@roo-code/types" + +vi.mock("get-folder-size", () => ({ default: { loose: vi.fn(async () => 1234) } })) +vi.mock("../../../utils/storage", () => ({ getTaskDirectoryPath: vi.fn(async () => "/tmp/task") })) +vi.mock("../../../i18n", () => ({ t: (k: string) => k })) + +import { taskMetadata } from "../taskMetadata" + +const say = (say: ClineMessage["say"], text: string, ts: number): ClineMessage => ({ ts, type: "say", say, text }) + +const base = { + taskId: "t1", + taskNumber: 1, + globalStoragePath: "/tmp", + workspace: "/ws", +} + +describe("taskMetadata – usageByModel", () => { + it("records per provider+model usage on the history item", async () => { + const messages = [ + say("text", "do the thing", 1), + say( + "api_req_started", + JSON.stringify({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 900, + tokensOut: 90, + cost: 0, + }), + 2, + ), + say( + "api_req_started", + JSON.stringify({ + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + tokensIn: 100, + tokensOut: 10, + cost: 0.2, + }), + 3, + ), + ] + const { historyItem } = await taskMetadata({ ...base, messages }) + + expect(historyItem.usageByModel).toEqual([ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 900, + tokensOut: 90, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + requests: 1, + tokensIn: 100, + tokensOut: 10, + cost: 0.2, + }, + ]) + // Existing totals are unchanged by the new breakdown. + expect(historyItem.tokensIn).toBe(1000) + expect(historyItem.totalCost).toBeCloseTo(0.2) + }) + + it("omits usageByModel entirely when nothing was recorded, keeping old items' shape", async () => { + const { historyItem } = await taskMetadata({ ...base, messages: [say("text", "hello", 1)] }) + expect(historyItem).not.toHaveProperty("usageByModel") + }) + + it("omits usageByModel for a task with no messages", async () => { + const { historyItem } = await taskMetadata({ ...base, messages: [] }) + expect(historyItem).not.toHaveProperty("usageByModel") + expect(historyItem.tokensIn).toBe(0) + }) +}) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts new file mode 100644 index 0000000000..092272ff6d --- /dev/null +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest" + +import { providerIdentifiers, type ClineMessage } from "@roo-code/types" + +import { summarizeUsageByModel } from "../usageByModel" + +const req = (data: Record, ts = 1): ClineMessage => ({ + ts, + type: "say", + say: "api_req_started", + text: JSON.stringify(data), +}) + +describe("summarizeUsageByModel", () => { + it("groups requests by the provider+model recorded on each request", () => { + const result = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 1000, + tokensOut: 100, + cost: 0, + }), + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 500, + tokensOut: 50, + cost: 0, + }), + req({ + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-3.1-pro-preview", + tokensIn: 200, + tokensOut: 20, + cost: 0.4, + }), + ]) + + expect(result).toEqual([ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 2, + tokensIn: 1500, + tokensOut: 150, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-3.1-pro-preview", + requests: 1, + tokensIn: 200, + tokensOut: 20, + cost: 0.4, + }, + ]) + }) + + it("keeps a task that switched providers mid-way split by what actually served each request", () => { + const result = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 100, + tokensOut: 10, + }), + req({ apiProvider: providerIdentifiers.gemini, modelId: "gemini-pro", tokensIn: 300, tokensOut: 30 }), + ]) + expect(result.map((r) => r.apiProvider)).toEqual([ + providerIdentifiers.githubCopilot, + providerIdentifiers.gemini, + ]) + }) + + it("sums cache counters only when reported", () => { + const [entry] = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.anthropic, + modelId: "m", + tokensIn: 1, + tokensOut: 1, + cacheReads: 5, + cacheWrites: 2, + }), + req({ apiProvider: providerIdentifiers.anthropic, modelId: "m", tokensIn: 1, tokensOut: 1, cacheReads: 3 }), + ]) + expect(entry.cacheReads).toBe(8) + expect(entry.cacheWrites).toBe(2) + + const [bare] = summarizeUsageByModel([req({ apiProvider: "a", modelId: "m", tokensIn: 1, tokensOut: 1 })]) + expect(bare).not.toHaveProperty("cacheReads") + }) + + it("puts requests recorded before this was tracked in an unrecorded entry rather than guessing", () => { + const [entry] = summarizeUsageByModel([req({ apiProtocol: "openai", tokensIn: 10, tokensOut: 5, cost: 0 })]) + expect(entry).toMatchObject({ apiProvider: undefined, modelId: undefined, requests: 1, tokensIn: 10 }) + }) + + it("ignores in-flight placeholders and non-request messages", () => { + const result = summarizeUsageByModel([ + req({ apiProtocol: "openai", apiProvider: providerIdentifiers.gemini, modelId: "m" }), // no counts yet + { ts: 2, type: "say", say: "text", text: "hello" }, + { ts: 3, type: "say", say: "api_req_started", text: "not json" }, + { ts: 4, type: "say", say: "api_req_started" }, + ]) + expect(result).toEqual([]) + }) + + it("returns an empty list for no messages", () => { + expect(summarizeUsageByModel([])).toEqual([]) + }) +}) diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index c52b0bdc33..827095f67a 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -5,6 +5,7 @@ import type { ClineMessage, HistoryItem } from "@roo-code/types" import { combineApiRequests } from "../../shared/combineApiRequests" import { combineCommandSequences } from "../../shared/combineCommandSequences" +import { summarizeUsageByModel } from "./usageByModel" import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" @@ -89,6 +90,8 @@ export async function taskMetadata({ } } + const usageByModel = hasMessages ? summarizeUsageByModel(messages) : [] + // Create historyItem once with pre-calculated values. // initialStatus is included when provided (e.g., "active" for child tasks) // to ensure the status is set from the very first save, avoiding race conditions @@ -107,6 +110,8 @@ export async function taskMetadata({ cacheWrites: tokenUsage.totalCacheWrites, cacheReads: tokenUsage.totalCacheReads, totalCost: tokenUsage.totalCost, + // Only emitted when something was recorded, so older tasks and empty tasks keep their exact shape. + ...(usageByModel.length > 0 ? { usageByModel } : {}), size: taskDirSize, workspace, mode, diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts new file mode 100644 index 0000000000..16027c281e --- /dev/null +++ b/src/core/task-persistence/usageByModel.ts @@ -0,0 +1,51 @@ +import type { ClineMessage, UsageByModelEntry } from "@roo-code/types" + +/** + * Totals per provider+model from a task's `api_req_started` messages. + * + * Provider and model are read from the message itself (recorded when the + * request was made), never from the *current* profile, so a task that ran on + * Copilot keeps saying so after the user switches providers. A task can span + * several models, hence a list. + * + * Messages written before this was recorded carry no provider/model; their + * tokens are grouped under an entry with both fields undefined ("not + * recorded") rather than guessed. + */ +export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEntry[] { + const byKey = new Map() + + for (const message of messages) { + if (message.type !== "say" || message.say !== "api_req_started" || !message.text) continue + + let parsed: Record + try { + parsed = JSON.parse(message.text) + } catch { + continue + } + + // A placeholder (request not finished) has no token counts yet. + if (typeof parsed.tokensIn !== "number" && typeof parsed.tokensOut !== "number") continue + + const apiProvider = typeof parsed.apiProvider === "string" ? parsed.apiProvider : undefined + const modelId = typeof parsed.modelId === "string" ? parsed.modelId : undefined + const key = `${apiProvider ?? ""}\u0000${modelId ?? ""}` + + let entry = byKey.get(key) + if (!entry) { + entry = { apiProvider, modelId, requests: 0, tokensIn: 0, tokensOut: 0, cost: 0 } + byKey.set(key, entry) + } + + const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : 0) + entry.requests += 1 + entry.tokensIn += num(parsed.tokensIn) + entry.tokensOut += num(parsed.tokensOut) + entry.cost += num(parsed.cost) + if (typeof parsed.cacheReads === "number") entry.cacheReads = (entry.cacheReads ?? 0) + parsed.cacheReads + if (typeof parsed.cacheWrites === "number") entry.cacheWrites = (entry.cacheWrites ?? 0) + parsed.cacheWrites + } + + return [...byKey.values()] +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e573a7f107..d5e8cc7b79 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3936,6 +3936,9 @@ export class Task extends EventEmitter implements TaskLike { "api_req_started", JSON.stringify({ apiProtocol, + // Recorded now so Usage reflects what served this request, not the profile selected later. + apiProvider, + modelId, }), ) @@ -4013,6 +4016,8 @@ export class Task extends EventEmitter implements TaskLike { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ apiProtocol, + apiProvider, + modelId, } satisfies ClineApiReqInfo) await this.saveClineMessages() diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts new file mode 100644 index 0000000000..d83aa92aac --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -0,0 +1,215 @@ +// npx vitest core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts + +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers } from "@roo-code/types" + +const quotas = vi.hoisted(() => ({ + getProviderQuotas: vi.fn(), +})) + +// Keep the real `loadProfiles` (it is the code under test alongside the handler) and stub only the network part. +vi.mock("../../../services/language-models/providerQuotas", async (importOriginal) => ({ + ...(await importOriginal()), + getProviderQuotas: quotas.getProviderQuotas, +})) + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +const SECRET = "sk-super-secret" + +const makeProvider = (overrides: Record = {}) => + ({ + postMessageToWebview: vi.fn(), + providerSettingsManager: { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: SECRET, + id, + })), + }, + ...overrides, + }) as unknown as ClineProvider & { postMessageToWebview: ReturnType } + +describe("webviewMessageHandler – requestProviderQuotas", () => { + beforeEach(() => { + quotas.getProviderQuotas.mockReset() + quotas.getProviderQuotas.mockResolvedValue([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt: 1, + windows: [{ id: "key_limit", valueLabel: "$5.00 / $10.00 used", usedPercent: 50 }], + }, + ]) + }) + + it("loads every saved profile and posts the formatted quotas back", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.providerSettingsManager.getProfile).toHaveBeenCalledTimes(2) + expect(quotas.getProviderQuotas.mock.calls[0][0]).toHaveLength(2) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: expect.arrayContaining([ + expect.objectContaining({ providerId: providerIdentifiers.openrouter }), + ]), + requestId: undefined, + }) + }) + + it("does not force a refresh by default, and forces when the user presses refresh", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: false, + signal: expect.any(AbortSignal), + }) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas", bool: true }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: true, + signal: expect.any(AbortSignal), + }) + }) + + it("passes API keys to the host service but never posts them to the webview", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + // The service needs the key... + expect(JSON.stringify(quotas.getProviderQuotas.mock.calls[0][0])).toContain(SECRET) + // ...but nothing posted to the webview may contain it. + expect(JSON.stringify(provider.postMessageToWebview.mock.calls)).not.toContain(SECRET) + }) + + it("still answers when one profile cannot be loaded", async () => { + const provider = makeProvider() + provider.providerSettingsManager.getProfile = vi.fn(async ({ id }: { id: string }) => { + if (id === "a") throw new Error("corrupt secret storage") + return { apiProvider: providerIdentifiers.openrouter, openRouterApiKey: SECRET } as never + }) as never + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(quotas.getProviderQuotas.mock.calls[0][0]).toHaveLength(1) + expect(provider.postMessageToWebview).toHaveBeenCalled() + }) + + it("still answers, and logs, when the profile list cannot be read", async () => { + const log = vi.fn() + const provider = makeProvider({ log }) + provider.providerSettingsManager.listConfig = vi.fn(async () => { + throw new Error("secret storage unavailable") + }) as never + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) + expect(log).toHaveBeenCalledWith(expect.stringContaining("secret storage unavailable")) + }) + + it("still answers when the quota service throws something that is not an Error", async () => { + const log = vi.fn() + const provider = makeProvider({ log }) + quotas.getProviderQuotas.mockRejectedValue("nope") + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) + expect(log).toHaveBeenCalledWith(expect.stringContaining("Failed to load provider limits")) + }) + + it("answers with an empty list when there are no profiles", async () => { + quotas.getProviderQuotas.mockResolvedValue([]) + const provider = makeProvider() + provider.providerSettingsManager.listConfig = vi.fn(async () => []) as never + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) + }) + + describe("request lifecycle", () => { + it("echoes the request id on the answer", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-1" }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "providerQuotas", requestId: "req-1" }), + ) + }) + + it("passes an abort signal to the quota service", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-2" }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: false, + signal: expect.any(AbortSignal), + }) + }) + + it("aborts the work and posts nothing when the request is cancelled while it is running", async () => { + let signal: AbortSignal | undefined + let finish: (value: never[]) => void = () => {} + quotas.getProviderQuotas.mockImplementation((_profiles, options: { signal: AbortSignal }) => { + signal = options.signal + return new Promise((resolve) => (finish = resolve)) + }) + const provider = makeProvider() + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-3" }) + await vi.waitFor(() => expect(signal).toBeDefined()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-3" }) + + expect(signal!.aborted).toBe(true) + finish([]) + await running + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("does not post when the request was cancelled before the profiles finished loading", async () => { + const provider = makeProvider() + let release: (entries: { id: string }[]) => void = () => {} + provider.providerSettingsManager.listConfig = vi.fn( + () => new Promise((resolve) => (release = resolve)), + ) as never + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-4" }) + await vi.waitFor(() => expect(provider.providerSettingsManager.listConfig).toHaveBeenCalled()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-4" }) + release([{ id: "a" }]) + await running + + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("ignores a cancel for an unknown or missing id, and for a request that already finished", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-5" }) + provider.postMessageToWebview.mockClear() + + await expect( + webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-5" }), + ).resolves.not.toThrow() + await expect( + webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "nope" }), + ).resolves.not.toThrow() + await expect(webviewMessageHandler(provider, { type: "cancelProviderQuotas" })).resolves.not.toThrow() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index bd7c589958..207403c5da 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -30,6 +30,7 @@ import { RouterModelsMessageType, VsCodeLmModelsMessageType, isTelemetryOptedIn, + type ProviderQuota, } from "@roo-code/types" import { customToolRegistry } from "@roo-code/core" import { CloudService } from "@roo-code/cloud" @@ -130,6 +131,9 @@ import { const handlerFor = (apiConfiguration: ProviderSettings, provider: RouterName): ApiHandler => buildApiHandler({ ...apiConfiguration, apiProvider: provider }) +/** In-flight `requestProviderQuotas` work by request id, so a cancel message can abort it. */ +const pendingProviderQuotaRequests = new Map() + export const webviewMessageHandler = async ( provider: ClineProvider, message: WebviewMessage, @@ -1570,6 +1574,47 @@ export const webviewMessageHandler = async ( } break } + case VsCodeLmModelsMessageType.cancelProviderQuotas: + // The Usage tab was closed (or the request superseded): stop the profile load and any fetches. + if (message.requestId) pendingProviderQuotaRequests.get(message.requestId)?.abort() + break + case VsCodeLmModelsMessageType.requestProviderQuotas: { + // Always answer (unless cancelled): the webview shows a spinner until a response arrives and + // disables refresh while it waits, so a thrown error here would leave it stuck. The answer + // carries the request's id so the webview can ignore one that has been superseded. + const requestId = message.requestId + const controller = new AbortController() + if (requestId) pendingProviderQuotaRequests.set(requestId, controller) + let providerQuotas: ProviderQuota[] = [] + try { + const { getProviderQuotas, loadProfiles } = + await import("../../services/language-models/providerQuotas") + const profiles = await loadProfiles(provider.providerSettingsManager) + if (!controller.signal.aborted) { + providerQuotas = await getProviderQuotas(profiles, { + force: message.bool === true, + signal: controller.signal, + }) + } + } catch (error) { + if (!controller.signal.aborted) { + provider.log( + `[requestProviderQuotas] ${error instanceof Error ? error.message : "Failed to load provider limits"}`, + ) + } + } finally { + if (requestId) pendingProviderQuotaRequests.delete(requestId) + } + // Nobody is waiting for a cancelled request: skip the post. + if (!controller.signal.aborted) { + await provider.postMessageToWebview({ + type: VsCodeLmModelsMessageType.providerQuotas, + providerQuotas, + requestId, + }) + } + break + } case VsCodeLmModelsMessageType.githubCopilotManageAccount: try { await openGitHubAccountManagement() diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts new file mode 100644 index 0000000000..65996ad300 --- /dev/null +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -0,0 +1,204 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) + +import { parseCopilotQuota } from "../copilotQuota" + +const NOW = 1_800_000_000_000 + +// Shape recorded from GET api.github.com/copilot_internal/user (business plan). +const businessPayload = { + copilot_plan: "business", + quota_reset_date: "2026-11-01", + quota_reset_date_utc: "2026-11-01T00:00:00.000Z", + quota_snapshots: { + chat: { unlimited: true, entitlement: 0, remaining: 0 }, + completions: { unlimited: true, entitlement: 0, remaining: 0 }, + premium_interactions: { + entitlement: 140000, + remaining: 100610, + quota_remaining: 100610.7, + unlimited: false, + percent_remaining: 71.8, + }, + }, +} + +describe("parseCopilotQuota", () => { + it("derives used credits from the precise quota_remaining, like GitHub's panel", () => { + const q = parseCopilotQuota(businessPayload, NOW)! + expect(q.plan).toBe("business") + expect(q.unlimited).toBe(false) + expect(q.entitlement).toBe(140000) + expect(q.used).toBe(39389.3) + expect(q.usedPercent).toBeCloseTo(28.135, 2) + expect(q.resetAt).toBe(Date.parse("2026-11-01T00:00:00.000Z")) + expect(q.fetchedAt).toBe(NOW) + }) + + it("falls back to the integer remaining twin", () => { + const payload = structuredClone(businessPayload) + delete (payload.quota_snapshots.premium_interactions as Record).quota_remaining + expect(parseCopilotQuota(payload, NOW)!.used).toBe(39390) + }) + + it("falls back to percent_remaining when counts are missing", () => { + const q = parseCopilotQuota( + { quota_snapshots: { premium_interactions: { unlimited: false, percent_remaining: 40 } } }, + NOW, + )! + expect(q.used).toBeUndefined() + expect(q.entitlement).toBeUndefined() + expect(q.usedPercent).toBe(60) + }) + + it("leaves usage unset when neither counts nor percent_remaining are reported", () => { + const q = parseCopilotQuota({ quota_snapshots: { premium_interactions: { unlimited: false } } }, NOW)! + expect(q.used).toBeUndefined() + expect(q.usedPercent).toBeUndefined() + }) + + it("reports unlimited plans without numbers", () => { + const q = parseCopilotQuota( + { copilot_plan: "enterprise", quota_snapshots: { premium_interactions: { unlimited: true } } }, + NOW, + )! + expect(q).toMatchObject({ plan: "enterprise", unlimited: true }) + expect(q.usedPercent).toBeUndefined() + }) + + it("clamps over-consumption to 100%", () => { + const q = parseCopilotQuota( + { quota_snapshots: { premium_interactions: { entitlement: 300, quota_remaining: -12 } } }, + NOW, + )! + expect(q.used).toBe(300) + expect(q.usedPercent).toBe(100) + }) + + it("uses the date-only reset field when the UTC variant is absent", () => { + const payload = { ...businessPayload, quota_reset_date_utc: undefined } + expect(parseCopilotQuota(payload, NOW)!.resetAt).toBe(Date.parse("2026-11-01")) + }) + + it.each([null, undefined, "nope", [], {}, { quota_snapshots: {} }])("returns undefined for %j", (payload) => { + expect(parseCopilotQuota(payload, NOW)).toBeUndefined() + }) +}) + +// --- getGitHubCopilotQuota (network + auth paths) ---------------------------------------------- + +import * as vscodeApi from "vscode" +import { getGitHubCopilotQuota, resetGitHubCopilotQuotaCache } from "../copilotQuota" + +const getSession = vscodeApi.authentication.getSession as unknown as ReturnType + +const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status }) + +describe("getGitHubCopilotQuota", () => { + beforeEach(() => { + resetGitHubCopilotQuotaCache() + getSession.mockReset() + getSession.mockResolvedValue({ accessToken: "gho_token" }) + }) + + it("returns the parsed quota using the existing VS Code GitHub session, silently", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + const result = await getGitHubCopilotQuota({}, fetchSpy as never) + + expect(result).toMatchObject({ ok: true, quota: { plan: "business", entitlement: 140000 } }) + // Silent: must never raise a sign-in prompt from a background usage refresh. + expect(getSession).toHaveBeenCalledWith("github", expect.any(Array), { silent: true }) + expect(fetchSpy).toHaveBeenCalledWith( + "https://api.github.com/copilot_internal/user", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "token gho_token" }), + }), + ) + }) + + it("falls back to the second scope set when the first has no session", async () => { + getSession.mockResolvedValueOnce(undefined).mockResolvedValueOnce({ accessToken: "second" }) + const fetchSpy = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, fetchSpy as never) + expect(getSession).toHaveBeenCalledTimes(2) + expect(fetchSpy).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "token second" }) }), + ) + }) + + it("reports signedOut without calling GitHub when there is no session", async () => { + getSession.mockResolvedValue(undefined) + const fetchSpy = vi.fn() + expect(await getGitHubCopilotQuota({}, fetchSpy as never)).toEqual({ ok: false, reason: "signedOut" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it.each([ + [401, "signedOut"], + [403, "noCopilot"], + [404, "noCopilot"], + ] as const)("maps HTTP %s to %s", async (status, reason) => { + const result = await getGitHubCopilotQuota({}, (async () => json({}, status)) as never) + expect(result).toMatchObject({ ok: false, reason }) + }) + + it("reports other HTTP failures as an error with the status", async () => { + const result = await getGitHubCopilotQuota({}, (async () => json({}, 500)) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "GitHub API error 500" }) + }) + + it("reports noCopilot when the payload has no premium snapshot", async () => { + const result = await getGitHubCopilotQuota({}, (async () => json({ quota_snapshots: {} })) as never) + expect(result).toMatchObject({ ok: false, reason: "noCopilot" }) + }) + + it("turns network errors into a generic error result instead of throwing", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw new Error("offline") + }) as never) + // Arbitrary error text is never forwarded: it can embed URLs, headers or tokens. + expect(result).toEqual({ ok: false, reason: "error", message: "Request failed" }) + }) + + it("never leaks the access token through an error message", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw new Error("request to https://api.github.com failed: Authorization: token gho_token") + }) as never) + expect(JSON.stringify(result)).not.toContain("gho_token") + }) + + it("reports a timeout plainly", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw Object.assign(new Error("The operation timed out"), { name: "TimeoutError" }) + }) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "Request timed out" }) + }) + + it("caches successful results and bypasses the cache when forced", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, fetchSpy as never) + await getGitHubCopilotQuota({}, fetchSpy as never) + expect(fetchSpy).toHaveBeenCalledTimes(1) + await getGitHubCopilotQuota({ force: true }, fetchSpy as never) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it("does not cache failures", async () => { + const fail = vi.fn(async () => json({}, 500)) + await getGitHubCopilotQuota({}, fail as never) + await getGitHubCopilotQuota({}, fail as never) + expect(fail).toHaveBeenCalledTimes(2) + }) + + it("shares one in-flight request between concurrent callers", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + const [a, b] = await Promise.all([ + getGitHubCopilotQuota({}, fetchSpy as never), + getGitHubCopilotQuota({}, fetchSpy as never), + ]) + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(a).toEqual(b) + }) +}) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts new file mode 100644 index 0000000000..b27a7830aa --- /dev/null +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -0,0 +1,970 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers } from "@roo-code/types" + +vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) +vi.mock("../copilotQuota", () => ({ + getGitHubCopilotQuota: vi.fn(async () => ({ + ok: true, + quota: { + plan: "business", + unlimited: false, + entitlement: 140000, + used: 39389.3, + usedPercent: 28.1, + resetAt: 1, + fetchedAt: 2, + }, + })), +})) + +import { + getProviderQuotas, + loadProfiles, + parseDeepSeekBalance, + parseKimiCodeUsage, + parseMiniMaxRemains, + parseMoonshotBalance, + parseNanoGptUsage, + parseOpenCodeGoUsage, + parseOpenRouterKey, + parseZaiQuota, + resetProviderQuotaCache, + windowLabel, +} from "../providerQuotas" + +describe("parseOpenRouterKey", () => { + it("reports a bounded key limit as used / limit with a percent", () => { + expect(parseOpenRouterKey({ data: { limit: 50, limit_remaining: 37.6 } })).toEqual([ + { id: "key_limit", valueLabel: "$12.40 / $50.00 used", usedPercent: expect.closeTo(24.8, 5) }, + ]) + }) + + it("falls back to monthly spend for unlimited keys", () => { + expect(parseOpenRouterKey({ data: { limit: null, usage_monthly: 3.2 } })).toEqual([ + { id: "spend", valueLabel: "$3.20 spent this month" }, + ]) + }) + + it("returns undefined without data", () => { + expect(parseOpenRouterKey({})).toBeUndefined() + expect(parseOpenRouterKey({ data: {} })).toBeUndefined() + }) +}) + +describe("parseDeepSeekBalance", () => { + it("prefers a positive USD balance", () => { + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "CNY", total_balance: "10.00" }, + { currency: "USD", total_balance: "4.25" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "$4.25 left" }]) + }) + + it("uses ¥ for a CNY-only balance", () => { + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "CNY", total_balance: "8" }] })).toEqual([ + { id: "balance", valueLabel: "¥8.00 left" }, + ]) + }) + + it("returns undefined for empty or malformed payloads", () => { + expect(parseDeepSeekBalance({ balance_infos: [] })).toBeUndefined() + expect(parseDeepSeekBalance(null)).toBeUndefined() + }) +}) + +describe("parseDeepSeekBalance – fallbacks", () => { + it("falls back to a zero USD balance, then to the first row", () => { + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "EUR", total_balance: "0" }, + { currency: "USD", total_balance: "0" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "$0.00 left" }]) + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "EUR", total_balance: "3" }] })).toEqual([ + { id: "balance", valueLabel: "$3.00 left" }, + ]) + }) +}) + +describe("parseMoonshotBalance", () => { + it("reads available_balance", () => { + expect(parseMoonshotBalance({ data: { available_balance: 19.5 } })).toEqual([ + { id: "balance", valueLabel: "$19.50 left" }, + ]) + }) +}) + +describe("windowLabel", () => { + it("names rolling windows by length", () => { + expect(windowLabel(5 * 3600, "x")).toBe("5-hour") + expect(windowLabel(7 * 86400, "x")).toBe("Weekly") + expect(windowLabel(3 * 86400, "x")).toBe("3-day") + expect(windowLabel(undefined, "Tokens")).toBe("Tokens") + expect(windowLabel(90, "Tokens")).toBe("Tokens") + }) +}) + +describe("parseKimiCodeUsage", () => { + it("reads the ratio-based usages block in display order", () => { + expect( + parseKimiCodeUsage({ + usages: { + limit_month_total: { used_ratio: 0.1 }, + limit_5h: { used_ratio: 0.25, reset_time: 1_800_000_000 }, + limit_7d: { used_ratio: 0.5 }, + }, + }), + ).toEqual([ + { id: "limit_5h", valueLabel: "5-hour", usedPercent: 25, resetAt: 1_800_000_000_000 }, + { id: "limit_7d", valueLabel: "Weekly", usedPercent: 50, resetAt: undefined }, + { id: "limit_month_total", valueLabel: "Monthly", usedPercent: expect.closeTo(10, 5), resetAt: undefined }, + ]) + }) + + it("skips entries without a finite ratio", () => { + expect(parseKimiCodeUsage({ usages: { limit_5h: {}, limit_7d: { used_ratio: "x" } } })).toBeUndefined() + }) + + it("falls back to the legacy weekly usage block, deriving used from remaining", () => { + expect(parseKimiCodeUsage({ usage: { limit: 200, remaining: 150 } })).toEqual([ + { id: "weekly", valueLabel: "Weekly", usedPercent: 25, resetAt: undefined }, + ]) + }) + + it("returns undefined for junk", () => { + expect(parseKimiCodeUsage(null)).toBeUndefined() + expect(parseKimiCodeUsage({})).toBeUndefined() + }) +}) + +describe("parseZaiQuota", () => { + it("maps 5-hour and weekly limits, including credit counts", () => { + expect( + parseZaiQuota({ + code: 200, + data: { + limits: [ + { type: "TOKENS_LIMIT", unit: 3, number: 5, percentage: 40, nextResetTime: 1_800_000_000_000 }, + { type: "CREDIT_LIMIT", unit: 6, number: 1, usage: 2000, currentValue: 500 }, + { type: "TIME_LIMIT", percentage: 99 }, + ], + }, + }), + ).toEqual([ + { id: "limit_0", valueLabel: "5-hour", usedPercent: 40, resetAt: 1_800_000_000_000 }, + { id: "limit_1", valueLabel: "Weekly: 500 / 2,000 credits", usedPercent: 25, resetAt: undefined }, + ]) + }) + + it("treats a business failure inside HTTP 200 as no data", () => { + expect(parseZaiQuota({ success: false, code: 1001, msg: "expired", data: { limits: [] } })).toBeUndefined() + expect(parseZaiQuota({ code: 1001, data: { limits: [{ type: "TOKENS_LIMIT" }] } })).toBeUndefined() + }) +}) + +describe("parseMiniMaxRemains", () => { + const row = { + model_name: "MiniMax-M2", + current_interval_total_count: 100, + current_interval_remaining_percent: 80, + end_time: 1_800_000_000_000, + current_weekly_total_count: 1000, + current_weekly_usage_count: 900, + weekly_end_time: 1_900_000_000_000, + } + + it("reports the 5-hour window and a weekly window when the plan has one", () => { + expect(parseMiniMaxRemains({ model_remains: [{ model_name: "other" }, row] })).toEqual([ + { id: "5h", valueLabel: "5-hour", usedPercent: 20, resetAt: 1_800_000_000_000 }, + { id: "weekly", valueLabel: "Weekly", usedPercent: 10, resetAt: 1_900_000_000_000 }, + ]) + }) + + it("reads counters as used (not left) for the legacy coding plan", () => { + const windows = parseMiniMaxRemains( + { + model_remains: [ + { + model_name: "MiniMax-M2", + current_interval_total_count: 100, + current_interval_usage_count: 30, + current_weekly_total_count: 1000, + current_weekly_usage_count: 900, + }, + ], + }, + false, + )! + expect(windows[0].usedPercent).toBe(30) + expect(windows[1].usedPercent).toBe(90) + }) + + it("omits the weekly window when the plan tier does not apply it (status 3)", () => { + const result = parseMiniMaxRemains({ model_remains: [{ ...row, current_weekly_status: 3 }] }) + expect(result?.map((w) => w.id)).toEqual(["5h"]) + }) + + it("rejects an error envelope or an empty list", () => { + expect(parseMiniMaxRemains({ base_resp: { status_code: 1004 }, model_remains: [row] })).toBeUndefined() + expect(parseMiniMaxRemains({ model_remains: [] })).toBeUndefined() + }) +}) + +describe("parseNanoGptUsage", () => { + it("derives percent from percentUsed or used/limit and keeps state in the label", () => { + expect( + parseNanoGptUsage({ + state: "grace", + limits: { dailyInputTokens: 1000 }, + dailyInputTokens: { used: 250 }, + monthly: { percentUsed: 0.4, resetAt: 1_800_000_000 }, + }), + ).toEqual([ + { id: "daily", valueLabel: "Daily (grace)", usedPercent: 25, resetAt: undefined }, + { id: "monthly", valueLabel: "Monthly (grace)", usedPercent: 40, resetAt: 1_800_000_000_000 }, + ]) + }) + + it("shows no percent for a degraded window", () => { + const daily = parseNanoGptUsage({ dailyInputTokens: { percentUsed: 0.9, degraded: true } })![0] + expect(daily.usedPercent).toBeUndefined() + }) +}) + +describe("parseOpenCodeGoUsage", () => { + it("maps rolling, weekly and monthly windows that carry a percent and a reset", () => { + expect( + parseOpenCodeGoUsage({ + usage: { + rolling: { percent: 12, resetsAt: 1_800_000_000 }, + weekly: { percent: 140, resetsAt: 1_800_000_001 }, + monthly: { percent: 5 }, + }, + }), + ).toEqual([ + { id: "5h", valueLabel: "5-hour", usedPercent: 12, resetAt: 1_800_000_000_000 }, + { id: "weekly", valueLabel: "Weekly", usedPercent: 100, resetAt: 1_800_000_001_000 }, + ]) + }) +}) + +describe("balance parser edge cases", () => { + it("DeepSeek: rejects non-object and empty payloads, and a row without a balance", () => { + expect(parseDeepSeekBalance(null)).toBeUndefined() + expect(parseDeepSeekBalance({ balance_infos: [] })).toBeUndefined() + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "USD" }] })).toBeUndefined() + expect(parseDeepSeekBalance({ balance_infos: ["x", null] })).toBeUndefined() + }) + + it("DeepSeek: prefers a positive CNY balance over a zero USD one, then any USD row, then the first row", () => { + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "USD", total_balance: "0" }, + { currency: "CNY", total_balance: "12" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "¥12.00 left" }]) + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "EUR", total_balance: "0" }, + { currency: "USD", total_balance: "0" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "$0.00 left" }]) + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "EUR", total_balance: "3" }] })).toEqual([ + { id: "balance", valueLabel: "$3.00 left" }, + ]) + }) + + it("Moonshot: returns undefined without an available_balance", () => { + expect(parseMoonshotBalance({ data: {} })).toBeUndefined() + expect(parseMoonshotBalance(null)).toBeUndefined() + }) +}) + +describe("parser edge cases", () => { + it("Kimi Code: skips a legacy usage block with no limit, and derives used from `used`", () => { + expect(parseKimiCodeUsage({ usage: { used: 5 } })).toBeUndefined() + expect(parseKimiCodeUsage({ usage: { limit: 100, used: 40, resetTime: 1_800_000_000 } })).toEqual([ + { id: "weekly", valueLabel: "Weekly", usedPercent: 40, resetAt: 1_800_000_000_000 }, + ]) + expect(parseKimiCodeUsage({ usage: { limit: 100 } })).toBeUndefined() + }) + + it("z.ai: ignores non-object, wrong-type and unit-less limits; falls back to counts for percent", () => { + expect(parseZaiQuota("nope")).toBeUndefined() + expect(parseZaiQuota({ data: {} })).toBeUndefined() + expect(parseZaiQuota({ data: { limits: [null, { type: "TIME_LIMIT" }] } })).toBeUndefined() + expect( + parseZaiQuota({ + data: { limits: [{ type: "TOKENS_LIMIT", unit: 9, number: 5, usage: 200, currentValue: 50 }] }, + }), + ).toEqual([{ id: "limit_0", valueLabel: "Tokens", usedPercent: 25, resetAt: undefined }]) + expect(parseZaiQuota({ data: { limits: [{ type: "CREDIT_LIMIT", usage: 0, currentValue: 0 }] } })).toEqual([ + { id: "limit_0", valueLabel: "Credits: 0 / 0 credits", usedPercent: undefined, resetAt: undefined }, + ]) + }) + + it("MiniMax: picks a chat-named or percent-bearing model when no minimax-m model exists", () => { + expect(parseMiniMaxRemains(null)).toBeUndefined() + expect(parseMiniMaxRemains({ model_remains: ["x", null] })).toBeUndefined() + + const byName = parseMiniMaxRemains({ + model_remains: [ + { model_name: "other", current_interval_remaining_percent: 10 }, + { model_name: "Chat", current_interval_remaining_percent: 60 }, + ], + }) + expect(byName?.[0].usedPercent).toBe(40) + + const byPercent = parseMiniMaxRemains({ + model_remains: [{ model_name: "a" }, { model_name: "b", current_interval_remaining_percent: 75 }], + }) + expect(byPercent?.[0].usedPercent).toBe(25) + + const first = parseMiniMaxRemains({ model_remains: [{ model_name: "only" }] }) + expect(first).toEqual([{ id: "5h", valueLabel: "5-hour", usedPercent: undefined, resetAt: undefined }]) + expect( + parseMiniMaxRemains({ model_remains: [{ current_interval_remaining_percent: 5 }] })?.[0].usedPercent, + ).toBe(95) + }) + + it("MiniMax: skips a weekly window with no data and clamps an out-of-range percent", () => { + const [interval, weekly] = parseMiniMaxRemains({ + model_remains: [{ model_name: "MiniMax-M2", current_interval_remaining_percent: 150 }], + })! + expect(interval.usedPercent).toBe(0) + expect(weekly).toBeUndefined() + }) + + it("NanoGPT: handles non-object input, legacy `daily`, nested limits and unusable windows", () => { + expect(parseNanoGptUsage(null)).toBeUndefined() + expect(parseNanoGptUsage({})).toBeUndefined() + expect( + parseNanoGptUsage({ + daily: { used: 30, limit: 120, resetAt: "2026-05-01T00:00:00Z" }, + monthly: { used: 10, limits: { monthly: 100 } }, + period: { currentPeriodEnd: 1_800_000_000 }, + }), + ).toEqual([ + { id: "daily", valueLabel: "Daily", usedPercent: 25, resetAt: Date.parse("2026-05-01T00:00:00Z") }, + { id: "monthly", valueLabel: "Monthly", usedPercent: 10, resetAt: 1_800_000_000_000 }, + ]) + expect(parseNanoGptUsage({ daily: { used: 30, limits: { daily: 60 } } })?.[0].usedPercent).toBe(50) + expect(parseNanoGptUsage({ monthly: { used: 5, limit: 0 } })?.[0].usedPercent).toBeUndefined() + expect(parseNanoGptUsage({ monthly: {} })?.[0].usedPercent).toBeUndefined() + }) + + it("OpenCode Go: returns undefined without a usage block or with no usable window", () => { + expect(parseOpenCodeGoUsage(null)).toBeUndefined() + expect(parseOpenCodeGoUsage({})).toBeUndefined() + expect(parseOpenCodeGoUsage({ usage: { rolling: { percent: 10 } } })).toBeUndefined() + }) +}) + +describe("getProviderQuotas – Copilot window and misc", () => { + beforeEach(() => resetProviderQuotaCache()) + + it("shows a percent-only Copilot window, and 'Unlimited' for unlimited plans", async () => { + const { getGitHubCopilotQuota } = await import("../copilotQuota") + const mocked = vi.mocked(getGitHubCopilotQuota) + + mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: false, usedPercent: 41.6, fetchedAt: 1 } }) + const [pct] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + expect(pct).toMatchObject({ ok: true, windows: [{ valueLabel: "42% used", usedPercent: 41.6 }] }) + + resetProviderQuotaCache() + mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: false, fetchedAt: 1 } }) + const [none] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + // Unknown usage is reported as unknown (no bar), never as a measured 0%. + expect(none).toMatchObject({ ok: true, windows: [{ valueLabel: "Usage not reported" }] }) + expect((none as { windows: Array<{ usedPercent?: number }> }).windows[0].usedPercent).toBeUndefined() + + resetProviderQuotaCache() + mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: true, resetAt: 7, fetchedAt: 1 } }) + const [unl] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + expect(unl).toMatchObject({ ok: true, windows: [{ valueLabel: "Unlimited", resetAt: 7 }] }) + }) + + it("passes Copilot failures through with their reason and message", async () => { + const { getGitHubCopilotQuota } = await import("../copilotQuota") + vi.mocked(getGitHubCopilotQuota).mockResolvedValueOnce({ ok: false, reason: "error", message: "boom" }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + expect(q).toMatchObject({ ok: false, reason: "error", message: "boom" }) + }) + + it("uses the default z.ai line when the profile sets none", async () => { + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => new Response("{}")) + await getProviderQuotas([{ apiProvider: providerIdentifiers.zai, zaiApiKey: "k" } as never], {}, fetchSpy) + expect(fetchSpy.mock.calls[0][0]).toBe("https://api.z.ai/api/monitor/usage/quota/limit") + }) + + it("reports an error when a keyed provider answers with no usable data, and surfaces the last failure", async () => { + const [empty] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.nanogpt, nanoGptApiKey: "k" } as never], + {}, + vi.fn(async () => new Response("{}")), + ) + expect(empty).toMatchObject({ ok: false, reason: "error", message: "No usage data" }) + + resetProviderQuotaCache() + const [failed] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.minimax, minimaxApiKey: "k" } as never], + {}, + vi.fn(async () => new Response("", { status: 500 })), + ) + expect(failed).toMatchObject({ ok: false, reason: "error", message: "HTTP 500" }) + }) + + it("treats an auth failure on the first MiniMax URL as a missing/invalid key without trying the fallback", async () => { + const fetchSpy = vi.fn( + async (_url: RequestInfo | URL, _init?: RequestInit) => new Response("", { status: 401 }), + ) + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.minimax, minimaxApiKey: "k" } as never], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "noKey" }) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) +}) + +describe("getProviderQuotas – coding-plan providers", () => { + beforeEach(() => resetProviderQuotaCache()) + + it("queries z.ai on the host matching the profile's API line", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ code: 200, data: { limits: [] } }))) + await getProviderQuotas( + [{ apiProvider: providerIdentifiers.zai, zaiApiKey: "k", zaiApiLine: "china_coding" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledWith( + "https://open.bigmodel.cn/api/monitor/usage/quota/limit", + expect.anything(), + ) + }) + + it("falls back to the coding-plan endpoint for MiniMax and reads its counters as used", async () => { + const body = { + model_remains: [ + { + model_name: "MiniMax-M2", + current_interval_total_count: 100, + current_interval_usage_count: 30, + }, + ], + } + const fetchSpy = vi + .fn() + .mockResolvedValueOnce(new Response("", { status: 404 })) + .mockResolvedValueOnce(new Response(JSON.stringify(body))) + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.minimax, minimaxApiKey: "k" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy.mock.calls.map((c) => c[0])).toEqual([ + "https://api.minimax.io/v1/token_plan/remains", + "https://api.minimax.io/v1/api/openplatform/coding_plan/remains", + ]) + expect(q).toMatchObject({ ok: true, windows: [{ id: "5h", usedPercent: 30 }] }) + }) + + it("uses the China MiniMax hosts for a minimaxi.com base URL", async () => { + const fetchSpy = vi.fn( + async (_url: RequestInfo | URL, _init?: RequestInit) => new Response("", { status: 404 }), + ) + await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.minimax, + minimaxApiKey: "k", + minimaxBaseUrl: "https://api.minimaxi.com/anthropic", + } as never, + ], + {}, + fetchSpy, + ) + expect(fetchSpy.mock.calls.map((c) => c[0])).toEqual([ + "https://api.minimaxi.com/v1/token_plan/remains", + "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains", + ]) + }) + + it("never sends a MiniMax key to an unknown base URL", async () => { + const fetchSpy = vi.fn() + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.minimax, + minimaxApiKey: "k", + minimaxBaseUrl: "https://gateway.example/anthropic", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("reports noKey for a Kimi Code profile signed in with OAuth (no API key)", async () => { + const fetchSpy = vi.fn() + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.kimiCode } as never], {}, fetchSpy) + expect(q).toMatchObject({ ok: false, reason: "noKey" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("calls NanoGPT, Kimi Code and OpenCode Go on their own hosts only", async () => { + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => new Response(JSON.stringify({}))) + await getProviderQuotas( + [ + { apiProvider: providerIdentifiers.nanogpt, nanoGptApiKey: "k" } as never, + { apiProvider: providerIdentifiers.kimiCode, kimiCodeApiKey: "k" } as never, + { apiProvider: providerIdentifiers.opencodeGo, opencodeGoApiKey: "k" } as never, + ], + {}, + fetchSpy, + ) + expect(fetchSpy.mock.calls.map((c) => c[0]).sort()).toEqual([ + "https://api.kimi.com/coding/v1/usages", + "https://nano-gpt.com/api/subscription/v1/usage", + "https://opencode.ai/zen/go/v1/usage", + ]) + }) +}) + +describe("getProviderQuotas", () => { + beforeEach(() => resetProviderQuotaCache()) + + const okFetch = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + + it("only queries providers that are connected", async () => { + const result = await getProviderQuotas([{ apiProvider: providerIdentifiers.anthropic } as never], {}, okFetch) + expect(result).toEqual([]) + expect(okFetch).not.toHaveBeenCalled() + }) + + it("returns Copilot credits formatted for display", async () => { + const [copilot] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + expect(copilot).toMatchObject({ + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + windows: [{ id: "premium", valueLabel: "39,389.3 / 140,000 used", usedPercent: 28.1, resetAt: 1 }], + }) + }) + + it("reports noKey when the profile has no API key", async () => { + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.openrouter } as never], {}, okFetch) + expect(q).toMatchObject({ providerId: providerIdentifiers.openrouter, ok: false, reason: "noKey" }) + expect(okFetch).not.toHaveBeenCalled() + }) + + it("treats a malformed custom base URL as a different host and never sends the key", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "not a url", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("accepts the provider's own host written with a trailing path or slash", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "https://openrouter.ai/api/v1/", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: true }) + }) + + it("never sends a key to a custom base URL", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "https://evil.example/api/v1", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("calls the provider's own host with the saved key", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-test" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledWith( + "https://openrouter.ai/api/v1/key", + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer sk-test" }) }), + ) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "$5.00 / $10.00 used" }] }) + }) + + it("maps 401 to noKey", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "bad" } as never], + {}, + vi.fn(async () => new Response("", { status: 401 })), + ) + expect(q).toMatchObject({ providerId: providerIdentifiers.deepseek, ok: false, reason: "noKey" }) + }) + + it("queries a profile on the provider's own host even when an earlier profile uses a custom gateway", async () => { + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => { + return new Response(JSON.stringify({ balance_infos: [{ currency: "USD", total_balance: "7.5" }] })) + }) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.deepseek, + deepSeekApiKey: "gateway-key", + deepSeekBaseUrl: "https://gateway.example.com/v1", + } as never, + { apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "real-key" } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "$7.50 left" }] }) + expect(String(fetchSpy.mock.calls[0][0])).toContain("api.deepseek.com") + // The gateway profile's key is never sent anywhere. + expect(JSON.stringify(fetchSpy.mock.calls)).not.toContain("gateway-key") + }) + + it("reports unsupported only when every keyed profile uses a custom gateway", async () => { + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.deepseek, + deepSeekApiKey: "k", + deepSeekBaseUrl: "https://gateway.example.com/v1", + } as never, + ], + {}, + vi.fn(), + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + }) + + it("does not cache a missing key: adding one shows up on the next call", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const [before] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter } as never], + {}, + fetchSpy, + ) + expect(before).toMatchObject({ ok: false, reason: "noKey" }) + + const [after] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never], + {}, + fetchSpy, + ) + expect(after).toMatchObject({ ok: true }) + }) + + it("does not cache a failure: a retry within the TTL is fetched again", async () => { + const fetchSpy = vi + .fn() + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + + const [first] = await getProviderQuotas(profiles, {}, fetchSpy) + expect(first).toMatchObject({ ok: false, reason: "error" }) + const [second] = await getProviderQuotas(profiles, {}, fetchSpy) + expect(second).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it("refetches when the API key changes", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never], + {}, + fetchSpy, + ) + await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-two" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it("does not hand a result fetched with one key to a caller using another, even while the first is in flight", async () => { + const pending = new Map void>() + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((resolve) => { + pending.set(String((init?.headers as Record).Authorization), resolve) + }), + ) + const body = (remaining: number) => + new Response(JSON.stringify({ data: { limit: 10, limit_remaining: remaining } })) + + const first = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never], + {}, + fetchSpy, + ) + const second = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-two" } as never], + {}, + fetchSpy, + ) + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2)) + // Each request went out with its own key, and nothing else. + expect([...pending.keys()].sort()).toEqual(["Bearer sk-one", "Bearer sk-two"]) + + // Answer them with different data, in the opposite order, so a mix-up would show. + pending.get("Bearer sk-two")!(body(2)) + pending.get("Bearer sk-one")!(body(9)) + + const [[a], [b]] = await Promise.all([first, second]) + expect(a).toMatchObject({ ok: true, windows: [{ usedPercent: 10 }] }) + expect(b).toMatchObject({ ok: true, windows: [{ usedPercent: 80 }] }) + }) + + it("drops a cached success when a forced refresh fails, so the next normal call retries", async () => { + const ok = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + const fetchSpy = vi + .fn() + .mockResolvedValueOnce(ok()) + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(ok()) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + + await getProviderQuotas(profiles, {}, fetchSpy) + const [failed] = await getProviderQuotas(profiles, { force: true }, fetchSpy) + expect(failed).toMatchObject({ ok: false, reason: "error" }) + + // The old success must not be served as if it were current. + const [retry] = await getProviderQuotas(profiles, {}, fetchSpy) + expect(retry).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(3) + }) + + it("aborts the network request when the only caller goes away", async () => { + let seen: AbortSignal | undefined + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + seen = init?.signal ?? undefined + seen?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError"))) + }), + ) + const controller = new AbortController() + const result = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never], + { signal: controller.signal }, + fetchSpy, + ) + await vi.waitFor(() => expect(seen).toBeDefined()) + expect(seen!.aborted).toBe(false) + + controller.abort() + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(seen!.aborted).toBe(true) + }) + + it("keeps the shared request running until its last caller leaves", async () => { + let seen: AbortSignal | undefined + let release: (r: Response) => void = () => {} + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((resolve) => { + seen = init?.signal ?? undefined + release = resolve + }), + ) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + const leaver = new AbortController() + + const staying = getProviderQuotas(profiles, {}, fetchSpy) + const leaving = getProviderQuotas(profiles, { signal: leaver.signal }, fetchSpy) + await vi.waitFor(() => expect(seen).toBeDefined()) + + leaver.abort() + await expect(leaving).rejects.toMatchObject({ name: "AbortError" }) + // Someone is still waiting, so the shared fetch must keep going. + expect(seen!.aborted).toBe(false) + + release(new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const [quota] = await staying + expect(quota).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it("rejects at once with an already-aborted signal, without keeping a request alive", async () => { + const fetchSpy = vi.fn(() => new Promise(() => {})) + const controller = new AbortController() + controller.abort() + await expect( + getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never], + { signal: controller.signal }, + fetchSpy, + ), + ).rejects.toMatchObject({ name: "AbortError" }) + }) + + it("shares one request between concurrent callers for the same credentials", async () => { + let release: (r: Response) => void = () => {} + const fetchSpy = vi.fn(() => new Promise((resolve) => (release = resolve))) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + + const a = getProviderQuotas(profiles, {}, fetchSpy) + const b = getProviderQuotas(profiles, { force: true }, fetchSpy) + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled()) + release(new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + + const [ra, rb] = await Promise.all([a, b]) + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(ra).toEqual(rb) + }) + + it("caches results for the same set of providers unless forced", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + await getProviderQuotas(profiles, {}, fetchSpy) + await getProviderQuotas(profiles, {}, fetchSpy) + expect(fetchSpy).toHaveBeenCalledTimes(1) + await getProviderQuotas(profiles, { force: true }, fetchSpy) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) +}) + +describe("loadProfiles", () => { + it("loads every saved profile through the manager, with keys", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn( + async ({ id }: { id: string }) => ({ apiProvider: "x", openRouterApiKey: `key-${id}` }) as never, + ), + } + const profiles = await loadProfiles(source) + expect(profiles).toEqual([ + { apiProvider: "x", openRouterApiKey: "key-a" }, + { apiProvider: "x", openRouterApiKey: "key-b" }, + ]) + expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }) + }) + + it("skips a profile that fails to load instead of failing the whole refresh", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "ok" }, { id: "broken" }]), + getProfile: vi.fn(async ({ id }: { id: string }) => { + if (id === "broken") throw new Error("corrupt secret storage") + return { apiProvider: "x" } as never + }), + } + expect(await loadProfiles(source)).toEqual([{ apiProvider: "x" }]) + }) + + it("returns an empty list when there are no profiles", async () => { + expect(await loadProfiles({ listConfig: async () => [], getProfile: vi.fn() })).toEqual([]) + }) +}) + +describe("error handling never leaks credentials", () => { + beforeEach(() => resetProviderQuotaCache()) + + it("reports a generic message for network errors that embed the key", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "sk-secret" } as never], + {}, + vi.fn(async () => { + throw new Error("connect ECONNREFUSED https://api.deepseek.com Authorization: Bearer sk-secret") + }), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "Request failed" }) + expect(JSON.stringify(q)).not.toContain("sk-secret") + }) + + it("reports only the status for non-auth HTTP failures", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.moonshot, moonshotApiKey: "sk" } as never], + {}, + vi.fn(async () => new Response("secret internal page", { status: 502 })), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "HTTP 502" }) + }) + + it("treats an empty payload as an error", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "sk" } as never], + {}, + vi.fn(async () => new Response(JSON.stringify({ balance_infos: [] }))), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "No usage data" }) + }) + + it("surfaces Copilot failures with their reason", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ ok: false, reason: "signedOut" }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: false, reason: "signedOut" }) + }) + + it("shows Copilot unlimited plans without numbers", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ + ok: true, + quota: { plan: "enterprise", unlimited: true, fetchedAt: 1, resetAt: 2 }, + }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: true, windows: [{ id: "premium", valueLabel: "Unlimited", resetAt: 2 }] }) + }) + + it("falls back to a percent label when counts are missing", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ + ok: true, + quota: { unlimited: false, usedPercent: 60, fetchedAt: 1 }, + }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "60% used", usedPercent: 60 }] }) + }) +}) diff --git a/src/services/language-models/__tests__/quotaUtils.spec.ts b/src/services/language-models/__tests__/quotaUtils.spec.ts new file mode 100644 index 0000000000..30f5185140 --- /dev/null +++ b/src/services/language-models/__tests__/quotaUtils.spec.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from "vitest" + +import { + QuotaHttpError, + asRecord, + clampPercent, + formatCredits, + formatMoney, + roundToTenth, + safeErrorMessage, + toEpochMs, + toNumber, +} from "../quotaUtils" + +describe("toEpochMs", () => { + it("treats numbers below 1e12 as epoch seconds and larger ones as milliseconds", () => { + expect(toEpochMs(1_800_000_000)).toBe(1_800_000_000_000) + expect(toEpochMs(1_800_000_000_000)).toBe(1_800_000_000_000) + }) + + it("parses date strings", () => { + expect(toEpochMs("2026-01-02T03:04:05Z")).toBe(Date.parse("2026-01-02T03:04:05Z")) + }) + + it("rejects zero, negatives, non-finite numbers, blank and unparseable strings, and other types", () => { + expect(toEpochMs(0)).toBeUndefined() + expect(toEpochMs(-5)).toBeUndefined() + expect(toEpochMs(Number.NaN)).toBeUndefined() + expect(toEpochMs(Number.POSITIVE_INFINITY)).toBeUndefined() + expect(toEpochMs(" ")).toBeUndefined() + expect(toEpochMs("not a date")).toBeUndefined() + expect(toEpochMs(null)).toBeUndefined() + expect(toEpochMs({})).toBeUndefined() + }) +}) + +describe("toNumber", () => { + it.each([ + [5, 5], + [0, 0], + ["12.5", 12.5], + [" 7 ", 7], + ])("accepts %j", (input, expected) => expect(toNumber(input)).toBe(expected)) + + it.each([[NaN], [Infinity], [""], [" "], ["abc"], [null], [undefined], [{}], [[]]])("rejects %j", (input) => + expect(toNumber(input)).toBeUndefined(), + ) +}) + +describe("asRecord", () => { + it("returns plain objects only", () => { + expect(asRecord({ a: 1 })).toEqual({ a: 1 }) + for (const bad of [null, undefined, [], "x", 1]) expect(asRecord(bad)).toBeUndefined() + }) +}) + +describe("clampPercent", () => { + it.each([ + [-5, 0], + [0, 0], + [42.5, 42.5], + [100, 100], + [250, 100], + ])("%j → %j", (input, expected) => expect(clampPercent(input)).toBe(expected)) +}) + +describe("formatMoney", () => { + it("formats with two decimals and a sign before the symbol", () => { + expect(formatMoney(12.4)).toBe("$12.40") + expect(formatMoney(-0.5)).toBe("-$0.50") + expect(formatMoney(8, "¥")).toBe("¥8.00") + }) +}) + +describe("formatCredits", () => { + it("keeps whole numbers whole and fractions to one decimal", () => { + expect(formatCredits(140000)).toBe("140,000") + expect(formatCredits(39389.34)).toBe("39,389.3") + }) +}) + +describe("roundToTenth", () => { + it("rounds to one decimal", () => { + expect(roundToTenth(39389.26)).toBe(39389.3) + expect(roundToTenth(5)).toBe(5) + }) +}) + +describe("safeErrorMessage", () => { + it("keeps the status for HTTP errors", () => { + expect(safeErrorMessage(new QuotaHttpError(503))).toBe("HTTP 503") + }) + + it("normalises timeouts and aborts", () => { + const timeout = Object.assign(new Error("x"), { name: "TimeoutError" }) + const abort = Object.assign(new Error("x"), { name: "AbortError" }) + expect(safeErrorMessage(timeout)).toBe("Request timed out") + expect(safeErrorMessage(abort)).toBe("Request timed out") + }) + + it("never leaks the text of arbitrary errors (could embed URLs, headers or keys)", () => { + const leaky = new Error( + "fetch failed: https://api.example.com/v1?key=sk-secret Authorization: Bearer sk-secret", + ) + expect(safeErrorMessage(leaky)).toBe("Request failed") + expect(safeErrorMessage("sk-secret")).toBe("Request failed") + expect(safeErrorMessage(undefined)).toBe("Request failed") + }) +}) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts new file mode 100644 index 0000000000..6a8c6be07f --- /dev/null +++ b/src/services/language-models/copilotQuota.ts @@ -0,0 +1,132 @@ +import * as vscode from "vscode" + +import { githubCopilotLanguageModel, type GitHubCopilotQuota, type GitHubCopilotQuotaResult } from "@roo-code/types" + +import { asRecord, clampPercent, roundToTenth, safeErrorMessage, toNumber } from "./quotaUtils" + +/** + * GitHub Copilot premium-request allowance for the Usage tab. + * + * Source: `GET https://api.github.com/copilot_internal/user`, the endpoint + * Copilot Chat's own usage panel reads. It returns + * `quota_snapshots.premium_interactions` plus `quota_reset_date`. + * + * Auth reuses the GitHub session VS Code already holds for Copilot (silent: + * never prompts). The token is only ever sent to api.github.com. + */ + +const COPILOT_USER_URL = "https://api.github.com/copilot_internal/user" +const REQUEST_TIMEOUT_MS = 15_000 +/** Avoid hammering the endpoint when the Usage tab is reopened repeatedly. */ +const CACHE_TTL_MS = 60_000 + +/** + * Parses the endpoint payload. Pure, so it is unit-tested against recorded + * shapes. Returns undefined when the payload has no premium snapshot at all. + * + * Semantics mirror Copilot Chat (CopilotUserQuotaInfo): + * - `quota_remaining` is the precise (fractional) twin of `remaining`; prefer it. + * - used = entitlement - remaining, so the figure matches GitHub's own panel. + * - `percent_remaining` is a server-computed fallback when counts are missing. + */ +export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCopilotQuota | undefined { + const root = asRecord(payload) + const snapshot = asRecord(asRecord(root?.quota_snapshots)?.premium_interactions) + if (!root || !snapshot) return undefined + + const plan = typeof root.copilot_plan === "string" ? root.copilot_plan : undefined + const resetRaw = root.quota_reset_date_utc ?? root.quota_reset_date + const resetAt = + typeof resetRaw === "string" && !Number.isNaN(Date.parse(resetRaw)) ? Date.parse(resetRaw) : undefined + + if (snapshot.unlimited === true) { + return { plan, unlimited: true, resetAt, fetchedAt: now } + } + + const entitlement = toNumber(snapshot.entitlement) + const remaining = toNumber(snapshot.quota_remaining) ?? toNumber(snapshot.remaining) + + let used: number | undefined + let usedPercent: number | undefined + + if (entitlement !== undefined && entitlement > 0 && remaining !== undefined) { + used = roundToTenth(Math.min(entitlement, Math.max(0, entitlement - remaining))) + usedPercent = clampPercent((used / entitlement) * 100) + } else { + const percentRemaining = toNumber(snapshot.percent_remaining) + if (percentRemaining !== undefined) usedPercent = clampPercent(100 - percentRemaining) + } + + return { + plan, + unlimited: false, + entitlement: entitlement !== undefined && entitlement > 0 ? entitlement : undefined, + used, + usedPercent, + resetAt, + fetchedAt: now, + } +} + +async function getGitHubToken(): Promise { + const { authProviderId, authScopeSets } = githubCopilotLanguageModel + for (const scopes of authScopeSets) { + const session = await vscode.authentication.getSession(authProviderId, scopes, { silent: true }) + if (session) return session.accessToken + } + return undefined +} + +let cache: { at: number; result: GitHubCopilotQuotaResult } | undefined +let inFlight: Promise | undefined + +/** + * Fetches the quota. Concurrent callers share one request; successful results + * are cached briefly unless `force` is set (the Refresh button). + */ +export function getGitHubCopilotQuota( + { force = false }: { force?: boolean } = {}, + fetchImpl: typeof fetch = fetch, +): Promise { + if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) return Promise.resolve(cache.result) + if (inFlight) return inFlight + + inFlight = (async (): Promise => { + try { + const token = await getGitHubToken() + if (!token) return { ok: false, reason: "signedOut" } + + const response = await fetchImpl(COPILOT_USER_URL, { + method: "GET", + headers: { + Authorization: `token ${token}`, + Accept: "application/json", + "Editor-Version": `vscode/${vscode.version}`, + "X-Github-Api-Version": "2025-04-01", + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + + if (response.status === 401) return { ok: false, reason: "signedOut" } + if (response.status === 403 || response.status === 404) return { ok: false, reason: "noCopilot" } + if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } + + const quota = parseCopilotQuota(await response.json()) + const result: GitHubCopilotQuotaResult = quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } + if (result.ok) cache = { at: Date.now(), result } + return result + } catch (error) { + return { ok: false, reason: "error", message: safeErrorMessage(error) } + } finally { + inFlight = undefined + } + })() + + return inFlight +} + +/** @internal test hook */ +export function resetGitHubCopilotQuotaCache() { + cache = undefined + inFlight = undefined +} diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts new file mode 100644 index 0000000000..4286b4f6d1 --- /dev/null +++ b/src/services/language-models/providerQuotas.ts @@ -0,0 +1,673 @@ +import { + providerIdentifiers, + type ProviderQuota, + type ProviderQuotaWindow, + type ProviderSettings, +} from "@roo-code/types" + +import { getGitHubCopilotQuota } from "./copilotQuota" +import { + QuotaHttpError, + asRecord, + clampPercent, + formatCredits, + formatMoney, + safeErrorMessage, + toEpochMs, + toNumber, +} from "./quotaUtils" + +/** + * Live limits/balances for every connected provider that exposes a usage API, + * for the Usage tab's "Provider limits" section. + * + * Endpoints are each provider's own usage API. Credentials come from the + * user's saved Zoo profiles and are sent only to that provider's own host. + * Providers without a usage API are simply not listed. + */ + +const REQUEST_TIMEOUT_MS = 15_000 +const CACHE_TTL_MS = 60_000 + +// Parsers (pure; unit-tested) ---------------------------------------------------------------------- + +/** OpenRouter `GET /api/v1/key` → key limit, or monthly spend when unlimited. */ +export function parseOpenRouterKey(payload: unknown): ProviderQuotaWindow[] | undefined { + const data = asRecord(asRecord(payload)?.data) + if (!data) return undefined + + const limit = toNumber(data.limit) + const remaining = toNumber(data.limit_remaining) + + if (limit !== undefined && limit > 0 && remaining !== undefined) { + const used = Math.max(0, limit - remaining) + return [ + { + id: "key_limit", + valueLabel: `${formatMoney(used)} / ${formatMoney(limit)} used`, + usedPercent: clampPercent((used / limit) * 100), + }, + ] + } + + const usage = toNumber(data.usage_monthly) ?? toNumber(data.usage) + return usage === undefined ? undefined : [{ id: "spend", valueLabel: `${formatMoney(usage)} spent this month` }] +} + +/** DeepSeek `GET /user/balance` → prepaid balance (prefers a positive USD balance). */ +export function parseDeepSeekBalance(payload: unknown): ProviderQuotaWindow[] | undefined { + const infos = asRecord(payload)?.balance_infos + if (!Array.isArray(infos) || infos.length === 0) return undefined + + const rows = infos.map(asRecord).filter((r): r is Record => !!r) + const pick = + rows.find((r) => r.currency === "USD" && (toNumber(r.total_balance) ?? 0) > 0) ?? + rows.find((r) => r.currency === "CNY" && (toNumber(r.total_balance) ?? 0) > 0) ?? + rows.find((r) => r.currency === "USD") ?? + rows[0] + + const balance = toNumber(pick?.total_balance) + if (balance === undefined) return undefined + return [{ id: "balance", valueLabel: `${formatMoney(balance, pick?.currency === "CNY" ? "¥" : "$")} left` }] +} + +/** Moonshot `GET /v1/users/me/balance` → available balance. */ +export function parseMoonshotBalance(payload: unknown): ProviderQuotaWindow[] | undefined { + const balance = toNumber(asRecord(asRecord(payload)?.data)?.available_balance) + return balance === undefined ? undefined : [{ id: "balance", valueLabel: `${formatMoney(balance)} left` }] +} + +/** "5-hour", "Weekly", "3d", … for a rolling window of the given length. */ +export function windowLabel(seconds: number | undefined, fallback: string): string { + if (!seconds || seconds <= 0) return fallback + if (seconds === 7 * 86400) return "Weekly" + if (seconds % 86400 === 0) return `${seconds / 86400}-day` + if (seconds % 3600 === 0) return `${seconds / 3600}-hour` + return fallback +} + +/** Kimi Code `GET /coding/v1/usages` → 5-hour / weekly / monthly usage. */ +export function parseKimiCodeUsage(payload: unknown): ProviderQuotaWindow[] | undefined { + const root = asRecord(payload) + if (!root) return undefined + const windows: ProviderQuotaWindow[] = [] + + const usages = asRecord(root.usages) + if (usages) { + // `used_ratio` is a 0-1 fraction, in the order Kimi's own CLI renders it. + for (const [key, label] of [ + ["limit_5h", "5-hour"], + ["limit_7d", "Weekly"], + ["limit_month_total", "Monthly"], + ] as const) { + const entry = asRecord(usages[key]) + const ratio = toNumber(entry?.used_ratio) + if (ratio === undefined) continue + windows.push({ + id: key, + valueLabel: label, + usedPercent: clampPercent(ratio * 100), + resetAt: toEpochMs(entry?.reset_time), + }) + } + return windows.length ? windows : undefined + } + + // Pre-ratio payload: one counted weekly `usage` block. + const usage = asRecord(root.usage) + const limit = toNumber(usage?.limit) + if (usage && limit && limit > 0) { + const used = toNumber(usage.used) + const remaining = toNumber(usage.remaining) + const usedAmount = used ?? (remaining !== undefined ? limit - remaining : undefined) + if (usedAmount !== undefined) { + windows.push({ + id: "weekly", + valueLabel: "Weekly", + usedPercent: clampPercent((usedAmount / limit) * 100), + resetAt: toEpochMs(usage.resetTime), + }) + } + } + return windows.length ? windows : undefined +} + +/** + * z.ai / Zhipu `GET /api/monitor/usage/quota/limit`. Business failures arrive + * as HTTP 200 with `{ code, msg, success: false }`, so the envelope is checked + * first. TOKENS_LIMIT / CREDIT_LIMIT entries are the 5-hour and weekly windows. + */ +export function parseZaiQuota(payload: unknown): ProviderQuotaWindow[] | undefined { + const root = asRecord(payload) + if (!root) return undefined + const code = root.code + if (root.success === false || (code !== undefined && code !== null && code !== 200)) return undefined + + const limits = asRecord(root.data)?.limits + if (!Array.isArray(limits)) return undefined + + const windows: ProviderQuotaWindow[] = [] + for (const raw of limits) { + const limit = asRecord(raw) + if (!limit || (limit.type !== "TOKENS_LIMIT" && limit.type !== "CREDIT_LIMIT")) continue + + // unit 3 = hours, unit 6 = weeks. + const number = toNumber(limit.number) + const unitSeconds = limit.unit === 3 ? 3600 : limit.unit === 6 ? 7 * 86400 : undefined + const seconds = number && unitSeconds ? number * unitSeconds : undefined + + const total = toNumber(limit.usage) + const consumed = toNumber(limit.currentValue) + const percentage = toNumber(limit.percentage) + const usedPercent = + percentage !== undefined + ? clampPercent(percentage) + : total && total > 0 && consumed !== undefined + ? clampPercent((consumed / total) * 100) + : undefined + + windows.push({ + id: `limit_${windows.length}`, + valueLabel: + limit.type === "CREDIT_LIMIT" && total !== undefined && consumed !== undefined + ? `${windowLabel(seconds, "Credits")}: ${formatCredits(consumed)} / ${formatCredits(total)} credits` + : windowLabel(seconds, "Tokens"), + usedPercent, + resetAt: toEpochMs(limit.nextResetTime), + }) + } + return windows.length ? windows : undefined +} + +/** + * MiniMax `GET /v1/token_plan/remains` (or the legacy coding-plan endpoint) → + * 5-hour interval and, when the plan has one, weekly usage for the chat model. + */ +export function parseMiniMaxRemains(payload: unknown, isTokenPlan = true): ProviderQuotaWindow[] | undefined { + const root = asRecord(payload) + const base = asRecord(root?.base_resp) + if (base && base.status_code !== 0) return undefined + const remains = root?.model_remains + if (!Array.isArray(remains) || remains.length === 0) return undefined + + const rows = remains.map(asRecord).filter((r): r is Record => !!r) + const name = (r: Record) => (typeof r.model_name === "string" ? r.model_name : "") + const model = + rows.find((r) => /^minimax-m/i.test(name(r)) && (toNumber(r.current_interval_total_count) ?? 0) > 0) ?? + rows.find((r) => ["general", "chat", "text"].includes(name(r).toLowerCase())) ?? + rows.find((r) => typeof r.current_interval_remaining_percent === "number") ?? + rows[0] + if (!model) return undefined + + const usedPercentOf = (prefix: "interval" | "weekly"): number | undefined => { + const remainingPercent = toNumber(model[`current_${prefix}_remaining_percent`]) + if (remainingPercent !== undefined) return clampPercent(100 - clampPercent(remainingPercent)) + const total = toNumber(model[`current_${prefix}_total_count`]) + const raw = toNumber(model[`current_${prefix}_usage_count`]) + if (!total || total <= 0 || raw === undefined) return undefined + // Token-plan counters report what is left; coding-plan counters report what is used. + const used = isTokenPlan ? Math.max(0, total - raw) : raw + return clampPercent((used / total) * 100) + } + + const windows: ProviderQuotaWindow[] = [ + { + id: "5h", + valueLabel: "5-hour", + usedPercent: usedPercentOf("interval"), + resetAt: toEpochMs(model.end_time), + }, + ] + + // Status 3 = the window does not apply to this plan tier. + const weeklyApplies = toNumber(model.current_weekly_status) !== 3 + const weeklyPercent = usedPercentOf("weekly") + if (weeklyApplies && weeklyPercent !== undefined) { + windows.push({ + id: "weekly", + valueLabel: "Weekly", + usedPercent: weeklyPercent, + resetAt: toEpochMs(model.weekly_end_time), + }) + } + return windows +} + +/** NanoGPT `GET /api/subscription/v1/usage` → daily / weekly / monthly allowance. */ +export function parseNanoGptUsage(payload: unknown): ProviderQuotaWindow[] | undefined { + const root = asRecord(payload) + if (!root) return undefined + + const state = typeof root.state === "string" ? root.state : "active" + const limits = asRecord(root.limits) + const dailyQuota = root.dailyInputTokens !== undefined ? root.dailyInputTokens : root.daily + const monthly = asRecord(root.monthly) + + const entries: { id: string; label: string; quota: unknown; limit: unknown; resetAt: unknown }[] = [ + { + id: "daily", + label: "Daily", + quota: dailyQuota, + limit: + root.dailyInputTokens !== undefined + ? limits?.dailyInputTokens + : (asRecord(dailyQuota)?.limit ?? asRecord(asRecord(dailyQuota)?.limits)?.daily), + resetAt: asRecord(dailyQuota)?.resetAt, + }, + { + id: "weekly", + label: "Weekly", + quota: root.weeklyInputTokens, + limit: limits?.weeklyInputTokens, + resetAt: asRecord(root.weeklyInputTokens)?.resetAt, + }, + { + id: "monthly", + label: "Monthly", + quota: monthly, + limit: monthly?.limit ?? asRecord(monthly?.limits)?.monthly, + resetAt: monthly?.resetAt ?? asRecord(root.period)?.currentPeriodEnd, + }, + ] + + const windows: ProviderQuotaWindow[] = [] + for (const { id, label, quota, limit, resetAt } of entries) { + const q = asRecord(quota) + if (!q) continue + let usedPercent: number | undefined + if (!q.degraded) { + const fraction = toNumber(q.percentUsed) + const used = toNumber(q.used) + const cap = toNumber(limit) + if (fraction !== undefined) usedPercent = clampPercent(fraction * 100) + else if (used !== undefined && cap !== undefined && cap > 0) usedPercent = clampPercent((used / cap) * 100) + } + windows.push({ + id, + valueLabel: state !== "active" ? `${label} (${state})` : label, + usedPercent, + resetAt: toEpochMs(resetAt), + }) + } + return windows.length ? windows : undefined +} + +/** OpenCode Go `GET /zen/go/v1/usage` (API key) → rolling 5-hour, weekly, monthly percent used. */ +export function parseOpenCodeGoUsage(payload: unknown): ProviderQuotaWindow[] | undefined { + const usage = asRecord(asRecord(payload)?.usage) + if (!usage) return undefined + + const windows: ProviderQuotaWindow[] = [] + for (const [id, field, label] of [ + ["5h", "rolling", "5-hour"], + ["weekly", "weekly", "Weekly"], + ["monthly", "monthly", "Monthly"], + ] as const) { + const entry = asRecord(usage[field]) + const percent = toNumber(entry?.percent) + const resetAt = toEpochMs(entry?.resetsAt) + if (percent === undefined || resetAt === undefined) continue + windows.push({ id, valueLabel: label, usedPercent: clampPercent(percent), resetAt }) + } + return windows.length ? windows : undefined +} + +// Fetchers ---------------------------------------------------------------------------------------- + +type Fetch = typeof fetch + +async function getJson(fetchImpl: Fetch, url: string, apiKey: string, signal?: AbortSignal): Promise { + const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) + const response = await fetchImpl(url, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, + signal: signal ? AbortSignal.any([signal, timeout]) : timeout, + }) + if (response.status === 401 || response.status === 403) throw new AuthError() + if (!response.ok) throw new QuotaHttpError(response.status) + return response.json() +} + +class AuthError extends Error { + constructor() { + super("Invalid or expired API key") + } +} + +interface KeyedProvider { + providerId: string + /** + * Returns the API key and the usage URLs to try, in order. `urls` is + * undefined when the profile targets a host we must not send the key to + * (a custom gateway); the provider is then reported as unsupported. + */ + credentials: (s: ProviderSettings) => { apiKey?: string; urls?: string[] } + parse: (payload: unknown, urlIndex: number) => ProviderQuotaWindow[] | undefined +} + +const sameOrigin = (a: string, b: string) => { + try { + return new URL(a).origin === new URL(b).origin + } catch { + return false + } +} + +/** The provider's own default host, unless the profile points at a different one. */ +const ownHost = (defaultBase: string, path: string, baseUrl?: string): string[] | undefined => + baseUrl && !sameOrigin(baseUrl, defaultBase) ? undefined : [defaultBase + path] + +/** z.ai (international) and Zhipu (China) host the same monitor API on different origins. */ +const ZAI_ORIGINS: Record = { + international_coding: "https://api.z.ai", + international_api: "https://api.z.ai", + china_coding: "https://open.bigmodel.cn", + china_api: "https://open.bigmodel.cn", +} + +const KEYED_PROVIDERS: KeyedProvider[] = [ + { + providerId: providerIdentifiers.openrouter, + credentials: (s) => ({ + apiKey: s.openRouterApiKey, + urls: ownHost("https://openrouter.ai/api/v1", "/key", s.openRouterBaseUrl), + }), + parse: parseOpenRouterKey, + }, + { + providerId: providerIdentifiers.deepseek, + credentials: (s) => ({ + apiKey: s.deepSeekApiKey, + urls: ownHost("https://api.deepseek.com", "/user/balance", s.deepSeekBaseUrl), + }), + parse: parseDeepSeekBalance, + }, + { + providerId: providerIdentifiers.moonshot, + credentials: (s) => ({ + apiKey: s.moonshotApiKey, + urls: ownHost("https://api.moonshot.ai/v1", "/users/me/balance", s.moonshotBaseUrl), + }), + parse: parseMoonshotBalance, + }, + { + // Only API-key profiles are queried; OAuth sign-ins have no key to send. + providerId: providerIdentifiers.kimiCode, + credentials: (s) => ({ + apiKey: s.kimiCodeApiKey, + urls: ["https://api.kimi.com/coding/v1/usages"], + }), + parse: parseKimiCodeUsage, + }, + { + providerId: providerIdentifiers.zai, + credentials: (s) => ({ + apiKey: s.zaiApiKey, + urls: [`${ZAI_ORIGINS[s.zaiApiLine ?? "international_coding"]}/api/monitor/usage/quota/limit`], + }), + parse: parseZaiQuota, + }, + { + providerId: providerIdentifiers.minimax, + credentials: (s) => { + const base = s.minimaxBaseUrl || "https://api.minimax.io/anthropic" + if (sameOrigin(base, "https://api.minimax.io")) { + return { + apiKey: s.minimaxApiKey, + urls: [ + "https://api.minimax.io/v1/token_plan/remains", + "https://api.minimax.io/v1/api/openplatform/coding_plan/remains", + ], + } + } + if (sameOrigin(base, "https://api.minimaxi.com")) { + return { + apiKey: s.minimaxApiKey, + urls: [ + "https://api.minimaxi.com/v1/token_plan/remains", + "https://www.minimaxi.com/v1/api/openplatform/coding_plan/remains", + ], + } + } + return { apiKey: s.minimaxApiKey } + }, + // The first URL is the token plan, whose counters report what is left. + parse: (payload, urlIndex) => parseMiniMaxRemains(payload, urlIndex === 0), + }, + { + providerId: providerIdentifiers.nanogpt, + credentials: (s) => ({ + apiKey: s.nanoGptApiKey, + urls: ["https://nano-gpt.com/api/subscription/v1/usage"], + }), + parse: parseNanoGptUsage, + }, + { + providerId: providerIdentifiers.opencodeGo, + credentials: (s) => ({ + apiKey: s.opencodeGoApiKey, + urls: ["https://opencode.ai/zen/go/v1/usage"], + }), + parse: parseOpenCodeGoUsage, + }, +] + +type KeyedCredentials = ReturnType + +/** + * The credentials to query with: a profile whose host is safe to query wins over + * one pointed at a custom gateway, which is only a fallback (reported as + * unsupported when nothing better exists). + */ +function pickCredentials(provider: KeyedProvider, profiles: ProviderSettings[]): KeyedCredentials | undefined { + const withKey = profiles + .filter((p) => p.apiProvider === provider.providerId) + .map(provider.credentials) + .filter((c) => !!c.apiKey) + return withKey.find((c) => c.urls?.length) ?? withKey[0] +} + +/** Resolves without any network call when there is nothing to query. */ +function unqueryable(provider: KeyedProvider, creds: KeyedCredentials | undefined): ProviderQuota | undefined { + const base = { providerId: provider.providerId, ok: false, fetchedAt: Date.now() } as const + if (!creds?.apiKey) return { ...base, reason: "noKey" } + // SAFETY: never send a key to a host other than the provider's own API. A + // profile pointed at a custom gateway is reported as unsupported instead. + if (!creds.urls?.length) return { ...base, reason: "unsupported" } + return undefined +} + +async function fetchKeyed( + provider: KeyedProvider, + creds: KeyedCredentials, + fetchImpl: Fetch, + signal?: AbortSignal, +): Promise { + const now = Date.now() + if (!creds.apiKey || !creds.urls?.length) { + return { providerId: provider.providerId, ok: false, reason: "unsupported", fetchedAt: now } + } + + try { + // Try each URL in order; a failing fallback only matters if no URL yields data. + let windows: ProviderQuotaWindow[] | undefined + let lastError: unknown + for (const [index, url] of creds.urls.entries()) { + try { + windows = provider.parse(await getJson(fetchImpl, url, creds.apiKey, signal), index) + if (windows?.length) break + } catch (error) { + if (error instanceof AuthError) throw error + lastError = error + } + } + if (!windows?.length && lastError) throw lastError + return windows?.length + ? { providerId: provider.providerId, ok: true, windows, fetchedAt: now } + : { providerId: provider.providerId, ok: false, reason: "error", message: "No usage data", fetchedAt: now } + } catch (error) { + return { + providerId: provider.providerId, + ok: false, + reason: error instanceof AuthError ? "noKey" : "error", + message: error instanceof AuthError ? undefined : safeErrorMessage(error), + fetchedAt: now, + } + } +} + +async function fetchCopilot(force: boolean): Promise { + const result = await getGitHubCopilotQuota({ force }) + const providerId = providerIdentifiers.githubCopilot + if (!result.ok) + return { providerId, ok: false, reason: result.reason, message: result.message, fetchedAt: Date.now() } + + const q = result.quota + const window: ProviderQuotaWindow = q.unlimited + ? { id: "premium", valueLabel: "Unlimited", resetAt: q.resetAt } + : { + id: "premium", + valueLabel: + q.used !== undefined && q.entitlement !== undefined + ? `${formatCredits(q.used)} / ${formatCredits(q.entitlement)} used` + : q.usedPercent !== undefined + ? `${Math.round(q.usedPercent)}% used` + : // Neither counts nor a percentage were reported: that is unknown, not a measured zero. + "Usage not reported", + usedPercent: q.usedPercent, + resetAt: q.resetAt, + } + return { providerId, ok: true, plan: q.plan, windows: [window], fetchedAt: q.fetchedAt } +} + +// Entry point ------------------------------------------------------------------------------------- + +/** The slice of `ProviderSettingsManager` this service needs (keeps it testable without the manager). */ +export interface ProfileSource { + listConfig(): Promise<{ id: string }[]> + getProfile(params: { id: string }): Promise +} + +/** + * Loads full profiles (including API keys) host-side. A profile that fails to + * load is skipped rather than failing the whole refresh. Keys never leave the + * extension host: only formatted quota rows are posted to the webview. + */ +export async function loadProfiles(source: ProfileSource): Promise { + const entries = await source.listConfig() + const profiles = await Promise.all( + entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined)), + ) + return profiles.filter((p): p is ProviderSettings => !!p) +} + +// Successful results only, per provider and URL set. A missing key, a sign-out or a +// transient error is never pinned, so fixing it shows up on the next open instead of +// after the TTL. An entry is reused only for the exact key it was fetched with, so +// switching keys refetches. The key is compared, never hashed or used in a map key. +const cache = new Map() +interface Inflight { + apiKey: string + request: Promise + /** Callers still waiting on this request. The shared fetch is aborted only when the last one leaves. */ + waiters: number + controller: AbortController +} +const inflight = new Map() + +/** Identifies a provider and the URLs it is queried at (the credential itself is not part of it). */ +function cacheKey(provider: KeyedProvider, creds: KeyedCredentials): string { + return `${provider.providerId}|${(creds.urls ?? []).join(",")}` +} + +/** Rejects as soon as `signal` aborts, without cancelling `work` (which other callers may still be using). */ +function until(work: Promise, signal: AbortSignal | undefined, onAbort: () => void): Promise { + if (!signal) return work + return new Promise((resolve, reject) => { + const abort = () => { + onAbort() + reject(new DOMException("Aborted", "AbortError")) + } + if (signal.aborted) return abort() + signal.addEventListener("abort", abort, { once: true }) + work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) + }) +} + +function quotaForKeyedProvider( + provider: KeyedProvider, + profiles: ProviderSettings[], + force: boolean, + fetchImpl: Fetch, + signal?: AbortSignal, +): Promise { + const creds = pickCredentials(provider, profiles) + const skipped = unqueryable(provider, creds) + if (skipped) return Promise.resolve(skipped) + + const resolved = creds as KeyedCredentials + const apiKey = resolved.apiKey as string + const key = cacheKey(provider, resolved) + const hit = cache.get(key) + if (!force && hit && hit.apiKey === apiKey && Date.now() - hit.at < CACHE_TTL_MS) return Promise.resolve(hit.result) + + // Concurrent callers (e.g. a refresh while the first load is still running) share one request. + let entry = inflight.get(key) + if (!entry || entry.apiKey !== apiKey) { + const controller = new AbortController() + const request: Promise = fetchKeyed(provider, resolved, fetchImpl, controller.signal) + .then((result) => { + if (result.ok) { + cache.set(key, { at: Date.now(), apiKey, result }) + } else { + // A failed refresh must not leave an older success to be served as if it were current. + cache.delete(key) + } + return result + }) + .finally(() => { + if (inflight.get(key)?.request === request) inflight.delete(key) + }) + entry = { apiKey, request, waiters: 0, controller } + inflight.set(key, entry) + } + + const shared = entry + shared.waiters++ + let left = false + const leave = () => { + if (left) return + left = true + // Last one out cancels the network work; earlier leavers just stop waiting. + if (--shared.waiters === 0) shared.controller.abort() + } + return until(shared.request, signal, leave).finally(leave) +} + +/** + * Quotas for every *connected* provider with a usage API. `profiles` are the + * user's saved provider settings; only providers present there are queried. + */ +export async function getProviderQuotas( + profiles: ProviderSettings[], + { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}, + fetchImpl: Fetch = fetch, +): Promise { + const connected = new Set(profiles.map((p) => p.apiProvider).filter(Boolean) as string[]) + + const tasks: Promise[] = [] + if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force)) + for (const provider of KEYED_PROVIDERS) { + if (connected.has(provider.providerId)) { + tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl, signal)) + } + } + + return Promise.all(tasks) +} + +/** @internal test hook */ +export function resetProviderQuotaCache() { + cache.clear() + inflight.clear() +} diff --git a/src/services/language-models/quotaUtils.ts b/src/services/language-models/quotaUtils.ts new file mode 100644 index 0000000000..f0e7d68d99 --- /dev/null +++ b/src/services/language-models/quotaUtils.ts @@ -0,0 +1,66 @@ +/** + * Small, pure helpers shared by the quota services (`copilotQuota`, + * `providerQuotas`). Kept in one place so payload parsing behaves identically + * everywhere and is tested once. + */ + +export const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + return undefined +} + +export const asRecord = (value: unknown): Record | undefined => + value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined + +export const clampPercent = (value: number): number => Math.min(100, Math.max(0, value)) + +/** + * Epoch milliseconds from a provider timestamp. Numbers below 1e12 are treated + * as epoch seconds; strings are parsed as dates. Anything else is undefined. + */ +export const toEpochMs = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value) && value > 0) { + return value < 1_000_000_000_000 ? value * 1000 : value + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Date.parse(value) + return Number.isNaN(parsed) ? undefined : parsed + } + return undefined +} + +/** `$12.40`, `-$0.50`, `¥8.00`. */ +export const formatMoney = (value: number, symbol = "$"): string => + `${value < 0 ? "-" : ""}${symbol}${Math.abs(value).toFixed(2)}` + +/** `140,000` / `39,389.3`: whole numbers stay whole, fractions keep one decimal. */ +export const formatCredits = (value: number): string => + value.toLocaleString("en-US", { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) + +/** One decimal, matching GitHub's own panel. */ +export const roundToTenth = (value: number): number => Math.round(value * 10) / 10 + +/** + * Turns an unknown failure into a short, safe string for the UI. Only the + * well-understood cases keep their text; anything else (network stack errors + * can embed URLs or headers) collapses to a generic message so credentials + * can never ride an error back to the webview. + */ +export class QuotaHttpError extends Error { + constructor(readonly status: number) { + super(`HTTP ${status}`) + this.name = "QuotaHttpError" + } +} + +export function safeErrorMessage(error: unknown): string { + if (error instanceof QuotaHttpError) return error.message + if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) { + return "Request timed out" + } + return "Request failed" +} diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index 85d6919db4..8575d83fbc 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -415,6 +415,18 @@ export const stories: Record = { ) }, + "usage-settings": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, + "usage-settings-degraded": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, + "usage-settings-empty": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, "ui-settings": async () => { const { UISettingsStory } = await import("@/components/settings/__tests__/UISettings.visual.fixture") return diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx new file mode 100644 index 0000000000..fb2d863c07 --- /dev/null +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -0,0 +1,186 @@ +import { useCallback, useEffect, useRef, useState } from "react" +import { RefreshCw } from "lucide-react" + +import { VsCodeLmModelsMessageType, type ProviderQuota } from "@roo-code/types" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@src/lib/utils" +import { vscode } from "@src/utils/vscode" +import i18next from "i18next" +import { providerLabel } from "@src/utils/providerLabel" + +const barColor = (pct: number) => + pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" + +/** + * "Provider limits": live credits/balances for every connected provider that + * exposes a usage API (GitHub Copilot, OpenRouter, DeepSeek, Moonshot, Kimi Code, + * z.ai, MiniMax, NanoGPT, OpenCode Go), one + * compact row each. Fetched by the extension host; keys never reach the webview. + */ +export const ProviderLimits = () => { + const { t } = useAppTranslation() + const [quotas, setQuotas] = useState() + const [loading, setLoading] = useState(true) + + // Only the newest request counts: an answer for an older one (a slow first load that a refresh + // overtook, or one that outlived this component) is ignored. + const latestRequestId = useRef(undefined) + const counter = useRef(0) + + const cancelLatest = useCallback(() => { + if (latestRequestId.current) { + vscode.postMessage({ + type: VsCodeLmModelsMessageType.cancelProviderQuotas, + requestId: latestRequestId.current, + }) + latestRequestId.current = undefined + } + }, []) + + const request = useCallback( + (force: boolean) => { + cancelLatest() + const requestId = `provider-quotas-${++counter.current}` + latestRequestId.current = requestId + setLoading(true) + vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force, requestId }) + }, + [cancelLatest], + ) + + useEffect(() => { + const onMessage = (event: MessageEvent) => { + if (event.data?.type !== VsCodeLmModelsMessageType.providerQuotas) return + if (event.data.requestId !== latestRequestId.current) return + latestRequestId.current = undefined + setQuotas(event.data.providerQuotas ?? []) + setLoading(false) + } + window.addEventListener("message", onMessage) + request(false) + return () => { + window.removeEventListener("message", onMessage) + // Tell the host nobody is waiting any more so it can stop the profile load and any fetches. + cancelLatest() + } + }, [request, cancelLatest]) + + // Nothing connected that has a usage API: hide the section entirely. + if (quotas && quotas.length === 0) return null + + const fetchedAt = quotas?.reduce((max, q) => Math.max(max, q.fetchedAt), 0) + + return ( +
+
+

+ {t("settings:usage.limits.title")} +

+ +
+ +
+ {!quotas && ( +
+ {t("settings:usage.limits.loading")} +
+ )} + {quotas?.map((q) => ( + + new Date(ts).toLocaleDateString(i18next.language || "en", { dateStyle: "medium" }) + } + /> + ))} +
+
+ ) +} + +const QuotaRow = ({ + quota, + name, + formatReset, +}: { + quota: ProviderQuota + name: string + formatReset: (ts: number) => string +}) => { + const { t } = useAppTranslation() + + return ( +
+
+ + {name} + {quota.ok && quota.plan && ( + {quota.plan} + )} + {!quota.ok && ( + + {t(`settings:usage.limits.${quota.reason}`)} + + )} +
+ + {quota.ok && + quota.windows.map((w) => ( +
+
+ {w.valueLabel} + {w.usedPercent !== undefined && ( + + {Math.round(w.usedPercent)}% + + )} +
+ {w.usedPercent !== undefined && ( +
+
+
+ )} + {w.resetAt && ( +
+ {t("settings:usage.limits.resets", { date: formatReset(w.resetAt) })} +
+ )} +
+ ))} +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6100a373e1..050250a905 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -17,6 +17,7 @@ import { FlaskConical, TriangleAlert, Globe, + ChartColumn, Info, MessageSquare, LucideIcon, @@ -76,6 +77,7 @@ import { TerminalSettings } from "./TerminalSettings" import { ExperimentalSettings } from "./ExperimentalSettings" import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" +import { UsageSettings } from "./UsageSettings" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" import { SlashCommandsSettings } from "./SlashCommandsSettings" @@ -116,6 +118,7 @@ export const sectionNames = [ "ui", "experimental", "language", + "usage", "about", ] as const @@ -558,6 +561,7 @@ const SettingsView = forwardRef(({ onDone, t { id: "ui", icon: Glasses }, { id: "experimental", icon: FlaskConical }, { id: "language", icon: Globe }, + { id: "usage", icon: ChartColumn }, { id: "about", icon: Info }, ], [], // No dependencies needed now @@ -974,6 +978,9 @@ const SettingsView = forwardRef(({ onDone, t )} + {/* Usage Section */} + {renderTab === "usage" && } + {/* About Section */} {renderTab === "about" && ( `$${cost.toFixed(2)}` + +const PALETTE = [ + "var(--vscode-charts-blue)", + "var(--vscode-charts-purple)", + "var(--vscode-charts-green)", + "var(--vscode-charts-orange)", + "var(--vscode-charts-yellow)", + "var(--vscode-charts-red)", +] + +const WEEKDAY_FMT = new Intl.DateTimeFormat(undefined, { weekday: "narrow" }) +const DAY_FMT = new Intl.DateTimeFormat(undefined, { day: "numeric" }) + +/** + * Usage tab. + * + * 1. Provider limits: live credits/balances from each connected provider. + * 2. Usage history: rollup of local task history (tokens, cost when the + * provider reports a price, breakdowns, top tasks). + * + * Subscription providers (e.g. GitHub Copilot) record no per-task price, so + * when a range has tokens but no cost the history switches to tokens and the + * cost stat is hidden rather than shown as a misleading "$0.00". + */ +export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { + const { t } = useAppTranslation() + const { taskHistory, cwd } = useExtensionState() + + const [range, setRange] = useState("7d") + const [scope, setScope] = useState<"workspace" | "all">("workspace") + + const workspace = scope === "workspace" ? cwd : undefined + + // Provider and model come from what each request recorded, never from the + // currently selected profile, so switching providers doesn't relabel history. + const history = useMemo(() => taskHistory ?? [], [taskHistory]) + + // One "now" for the view and for the export, rolled over at the next local midnight. Without it a + // tab left open across midnight keeps yesterday's window while Export (computed at click time) + // would use today's, and the two would disagree. + const [now, setNow] = useState(() => Date.now()) + useEffect(() => { + const nextMidnight = new Date(now) + nextMidnight.setHours(24, 0, 0, 0) + const timer = setTimeout(() => setNow(Date.now()), nextMidnight.getTime() - now + 1000) + return () => clearTimeout(timer) + }, [now]) + + const summary = useMemo(() => summarizeUsage(history, { range, workspace, now }), [history, range, workspace, now]) + + const providerName = (key: string) => (key === UNRECORDED ? t("settings:usage.notRecorded") : providerLabel(key)) + + const metric: UsageMetric = pickMetric(summary) + const value = (cost: number, tokens: number) => (metric === "cost" ? cost : tokens) + const fmt = (cost: number, tokens: number) => (metric === "cost" ? formatCost(cost) : formatLargeNumber(tokens)) + const total = metricTotal(summary, metric) + + // Row view-model: the priced/unpriced rule lives in `bucketView` (pure, tested). + const bucketRow = (b: { cost: number; tokens: number; priced: boolean }) => { + const view = bucketView(b, metric, total) + return { + ...view, + label: view.unpriced + ? `${formatLargeNumber(b.tokens)} ${t("settings:usage.tokensShort")} · ${t("settings:usage.noPrice")}` + : fmt(b.cost, b.tokens), + } + } + + const maxDaily = Math.max(0, ...summary.daily.map((d) => value(d.cost, d.tokens))) + const showDayLabels = summary.daily.length <= 14 + const isEmpty = summary.taskCount === 0 + + const handleExport = () => { + const csv = usageToCsv(history, { range, workspace, now }) + const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" })) + const a = document.createElement("a") + a.href = url + a.download = `zoo-usage-${range}.csv` + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+ {t("settings:sections.usage")} + +
+
+ + +
+
+ {t("settings:usage.history")} + {!isEmpty && ( + + )} +
+ + {/* Filters */} + +
+
+ {USAGE_RANGES.map((r) => ( + + ))} +
+ +
+
+ + {isEmpty ? ( +
+ {t("settings:usage.empty")} +
+ ) : ( + <> + {/* Three compact stat cards, as in the design mock */} +
+ {metric === "cost" ? ( + + ) : ( + + )} + {metric === "cost" ? ( + 0 + ? t("settings:usage.stats.cacheHit", { + percent: Math.round(summary.cacheHitRate * 100), + }) + : undefined + } + /> + ) : ( + m.key !== UNRECORDED).length || "–", + )} + label={t("settings:usage.stats.models")} + /> + )} + +
+ + {metric === "tokens" && ( +

+ {t("settings:usage.noPricingNote")} +

+ )} + + {summary.daily.length > 1 && ( + +
+ {summary.daily.map((d) => { + const v = value(d.cost, d.tokens) + return ( +
0 + ? "bg-vscode-charts-blue" + : "bg-vscode-input-background", + )} + style={{ + height: + maxDaily > 0 && v > 0 + ? `${Math.max(6, (v / maxDaily) * 100)}%` + : "2px", + }} + title={`${new Date(d.day).toLocaleDateString()} · ${fmt(d.cost, d.tokens)}`} + /> + ) + })} +
+ {showDayLabels && ( +
+ {summary.daily.map((d) => ( + + {summary.daily.length <= 7 + ? WEEKDAY_FMT.format(d.day) + : DAY_FMT.format(d.day)} + + ))} +
+ )} + + )} + + ({ + key: b.key, + name: providerName(b.key), + ...bucketRow(b), + }))} + testId="usage-by-provider" + /> + ({ + key: b.key, + name: modelIdFromKey(b.key) ?? t("settings:usage.notRecorded"), + subtitle: b.apiProvider ? providerLabel(b.apiProvider) : undefined, + ...bucketRow(b), + }))} + testId="usage-by-model" + /> +

+ {t("settings:usage.byMode")}:{" "} + {summary.byMode + .map((m) => `${m.key} ${bucketRow(m).label.split(" · ")[0]}`) + .join(" · ")} +

+ + +
+ {summary.topTasks.map((task) => ( + + ))} +
+
+ +

+ {t("settings:usage.disclaimer")} +

+ + )} +
+
+
+
+ ) +} + +const SectionTitle = ({ children }: { children: React.ReactNode }) => ( +

{children}

+) + +const Block = ({ title, children, testId }: { title: string; children: React.ReactNode; testId?: string }) => ( +
+
{title}
+ {children} +
+) + +const Stat = ({ value, label, hint }: { value: string; label: string; hint?: string }) => ( +
+
{value}
+
{label}
+ {hint &&
{hint}
} +
+) + +const Breakdown = ({ + title, + rows, + testId, +}: { + title: string + rows: { key: string; name: string; subtitle?: string; label: string; percent: number; unpriced?: boolean }[] + testId: string +}) => ( + +
+ {rows.slice(0, 5).map((row, i) => { + const pct = row.percent + const color = row.unpriced ? "var(--vscode-descriptionForeground)" : PALETTE[i % PALETTE.length] + return ( +
+ + + {row.name} + {row.subtitle && ( + {row.subtitle} + )} + + + {row.label} + + + + + + {row.unpriced ? "–" : `${pct}%`} + +
+ ) + })} +
+
+) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx new file mode 100644 index 0000000000..01046c9bb9 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -0,0 +1,255 @@ +import { act, fireEvent, render, screen } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type ProviderQuota } from "@roo-code/types" + +import { ProviderLimits } from "../ProviderLimits" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +// Render the key plus its interpolation values so assertions can see the formatted output. +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, options?: Record) => + options ? `${key} ${Object.values(options).join(" ")}` : key, + }), +})) + +/** The id the component put on its most recent request (the host echoes it back). */ +const lastRequestId = () => { + const requests = post.mock.calls.map((c) => c[0]).filter((m) => m.type === "requestProviderQuotas") + return requests.at(-1)?.requestId as string +} + +/** Answers the most recent request, or the given id. Pass `null` to send an answer with no id at all. */ +const respond = (providerQuotas: ProviderQuota[], id?: string | null) => + act(() => { + const requestId = id === null ? undefined : (id ?? lastRequestId()) + window.dispatchEvent( + new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas, requestId } }), + ) + }) + +describe("ProviderLimits", () => { + beforeEach(() => { + post.mockClear() + }) + + it("asks the extension for quotas on mount without forcing", () => { + render() + expect(post).toHaveBeenCalledWith({ + type: "requestProviderQuotas", + bool: false, + requestId: expect.stringMatching(/^provider-quotas-\d+$/), + }) + }) + + it("shows a loading row until the first response", () => { + render() + expect(screen.getByTestId("provider-limits")).toBeTruthy() + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`)).toBeNull() + }) + + it("renders credits with a progress bar and reset date", () => { + render() + respond([ + { + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + fetchedAt: Date.now(), + windows: [ + { + id: "premium", + valueLabel: "39,389.3 / 140,000 used", + usedPercent: 28.1, + resetAt: Date.parse("2026-11-01T12:00:00Z"), + }, + ], + }, + ]) + + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`) + expect(row.textContent).toContain("GitHub Copilot") + expect(row.textContent).toContain("business") + expect(row.textContent).toContain("39,389.3 / 140,000 used") + expect(row.textContent).toContain("28%") + const bar = row.querySelector("[role=progressbar]")! + expect(bar.getAttribute("aria-valuenow")).toBe("28") + // The translation key plus the formatted reset date, not just the key. + expect(row.textContent).toContain( + `settings:usage.limits.resets ${new Date(Date.parse("2026-11-01T12:00:00Z")).toLocaleDateString("en", { dateStyle: "medium" })}`, + ) + }) + + it("omits the bar for balance-only providers", () => { + render() + respond([ + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: 1, + windows: [{ id: "balance", valueLabel: "$4.25 left" }], + }, + ]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.deepseek}`) + expect(row.textContent).toContain("$4.25 left") + expect(row.querySelector("[role=progressbar]")).toBeNull() + }) + + it("writes the reset date and the updated time in the display language", () => { + render() + const resetAt = new Date(2026, 10, 1, 12, 0).getTime() + const fetchedAt = new Date(2026, 10, 1, 14, 41).getTime() + respond([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt, + windows: [{ id: "key_limit", valueLabel: "x", usedPercent: 10, resetAt }], + }, + ]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + expect(row.textContent).toContain( + `settings:usage.limits.resets ${new Date(resetAt).toLocaleDateString("en", { dateStyle: "medium" })}`, + ) + expect(screen.getByTestId("provider-limits-refresh").textContent).toContain( + `settings:usage.limits.updated ${new Date(fetchedAt).toLocaleTimeString("en", { hour: "numeric", minute: "2-digit" })}`, + ) + }) + + it.each(["noKey", "noCopilot", "signedOut", "unsupported", "error"] as const)( + "shows the quiet status text for %s", + (reason) => { + render() + respond([{ providerId: providerIdentifiers.openrouter, ok: false, reason, fetchedAt: 1 }]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + expect(row.textContent).toContain(`settings:usage.limits.${reason}`) + expect(row.querySelector("[role=progressbar]")).toBeNull() + }, + ) + + it.each([ + [50, "bg-vscode-charts-green"], + [80, "bg-vscode-charts-yellow"], + [95, "bg-vscode-charts-red"], + ])("colours the bar by how much of the allowance is used (%i%%)", (usedPercent, expected) => { + render() + respond([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt: 1, + windows: [{ id: "key_limit", valueLabel: "x", usedPercent }], + }, + ]) + const bar = screen + .getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + .querySelector("[role=progressbar] > div")! + expect(bar.className).toContain(expected) + }) + + it("treats a response without a providerQuotas list as empty", () => { + render() + act(() => { + window.dispatchEvent( + new MessageEvent("message", { data: { type: "providerQuotas", requestId: lastRequestId() } }), + ) + }) + expect(screen.queryByTestId("provider-limits")).toBeNull() + }) + + it("hides the whole section when nothing connected has a usage API", () => { + render() + respond([]) + expect(screen.queryByTestId("provider-limits")).toBeNull() + }) + + it("refresh forces a new request", () => { + render() + respond([ + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: 1, + windows: [{ id: "b", valueLabel: "$1.00 left" }], + }, + ]) + post.mockClear() + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + expect(post).toHaveBeenCalledWith({ + type: "requestProviderQuotas", + bool: true, + requestId: expect.stringMatching(/^provider-quotas-\d+$/), + }) + }) + + it("ignores unrelated messages", () => { + render() + act(() => { + window.dispatchEvent(new MessageEvent("message", { data: { type: "somethingElse" } })) + }) + expect(screen.getByTestId("provider-limits")).toBeTruthy() + }) + + describe("request lifecycle", () => { + const ok = (providerId: string = providerIdentifiers.deepseek): ProviderQuota => ({ + providerId, + ok: true, + fetchedAt: 1, + windows: [{ id: "b", valueLabel: "$1.00 left" }], + }) + + it("ignores an answer for a request that a refresh has overtaken", () => { + render() + const first = lastRequestId() + respond([ok(providerIdentifiers.deepseek)]) + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + const second = lastRequestId() + expect(second).not.toBe(first) + + // A late answer to the first request must not replace the newer state or end the spinner. + respond([ok(providerIdentifiers.moonshot)], first) + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.moonshot}`)).toBeNull() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(true) + + respond([ok(providerIdentifiers.moonshot)], second) + expect(screen.getByTestId(`provider-limit-${providerIdentifiers.moonshot}`)).toBeTruthy() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(false) + }) + + it("ignores an answer that carries no request id or an unknown one", () => { + render() + respond([ok()], null) + respond([ok()], "provider-quotas-999") + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.deepseek}`)).toBeNull() + }) + + it("does not offer a refresh while a request is pending, so a newer one never races an older one", () => { + render() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(true) + post.mockClear() + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + expect(post).not.toHaveBeenCalled() + }) + + it("cancels the in-flight request on unmount and ignores a late answer", () => { + const { unmount } = render() + const pending = lastRequestId() + post.mockClear() + unmount() + expect(post).toHaveBeenCalledWith({ type: "cancelProviderQuotas", requestId: pending }) + // Must not throw or update state after unmount. + expect(() => respond([ok()], pending)).not.toThrow() + }) + + it("does not cancel a request that has already been answered", () => { + const { unmount } = render() + respond([ok()]) + post.mockClear() + unmount() + expect(post).not.toHaveBeenCalledWith(expect.objectContaining({ type: "cancelProviderQuotas" })) + }) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx new file mode 100644 index 0000000000..f7518755ad --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx @@ -0,0 +1,276 @@ +import { fireEvent, render, screen } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) + +vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
})) + +// Radix Select does not open in jsdom; a native onValueChange(e.target.value)}> + {children} + + ), + SelectTrigger: ({ children }: { children: React.ReactNode }) => <>{children}, + SelectValue: () => null, + SelectContent: ({ children }: { children: React.ReactNode }) => <>{children}, + SelectItem: ({ value, children }: { value: string; children: React.ReactNode }) => ( + + ), +})) + +const NOW = Date.now() +const DAY = 24 * 60 * 60 * 1000 + +let n = 0 +const task = (overrides: Partial = {}): HistoryItem => ({ + id: `t${++n}`, + number: n, + ts: NOW - 60_000, + task: `Task ${n}`, + tokensIn: 1000, + tokensOut: 100, + totalCost: 0, + workspace: "/ws/a", + mode: "code", + ...overrides, +}) + +const withUsage = (overrides: Partial & { cost?: number; cacheReads?: number } = {}) => { + const { cost = 1, ...rest } = overrides + return task({ + totalCost: cost, + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost, + }, + ], + ...rest, + }) +} + +describe("UsageSettings – branches", () => { + beforeEach(() => { + post.mockClear() + state.taskHistory = [] + state.cwd = "/ws/a" + }) + + it("widening the scope to all workspaces includes tasks from other workspaces", () => { + state.taskHistory = [withUsage({ cost: 1 }), withUsage({ cost: 2, workspace: "/ws/other" })] + render() + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") + fireEvent.change(screen.getByTestId("scope-select"), { target: { value: "all" } }) + expect(screen.getByTestId("usage-stats").textContent).toContain("$3.00") + fireEvent.change(screen.getByTestId("scope-select"), { target: { value: "workspace" } }) + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") + }) + + it("shows the cache hit rate only when something was read from the cache", () => { + state.taskHistory = [withUsage({ cacheReads: 3000 })] + const { unmount } = render() + expect(screen.getByTestId("usage-stats").textContent).toContain("settings:usage.stats.cacheHit") + unmount() + + state.taskHistory = [withUsage()] + render() + expect(screen.getByTestId("usage-stats").textContent).not.toContain("settings:usage.stats.cacheHit") + }) + + it("labels chart bars by weekday for a week, and by date for a longer range", () => { + state.taskHistory = [withUsage()] + render() + const weekLabels = Array.from(screen.getByTestId("usage-daily-chart").parentElement!.querySelectorAll("span")) + .map((s) => s.textContent) + .filter(Boolean) + fireEvent.click(screen.getByTestId("usage-range-30d")) + const monthChart = screen.getByTestId("usage-daily-chart") + expect(monthChart.children.length).toBeGreaterThan(14) + expect(weekLabels.length).toBeGreaterThan(0) + }) + + it("shows an unpriced top task by tokens and a priced one by cost when metrics are mixed", () => { + state.taskHistory = [ + withUsage({ cost: 2, task: "priced task" }), + task({ + task: "unpriced task", + totalCost: 0, + tokensIn: 5000, + tokensOut: 500, + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "gpt-5", + requests: 1, + tokensIn: 5000, + tokensOut: 500, + cost: 0, + }, + ], + }), + ] + render() + // Each task is one button; find it by its title and check that row on its own. + const rowFor = (title: string) => screen.getByText(title).closest("button")!.textContent! + + const priced = rowFor("priced task") + expect(priced).toContain("$2.00") + expect(priced).not.toContain("settings:usage.tokensShort") + + const unpriced = rowFor("unpriced task") + expect(unpriced).toContain("settings:usage.tokensShort") + expect(unpriced).not.toContain("$") + }) + + it("shows only the model when no provider was recorded, and nothing when no usage was recorded", () => { + state.taskHistory = [ + task({ + task: "model only", + totalCost: 1, + usageByModel: [{ modelId: "mystery-model", requests: 1, tokensIn: 10, tokensOut: 1, cost: 1 }], + }), + task({ task: "no usage rows", totalCost: 0.5 }), + ] + render() + const rows = screen.getByTestId("usage-top-tasks").textContent! + expect(rows).toContain("mystery-model") + expect(rows).not.toContain("·") + }) + + it("renders the empty state when the task history is not available yet", () => { + state.taskHistory = undefined as never + render() + expect(screen.getByTestId("usage-empty")).toBeTruthy() + }) + + it("labels chart bars by date when more than a week of days is charted", () => { + state.taskHistory = Array.from({ length: 9 }, (_, i) => withUsage({ ts: NOW - i * DAY })) + render() + fireEvent.click(screen.getByTestId("usage-range-all")) + const chart = screen.getByTestId("usage-daily-chart") + expect(chart.children.length).toBeGreaterThan(7) + expect(chart.children.length).toBeLessThanOrEqual(14) + // Date labels (e.g. "Oct 27") sit under the bars, one per day, each with a day number. + const labelRow = chart.nextElementSibling as HTMLElement + expect(labelRow.children).toHaveLength(chart.children.length) + expect(labelRow.textContent).toMatch(/\d/) + }) + + it("treats tasks with no recorded tokens or cost as zero rather than failing", () => { + // Mixed metrics (one priced task) so unpriced rows are shown by tokens. + state.taskHistory = [ + withUsage({ cost: 2 }), + task({ + task: "bare", + totalCost: undefined as never, + tokensIn: undefined as never, + tokensOut: undefined as never, + }), + ] + render() + expect(screen.getByTestId("usage-top-tasks").textContent).toContain("bare") + }) + + it("treats a task with no cost as zero when ranking by tokens", () => { + state.taskHistory = [ + task({ + task: "tokens only", + totalCost: undefined as never, + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "gpt-5", + requests: 1, + tokensIn: 100, + tokensOut: 10, + cost: 0, + }, + ], + }), + ] + render() + expect(screen.getByTestId("usage-top-tasks").textContent).toContain("tokens only") + }) + it("shows the cost for a priced task whose token counters are missing", () => { + state.taskHistory = [ + task({ + task: "priced no counters", + totalCost: 3, + tokensIn: undefined as never, + tokensOut: undefined as never, + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1, + tokensOut: 1, + cost: 3, + }, + ], + }), + ] + render() + expect(screen.getByTestId("usage-top-tasks").textContent).toContain("$3.00") + }) + + it("hides the export button when the task history is not available", () => { + state.taskHistory = undefined as never + render() + expect(screen.queryByTestId("usage-export-csv")).toBeNull() + }) + + it("shows a token count for an unpriced task whose token counters are missing", () => { + state.taskHistory = [ + withUsage({ cost: 2 }), + task({ + task: "no counters", + totalCost: 0, + tokensIn: undefined as never, + tokensOut: undefined as never, + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "gpt-5", + requests: 1, + tokensIn: 1, + tokensOut: 1, + cost: 0, + }, + ], + }), + ] + render() + const text = screen.getByTestId("usage-top-tasks").textContent! + expect(text).toContain("no counters") + expect(text).toContain("settings:usage.tokensShort") + }) + + it("excludes tasks older than the range", () => { + state.taskHistory = [withUsage({ cost: 1 }), withUsage({ cost: 9, ts: NOW - 20 * DAY })] + render() + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$10.00") + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx new file mode 100644 index 0000000000..f688f68287 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx @@ -0,0 +1,103 @@ +import { act, fireEvent, render, screen } from "@testing-library/react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) + +vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
})) + +// Spy on the real export so the test can see which "now" it was given. +const csvSpy = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/usage", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + usageToCsv: (...args: Parameters) => { + csvSpy(...args) + return actual.usageToCsv(...args) + }, + } +}) + +const MINUTE = 60_000 +// 23:50 local on 2026-10-27: ten minutes before midnight. +const BEFORE_MIDNIGHT = new Date(2026, 9, 27, 23, 50).getTime() + +const todaysTask: HistoryItem = { + id: "late", + number: 1, + ts: new Date(2026, 9, 27, 23, 0).getTime(), + task: "Late task", + tokensIn: 1000, + tokensOut: 100, + totalCost: 4, + workspace: "/ws/a", + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost: 4, + }, + ], +} + +describe("UsageSettings – across local midnight", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] }) + vi.setSystemTime(BEFORE_MIDNIGHT) + post.mockClear() + csvSpy.mockClear() + state.taskHistory = [todaysTask] + state.cwd = "/ws/a" + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("rolls the Today view over at midnight instead of keeping yesterday's tasks", () => { + render() + fireEvent.click(screen.getByTestId("usage-range-today")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$4.00") + + // Midnight plus the one-second margin: the view must re-evaluate "today" on its own. + act(() => { + vi.advanceTimersByTime(10 * MINUTE + 5_000) + }) + + expect(screen.queryByTestId("usage-stats")).toBeNull() + expect(screen.getByTestId("usage-empty")).toBeTruthy() + }) + + it("keeps the view and the export on the same day, even if the clock moves past midnight first", () => { + render() + fireEvent.click(screen.getByTestId("usage-range-today")) + Object.assign(URL, { createObjectURL: vi.fn(() => "blob:x"), revokeObjectURL: vi.fn() }) + const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) + + // The wall clock passes midnight but the roll-over timer has not fired yet: the view still shows the + // old day, so Export must use that same day rather than a fresh Date.now(). + vi.setSystemTime(BEFORE_MIDNIGHT + 20 * MINUTE) + fireEvent.click(screen.getByTestId("usage-export-csv")) + + expect(csvSpy).toHaveBeenCalledTimes(1) + expect(csvSpy.mock.calls[0][1]).toMatchObject({ range: "today", now: BEFORE_MIDNIGHT }) + click.mockRestore() + }) + + it("does not leave a timer running after unmount", () => { + const { unmount } = render() + unmount() + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx new file mode 100644 index 0000000000..7f7c2acb54 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -0,0 +1,263 @@ +import { fireEvent, render, screen, within } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) + +// The provider-limits section talks to the extension host; it has its own spec. +vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
})) + +const NOW = Date.now() + +let n = 0 +const task = (overrides: Partial = {}): HistoryItem => ({ + id: `t${++n}`, + number: n, + ts: NOW - 60_000, + task: `Task ${n}`, + tokensIn: 1000, + tokensOut: 100, + totalCost: 0, + workspace: "/ws/a", + mode: "code", + ...overrides, +}) + +const priced = (cost: number, overrides: Partial = {}) => + task({ + totalCost: cost, + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost, + }, + ], + ...overrides, + }) + +const copilot = (overrides: Partial = {}) => + task({ + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost: 0, + }, + ], + ...overrides, + }) + +describe("UsageSettings", () => { + beforeEach(() => { + post.mockClear() + state.taskHistory = [] + state.cwd = "/ws/a" + }) + + it("always renders the provider limits section", () => { + render() + expect(screen.getByTestId("provider-limits-stub")).toBeTruthy() + }) + + it("shows an empty state with no tasks", () => { + render() + expect(screen.getByTestId("usage-empty")).toBeTruthy() + expect(screen.queryByTestId("usage-stats")).toBeNull() + expect(screen.queryByTestId("usage-export-csv")).toBeNull() + }) + + describe("when a provider reports a price", () => { + beforeEach(() => { + state.taskHistory = [priced(1.25), priced(0.5)] + }) + + it("shows cost first and tokens second", () => { + render() + const stats = screen.getByTestId("usage-stats") + expect(stats.textContent).toContain("$1.75") + expect(stats.textContent).toContain("2.2") // 2 tasks x 1,100 tokens (suffix is a translation key in tests) + expect(screen.queryByTestId("usage-no-pricing-note")).toBeNull() + }) + + it("charts and ranks by cost", () => { + render() + const top = screen.getByTestId("usage-top-tasks") + expect(top.textContent).toContain("$1.25") + expect(within(screen.getByTestId("usage-by-provider")).getByText("settings:usage.byProvider")).toBeTruthy() + }) + }) + + describe("when no provider reports a price (e.g. GitHub Copilot)", () => { + beforeEach(() => { + state.taskHistory = [copilot()] + }) + + it("never shows a dollar figure and explains why", () => { + render() + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$") + expect(screen.getByTestId("usage-no-pricing-note")).toBeTruthy() + }) + + it("leads with tokens and the in/out split", () => { + render() + const stats = screen.getByTestId("usage-stats").textContent! + expect(stats).toContain("1.1") + expect(stats).toContain("settings:usage.stats.inOut".split(".").pop()!) + }) + }) + + describe("mixed priced and unpriced providers", () => { + beforeEach(() => { + state.taskHistory = [priced(1.5), copilot({ tokensIn: 16_000_000 })] + }) + + it("prioritises cost over tokens when any provider reports a price", () => { + render() + const stats = screen.getByTestId("usage-stats").textContent! + expect(stats).toContain("$1.50") + expect(screen.queryByTestId("usage-no-pricing-note")).toBeNull() + }) + + it("shows subscription providers as tokens with a no-price tag, not $0.00 · 0%", () => { + render() + const providers = screen.getByTestId("usage-by-provider") + const unpriced = within(providers).getByTestId("usage-unpriced") + expect(unpriced.textContent).toContain("noPrice") + expect(unpriced.textContent).not.toContain("$0.00") + // The unpriced row has no share: it shows "–", never "0%". + const row = unpriced.parentElement! + expect(row.textContent).toContain("–") + expect(row.textContent).not.toMatch(/(^|\D)0%/) + expect(row.querySelector("span[style*='width: 0%']")).not.toBeNull() + }) + + it("gives priced providers their cost and share", () => { + render() + const providers = screen.getByTestId("usage-by-provider") + expect(providers.textContent).toContain("$1.50") + expect(providers.textContent).toContain("100%") + }) + + it("shows an unpriced top task by tokens rather than $0.00", () => { + render() + expect(screen.getByTestId("usage-top-tasks").textContent).not.toContain("$0.00") + }) + }) + + describe("attribution", () => { + it("lists the provider and model that were recorded", () => { + state.taskHistory = [copilot()] + render() + expect(screen.getByTestId("usage-by-provider").textContent).toContain("GitHub Copilot") + expect(screen.getByTestId("usage-by-model").textContent).toContain("claude-opus-5.5") + }) + + it("labels tasks with no recorded provider as not recorded rather than guessing", () => { + state.taskHistory = [task()] + render() + expect(screen.getByTestId("usage-by-provider").textContent).toContain("notRecorded") + expect(screen.getByTestId("usage-by-model").textContent).toContain("notRecorded") + }) + + it("shows provider · model under each top task", () => { + state.taskHistory = [copilot()] + render() + expect(screen.getByTestId("usage-top-tasks").textContent).toContain("GitHub Copilot · claude-opus-5.5") + }) + + it("summarises modes on a single line", () => { + state.taskHistory = [priced(1, { mode: "code" }), priced(0.5, { mode: "architect" })] + render() + const line = screen.getByTestId("usage-by-mode").textContent! + expect(line).toContain("code $1.00") + expect(line).toContain("architect $0.50") + }) + }) + + describe("filters", () => { + it("defaults to this workspace and can widen to all", () => { + state.taskHistory = [priced(1), priced(2, { workspace: "/ws/other" })] + render() + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") + }) + + it("changes the range", () => { + state.taskHistory = [priced(1), priced(5, { ts: NOW - 20 * 24 * 60 * 60 * 1000 })] + render() + // default 7 days → only the recent task + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + fireEvent.click(screen.getByTestId("usage-range-30d")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$6.00") + fireEvent.click(screen.getByTestId("usage-range-today")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + }) + + it("includes everything for the all-time range", () => { + state.taskHistory = [priced(1), priced(5, { ts: NOW - 200 * 24 * 60 * 60 * 1000 })] + render() + fireEvent.click(screen.getByTestId("usage-range-all")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$6.00") + }) + + it("marks the active range for assistive tech", () => { + render() + expect(screen.getByTestId("usage-range-7d").getAttribute("aria-checked")).toBe("true") + expect(screen.getByTestId("usage-range-30d").getAttribute("aria-checked")).toBe("false") + }) + }) + + describe("interactions", () => { + it("opens a task when its row is clicked", () => { + const t = priced(1) + state.taskHistory = [t] + render() + fireEvent.click(within(screen.getByTestId("usage-top-tasks")).getByRole("button")) + expect(post).toHaveBeenCalledWith({ type: "showTaskWithId", text: t.id }) + }) + + it("exports a CSV of the filtered range", () => { + state.taskHistory = [priced(1)] + const createObjectURL = vi.fn(() => "blob:x") + const revokeObjectURL = vi.fn() + Object.assign(URL, { createObjectURL, revokeObjectURL }) + const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) + + render() + fireEvent.click(screen.getByTestId("usage-export-csv")) + + expect(createObjectURL).toHaveBeenCalledTimes(1) + expect(click).toHaveBeenCalled() + expect(revokeObjectURL).toHaveBeenCalledWith("blob:x") + click.mockRestore() + }) + }) + + it("exposes the chart to assistive tech as a single labelled image", () => { + state.taskHistory = [priced(1)] + render() + const chart = screen.getByTestId("usage-daily-chart") + expect(chart.getAttribute("role")).toBe("img") + expect(chart.getAttribute("aria-label")).toBeTruthy() + }) + + it("draws one chart bar per day for a bounded range", () => { + state.taskHistory = [priced(1)] + render() + expect(screen.getByTestId("usage-daily-chart").children).toHaveLength(7) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx new file mode 100644 index 0000000000..1dbb6e3c9c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx @@ -0,0 +1,143 @@ +import { useEffect } from "react" + +import { providerIdentifiers, VsCodeLmModelsMessageType, type HistoryItem, type ProviderQuota } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" +import { AppProviders } from "../../../../playwright/AppProviders" +import { vscode } from "@src/utils/vscode" + +/** Matches the clock the visual test pins, so the history and the "updated" label are stable. */ +export const USAGE_NOW = new Date("2026-06-01T12:00:00Z").getTime() +const DAY = 24 * 60 * 60 * 1000 +const WORKSPACE = "/work/project" + +const task = (n: number, daysAgo: number, overrides: Partial): HistoryItem => ({ + id: `usage-task-${n}`, + number: n, + ts: USAGE_NOW - daysAgo * DAY, + task: `Task ${n}`, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: WORKSPACE, + mode: "code", + ...overrides, +}) + +const used = (apiProvider: string, modelId: string, tokensIn: number, tokensOut: number, cost: number) => [ + { apiProvider, modelId, requests: 4, tokensIn, tokensOut, cost }, +] + +/** A week of mixed priced (Anthropic) and unpriced (Copilot) work. */ +const history: HistoryItem[] = [ + task(1, 0, { + task: "Add a loading spinner to the Save button", + tokensIn: 52_000, + tokensOut: 4_100, + totalCost: 1.84, + cacheReads: 31_000, + usageByModel: used(providerIdentifiers.anthropic, "claude-sonnet-5", 52_000, 4_100, 1.84), + }), + task(2, 1, { + task: "Refactor the auth middleware", + tokensIn: 91_000, + tokensOut: 8_800, + totalCost: 3.27, + cacheReads: 60_000, + mode: "architect", + usageByModel: used(providerIdentifiers.anthropic, "claude-sonnet-5", 91_000, 8_800, 3.27), + }), + task(3, 2, { + task: "Write unit tests for the billing service", + tokensIn: 210_000, + tokensOut: 15_000, + totalCost: 0, + usageByModel: used(providerIdentifiers.githubCopilot, "gpt-5", 210_000, 15_000, 0), + }), + task(4, 4, { + task: "Investigate the flaky checkout test", + tokensIn: 34_000, + tokensOut: 2_900, + totalCost: 0.61, + mode: "debug", + usageByModel: used(providerIdentifiers.anthropic, "claude-haiku-5", 34_000, 2_900, 0.61), + }), +] + +const healthyQuotas: ProviderQuota[] = [ + { + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + fetchedAt: USAGE_NOW, + windows: [ + { + id: "premium", + valueLabel: "39,389 / 140,000 used", + usedPercent: 28, + resetAt: new Date("2026-07-01T00:00:00Z").getTime(), + }, + ], + }, + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: USAGE_NOW, + windows: [{ id: "balance", valueLabel: "$4.25 left" }], + }, +] + +const degradedQuotas: ProviderQuota[] = [ + { + providerId: providerIdentifiers.githubCopilot, + ok: false, + reason: "signedOut", + fetchedAt: USAGE_NOW, + }, + { providerId: providerIdentifiers.openrouter, ok: false, reason: "noKey", fetchedAt: USAGE_NOW }, + { providerId: providerIdentifiers.deepseek, ok: false, reason: "error", message: "HTTP 500", fetchedAt: USAGE_NOW }, +] + +/** Plays the extension host: answers the component's quota request, echoing its request id. */ +function FakeHost({ quotas }: { quotas: ProviderQuota[] }) { + useEffect(() => { + const original = vscode.postMessage.bind(vscode) + vscode.postMessage = ((message: { type: string; requestId?: string }) => { + if (message.type === VsCodeLmModelsMessageType.requestProviderQuotas) { + queueMicrotask(() => + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: VsCodeLmModelsMessageType.providerQuotas, + providerQuotas: quotas, + requestId: message.requestId, + }, + }), + ), + ) + return + } + return original(message as never) + }) as typeof vscode.postMessage + return () => { + vscode.postMessage = original as typeof vscode.postMessage + } + }, [quotas]) + return null +} + +export type UsageStoryVariant = "populated" | "degraded" | "empty" + +export function UsageSettingsStory({ variant = "populated" }: { variant?: UsageStoryVariant }) { + const quotas = variant === "degraded" ? degradedQuotas : variant === "empty" ? [] : healthyQuotas + return ( + + +
+ +
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx new file mode 100644 index 0000000000..5b227ac29c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx @@ -0,0 +1,59 @@ +import { expect, test } from "../../../../playwright/coverage-fixture" +import { expectBoundedLayout } from "../../../../playwright/layout-contracts" +import { mountedStory } from "../../../../playwright/mounted-story" +import { applyVisualTheme, visualThemes } from "../../../../playwright/themes" + +const NOW = new Date("2026-06-01T12:00:00Z") + +for (const theme of visualThemes) { + test(`renders the Usage tab with provider limits and history in the VS Code ${theme.name} theme`, async ({ + mount, + page, + }) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings")) + await applyVisualTheme(page, theme) + const story = component.getByTestId("usage-story") + + // Limits arrive from the (fake) host; wait for them so the screenshot is the settled state. + await expect(story.getByTestId(`provider-limit-github-copilot`)).toBeVisible() + await expect(story.getByTestId("usage-stats")).toBeVisible() + await expect(story.getByTestId("usage-daily-chart")).toBeVisible() + + await expect(story).toHaveScreenshot(`usage-settings-${theme.name}.png`) + const exportButton = story.getByTestId("usage-export-csv") + await expectBoundedLayout(page, story, { focusedControl: exportButton }) + }) +} + +test("shows quiet statuses for provider limits that cannot be read, and keeps the history", async ({ mount, page }) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings-degraded")) + await applyVisualTheme(page, visualThemes[0]) + const story = component.getByTestId("usage-story") + + await expect(story.getByTestId("provider-limit-github-copilot")).toBeVisible() + await expect(story.getByTestId("provider-limit-openrouter")).toBeVisible() + await expect(story.getByTestId("provider-limit-deepseek")).toBeVisible() + // No bars for providers that report nothing, but the usage history is unaffected. + await expect(story.locator("[role=progressbar]")).toHaveCount(0) + await expect(story.getByTestId("usage-stats")).toBeVisible() + + await expect(story).toHaveScreenshot("usage-settings-degraded.png") +}) + +test("shows the empty state, and hides the limits section when no provider has a usage API", async ({ + mount, + page, +}) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings-empty")) + await applyVisualTheme(page, visualThemes[0]) + const story = component.getByTestId("usage-story") + + await expect(story.getByTestId("usage-empty")).toBeVisible() + await expect(story.getByTestId("provider-limits")).toHaveCount(0) + await expect(story.getByTestId("usage-export-csv")).toHaveCount(0) + + await expect(story).toHaveScreenshot("usage-settings-empty.png") +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d39d8edd6a..bb82add8fc 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Sobre Zoo Code", "skills": "Skills", - "rules": "Regles" + "rules": "Regles", + "usage": "Ús" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "La descripció ha de tenir com a màxim 1024 caràcters" }, "footer": "Crea les teves pròpies skills amb el mode Skill Writer, disponible al Modes Marketplace." + }, + "usage": { + "description": "Tokens i cost de l'historial de tasques de Zoo Code en aquesta màquina.", + "range": { + "label": "Interval de temps", + "today": "Avui", + "7d": "7 dies", + "30d": "30 dies", + "all": "Tot el temps" + }, + "scope": { + "label": "Mostra", + "workspace": "Aquest espai de treball", + "all": "Tots els espais de treball" + }, + "empty": "No hi ha tasques en aquest interval de temps.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasques", + "cacheHit": "{{percent}}% de la memòria cau", + "inOut": "{{input}} d'entrada · {{output}} de sortida", + "models": "models" + }, + "dailyCost": "Cost diari", + "byMode": "Per mode", + "topTasks": "Tasques més cares", + "disclaimer": "Els costos són estimacions de l'historial de tasques i poden diferir de la factura del teu proveïdor.", + "exportCsv": "Exporta CSV", + "noPricingNote": "Alguns proveïdors (p. ex. GitHub Copilot) facturen per subscripció i no informen d'un preu per tasca, per això aquest historial es mostra en tokens. Consulta els límits dels proveïdors a dalt per als crèdits.", + "dailyTokens": "Tokens diaris", + "topTasksTokens": "Tasques més grans", + "byProvider": "Per proveïdor", + "history": "Historial d'ús", + "limits": { + "title": "Límits dels proveïdors", + "refresh": "Actualitza", + "updated": "Actualitzat {{time}}", + "loading": "Comprovant els proveïdors connectats…", + "resets": "Es restableix {{date}}", + "signedOut": "No has iniciat sessió", + "noCopilot": "Sense quota de Copilot", + "noKey": "Clau d'API absent o no vàlida", + "unsupported": "URL base personalitzada — no comprovat", + "error": "No s'ha pogut carregar" + }, + "byModel": "Per model", + "notRecorded": "No registrat", + "noPrice": "sense preu", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 983db1df5d..7b2ba025ce 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -42,7 +42,8 @@ "language": "Sprache", "about": "Über Zoo Code", "skills": "Skills", - "rules": "Regeln" + "rules": "Regeln", + "usage": "Nutzung" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Beschreibung muss 1024 Zeichen oder weniger sein" }, "footer": "Erstelle deine eigenen Skills mit dem Skill Writer Modus, verfügbar im Modes Marketplace." + }, + "usage": { + "description": "Tokens und Kosten aus deinem Zoo-Code-Aufgabenverlauf auf diesem Rechner.", + "range": { + "label": "Zeitraum", + "today": "Heute", + "7d": "7 Tage", + "30d": "30 Tage", + "all": "Gesamter Zeitraum" + }, + "scope": { + "label": "Anzeigen", + "workspace": "Dieser Workspace", + "all": "Alle Workspaces" + }, + "empty": "Keine Aufgaben in diesem Zeitraum.", + "stats": { + "cost": "Kosten", + "tokens": "Tokens", + "tasks": "Aufgaben", + "cacheHit": "{{percent}} % aus dem Cache", + "inOut": "{{input}} ein · {{output}} aus", + "models": "Modelle" + }, + "dailyCost": "Tägliche Kosten", + "byMode": "Nach Modus", + "topTasks": "Teuerste Aufgaben", + "disclaimer": "Die Kosten sind Schätzungen aus dem Aufgabenverlauf und können von der Rechnung deines Anbieters abweichen.", + "exportCsv": "CSV exportieren", + "noPricingNote": "Einige Anbieter (z. B. GitHub Copilot) rechnen per Abonnement ab und melden keinen Preis pro Aufgabe, daher wird dieser Verlauf in Tokens angezeigt. Credits findest du oben unter Anbieterlimits.", + "dailyTokens": "Tägliche Tokens", + "topTasksTokens": "Größte Aufgaben", + "byProvider": "Nach Anbieter", + "history": "Nutzungsverlauf", + "limits": { + "title": "Anbieterlimits", + "refresh": "Aktualisieren", + "updated": "Aktualisiert {{time}}", + "loading": "Verbundene Anbieter werden geprüft…", + "resets": "Zurücksetzung {{date}}", + "signedOut": "Nicht angemeldet", + "noCopilot": "Kein Copilot-Kontingent", + "noKey": "API-Schlüssel fehlt oder ist ungültig", + "unsupported": "Benutzerdefinierte Basis-URL – nicht geprüft", + "error": "Konnte nicht geladen werden" + }, + "byModel": "Nach Modell", + "notRecorded": "Nicht erfasst", + "noPrice": "kein Preis", + "tokensShort": "Tok.", + "chartLabel": "{{metric}}, insgesamt {{total}}" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index ea924ce79e..ad2a3f87cf 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -42,7 +42,8 @@ "ui": "UI", "experimental": "Experimental", "language": "Language", - "about": "About Zoo Code" + "about": "About Zoo Code", + "usage": "Usage" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "output": "Output", "cacheReads": "Cache reads" } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache", + "inOut": "{{input}} in · {{output}} out", + "models": "models" + }, + "dailyCost": "Daily cost", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "history": "Usage history", + "limits": { + "title": "Provider limits", + "refresh": "Refresh", + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" + }, + "byModel": "By model", + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 57612bf3e5..474a5c2437 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Acerca de Zoo Code", "skills": "Skills", - "rules": "Reglas" + "rules": "Reglas", + "usage": "Uso" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "La descripción debe tener como máximo 1024 caracteres" }, "footer": "Crea tus propias skills con el modo Skill Writer, disponible en el Modes Marketplace." + }, + "usage": { + "description": "Tokens y coste del historial de tareas de Zoo Code en este equipo.", + "range": { + "label": "Intervalo de tiempo", + "today": "Hoy", + "7d": "7 días", + "30d": "30 días", + "all": "Todo el tiempo" + }, + "scope": { + "label": "Mostrar", + "workspace": "Este espacio de trabajo", + "all": "Todos los espacios de trabajo" + }, + "empty": "No hay tareas en este intervalo de tiempo.", + "stats": { + "cost": "coste", + "tokens": "tokens", + "tasks": "tareas", + "cacheHit": "{{percent}} % desde la caché", + "inOut": "{{input}} de entrada · {{output}} de salida", + "models": "modelos" + }, + "dailyCost": "Coste diario", + "byMode": "Por modo", + "topTasks": "Tareas más caras", + "disclaimer": "Los costes son estimaciones del historial de tareas y pueden diferir de la factura de tu proveedor.", + "exportCsv": "Exportar CSV", + "noPricingNote": "Algunos proveedores (p. ej. GitHub Copilot) facturan por suscripción y no informan de un precio por tarea, por lo que este historial se muestra en tokens. Consulta los límites de los proveedores arriba para ver los créditos.", + "dailyTokens": "Tokens diarios", + "topTasksTokens": "Tareas más grandes", + "byProvider": "Por proveedor", + "history": "Historial de uso", + "limits": { + "title": "Límites de los proveedores", + "refresh": "Actualizar", + "updated": "Actualizado {{time}}", + "loading": "Comprobando los proveedores conectados…", + "resets": "Se restablece {{date}}", + "signedOut": "Sesión no iniciada", + "noCopilot": "Sin cuota de Copilot", + "noKey": "Falta la clave de API o no es válida", + "unsupported": "URL base personalizada: no comprobado", + "error": "No se pudo cargar" + }, + "byModel": "Por modelo", + "notRecorded": "No registrado", + "noPrice": "sin precio", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index a91a5137d3..2a8fe1e993 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -42,7 +42,8 @@ "language": "Langue", "about": "À propos de Zoo Code", "skills": "Skills", - "rules": "Règles" + "rules": "Règles", + "usage": "Utilisation" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "La description doit contenir au maximum 1024 caractères" }, "footer": "Créez vos propres skills avec le mode Skill Writer, disponible dans le Modes Marketplace." + }, + "usage": { + "description": "Tokens et coût de l'historique des tâches de Zoo Code sur cette machine.", + "range": { + "label": "Période", + "today": "Aujourd'hui", + "7d": "7 jours", + "30d": "30 jours", + "all": "Tout" + }, + "scope": { + "label": "Afficher", + "workspace": "Cet espace de travail", + "all": "Tous les espaces de travail" + }, + "empty": "Aucune tâche sur cette période.", + "stats": { + "cost": "coût", + "tokens": "tokens", + "tasks": "tâches", + "cacheHit": "{{percent}} % depuis le cache", + "inOut": "{{input}} en entrée · {{output}} en sortie", + "models": "modèles" + }, + "dailyCost": "Coût quotidien", + "byMode": "Par mode", + "topTasks": "Tâches les plus coûteuses", + "disclaimer": "Les coûts sont des estimations issues de l'historique des tâches et peuvent différer de la facture de votre fournisseur.", + "exportCsv": "Exporter en CSV", + "noPricingNote": "Certains fournisseurs (p. ex. GitHub Copilot) facturent par abonnement et ne communiquent pas de prix par tâche ; cet historique est donc affiché en tokens. Consultez les limites des fournisseurs ci-dessus pour les crédits.", + "dailyTokens": "Tokens quotidiens", + "topTasksTokens": "Plus grosses tâches", + "byProvider": "Par fournisseur", + "history": "Historique d'utilisation", + "limits": { + "title": "Limites des fournisseurs", + "refresh": "Actualiser", + "updated": "Mis à jour {{time}}", + "loading": "Vérification des fournisseurs connectés…", + "resets": "Réinitialisation {{date}}", + "signedOut": "Non connecté", + "noCopilot": "Aucun quota Copilot", + "noKey": "Clé API manquante ou invalide", + "unsupported": "URL de base personnalisée — non vérifiée", + "error": "Impossible de charger" + }, + "byModel": "Par modèle", + "notRecorded": "Non enregistré", + "noPrice": "sans prix", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index bc6e973623..738875d3b4 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -42,7 +42,8 @@ "language": "भाषा", "about": "परिचय", "skills": "Skills", - "rules": "नियम" + "rules": "नियम", + "usage": "उपयोग" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "विवरण 1024 वर्णों से अधिक नहीं होना चाहिए" }, "footer": "Skill Writer मोड के साथ अपने स्वयं के Skills बनाएं, Modes Marketplace में उपलब्ध।" + }, + "usage": { + "description": "इस मशीन पर आपके Zoo Code टास्क इतिहास से टोकन और लागत।", + "range": { + "label": "समय सीमा", + "today": "आज", + "7d": "7 दिन", + "30d": "30 दिन", + "all": "सभी समय" + }, + "scope": { + "label": "दिखाएँ", + "workspace": "यह वर्कस्पेस", + "all": "सभी वर्कस्पेस" + }, + "empty": "इस समय सीमा में कोई टास्क नहीं।", + "stats": { + "cost": "लागत", + "tokens": "टोकन", + "tasks": "टास्क", + "cacheHit": "{{percent}}% कैश से", + "inOut": "{{input}} इनपुट · {{output}} आउटपुट", + "models": "मॉडल" + }, + "dailyCost": "दैनिक लागत", + "byMode": "मोड के अनुसार", + "topTasks": "सबसे महंगे टास्क", + "disclaimer": "लागत टास्क इतिहास से अनुमान हैं और आपके प्रदाता के बिल से भिन्न हो सकती हैं।", + "exportCsv": "CSV निर्यात करें", + "noPricingNote": "कुछ प्रदाता (जैसे GitHub Copilot) सदस्यता के आधार पर बिल करते हैं और प्रति-टास्क मूल्य नहीं बताते, इसलिए यह इतिहास टोकन में दिखाया गया है। क्रेडिट के लिए ऊपर प्रदाता सीमाएँ देखें।", + "dailyTokens": "दैनिक टोकन", + "topTasksTokens": "सबसे बड़े टास्क", + "byProvider": "प्रदाता के अनुसार", + "history": "उपयोग इतिहास", + "limits": { + "title": "प्रदाता सीमाएँ", + "refresh": "रीफ़्रेश करें", + "updated": "{{time}} को अपडेट किया गया", + "loading": "कनेक्टेड प्रदाताओं की जाँच हो रही है…", + "resets": "{{date}} को रीसेट होगा", + "signedOut": "साइन इन नहीं है", + "noCopilot": "कोई Copilot कोटा नहीं", + "noKey": "API कुंजी अनुपलब्ध या अमान्य", + "unsupported": "कस्टम बेस URL — जाँच नहीं की गई", + "error": "लोड नहीं हो सका" + }, + "byModel": "मॉडल के अनुसार", + "notRecorded": "दर्ज नहीं", + "noPrice": "कोई मूल्य नहीं", + "tokensShort": "टोकन", + "chartLabel": "{{metric}}, कुल {{total}}" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 3ed57f373e..c929956917 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -42,7 +42,8 @@ "language": "Bahasa", "about": "Tentang Zoo Code", "skills": "Skills", - "rules": "Aturan" + "rules": "Aturan", + "usage": "Penggunaan" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Deskripsi harus 1024 karakter atau kurang" }, "footer": "Buat skills Anda sendiri dengan mode Skill Writer, tersedia di Modes Marketplace." + }, + "usage": { + "description": "Token dan biaya dari riwayat tugas Zoo Code Anda di mesin ini.", + "range": { + "label": "Rentang waktu", + "today": "Hari ini", + "7d": "7 hari", + "30d": "30 hari", + "all": "Sepanjang waktu" + }, + "scope": { + "label": "Tampilkan", + "workspace": "Workspace ini", + "all": "Semua workspace" + }, + "empty": "Tidak ada tugas dalam rentang waktu ini.", + "stats": { + "cost": "biaya", + "tokens": "token", + "tasks": "tugas", + "cacheHit": "{{percent}}% dari cache", + "inOut": "{{input}} masuk · {{output}} keluar", + "models": "model" + }, + "dailyCost": "Biaya harian", + "byMode": "Menurut mode", + "topTasks": "Tugas termahal", + "disclaimer": "Biaya adalah perkiraan dari riwayat tugas dan bisa berbeda dari tagihan penyedia Anda.", + "exportCsv": "Ekspor CSV", + "noPricingNote": "Beberapa penyedia (mis. GitHub Copilot) menagih berdasarkan langganan dan tidak melaporkan harga per tugas, sehingga riwayat ini ditampilkan dalam token. Lihat Batas penyedia di atas untuk kredit.", + "dailyTokens": "Token harian", + "topTasksTokens": "Tugas terbesar", + "byProvider": "Menurut penyedia", + "history": "Riwayat penggunaan", + "limits": { + "title": "Batas penyedia", + "refresh": "Segarkan", + "updated": "Diperbarui {{time}}", + "loading": "Memeriksa penyedia yang terhubung…", + "resets": "Diatur ulang {{date}}", + "signedOut": "Belum masuk", + "noCopilot": "Tidak ada kuota Copilot", + "noKey": "Kunci API hilang atau tidak valid", + "unsupported": "URL dasar kustom — tidak diperiksa", + "error": "Tidak dapat dimuat" + }, + "byModel": "Menurut model", + "notRecorded": "Tidak dicatat", + "noPrice": "tanpa harga", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index b708304e63..46a62a2ed7 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -42,7 +42,8 @@ "language": "Lingua", "about": "Informazioni su Zoo Code", "skills": "Skills", - "rules": "Regole" + "rules": "Regole", + "usage": "Utilizzo" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "La descrizione deve essere di massimo 1024 caratteri" }, "footer": "Crea le tue skill con la modalità Skill Writer, disponibile in Modes Marketplace." + }, + "usage": { + "description": "Token e costi dalla cronologia delle attività di Zoo Code su questo computer.", + "range": { + "label": "Intervallo di tempo", + "today": "Oggi", + "7d": "7 giorni", + "30d": "30 giorni", + "all": "Sempre" + }, + "scope": { + "label": "Mostra", + "workspace": "Questo workspace", + "all": "Tutti i workspace" + }, + "empty": "Nessuna attività in questo intervallo di tempo.", + "stats": { + "cost": "costo", + "tokens": "token", + "tasks": "attività", + "cacheHit": "{{percent}}% dalla cache", + "inOut": "{{input}} in ingresso · {{output}} in uscita", + "models": "modelli" + }, + "dailyCost": "Costo giornaliero", + "byMode": "Per modalità", + "topTasks": "Attività più costose", + "disclaimer": "I costi sono stime basate sulla cronologia delle attività e possono differire dalla fattura del tuo provider.", + "exportCsv": "Esporta CSV", + "noPricingNote": "Alcuni provider (es. GitHub Copilot) fatturano tramite abbonamento e non riportano un prezzo per attività, quindi questa cronologia è mostrata in token. Vedi i limiti dei provider sopra per i crediti.", + "dailyTokens": "Token giornalieri", + "topTasksTokens": "Attività più grandi", + "byProvider": "Per provider", + "history": "Cronologia di utilizzo", + "limits": { + "title": "Limiti dei provider", + "refresh": "Aggiorna", + "updated": "Aggiornato {{time}}", + "loading": "Controllo dei provider collegati…", + "resets": "Si reimposta {{date}}", + "signedOut": "Accesso non effettuato", + "noCopilot": "Nessuna quota Copilot", + "noKey": "Chiave API mancante o non valida", + "unsupported": "URL di base personalizzato — non controllato", + "error": "Impossibile caricare" + }, + "byModel": "Per modello", + "notRecorded": "Non registrato", + "noPrice": "senza prezzo", + "tokensShort": "tok", + "chartLabel": "{{metric}}, totale {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index f6959b50dc..8d3e18b157 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -42,7 +42,8 @@ "language": "言語", "about": "Zoo Codeについて", "skills": "Skills", - "rules": "ルール" + "rules": "ルール", + "usage": "使用状況" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "説明は1024文字以内である必要があります" }, "footer": "スキルライターモードで独自のスキルを作成します。モードマーケットプレイスで入手可能です。" + }, + "usage": { + "description": "このマシン上の Zoo Code のタスク履歴によるトークンとコスト。", + "range": { + "label": "期間", + "today": "今日", + "7d": "7日間", + "30d": "30日間", + "all": "全期間" + }, + "scope": { + "label": "表示", + "workspace": "このワークスペース", + "all": "すべてのワークスペース" + }, + "empty": "この期間にタスクはありません。", + "stats": { + "cost": "コスト", + "tokens": "トークン", + "tasks": "タスク", + "cacheHit": "{{percent}}% がキャッシュから", + "inOut": "入力 {{input}} · 出力 {{output}}", + "models": "モデル" + }, + "dailyCost": "日別コスト", + "byMode": "モード別", + "topTasks": "コストの高いタスク", + "disclaimer": "コストはタスク履歴からの推定値であり、プロバイダーの請求額と異なる場合があります。", + "exportCsv": "CSV をエクスポート", + "noPricingNote": "一部のプロバイダー(GitHub Copilot など)はサブスクリプション課金で、タスクごとの価格を報告しないため、この履歴はトークンで表示されます。クレジットについては上のプロバイダーの上限を参照してください。", + "dailyTokens": "日別トークン", + "topTasksTokens": "大きいタスク", + "byProvider": "プロバイダー別", + "history": "使用履歴", + "limits": { + "title": "プロバイダーの上限", + "refresh": "更新", + "updated": "{{time}} に更新", + "loading": "接続済みのプロバイダーを確認中…", + "resets": "{{date}} にリセット", + "signedOut": "サインインしていません", + "noCopilot": "Copilot のクォータなし", + "noKey": "API キーがないか無効です", + "unsupported": "カスタムベース URL — 確認していません", + "error": "読み込めませんでした" + }, + "byModel": "モデル別", + "notRecorded": "記録なし", + "noPrice": "価格なし", + "tokensShort": "トークン", + "chartLabel": "{{metric}}、合計 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 6cadbd8a3b..55adcdf557 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -42,7 +42,8 @@ "language": "언어", "about": "Zoo Code 정보", "skills": "Skills", - "rules": "규칙" + "rules": "규칙", + "usage": "사용량" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "설명은 1024자 이하여야 합니다" }, "footer": "스킬 작성자 모드로 자신만의 스킬을 만들 수 있습니다. 모드 마켓플레이스에서 사용 가능합니다." + }, + "usage": { + "description": "이 컴퓨터의 Zoo Code 작업 기록에서 집계한 토큰과 비용입니다.", + "range": { + "label": "기간", + "today": "오늘", + "7d": "7일", + "30d": "30일", + "all": "전체 기간" + }, + "scope": { + "label": "표시", + "workspace": "이 워크스페이스", + "all": "모든 워크스페이스" + }, + "empty": "이 기간에 작업이 없습니다.", + "stats": { + "cost": "비용", + "tokens": "토큰", + "tasks": "작업", + "cacheHit": "{{percent}}% 캐시에서", + "inOut": "입력 {{input}} · 출력 {{output}}", + "models": "모델" + }, + "dailyCost": "일별 비용", + "byMode": "모드별", + "topTasks": "비용이 큰 작업", + "disclaimer": "비용은 작업 기록을 기반으로 한 추정치이며 공급자의 청구서와 다를 수 있습니다.", + "exportCsv": "CSV 내보내기", + "noPricingNote": "일부 공급자(예: GitHub Copilot)는 구독 방식으로 청구하며 작업별 가격을 보고하지 않으므로 이 기록은 토큰으로 표시됩니다. 크레딧은 위의 공급자 한도를 확인하세요.", + "dailyTokens": "일별 토큰", + "topTasksTokens": "큰 작업", + "byProvider": "공급자별", + "history": "사용 기록", + "limits": { + "title": "공급자 한도", + "refresh": "새로 고침", + "updated": "{{time}}에 업데이트됨", + "loading": "연결된 공급자를 확인하는 중…", + "resets": "{{date}}에 초기화", + "signedOut": "로그인하지 않음", + "noCopilot": "Copilot 할당량 없음", + "noKey": "API 키가 없거나 올바르지 않음", + "unsupported": "사용자 지정 기본 URL — 확인하지 않음", + "error": "불러올 수 없음" + }, + "byModel": "모델별", + "notRecorded": "기록되지 않음", + "noPrice": "가격 없음", + "tokensShort": "토큰", + "chartLabel": "{{metric}}, 합계 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 992e9c53e0..c29cfc42b5 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -42,7 +42,8 @@ "language": "Taal", "about": "Over Zoo Code", "skills": "Skills", - "rules": "Regels" + "rules": "Regels", + "usage": "Gebruik" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Beschrijving moet maximaal 1024 tekens zijn" }, "footer": "Maak je eigen skills met de Skill Writer modus, beschikbaar in de Modes Marketplace." + }, + "usage": { + "description": "Tokens en kosten uit je Zoo Code-takengeschiedenis op deze machine.", + "range": { + "label": "Tijdsbereik", + "today": "Vandaag", + "7d": "7 dagen", + "30d": "30 dagen", + "all": "Alle tijd" + }, + "scope": { + "label": "Tonen", + "workspace": "Deze werkruimte", + "all": "Alle werkruimtes" + }, + "empty": "Geen taken in dit tijdsbereik.", + "stats": { + "cost": "kosten", + "tokens": "tokens", + "tasks": "taken", + "cacheHit": "{{percent}}% uit cache", + "inOut": "{{input}} in · {{output}} uit", + "models": "modellen" + }, + "dailyCost": "Dagelijkse kosten", + "byMode": "Per modus", + "topTasks": "Duurste taken", + "disclaimer": "Kosten zijn schattingen op basis van de takengeschiedenis en kunnen afwijken van de rekening van je provider.", + "exportCsv": "CSV exporteren", + "noPricingNote": "Sommige providers (bijv. GitHub Copilot) factureren via een abonnement en melden geen prijs per taak, daarom wordt deze geschiedenis in tokens getoond. Zie hierboven de providerlimieten voor credits.", + "dailyTokens": "Dagelijkse tokens", + "topTasksTokens": "Grootste taken", + "byProvider": "Per provider", + "history": "Gebruiksgeschiedenis", + "limits": { + "title": "Providerlimieten", + "refresh": "Vernieuwen", + "updated": "Bijgewerkt {{time}}", + "loading": "Verbonden providers controleren…", + "resets": "Reset {{date}}", + "signedOut": "Niet aangemeld", + "noCopilot": "Geen Copilot-quotum", + "noKey": "API-sleutel ontbreekt of is ongeldig", + "unsupported": "Aangepaste basis-URL — niet gecontroleerd", + "error": "Kon niet laden" + }, + "byModel": "Per model", + "notRecorded": "Niet vastgelegd", + "noPrice": "geen prijs", + "tokensShort": "tok", + "chartLabel": "{{metric}}, totaal {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 459675b0e1..f65684ed5b 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -42,7 +42,8 @@ "language": "Język", "about": "O Zoo Code", "skills": "Skills", - "rules": "Reguły" + "rules": "Reguły", + "usage": "Użycie" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Opis musi mieć maksymalnie 1024 znaki" }, "footer": "Twórz własne umiejętności za pomocą trybu Skill Writer, dostępnego w Modes Marketplace." + }, + "usage": { + "description": "Tokeny i koszty z historii zadań Zoo Code na tym komputerze.", + "range": { + "label": "Zakres czasu", + "today": "Dzisiaj", + "7d": "7 dni", + "30d": "30 dni", + "all": "Cały czas" + }, + "scope": { + "label": "Pokaż", + "workspace": "Ten obszar roboczy", + "all": "Wszystkie obszary robocze" + }, + "empty": "Brak zadań w tym zakresie czasu.", + "stats": { + "cost": "koszt", + "tokens": "tokeny", + "tasks": "zadania", + "cacheHit": "{{percent}}% z pamięci podręcznej", + "inOut": "{{input}} wejście · {{output}} wyjście", + "models": "modele" + }, + "dailyCost": "Koszt dzienny", + "byMode": "Według trybu", + "topTasks": "Najdroższe zadania", + "disclaimer": "Koszty są szacunkami z historii zadań i mogą różnić się od rachunku od dostawcy.", + "exportCsv": "Eksportuj CSV", + "noPricingNote": "Niektórzy dostawcy (np. GitHub Copilot) rozliczają się w ramach subskrypcji i nie podają ceny za zadanie, dlatego ta historia jest pokazana w tokenach. Kredyty znajdziesz wyżej w limitach dostawców.", + "dailyTokens": "Tokeny dzienne", + "topTasksTokens": "Największe zadania", + "byProvider": "Według dostawcy", + "history": "Historia użycia", + "limits": { + "title": "Limity dostawców", + "refresh": "Odśwież", + "updated": "Zaktualizowano {{time}}", + "loading": "Sprawdzanie połączonych dostawców…", + "resets": "Reset {{date}}", + "signedOut": "Nie zalogowano", + "noCopilot": "Brak limitu Copilot", + "noKey": "Brak klucza API lub jest nieprawidłowy", + "unsupported": "Niestandardowy bazowy URL — nie sprawdzono", + "error": "Nie udało się wczytać" + }, + "byModel": "Według modelu", + "notRecorded": "Nie zapisano", + "noPrice": "brak ceny", + "tokensShort": "tok.", + "chartLabel": "{{metric}}, łącznie {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 500babaaa8..ad47efbbe9 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Sobre", "skills": "Skills", - "rules": "Regras" + "rules": "Regras", + "usage": "Uso" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "A descrição deve ter no máximo 1024 caracteres" }, "footer": "Crie suas próprias skills com o modo Skill Writer, disponível em Modes Marketplace." + }, + "usage": { + "description": "Tokens e custo do histórico de tarefas do Zoo Code nesta máquina.", + "range": { + "label": "Intervalo de tempo", + "today": "Hoje", + "7d": "7 dias", + "30d": "30 dias", + "all": "Todo o período" + }, + "scope": { + "label": "Mostrar", + "workspace": "Este espaço de trabalho", + "all": "Todos os espaços de trabalho" + }, + "empty": "Nenhuma tarefa neste intervalo de tempo.", + "stats": { + "cost": "custo", + "tokens": "tokens", + "tasks": "tarefas", + "cacheHit": "{{percent}}% do cache", + "inOut": "{{input}} de entrada · {{output}} de saída", + "models": "modelos" + }, + "dailyCost": "Custo diário", + "byMode": "Por modo", + "topTasks": "Tarefas mais caras", + "disclaimer": "Os custos são estimativas do histórico de tarefas e podem diferir da fatura do seu provedor.", + "exportCsv": "Exportar CSV", + "noPricingNote": "Alguns provedores (ex.: GitHub Copilot) cobram por assinatura e não informam um preço por tarefa, por isso este histórico é mostrado em tokens. Veja os limites dos provedores acima para os créditos.", + "dailyTokens": "Tokens diários", + "topTasksTokens": "Maiores tarefas", + "byProvider": "Por provedor", + "history": "Histórico de uso", + "limits": { + "title": "Limites dos provedores", + "refresh": "Atualizar", + "updated": "Atualizado {{time}}", + "loading": "Verificando os provedores conectados…", + "resets": "Redefine {{date}}", + "signedOut": "Sem login", + "noCopilot": "Sem cota do Copilot", + "noKey": "Chave de API ausente ou inválida", + "unsupported": "URL base personalizada — não verificada", + "error": "Não foi possível carregar" + }, + "byModel": "Por modelo", + "notRecorded": "Não registrado", + "noPrice": "sem preço", + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 053ff0684d..a2ff469dbb 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -42,7 +42,8 @@ "language": "Язык", "about": "О Zoo Code", "skills": "Skills", - "rules": "Правила" + "rules": "Правила", + "usage": "Использование" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Описание должно быть не более 1024 символов" }, "footer": "Создавайте собственные навыки с режимом Skill Writer, доступным в Modes Marketplace." + }, + "usage": { + "description": "Токены и стоимость по истории задач Zoo Code на этом компьютере.", + "range": { + "label": "Период", + "today": "Сегодня", + "7d": "7 дней", + "30d": "30 дней", + "all": "Всё время" + }, + "scope": { + "label": "Показать", + "workspace": "Эта рабочая область", + "all": "Все рабочие области" + }, + "empty": "В этом периоде нет задач.", + "stats": { + "cost": "стоимость", + "tokens": "токены", + "tasks": "задачи", + "cacheHit": "{{percent}}% из кэша", + "inOut": "{{input}} вход · {{output}} выход", + "models": "модели" + }, + "dailyCost": "Стоимость по дням", + "byMode": "По режимам", + "topTasks": "Самые дорогие задачи", + "disclaimer": "Стоимость — это оценка по истории задач, она может отличаться от счёта вашего провайдера.", + "exportCsv": "Экспорт в CSV", + "noPricingNote": "Некоторые провайдеры (например, GitHub Copilot) тарифицируют по подписке и не сообщают цену за задачу, поэтому эта история показана в токенах. Кредиты смотрите выше в лимитах провайдеров.", + "dailyTokens": "Токены по дням", + "topTasksTokens": "Самые крупные задачи", + "byProvider": "По провайдерам", + "history": "История использования", + "limits": { + "title": "Лимиты провайдеров", + "refresh": "Обновить", + "updated": "Обновлено {{time}}", + "loading": "Проверка подключённых провайдеров…", + "resets": "Сброс {{date}}", + "signedOut": "Вход не выполнен", + "noCopilot": "Нет квоты Copilot", + "noKey": "Ключ API отсутствует или недействителен", + "unsupported": "Пользовательский базовый URL — не проверено", + "error": "Не удалось загрузить" + }, + "byModel": "По моделям", + "notRecorded": "Не записано", + "noPrice": "без цены", + "tokensShort": "ток.", + "chartLabel": "{{metric}}, всего {{total}}" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index b37f28d44d..257cafd6ec 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -42,7 +42,8 @@ "language": "Dil", "about": "Zoo Code Hakkında", "skills": "Skills", - "rules": "Kurallar" + "rules": "Kurallar", + "usage": "Kullanım" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Açıklama en fazla 1024 karakter olmalıdır" }, "footer": "Skill Writer modu ile kendi becerilerinizi oluşturun. Modes Marketplace'de mevcut." + }, + "usage": { + "description": "Bu makinedeki Zoo Code görev geçmişinizden token ve maliyet.", + "range": { + "label": "Zaman aralığı", + "today": "Bugün", + "7d": "7 gün", + "30d": "30 gün", + "all": "Tüm zamanlar" + }, + "scope": { + "label": "Göster", + "workspace": "Bu çalışma alanı", + "all": "Tüm çalışma alanları" + }, + "empty": "Bu zaman aralığında görev yok.", + "stats": { + "cost": "maliyet", + "tokens": "token", + "tasks": "görev", + "cacheHit": "%{{percent}} önbellekten", + "inOut": "{{input}} giriş · {{output}} çıkış", + "models": "model" + }, + "dailyCost": "Günlük maliyet", + "byMode": "Moda göre", + "topTasks": "En pahalı görevler", + "disclaimer": "Maliyetler görev geçmişinden yapılan tahminlerdir ve sağlayıcınızın faturasından farklı olabilir.", + "exportCsv": "CSV olarak dışa aktar", + "noPricingNote": "Bazı sağlayıcılar (ör. GitHub Copilot) abonelik üzerinden faturalandırır ve görev başına fiyat bildirmez; bu yüzden bu geçmiş token cinsinden gösterilir. Krediler için yukarıdaki sağlayıcı limitlerine bakın.", + "dailyTokens": "Günlük token", + "topTasksTokens": "En büyük görevler", + "byProvider": "Sağlayıcıya göre", + "history": "Kullanım geçmişi", + "limits": { + "title": "Sağlayıcı limitleri", + "refresh": "Yenile", + "updated": "Güncellendi: {{time}}", + "loading": "Bağlı sağlayıcılar kontrol ediliyor…", + "resets": "Sıfırlanma: {{date}}", + "signedOut": "Oturum açılmadı", + "noCopilot": "Copilot kotası yok", + "noKey": "API anahtarı eksik veya geçersiz", + "unsupported": "Özel temel URL — kontrol edilmedi", + "error": "Yüklenemedi" + }, + "byModel": "Modele göre", + "notRecorded": "Kaydedilmedi", + "noPrice": "fiyat yok", + "tokensShort": "tok", + "chartLabel": "{{metric}}, toplam {{total}}" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 94843217ae..55df86904e 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -42,7 +42,8 @@ "language": "Ngôn ngữ", "about": "Giới thiệu", "skills": "Skills", - "rules": "Quy tắc" + "rules": "Quy tắc", + "usage": "Mức sử dụng" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "Mô tả phải có tối đa 1024 ký tự" }, "footer": "Tạo các skill của riêng bạn với chế độ Skill Writer, có sẵn tại Modes Marketplace." + }, + "usage": { + "description": "Token và chi phí từ lịch sử nhiệm vụ Zoo Code của bạn trên máy này.", + "range": { + "label": "Khoảng thời gian", + "today": "Hôm nay", + "7d": "7 ngày", + "30d": "30 ngày", + "all": "Mọi lúc" + }, + "scope": { + "label": "Hiển thị", + "workspace": "Không gian làm việc này", + "all": "Tất cả không gian làm việc" + }, + "empty": "Không có nhiệm vụ nào trong khoảng thời gian này.", + "stats": { + "cost": "chi phí", + "tokens": "token", + "tasks": "nhiệm vụ", + "cacheHit": "{{percent}}% từ bộ nhớ đệm", + "inOut": "{{input}} vào · {{output}} ra", + "models": "mô hình" + }, + "dailyCost": "Chi phí hằng ngày", + "byMode": "Theo chế độ", + "topTasks": "Nhiệm vụ tốn kém nhất", + "disclaimer": "Chi phí là ước tính từ lịch sử nhiệm vụ và có thể khác với hóa đơn của nhà cung cấp.", + "exportCsv": "Xuất CSV", + "noPricingNote": "Một số nhà cung cấp (ví dụ GitHub Copilot) tính phí theo gói đăng ký và không báo cáo giá cho từng nhiệm vụ, nên lịch sử này được hiển thị bằng token. Xem Giới hạn nhà cung cấp ở trên để biết tín dụng.", + "dailyTokens": "Token hằng ngày", + "topTasksTokens": "Nhiệm vụ lớn nhất", + "byProvider": "Theo nhà cung cấp", + "history": "Lịch sử sử dụng", + "limits": { + "title": "Giới hạn nhà cung cấp", + "refresh": "Làm mới", + "updated": "Cập nhật {{time}}", + "loading": "Đang kiểm tra các nhà cung cấp đã kết nối…", + "resets": "Đặt lại {{date}}", + "signedOut": "Chưa đăng nhập", + "noCopilot": "Không có hạn mức Copilot", + "noKey": "Thiếu khóa API hoặc không hợp lệ", + "unsupported": "URL cơ sở tùy chỉnh — không kiểm tra", + "error": "Không thể tải" + }, + "byModel": "Theo mô hình", + "notRecorded": "Chưa ghi nhận", + "noPrice": "không có giá", + "tokensShort": "tok", + "chartLabel": "{{metric}}, tổng {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 8be0fe1532..81cc7bd614 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -42,7 +42,8 @@ "language": "语言", "about": "关于 Zoo Code", "skills": "Skills", - "rules": "规则" + "rules": "规则", + "usage": "用量" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "描述不得超过1024个字符" }, "footer": "使用技能编写器模式创建您自己的技能,可在 模式市集 中获得。" + }, + "usage": { + "description": "来自此设备上 Zoo Code 任务历史的 Token 和费用。", + "range": { + "label": "时间范围", + "today": "今天", + "7d": "7 天", + "30d": "30 天", + "all": "全部时间" + }, + "scope": { + "label": "显示", + "workspace": "此工作区", + "all": "所有工作区" + }, + "empty": "此时间范围内没有任务。", + "stats": { + "cost": "费用", + "tokens": "Token", + "tasks": "任务", + "cacheHit": "{{percent}}% 来自缓存", + "inOut": "输入 {{input}} · 输出 {{output}}", + "models": "模型" + }, + "dailyCost": "每日费用", + "byMode": "按模式", + "topTasks": "费用最高的任务", + "disclaimer": "费用为根据任务历史得出的估算值,可能与提供商的账单不同。", + "exportCsv": "导出 CSV", + "noPricingNote": "部分提供商(如 GitHub Copilot)按订阅计费,不报告每个任务的价格,因此此历史以 Token 显示。额度请参见上方的提供商限额。", + "dailyTokens": "每日 Token", + "topTasksTokens": "最大的任务", + "byProvider": "按提供商", + "history": "用量历史", + "limits": { + "title": "提供商限额", + "refresh": "刷新", + "updated": "更新于 {{time}}", + "loading": "正在检查已连接的提供商…", + "resets": "重置于 {{date}}", + "signedOut": "未登录", + "noCopilot": "无 Copilot 配额", + "noKey": "API 密钥缺失或无效", + "unsupported": "自定义基础 URL — 未检查", + "error": "无法加载" + }, + "byModel": "按模型", + "notRecorded": "未记录", + "noPrice": "无价格", + "tokensShort": "Token", + "chartLabel": "{{metric}},共 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 65eb5c67a5..75b4d2aba6 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -42,7 +42,8 @@ "language": "語言", "about": "關於 Zoo Code", "skills": "Skills", - "rules": "規則" + "rules": "規則", + "usage": "用量" }, "about": { "bugReport": { @@ -1233,5 +1234,56 @@ "descriptionTooLong": "說明不得超過1024個字元" }, "footer": "使用技能編寫器模式建立您自己的技能,可在 模式市集 中取得。" + }, + "usage": { + "description": "來自此裝置上 Zoo Code 任務歷史的 Token 與費用。", + "range": { + "label": "時間範圍", + "today": "今天", + "7d": "7 天", + "30d": "30 天", + "all": "全部時間" + }, + "scope": { + "label": "顯示", + "workspace": "此工作區", + "all": "所有工作區" + }, + "empty": "此時間範圍內沒有任務。", + "stats": { + "cost": "費用", + "tokens": "Token", + "tasks": "任務", + "cacheHit": "{{percent}}% 來自快取", + "inOut": "輸入 {{input}} · 輸出 {{output}}", + "models": "模型" + }, + "dailyCost": "每日費用", + "byMode": "依模式", + "topTasks": "費用最高的任務", + "disclaimer": "費用為根據任務歷史得出的估算值,可能與供應商的帳單不同。", + "exportCsv": "匯出 CSV", + "noPricingNote": "部分供應商(如 GitHub Copilot)按訂閱計費,不回報每個任務的價格,因此此歷史以 Token 顯示。額度請參閱上方的供應商限額。", + "dailyTokens": "每日 Token", + "topTasksTokens": "最大的任務", + "byProvider": "依供應商", + "history": "用量歷史", + "limits": { + "title": "供應商限額", + "refresh": "重新整理", + "updated": "更新於 {{time}}", + "loading": "正在檢查已連線的供應商…", + "resets": "重設於 {{date}}", + "signedOut": "未登入", + "noCopilot": "無 Copilot 配額", + "noKey": "API 金鑰遺失或無效", + "unsupported": "自訂基底 URL — 未檢查", + "error": "無法載入" + }, + "byModel": "依模型", + "notRecorded": "未記錄", + "noPrice": "無價格", + "tokensShort": "Token", + "chartLabel": "{{metric}},共 {{total}}" } } diff --git a/webview-ui/src/utils/__tests__/providerLabel.spec.ts b/webview-ui/src/utils/__tests__/providerLabel.spec.ts new file mode 100644 index 0000000000..e2401a4f45 --- /dev/null +++ b/webview-ui/src/utils/__tests__/providerLabel.spec.ts @@ -0,0 +1,14 @@ +import { providerIdentifiers } from "@roo-code/types" + +import { providerLabel } from "../providerLabel" + +describe("providerLabel", () => { + it("returns the display name for a known provider", () => { + expect(providerLabel(providerIdentifiers.githubCopilot)).toBe("GitHub Copilot") + expect(providerLabel(providerIdentifiers.anthropic)).toBe("Anthropic") + }) + + it("falls back to the raw id for unknown or retired providers", () => { + expect(providerLabel("some-retired-provider")).toBe("some-retired-provider") + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.dst.spec.ts b/webview-ui/src/utils/__tests__/usage.dst.spec.ts new file mode 100644 index 0000000000..4b2a58c027 --- /dev/null +++ b/webview-ui/src/utils/__tests__/usage.dst.spec.ts @@ -0,0 +1,109 @@ +import { afterAll, describe, expect, it } from "vitest" + +import type { HistoryItem } from "@roo-code/types" + +import { rangeStart, summarizeUsage, usageToCsv } from "../usage" + +// These cases only mean something in a zone that observes DST: in UTC a fixed 24h step and a calendar +// day are the same thing, so the old implementation would pass. The zone is pinned here, and every test +// asserts that its fixture really crosses an offset change, so a runner that ignores TZ fails loudly +// instead of passing vacuously. +// +// The zone must be set before any fixture below is computed (they are built at collection time), so it +// is set at module level rather than in beforeAll. +const previousZone = process.env.TZ +process.env.TZ = "America/New_York" + +afterAll(() => { + if (previousZone === undefined) delete process.env.TZ + else process.env.TZ = previousZone +}) + +const at = (y: number, m: number, d: number, h = 0, min = 0) => new Date(y, m, d, h, min).getTime() +const offsetOf = (ts: number) => new Date(ts).getTimezoneOffset() + +let counter = 0 +const item = (overrides: Partial): HistoryItem => ({ + id: `dst${++counter}`, + number: counter, + ts: 0, + task: `task ${counter}`, + tokensIn: 100, + tokensOut: 0, + totalCost: 1, + workspace: "/ws/a", + ...overrides, +}) + +// US clocks go forward on 2026-03-08 (a 23h day) and back on 2026-11-01 (a 25h day). +const SPRING = { now: at(2026, 2, 11, 15), firstDay: at(2026, 2, 5), lastDay: at(2026, 2, 11) } +const FALL = { now: at(2026, 10, 4, 15), firstDay: at(2026, 9, 29), lastDay: at(2026, 10, 4) } + +describe.each([ + ["spring forward (23h day)", SPRING], + ["fall back (25h day)", FALL], +])("a 7-day window across %s", (_name, w) => { + it("really crosses a clock change in this runner", () => { + expect(offsetOf(w.firstDay)).not.toBe(offsetOf(w.lastDay)) + }) + + it("starts at local midnight", () => { + const start = rangeStart("7d", w.now)! + expect(start).toBe(w.firstDay) + expect(new Date(start).getHours()).toBe(0) + expect(new Date(start).getMinutes()).toBe(0) + }) + + it("charts one bar per calendar day, ending on today", () => { + const { daily } = summarizeUsage([item({ ts: w.now - 60_000 })], { range: "7d", now: w.now }) + + expect(daily).toHaveLength(7) + // Every bar is a local midnight, on consecutive calendar dates, with no day skipped or repeated. + const dates = daily.map((d) => new Date(d.day)) + dates.forEach((d) => expect([d.getHours(), d.getMinutes()]).toEqual([0, 0])) + dates.slice(1).forEach((d, i) => { + const prev = new Date(dates[i]) + prev.setDate(prev.getDate() + 1) + expect(d.getTime()).toBe(prev.getTime()) + }) + expect(daily[0].day).toBe(w.firstDay) + expect(daily.at(-1)!.day).toBe(w.lastDay) + }) + + it("puts today's spend in today's bar, so the bars add up to the total", () => { + const history = [ + item({ ts: w.firstDay + 30 * 60_000, totalCost: 2 }), + item({ ts: w.lastDay + 30 * 60_000, totalCost: 3 }), + ] + const s = summarizeUsage(history, { range: "7d", now: w.now }) + expect(s.totalCost).toBe(5) + expect(s.daily.at(-1)).toMatchObject({ day: w.lastDay, cost: 3 }) + expect(s.daily[0]).toMatchObject({ day: w.firstDay, cost: 2 }) + expect(s.daily.reduce((sum, d) => sum + d.cost, 0)).toBe(s.totalCost) + }) + + it("includes a task from the first minute of the first day, and excludes the minute before it", () => { + const csv = usageToCsv( + [ + item({ task: "first-minute", ts: w.firstDay + 60_000 }), + item({ task: "just-before", ts: w.firstDay - 60_000 }), + ], + { range: "7d", now: w.now }, + ) + expect(csv).toContain("first-minute") + expect(csv).not.toContain("just-before") + }) +}) + +describe("a 30-day window across a clock change", () => { + it("charts 30 consecutive local days ending on today", () => { + const now = at(2026, 10, 3, 15) + expect(offsetOf(rangeStart("30d", now)!)).not.toBe(offsetOf(now)) + + const { daily } = summarizeUsage([item({ ts: now - 60_000 })], { range: "30d", now }) + expect(daily).toHaveLength(30) + expect(daily.at(-1)!.day).toBe(at(2026, 10, 3)) + daily.forEach((d) => expect(new Date(d.day).getHours()).toBe(0)) + expect(new Set(daily.map((d) => d.day)).size).toBe(30) + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts new file mode 100644 index 0000000000..7f5b8435b3 --- /dev/null +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -0,0 +1,484 @@ +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { + UNRECORDED, + bucketView, + metricTotal, + modelIdFromKey, + pickMetric, + primaryUsage, + rangeStart, + summarizeUsage, + usageToCsv, +} from "../usage" + +// Fixed "now": Wed 2026-10-14 15:00 local time. +const NOW = new Date(2026, 9, 14, 15, 0, 0).getTime() +const DAY = 24 * 60 * 60 * 1000 + +let counter = 0 +const item = (overrides: Partial): HistoryItem => ({ + id: `t${++counter}`, + number: counter, + ts: NOW, + task: `task ${counter}`, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: "/ws/a", + ...overrides, +}) + +describe("rangeStart", () => { + it("returns local midnight for today", () => { + expect(rangeStart("today", NOW)).toBe(new Date(2026, 9, 14).getTime()) + }) + + it("includes today in 7d / 30d windows", () => { + expect(rangeStart("7d", NOW)).toBe(new Date(2026, 9, 8).getTime()) + expect(rangeStart("30d", NOW)).toBe(new Date(2026, 8, 15).getTime()) + }) + + it("is unbounded for all", () => { + expect(rangeStart("all", NOW)).toBeUndefined() + }) +}) + +describe("summarizeUsage", () => { + const used = (apiProvider: string, modelId: string, tokensIn: number, tokensOut: number, cost: number) => [ + { apiProvider, modelId, requests: 1, tokensIn, tokensOut, cost }, + ] + const history = [ + item({ + totalCost: 1.5, + tokensIn: 1000, + tokensOut: 200, + cacheReads: 3000, + mode: "code", + usageByModel: used(providerIdentifiers.anthropic, "opus", 1000, 200, 1.5), + }), + item({ + totalCost: 0.5, + tokensIn: 500, + tokensOut: 100, + mode: "architect", + ts: NOW - DAY, + usageByModel: used(providerIdentifiers.githubCopilot, "gpt-5", 500, 100, 0.5), + }), + item({ + totalCost: 2, + tokensIn: 10, + tokensOut: 10, + mode: "code", + workspace: "/ws/b", + usageByModel: used(providerIdentifiers.anthropic, "opus", 10, 10, 2), + }), + // Outside 7d window + item({ + totalCost: 99, + mode: "code", + ts: NOW - 10 * DAY, + usageByModel: used(providerIdentifiers.anthropic, "opus", 0, 0, 99), + }), + ] + + it("totals only items inside the range", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.taskCount).toBe(3) + expect(s.totalCost).toBeCloseTo(4) + expect(s.totalTokens).toBe(1000 + 200 + 500 + 100 + 20) + }) + + it("filters by workspace", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW, workspace: "/ws/a" }) + expect(s.taskCount).toBe(2) + expect(s.totalCost).toBeCloseTo(2) + }) + + it("groups by recorded provider and by model, sorted by cost desc", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.byProvider.map((b) => [b.key, b.tasks])).toEqual([ + [providerIdentifiers.anthropic, 2], + [providerIdentifiers.githubCopilot, 1], + ]) + expect(s.byProvider[0].cost).toBeCloseTo(3.5) + expect(s.byModel.map((b) => [modelIdFromKey(b.key), b.apiProvider])).toEqual([ + ["opus", providerIdentifiers.anthropic], + ["gpt-5", providerIdentifiers.githubCopilot], + ]) + expect(s.byMode.map((b) => b.key)).toEqual(["code", "architect"]) + }) + + it("REGRESSION: provider/model come from what was recorded, not from any current profile", () => { + // The bug: usage was attributed via the *currently selected* profile, so + // switching providers relabelled past Copilot work as the new provider. + // Aggregation now takes no profile information at all. + const copilotTask = item({ + tokensIn: 16_700_000, + tokensOut: 83_400, + usageByModel: used(providerIdentifiers.githubCopilot, "claude-opus-5.5", 16_700_000, 83_400, 0), + apiConfigName: "default", // the profile name is irrelevant to attribution + }) + const before = summarizeUsage([copilotTask], { range: "all", now: NOW }) + + // "Switch provider": same history, but the user's profile now points at Gemini. + const after = summarizeUsage([{ ...copilotTask, apiConfigName: "gemini-profile" }], { range: "all", now: NOW }) + + for (const s of [before, after]) { + expect(s.byProvider.map((b) => b.key)).toEqual([providerIdentifiers.githubCopilot]) + expect(modelIdFromKey(s.byModel[0].key)).toBe("claude-opus-5.5") + } + }) + + it("attributes a task that switched providers mid-way to each provider by its own tokens", () => { + const s = summarizeUsage( + [ + item({ + tokensIn: 400, + tokensOut: 0, + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 100, + tokensOut: 0, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + requests: 1, + tokensIn: 300, + tokensOut: 0, + cost: 0, + }, + ], + }), + ], + { range: "all", now: NOW }, + ) + expect(s.byProvider.map((b) => [b.key, b.tokens])).toEqual([ + [providerIdentifiers.gemini, 300], + [providerIdentifiers.githubCopilot, 100], + ]) + expect(s.byProvider.every((b) => b.tasks === 1)).toBe(true) + }) + + it("buckets tasks with no recorded provider/model as unrecorded instead of guessing", () => { + const s = summarizeUsage([item({ totalCost: 1, tokensIn: 10, tokensOut: 5 })], { range: "all", now: NOW }) + expect(s.byProvider.map((b) => b.key)).toEqual([UNRECORDED]) + expect(s.byModel.map((b) => b.key)).toEqual([UNRECORDED]) + expect(modelIdFromKey(UNRECORDED)).toBeUndefined() + expect(s.byMode[0].key).toBe("unknown") + }) + + it("computes cache hit rate over input-side tokens", () => { + const s = summarizeUsage(history, { range: "today", now: NOW }) + // today: tokensIn 1000 + 10, cacheReads 3000 + expect(s.cacheHitRate).toBeCloseTo(3000 / (1010 + 3000)) + }) + + it("produces a dense daily series for bounded ranges", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.daily).toHaveLength(7) + expect(s.daily.at(-1)!.cost).toBeCloseTo(3.5) + expect(s.daily.at(-2)!.cost).toBeCloseTo(0.5) + expect(s.daily[0].cost).toBe(0) + }) + + it("returns top tasks by cost, limited", () => { + const s = summarizeUsage(history, { range: "all", now: NOW, topTaskLimit: 2 }) + expect(s.topTasks.map((t) => t.totalCost)).toEqual([99, 2]) + }) + + it("breaks cost ties by tokens so equal-priced tasks still rank meaningfully", () => { + const s = summarizeUsage( + [item({ id: "few", totalCost: 1, tokensIn: 10 }), item({ id: "many", totalCost: 1, tokensIn: 9_000 })], + { range: "all", now: NOW }, + ) + expect(s.topTasks.map((t) => t.id)).toEqual(["many", "few"]) + }) + + it("is safe on empty history", () => { + const s = summarizeUsage([], { range: "7d", now: NOW }) + expect(s.taskCount).toBe(0) + expect(s.totalCost).toBe(0) + expect(s.cacheHitRate).toBe(0) + }) + + it("tracks tokens per day alongside cost", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.daily.at(-1)!.tokens).toBe(1000 + 200 + 20) + }) + + it("ranks zero-cost tasks by tokens (subscription providers)", () => { + const s = summarizeUsage([item({ id: "small", tokensIn: 10 }), item({ id: "big", tokensIn: 5_000_000 })], { + range: "all", + now: NOW, + }) + expect(s.topTasks.map((t) => t.id)).toEqual(["big", "small"]) + }) + + it("ignores non-finite costs", () => { + const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) + expect(s.totalCost).toBe(0) + }) +}) + +describe("mixed priced and unpriced providers", () => { + const mixed = () => { + const mk = (provider: string, model: string, tokens: number, cost: number) => + item({ + tokensIn: tokens, + tokensOut: 0, + totalCost: cost, + usageByModel: [ + { apiProvider: provider, modelId: model, requests: 1, tokensIn: tokens, tokensOut: 0, cost }, + ], + }) + return summarizeUsage( + [ + mk(providerIdentifiers.githubCopilot, "claude-opus-5.5", 16_000_000, 0), + mk(providerIdentifiers.anthropic, "claude-sonnet-5", 200_000, 1.2), + ], + { range: "all", now: NOW }, + ) + } + + it("prioritises cost as soon as any usage in the range is priced", () => { + expect(pickMetric(mixed())).toBe("cost") + }) + + it("flags each bucket as priced or not so subscription usage isn't shown as $0.00", () => { + const s = mixed() + const byKey = Object.fromEntries(s.byProvider.map((b) => [b.key, b])) + expect(byKey[providerIdentifiers.anthropic].priced).toBe(true) + expect(byKey[providerIdentifiers.githubCopilot].priced).toBe(false) + expect(s.byModel.find((b) => modelIdFromKey(b.key) === "claude-opus-5.5")!.priced).toBe(false) + }) + + it("keeps all-unpriced ranges on tokens and all-priced ranges on cost", () => { + const unpriced = summarizeUsage([item({ tokensIn: 5, usageByModel: [] })], { range: "all", now: NOW }) + expect(pickMetric(unpriced)).toBe("tokens") + }) +}) + +describe("bucketView", () => { + const priced = { cost: 3, tokens: 1000, priced: true } + const unpriced = { cost: 0, tokens: 16_000_000, priced: false } + + it("cost mode: a priced bucket is its cost and its share of total cost", () => { + expect(bucketView(priced, "cost", 12)).toEqual({ value: 3, unpriced: false, percent: 25 }) + }) + + it("cost mode: an unpriced bucket carries tokens, is flagged, and has no share (never 0%)", () => { + expect(bucketView(unpriced, "cost", 12)).toEqual({ value: 0, unpriced: true, percent: 0 }) + }) + + it("tokens mode: everything is tokens and nothing is flagged unpriced", () => { + expect(bucketView(unpriced, "tokens", 32_000_000)).toEqual({ value: 16_000_000, unpriced: false, percent: 50 }) + expect(bucketView(priced, "tokens", 2000)).toEqual({ value: 1000, unpriced: false, percent: 50 }) + }) + + it("guards division by zero", () => { + expect(bucketView(priced, "cost", 0).percent).toBe(0) + }) + + it("rounds shares to whole numbers", () => { + expect(bucketView({ cost: 1, tokens: 0, priced: true }, "cost", 3).percent).toBe(33) + }) +}) + +describe("metricTotal", () => { + it("selects the headline value for the metric", () => { + const s = { totalCost: 2.5, totalTokens: 900 } + expect(metricTotal(s, "cost")).toBe(2.5) + expect(metricTotal(s, "tokens")).toBe(900) + }) +}) + +describe("primaryUsage", () => { + it("returns the provider+model that used the most tokens", () => { + const task = item({ + usageByModel: [ + { apiProvider: "a", modelId: "small", requests: 1, tokensIn: 10, tokensOut: 0, cost: 0 }, + { apiProvider: "b", modelId: "big", requests: 1, tokensIn: 900, tokensOut: 0, cost: 0 }, + ], + }) + expect(primaryUsage(task)).toEqual({ apiProvider: "b", modelId: "big" }) + }) + + it("is undefined when nothing was recorded", () => { + expect(primaryUsage(item({}))).toBeUndefined() + expect( + primaryUsage(item({ usageByModel: [{ requests: 1, tokensIn: 1, tokensOut: 1, cost: 0 }] })), + ).toBeUndefined() + }) +}) + +describe("pickMetric", () => { + it("uses cost when any cost was recorded", () => { + expect(pickMetric({ totalCost: 0.01, totalTokens: 10 })).toBe("cost") + }) + + it("falls back to tokens when tokens exist but cost is zero (e.g. GitHub Copilot)", () => { + expect(pickMetric({ totalCost: 0, totalTokens: 6_200_000 })).toBe("tokens") + }) + + it("stays on cost when there is no usage at all", () => { + expect(pickMetric({ totalCost: 0, totalTokens: 0 })).toBe("cost") + }) +}) + +describe("modelIdFromKey", () => { + it("returns undefined for the unrecorded bucket, the id after the provider separator, or a bare key", () => { + expect(modelIdFromKey(UNRECORDED)).toBeUndefined() + expect(modelIdFromKey("anthropic\u0000claude-sonnet-5")).toBe("claude-sonnet-5") + expect(modelIdFromKey("plain-model")).toBe("plain-model") + }) +}) + +describe("usage attribution edge cases", () => { + const entry = (overrides: Record) => ({ + requests: 1, + tokensIn: 10, + tokensOut: 0, + cost: 0, + ...overrides, + }) + + it("keeps the first entry when two entries used the same number of tokens", () => { + const task = item({ + usageByModel: [ + entry({ apiProvider: "a", modelId: "first", tokensIn: 50 }), + entry({ apiProvider: "b", modelId: "second", tokensIn: 50 }), + ], + }) + expect(primaryUsage(task)).toEqual({ apiProvider: "a", modelId: "first" }) + }) + + it("buckets a model with no provider under an empty provider key, and a provider with no model as unrecorded", () => { + const s = summarizeUsage( + [ + item({ + totalCost: 1, + usageByModel: [entry({ modelId: "orphan", cost: 1 }), entry({ apiProvider: "p", cost: 0 })], + }), + ], + { range: "all", now: NOW }, + ) + expect(new Set(s.byModel.map((b) => modelIdFromKey(b.key)))).toEqual(new Set([undefined, "orphan"])) + expect(new Set(s.byProvider.map((b) => b.key))).toEqual(new Set([UNRECORDED, "p"])) + }) + + it("counts a task once per bucket even when several of its requests land in it", () => { + const s = summarizeUsage( + [ + item({ + usageByModel: [ + entry({ apiProvider: "p", modelId: "m", tokensIn: 1 }), + entry({ apiProvider: "p", modelId: "m", tokensIn: 2 }), + ], + }), + ], + { range: "all", now: NOW }, + ) + expect(s.byProvider[0].tasks).toBe(1) + expect(s.byModel[0].tasks).toBe(1) + expect(s.byProvider[0].tokens).toBe(3) + }) + + it("orders top tasks by cost, then by tokens, and treats missing numbers as zero", () => { + const cheapBig = item({ id: "cheap-big", totalCost: 1, tokensIn: 900, tokensOut: 100 }) + const cheapSmall = item({ id: "cheap-small", totalCost: 1, tokensIn: 10, tokensOut: 0 }) + const pricey = item({ id: "pricey", totalCost: 5, tokensIn: 1, tokensOut: 0 }) + const bare = (id: string) => + item({ id, totalCost: undefined as never, tokensIn: undefined as never, tokensOut: undefined as never }) + const s = summarizeUsage([cheapSmall, bare("bare-1"), cheapBig, pricey, bare("bare-2")], { + range: "all", + now: NOW, + }) + expect(s.topTasks.map((t) => t.id).slice(0, 3)).toEqual(["pricey", "cheap-big", "cheap-small"]) + expect( + s.topTasks + .map((t) => t.id) + .slice(3) + .sort(), + ).toEqual(["bare-1", "bare-2"]) + }) +}) + +describe("usageToCsv formula safety", () => { + it.each(['=HYPERLINK("http://x")', "+1+1", "-2+3", "@SUM(A1)", "\tcmd", "\rcmd"])( + "neutralises a cell that starts with %j", + (text) => { + const csv = usageToCsv([item({ task: text, mode: text })], { range: "all", now: NOW }) + const row = csv.split("\n")[1] + // Each text cell is quoted and prefixed with an apostrophe, so it cannot start with a trigger character. + expect(row).toMatch(/^[^,]+,"'[=+\-@\t\r]/) + expect(row).not.toMatch(/,"[=+\-@\t\r]/) + }, + ) + + it("leaves ordinary text and the numeric columns alone", () => { + const csv = usageToCsv([item({ task: "Fix the = sign", totalCost: 1.5, tokensIn: 10 })], { + range: "all", + now: NOW, + }) + const row = csv.split("\n")[1] + expect(row).toContain('"Fix the = sign"') + expect(row).not.toContain("'Fix") + expect(row.endsWith(",1.5000")).toBe(true) + }) +}) + +describe("usageToCsv", () => { + it("sorts oldest first and leaves provider, model and counters blank or zero when nothing was recorded", () => { + const csv = usageToCsv( + [ + item({ task: "later", ts: NOW }), + item({ + task: "earlier", + title: undefined, + ts: NOW - DAY, + tokensIn: undefined as never, + tokensOut: undefined as never, + totalCost: undefined as never, + mode: undefined, + }), + ], + { range: "all", now: NOW }, + ) + const lines = csv.split("\n") + expect(lines[1]).toContain("earlier") + expect(lines[1].endsWith(',"","","",0,0,0,0,0.0000')).toBe(true) + expect(lines[2]).toContain("later") + }) + + it("exports a row for a task with neither a title nor a task text", () => { + const csv = usageToCsv([item({ task: undefined as never, title: undefined })], { range: "all", now: NOW }) + expect(csv.split("\n")[1]).toContain(',"",') + }) + + it("filters to a workspace and ignores one that is not set", () => { + const history = [item({ task: "in", workspace: "/ws/a" }), item({ task: "out", workspace: "/ws/b" })] + expect(usageToCsv(history, { range: "all", now: NOW, workspace: "/ws/a" }).split("\n")).toHaveLength(2) + expect(usageToCsv(history, { range: "all", now: NOW }).split("\n")).toHaveLength(3) + }) + + it("escapes quotes and newlines and respects the range", () => { + const csv = usageToCsv( + [ + item({ task: 'say "hi"\nthen go', totalCost: 0.12345, mode: "code" }), + item({ task: "old", ts: NOW - 40 * DAY }), + ], + { range: "30d", now: NOW }, + ) + const lines = csv.split("\n") + expect(lines[0]).toBe("date,task,mode,provider,model,tokensIn,tokensOut,cacheReads,cacheWrites,cost") + expect(lines).toHaveLength(2) + expect(lines[1]).toContain('"say ""hi"" then go"') + expect(lines[1].endsWith(",0.1235")).toBe(true) + }) +}) diff --git a/webview-ui/src/utils/providerLabel.ts b/webview-ui/src/utils/providerLabel.ts new file mode 100644 index 0000000000..740e0aef02 --- /dev/null +++ b/webview-ui/src/utils/providerLabel.ts @@ -0,0 +1,6 @@ +import { PROVIDERS } from "@src/components/settings/constants" + +const LABELS = new Map(PROVIDERS.map((p) => [p.value, p.label])) + +/** Human label for a provider id, falling back to the id itself for unknown/retired providers. */ +export const providerLabel = (id: string): string => LABELS.get(id) ?? id diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts new file mode 100644 index 0000000000..3659198f8d --- /dev/null +++ b/webview-ui/src/utils/usage.ts @@ -0,0 +1,340 @@ +import type { HistoryItem } from "@roo-code/types" + +/** + * Pure aggregation over locally persisted task history for the Usage tab. + * + * Notes on correctness: + * - `HistoryItem.totalCost` is a task's *own* spend; subtasks are separate + * history items. Summing every item therefore counts each dollar once. + * - Provider and model come from `HistoryItem.usageByModel`, recorded when each + * request was made. They are never derived from the *current* profile, so + * switching providers does not relabel past usage. Tasks created before this + * was recorded have no such data and are bucketed as `UNRECORDED`. + */ + +/** Bucket key for usage whose provider/model was not recorded (older tasks). */ +export const UNRECORDED = "__unrecorded__" + +export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const +export type UsageRange = (typeof USAGE_RANGES)[number] + +export interface UsageBucket { + key: string + cost: number + tokens: number + /** Tasks that contributed to this bucket (a task using two models counts once in each). */ + tasks: number + /** For model buckets: the provider that served it (undefined when unrecorded). */ + apiProvider?: string + /** + * True when this bucket has any cost > 0. When the tab is showing cost, a bucket + * with `priced: false` (e.g. GitHub Copilot, billed by subscription) must not be + * shown as "$0.00 · 0%": it has no price, not zero spend. + */ + priced: boolean +} + +export interface DailyUsage { + /** Local-midnight timestamp of the day. */ + day: number + cost: number + tokens: number +} + +/** + * Which metric the Usage tab should chart. Subscription-backed providers + * (e.g. GitHub Copilot via the VS Code LM API) report no per-token price, so + * their tasks persist `totalCost: 0`. Charting cost would show flat zeros. + */ +export type UsageMetric = "cost" | "tokens" + +export function pickMetric(summary: Pick): UsageMetric { + return summary.totalCost > 0 || summary.totalTokens === 0 ? "cost" : "tokens" +} + +/** + * What a breakdown row (provider / model / mode) should display for a bucket. + * + * Pure so the "priced vs unpriced" rule lives in one tested place instead of + * being re-derived by the component: + * - tokens mode: value = tokens + * - cost mode, priced bucket: value = cost, share of total cost + * - cost mode, unpriced: a subscription bucket (e.g. Copilot) has no price, + * not zero spend → value = tokens, `unpriced: true`, + * and no share (so no "$0.00 · 0%") + */ +export interface BucketView { + /** Numeric value the bar/share is computed from. */ + value: number + /** True when the bucket should be shown as tokens with a "no price" tag and no share. */ + unpriced: boolean + /** Whole-number share of the total, 0 for unpriced rows. */ + percent: number +} + +export function bucketView( + bucket: Pick, + metric: UsageMetric, + total: number, +): BucketView { + const unpriced = metric === "cost" && !bucket.priced + const value = metric === "cost" ? bucket.cost : bucket.tokens + return { value, unpriced, percent: unpriced || total <= 0 ? 0 : Math.round((value / total) * 100) } +} + +/** The metric's headline value for a summary (cost or tokens). */ +export function metricTotal(summary: Pick, metric: UsageMetric): number { + return metric === "cost" ? summary.totalCost : summary.totalTokens +} + +export interface UsageSummary { + totalCost: number + tokensIn: number + tokensOut: number + cacheReads: number + cacheWrites: number + totalTokens: number + /** Share of input-side tokens served from cache, 0–1. */ + cacheHitRate: number + taskCount: number + byProvider: UsageBucket[] + byModel: UsageBucket[] + byMode: UsageBucket[] + daily: DailyUsage[] + topTasks: HistoryItem[] +} + +export interface UsageOptions { + range: UsageRange + /** When set, only tasks from this workspace are included. */ + workspace?: string + now?: number + topTaskLimit?: number +} + +/** + * The provider+model that did most of a task's work (by tokens), for labelling + * a task row. Undefined when nothing was recorded. + */ +export function primaryUsage(item: HistoryItem): { apiProvider?: string; modelId?: string } | undefined { + const entries = item.usageByModel?.filter((e) => e.apiProvider || e.modelId) + if (!entries?.length) return undefined + const top = entries.reduce((a, b) => (b.tokensIn + b.tokensOut > a.tokensIn + a.tokensOut ? b : a)) + return { apiProvider: top.apiProvider, modelId: top.modelId } +} + +function startOfLocalDay(ts: number): number { + const d = new Date(ts) + d.setHours(0, 0, 0, 0) + return d.getTime() +} + +/** Local midnight `days` calendar days before `ts` (not `days * 24h`, which is off by an hour across a DST change). */ +function startOfLocalDayMinus(ts: number, days: number): number { + const d = new Date(ts) + d.setHours(0, 0, 0, 0) + d.setDate(d.getDate() - days) + return d.getTime() +} + +/** Inclusive lower bound for a range, or `undefined` for "all". */ +export function rangeStart(range: UsageRange, now: number): number | undefined { + switch (range) { + case "today": + return startOfLocalDay(now) + case "7d": + return startOfLocalDayMinus(now, 6) + case "30d": + return startOfLocalDayMinus(now, 29) + default: + return undefined + } +} + +function addTo( + map: Map, + key: string, + cost: number, + tokens: number, + countedForTask: Set, + extra?: Partial, +) { + const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0, priced: false, ...extra } + bucket.cost += cost + bucket.tokens += tokens + if (cost > 0) bucket.priced = true + // Count the task once per bucket even if several of its requests land here. + if (!countedForTask.has(key)) { + countedForTask.add(key) + bucket.tasks += 1 + } + map.set(key, bucket) +} + +const byCostDesc = (a: UsageBucket, b: UsageBucket) => b.cost - a.cost || b.tokens - a.tokens + +export function summarizeUsage(history: HistoryItem[], options: UsageOptions): UsageSummary { + const now = options.now ?? Date.now() + const start = rangeStart(options.range, now) + const topTaskLimit = options.topTaskLimit ?? 5 + + const items = history.filter( + (item) => + typeof item.ts === "number" && + (start === undefined || item.ts >= start) && + (!options.workspace || item.workspace === options.workspace), + ) + + const providers = new Map() + const models = new Map() + const modes = new Map() + const daily = new Map() + + let totalCost = 0 + let tokensIn = 0 + let tokensOut = 0 + let cacheReads = 0 + let cacheWrites = 0 + + for (const item of items) { + const cost = Number.isFinite(item.totalCost) ? item.totalCost : 0 + const tIn = item.tokensIn || 0 + const tOut = item.tokensOut || 0 + const tokens = tIn + tOut + + totalCost += cost + tokensIn += tIn + tokensOut += tOut + cacheReads += item.cacheReads || 0 + cacheWrites += item.cacheWrites || 0 + + // Provider/model from what was recorded per request. A task with no record + // (created before this was tracked) goes to a single "unrecorded" bucket. + const seenProviders = new Set() + const seenModels = new Set() + const entries = item.usageByModel?.length + ? item.usageByModel + : [ + { requests: 0, tokensIn: tIn, tokensOut: tOut, cost } as NonNullable< + HistoryItem["usageByModel"] + >[number], + ] + + for (const e of entries) { + const entryTokens = e.tokensIn + e.tokensOut + const providerKey = e.apiProvider || UNRECORDED + const modelKey = e.modelId ? `${e.apiProvider ?? ""}\u0000${e.modelId}` : UNRECORDED + addTo(providers, providerKey, e.cost, entryTokens, seenProviders) + addTo(models, modelKey, e.cost, entryTokens, seenModels, { apiProvider: e.apiProvider }) + } + + const modeSeen = new Set() + addTo(modes, item.mode || "unknown", cost, tokens, modeSeen) + + const day = startOfLocalDay(item.ts) + const entry = daily.get(day) ?? { cost: 0, tokens: 0 } + entry.cost += cost + entry.tokens += tokens + daily.set(day, entry) + } + + // Densify the daily series for bounded ranges so the chart has a bar per day + // (including zero-spend days). "all" stays sparse to avoid huge arrays. + const dailySeries: DailyUsage[] = [] + if (start !== undefined) { + // Step by calendar date, not by 24h: a 23h or 25h day (DST) would otherwise drift the + // cursor off local midnight and drop today's bar while today's tasks are still counted. + const cursor = new Date(start) + const last = startOfLocalDay(now) + for (let day = cursor.getTime(); day <= last; day = cursor.getTime()) { + const entry = daily.get(day) + dailySeries.push({ day, cost: entry?.cost ?? 0, tokens: entry?.tokens ?? 0 }) + cursor.setDate(cursor.getDate() + 1) + cursor.setHours(0, 0, 0, 0) + } + } else { + for (const [day, entry] of [...daily.entries()].sort(([a], [b]) => a - b)) { + dailySeries.push({ day, ...entry }) + } + } + + const inputSide = tokensIn + cacheReads + + return { + totalCost, + tokensIn, + tokensOut, + cacheReads, + cacheWrites, + totalTokens: tokensIn + tokensOut, + cacheHitRate: inputSide > 0 ? cacheReads / inputSide : 0, + taskCount: items.length, + byProvider: [...providers.values()].sort(byCostDesc), + byModel: [...models.values()].sort(byCostDesc), + byMode: [...modes.values()].sort(byCostDesc), + daily: dailySeries, + // Cost first, tokens as tie-breaker so zero-cost (subscription) usage still ranks meaningfully. + topTasks: [...items] + .sort( + (a, b) => + (b.totalCost || 0) - (a.totalCost || 0) || + (b.tokensIn || 0) + (b.tokensOut || 0) - ((a.tokensIn || 0) + (a.tokensOut || 0)), + ) + .slice(0, topTaskLimit), + } +} + +/** The model id from a `byModel` bucket key (or undefined for the unrecorded bucket). */ +export function modelIdFromKey(key: string): string | undefined { + if (key === UNRECORDED) return undefined + const i = key.indexOf("\u0000") + return i === -1 ? key : key.slice(i + 1) +} + +/** Serialises the filtered usage rows as CSV (one row per task). */ +export function usageToCsv(history: HistoryItem[], options: UsageOptions): string { + const start = rangeStart(options.range, options.now ?? Date.now()) + const header = [ + "date", + "task", + "mode", + "provider", + "model", + "tokensIn", + "tokensOut", + "cacheReads", + "cacheWrites", + "cost", + ] + + // Spreadsheet apps evaluate a quoted cell that starts with = + - @ (or tab/CR) as a formula, and the + // task text is user- and model-controlled. A leading apostrophe makes the cell inert text. + const escape = (value: string) => { + const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value + return `"${safe.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` + } + + const rows = history + .filter( + (item) => + (start === undefined || item.ts >= start) && + (!options.workspace || item.workspace === options.workspace), + ) + .sort((a, b) => a.ts - b.ts) + .map((item) => + [ + new Date(item.ts).toISOString(), + escape(item.title || item.task || ""), + escape(item.mode || ""), + escape(primaryUsage(item)?.apiProvider ?? ""), + escape(primaryUsage(item)?.modelId ?? ""), + item.tokensIn || 0, + item.tokensOut || 0, + item.cacheReads || 0, + item.cacheWrites || 0, + (item.totalCost || 0).toFixed(4), + ].join(","), + ) + + return [header.join(","), ...rows].join("\n") +} From c0891e465ddf22d6772c68c0bafac9bd58f47bd3 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 09:25:54 -0700 Subject: [PATCH 02/76] fix(types): add apiProvider and modelId to ClineApiReqInfo --- packages/types/src/vscode-extension-host.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 3870bf33d4..bf1b8e0767 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -955,6 +955,13 @@ export interface ClineApiReqInfo { cancelReason?: ClineApiReqCancelReason streamingFailedMessage?: string apiProtocol?: "anthropic" | "openai" + /** + * The provider and model that actually served this request, recorded at the + * time of the call. Usage reporting reads these instead of the *current* + * profile, so switching providers later doesn't relabel past requests. + */ + apiProvider?: string + modelId?: string } export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" From a7ba4adc313e4487e8caaeeec4e3f0b77fb508f1 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 09:34:33 -0700 Subject: [PATCH 03/76] test(visual): add Usage tab baselines --- .../__screenshots__/usage-settings-dark.png | Bin 0 -> 64531 bytes .../__screenshots__/usage-settings-degraded.png | Bin 0 -> 63770 bytes .../__screenshots__/usage-settings-empty.png | Bin 0 -> 12158 bytes .../usage-settings-high-contrast-light.png | Bin 0 -> 66509 bytes .../usage-settings-high-contrast.png | Bin 0 -> 66730 bytes .../__screenshots__/usage-settings-light.png | Bin 0 -> 66381 bytes 6 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-dark.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-degraded.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-empty.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast-light.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-light.png diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..7dde60d668b845079b8c1508f72211cf23ecf728 GIT binary patch literal 64531 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 TP-NW;-^lE9_jRi#P}qM1g@+Wy literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-degraded.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-degraded.png new file mode 100644 index 0000000000000000000000000000000000000000..13ff0ecbcda847dcea0d6262a3eb0fc88e370536 GIT binary patch literal 63770 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 literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast-light.png new file mode 100644 index 0000000000000000000000000000000000000000..54a460f2d353920a526d4e6c2ca11a0d8bad946c GIT binary patch literal 66509 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 C7>Mxz literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast.png new file mode 100644 index 0000000000000000000000000000000000000000..99a8f61e5174f05eb7abdca967be41f41e981dc1 GIT binary patch literal 66730 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 vULmcHFCI|Y5eOXT2@CpL0Z>m?RV;}VoW5h(A literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-light.png new file mode 100644 index 0000000000000000000000000000000000000000..79d87f53949618703a3b4bd060281fbb1fd4c7a7 GIT binary patch literal 66381 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 jcUO-V!GQ Date: Wed, 28 Oct 2026 09:45:11 -0700 Subject: [PATCH 04/76] fix(usage): wrap long text instead of clipping it at the reflow width --- webview-ui/src/components/settings/UsageSettings.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 1db7ecd019..9f1efc1c1d 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -330,9 +330,9 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes - {task.task} + {task.task} {primaryUsage(task) && ( - + {[ primaryUsage(task)?.apiProvider ? providerLabel(primaryUsage(task)!.apiProvider!) @@ -387,9 +387,11 @@ const Block = ({ title, children, testId }: { title: string; children: React.Rea const Stat = ({ value, label, hint }: { value: string; label: string; hint?: string }) => (
-
{value}
+
+ {value} +
{label}
- {hint &&
{hint}
} + {hint &&
{hint}
}
) @@ -413,7 +415,7 @@ const Breakdown = ({ className="grid grid-cols-[8px_minmax(0,1fr)_auto_minmax(40px,25%)_30px] items-center gap-2 text-xs" title={row.subtitle ? `${row.name} · ${row.subtitle}` : row.name}> - + {row.name} {row.subtitle && ( {row.subtitle} From 010a565cae34047206932e2f7432692ea077541a Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 10:41:35 -0700 Subject: [PATCH 05/76] fix(usage): address review feedback on quotas, cache hit rate, and retry --- packages/types/src/vscode-extension-host.ts | 2 + ...bviewMessageHandler.providerQuotas.spec.ts | 64 +++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 25 ++++++-- .../__tests__/copilotQuota.spec.ts | 24 +++++++ .../__tests__/providerQuotas.spec.ts | 50 +++++++++++++++ src/services/language-models/copilotQuota.ts | 21 ++++-- .../language-models/providerQuotas.ts | 3 +- .../components/settings/ProviderLimits.tsx | 21 ++++-- .../src/components/settings/UsageSettings.tsx | 4 +- .../__tests__/ProviderLimits.spec.tsx | 60 +++++++++++++++++ .../settings/__tests__/UsageSettings.spec.tsx | 10 +++ webview-ui/src/utils/__tests__/usage.spec.ts | 18 ++++-- webview-ui/src/utils/usage.ts | 6 +- 13 files changed, 281 insertions(+), 27 deletions(-) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index bf1b8e0767..e2808ebce0 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -160,6 +160,8 @@ export interface ExtensionMessage { }[] githubCopilotAccount?: string | null providerQuotas?: ProviderQuota[] + /** True when the quotas could not be loaded at all (as opposed to "no provider has a usage API"). */ + providerQuotasFailed?: boolean mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index d83aa92aac..914b9520a4 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -22,6 +22,7 @@ const SECRET = "sk-super-secret" const makeProvider = (overrides: Record = {}) => ({ postMessageToWebview: vi.fn(), + log: vi.fn(), providerSettingsManager: { listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), getProfile: vi.fn(async ({ id }: { id: string }) => ({ @@ -35,6 +36,7 @@ const makeProvider = (overrides: Record = {}) => describe("webviewMessageHandler – requestProviderQuotas", () => { beforeEach(() => { + // mockReset clears an earlier test's mockImplementation too, so a pending-promise stub cannot leak. quotas.getProviderQuotas.mockReset() quotas.getProviderQuotas.mockResolvedValue([ { @@ -57,6 +59,7 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { providerQuotas: expect.arrayContaining([ expect.objectContaining({ providerId: providerIdentifiers.openrouter }), ]), + providerQuotasFailed: false, requestId: undefined, }) }) @@ -110,6 +113,7 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [], + providerQuotasFailed: true, requestId: undefined, }) expect(log).toHaveBeenCalledWith(expect.stringContaining("secret storage unavailable")) @@ -125,6 +129,7 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [], + providerQuotasFailed: true, requestId: undefined, }) expect(log).toHaveBeenCalledWith(expect.stringContaining("Failed to load provider limits")) @@ -139,6 +144,7 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [], + providerQuotasFailed: false, requestId: undefined, }) }) @@ -197,6 +203,64 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(provider.postMessageToWebview).not.toHaveBeenCalled() }) + it("keeps two webviews apart: a cancel from one never aborts the other's request with the same id", async () => { + // Each webview has its own provider, and both number their requests from 1. Their loads are held open + // at the profile list, so both are pending at once when the cancel arrives. + quotas.getProviderQuotas.mockResolvedValue([]) + const releases: Array<(entries: { id: string }[]) => void> = [] + const pendingProvider = () => { + const provider = makeProvider() + provider.providerSettingsManager.listConfig = vi.fn( + () => new Promise((resolve) => releases.push(resolve)), + ) as never + return provider + } + const sidebar = pendingProvider() + const editorTab = pendingProvider() + + const first = webviewMessageHandler(sidebar, { + type: "requestProviderQuotas", + requestId: "provider-quotas-1", + }) + const second = webviewMessageHandler(editorTab, { + type: "requestProviderQuotas", + requestId: "provider-quotas-1", + }) + await vi.waitFor(() => expect(releases).toHaveLength(2)) + + // Closing Settings in the sidebar cancels the sidebar's request only. + await webviewMessageHandler(sidebar, { type: "cancelProviderQuotas", requestId: "provider-quotas-1" }) + releases.forEach((release) => release([{ id: "a" }])) + await Promise.all([first, second]) + + // The sidebar was cancelled and is not answered. The editor tab is still answered. With one shared + // map keyed by id alone, the cancel would have aborted the editor tab's request instead. + expect(sidebar.postMessageToWebview).not.toHaveBeenCalled() + expect(editorTab.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "providerQuotas", requestId: "provider-quotas-1" }), + ) + // Only the editor tab's request went on to the service; the sidebar's was stopped while loading. + expect(quotas.getProviderQuotas.mock.calls.length).toBeLessThanOrEqual(1) + }) + + it("marks the answer as failed when the quotas could not be loaded, and not when there are simply none", async () => { + const failing = makeProvider() + failing.providerSettingsManager.listConfig = vi.fn(async () => { + throw new Error("secret storage unavailable") + }) as never + await webviewMessageHandler(failing, { type: "requestProviderQuotas", requestId: "a" }) + expect(failing.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ providerQuotas: [], providerQuotasFailed: true }), + ) + + quotas.getProviderQuotas.mockResolvedValue([]) + const empty = makeProvider() + await webviewMessageHandler(empty, { type: "requestProviderQuotas", requestId: "b" }) + expect(empty.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ providerQuotas: [], providerQuotasFailed: false }), + ) + }) + it("ignores a cancel for an unknown or missing id, and for a request that already finished", async () => { const provider = makeProvider() await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-5" }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 207403c5da..11055fe2e9 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -131,8 +131,17 @@ import { const handlerFor = (apiConfiguration: ProviderSettings, provider: RouterName): ApiHandler => buildApiHandler({ ...apiConfiguration, apiProvider: provider }) -/** In-flight `requestProviderQuotas` work by request id, so a cancel message can abort it. */ -const pendingProviderQuotaRequests = new Map() +/** + * In-flight `requestProviderQuotas` work, so a cancel message can abort it. Each ClineProvider (sidebar, + * editor tab) has its own map: the webviews number their requests independently, so a shared map keyed by + * request id alone would let one webview cancel, or overwrite, another's request. + */ +const pendingProviderQuotaRequests = new WeakMap>() +const pendingQuotaFor = (provider: ClineProvider) => { + let pending = pendingProviderQuotaRequests.get(provider) + if (!pending) pendingProviderQuotaRequests.set(provider, (pending = new Map())) + return pending +} export const webviewMessageHandler = async ( provider: ClineProvider, @@ -1576,7 +1585,7 @@ export const webviewMessageHandler = async ( } case VsCodeLmModelsMessageType.cancelProviderQuotas: // The Usage tab was closed (or the request superseded): stop the profile load and any fetches. - if (message.requestId) pendingProviderQuotaRequests.get(message.requestId)?.abort() + if (message.requestId) pendingQuotaFor(provider).get(message.requestId)?.abort() break case VsCodeLmModelsMessageType.requestProviderQuotas: { // Always answer (unless cancelled): the webview shows a spinner until a response arrives and @@ -1584,8 +1593,9 @@ export const webviewMessageHandler = async ( // carries the request's id so the webview can ignore one that has been superseded. const requestId = message.requestId const controller = new AbortController() - if (requestId) pendingProviderQuotaRequests.set(requestId, controller) + if (requestId) pendingQuotaFor(provider).set(requestId, controller) let providerQuotas: ProviderQuota[] = [] + let providerQuotasFailed = false try { const { getProviderQuotas, loadProfiles } = await import("../../services/language-models/providerQuotas") @@ -1597,19 +1607,24 @@ export const webviewMessageHandler = async ( }) } } catch (error) { + providerQuotasFailed = true if (!controller.signal.aborted) { provider.log( `[requestProviderQuotas] ${error instanceof Error ? error.message : "Failed to load provider limits"}`, ) } } finally { - if (requestId) pendingProviderQuotaRequests.delete(requestId) + // Only remove our own entry: a newer request with the same id from this webview owns the slot now. + if (requestId && pendingQuotaFor(provider).get(requestId) === controller) { + pendingQuotaFor(provider).delete(requestId) + } } // Nobody is waiting for a cancelled request: skip the post. if (!controller.signal.aborted) { await provider.postMessageToWebview({ type: VsCodeLmModelsMessageType.providerQuotas, providerQuotas, + providerQuotasFailed, requestId, }) } diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index 65996ad300..413532038d 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -192,6 +192,30 @@ describe("getGitHubCopilotQuota", () => { expect(fail).toHaveBeenCalledTimes(2) }) + it.each([ + ["an HTTP error", async () => json({}, 500), { ok: false, reason: "error" }], + ["a signed-out answer", async () => json({}, 401), { ok: false, reason: "signedOut" }], + ["a missing Copilot plan", async () => json({}, 403), { ok: false, reason: "noCopilot" }], + [ + "a thrown error", + async () => { + throw new Error("network down") + }, + { ok: false, reason: "error" }, + ], + ])("drops an older success when a forced refresh fails with %s", async (_label, failure, expected) => { + const good = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, good as never) + + const failed = await getGitHubCopilotQuota({ force: true }, failure as never) + expect(failed).toMatchObject(expected) + + // The old success must not be served as if it were current: the next normal call fetches again. + const retry = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, retry as never) + expect(retry).toHaveBeenCalledTimes(1) + }) + it("shares one in-flight request between concurrent callers", async () => { const fetchSpy = vi.fn(async () => json(businessPayload)) const [a, b] = await Promise.all([ diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index b27a7830aa..a8cfbdbd38 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -438,6 +438,56 @@ describe("getProviderQuotas – Copilot window and misc", () => { }) }) +describe("Kimi Code authentication", () => { + beforeEach(() => resetProviderQuotaCache()) + + it("does not query an OAuth profile, even when an old API key is still stored in it", async () => { + const fetchSpy = vi.fn() + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.kimiCode, + kimiCodeAuthMethod: "oauth", + kimiCodeApiKey: "stale-key", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "noKey" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("queries an API-key profile with its key", async () => { + const fetchSpy = vi.fn( + async (_url: RequestInfo | URL, _init?: RequestInit) => + new Response(JSON.stringify({ usage: { limit: 100, used: 10 } })), + ) + await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.kimiCode, + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "real-key", + } as never, + ], + {}, + fetchSpy, + ) + expect((fetchSpy.mock.calls[0][1]?.headers as Record).Authorization).toBe("Bearer real-key") + }) + + it("treats a profile with no recorded method as an API-key profile", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ usage: { limit: 100, used: 10 } }))) + await getProviderQuotas( + [{ apiProvider: providerIdentifiers.kimiCode, kimiCodeApiKey: "k" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) +}) + describe("getProviderQuotas – coding-plan providers", () => { beforeEach(() => resetProviderQuotaCache()) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index 6a8c6be07f..754edbeca5 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -91,7 +91,7 @@ export function getGitHubCopilotQuota( if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) return Promise.resolve(cache.result) if (inFlight) return inFlight - inFlight = (async (): Promise => { + const fetchQuota = async (): Promise => { try { const token = await getGitHubToken() if (!token) return { ok: false, reason: "signedOut" } @@ -112,15 +112,22 @@ export function getGitHubCopilotQuota( if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } const quota = parseCopilotQuota(await response.json()) - const result: GitHubCopilotQuotaResult = quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } - if (result.ok) cache = { at: Date.now(), result } - return result + return quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } } catch (error) { return { ok: false, reason: "error", message: safeErrorMessage(error) } - } finally { - inFlight = undefined } - })() + } + + inFlight = fetchQuota() + .then((result) => { + // One place for every outcome: a success is cached, and anything else (signed out, no Copilot, an + // HTTP error, a thrown error) drops an older success so it is not served as if it were current. + cache = result.ok ? { at: Date.now(), result } : undefined + return result + }) + .finally(() => { + inFlight = undefined + }) return inFlight } diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 4286b4f6d1..433c11278e 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -394,7 +394,8 @@ const KEYED_PROVIDERS: KeyedProvider[] = [ // Only API-key profiles are queried; OAuth sign-ins have no key to send. providerId: providerIdentifiers.kimiCode, credentials: (s) => ({ - apiKey: s.kimiCodeApiKey, + // An OAuth sign-in has no API key to send, even if an old key is still in the profile. + apiKey: s.kimiCodeAuthMethod === "oauth" ? undefined : s.kimiCodeApiKey, urls: ["https://api.kimi.com/coding/v1/usages"], }), parse: parseKimiCodeUsage, diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index fb2d863c07..1bab564b08 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -9,6 +9,10 @@ import { vscode } from "@src/utils/vscode" import i18next from "i18next" import { providerLabel } from "@src/utils/providerLabel" +// Module-level, so a component that is unmounted and mounted again never reuses an id that an older, +// still-settling request carries. +let quotaRequestCounter = 0 + const barColor = (pct: number) => pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" @@ -22,11 +26,11 @@ export const ProviderLimits = () => { const { t } = useAppTranslation() const [quotas, setQuotas] = useState() const [loading, setLoading] = useState(true) + const [failed, setFailed] = useState(false) // Only the newest request counts: an answer for an older one (a slow first load that a refresh // overtook, or one that outlived this component) is ignored. const latestRequestId = useRef(undefined) - const counter = useRef(0) const cancelLatest = useCallback(() => { if (latestRequestId.current) { @@ -41,7 +45,7 @@ export const ProviderLimits = () => { const request = useCallback( (force: boolean) => { cancelLatest() - const requestId = `provider-quotas-${++counter.current}` + const requestId = `provider-quotas-${++quotaRequestCounter}` latestRequestId.current = requestId setLoading(true) vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force, requestId }) @@ -55,6 +59,7 @@ export const ProviderLimits = () => { if (event.data.requestId !== latestRequestId.current) return latestRequestId.current = undefined setQuotas(event.data.providerQuotas ?? []) + setFailed(event.data.providerQuotasFailed === true) setLoading(false) } window.addEventListener("message", onMessage) @@ -66,8 +71,9 @@ export const ProviderLimits = () => { } }, [request, cancelLatest]) - // Nothing connected that has a usage API: hide the section entirely. - if (quotas && quotas.length === 0) return null + // Nothing connected that has a usage API: hide the section entirely. A failed load is different: keep + // the section so the user can press Refresh to try again. + if (quotas && quotas.length === 0 && !failed) return null const fetchedAt = quotas?.reduce((max, q) => Math.max(max, q.fetchedAt), 0) @@ -99,6 +105,13 @@ export const ProviderLimits = () => {
+ {failed && ( +
+ {t("settings:usage.limits.error")} +
+ )} {!quotas && (
{t("settings:usage.limits.loading")} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 9f1efc1c1d..543bfcff7f 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -308,9 +308,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes {t("settings:usage.byMode")}:{" "} - {summary.byMode - .map((m) => `${m.key} ${bucketRow(m).label.split(" · ")[0]}`) - .join(" · ")} + {summary.byMode.map((m) => `${m.key} ${bucketRow(m).label}`).join(" · ")}

{ expect(post).not.toHaveBeenCalledWith(expect.objectContaining({ type: "cancelProviderQuotas" })) }) }) + + describe("a failed load", () => { + const respondFailed = () => + act(() => { + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: "providerQuotas", + providerQuotas: [], + providerQuotasFailed: true, + requestId: lastRequestId(), + }, + }), + ) + }) + + it("keeps the section and says so, instead of hiding it like 'no provider has a usage API'", () => { + render() + respondFailed() + expect(screen.getByTestId("provider-limits")).toBeTruthy() + expect(screen.getByTestId("provider-limits-failed").textContent).toContain("settings:usage.limits.error") + }) + + it("lets the user retry, and clears the failure when the retry works", () => { + render() + respondFailed() + + const before = lastRequestId() + post.mockClear() + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + expect(post).toHaveBeenCalledWith(expect.objectContaining({ type: "requestProviderQuotas", bool: true })) + expect(lastRequestId()).not.toBe(before) + + respond([ + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: 1, + windows: [{ id: "b", valueLabel: "$1.00 left" }], + }, + ]) + expect(screen.queryByTestId("provider-limits-failed")).toBeNull() + expect(screen.getByTestId(`provider-limit-${providerIdentifiers.deepseek}`)).toBeTruthy() + }) + + it("still hides the section when the load worked and nothing has a usage API", () => { + render() + respond([]) + expect(screen.queryByTestId("provider-limits")).toBeNull() + }) + }) + + it("never reuses a request id when the tab is closed and opened again", () => { + const first = render() + const firstId = lastRequestId() + first.unmount() + + render() + expect(lastRequestId()).not.toBe(firstId) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx index 7f7c2acb54..4c602677eb 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -188,6 +188,16 @@ describe("UsageSettings", () => { }) }) + describe("modes with and without a price", () => { + it("keeps 'no price' on an unpriced mode so it is not mistaken for a priced one", () => { + state.taskHistory = [priced(1, { mode: "code" }), copilot({ mode: "debug" })] + render() + const line = screen.getByTestId("usage-by-mode").textContent! + expect(line).toContain("code $1.00") + expect(line).toMatch(/debug .*settings:usage\.noPrice/) + }) + }) + describe("filters", () => { it("defaults to this workspace and can widen to all", () => { state.taskHistory = [priced(1), priced(2, { workspace: "/ws/other" })] diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 7f5b8435b3..96254cf033 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -173,10 +173,20 @@ describe("summarizeUsage", () => { expect(s.byMode[0].key).toBe("unknown") }) - it("computes cache hit rate over input-side tokens", () => { - const s = summarizeUsage(history, { range: "today", now: NOW }) - // today: tokensIn 1000 + 10, cacheReads 3000 - expect(s.cacheHitRate).toBeCloseTo(3000 / (1010 + 3000)) + it("computes the cache hit rate against tokensIn, which already counts the cached tokens", () => { + const s = summarizeUsage( + [item({ tokensIn: 4000, cacheReads: 3000 }), item({ tokensIn: 1000, cacheReads: 0 })], + { range: "all", now: NOW }, + ) + // 3000 of 5000 input tokens came from the cache: 60%, not 3000 / (5000 + 3000). + expect(s.cacheHitRate).toBeCloseTo(0.6) + }) + + it("never reports more than 100%, and 0 with no input", () => { + expect( + summarizeUsage([item({ tokensIn: 100, cacheReads: 250 })], { range: "all", now: NOW }).cacheHitRate, + ).toBe(1) + expect(summarizeUsage([item({ tokensIn: 0, cacheReads: 0 })], { range: "all", now: NOW }).cacheHitRate).toBe(0) }) it("produces a dense daily series for bounded ranges", () => { diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 3659198f8d..1d38c5de23 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -258,8 +258,6 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U } } - const inputSide = tokensIn + cacheReads - return { totalCost, tokensIn, @@ -267,7 +265,9 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U cacheReads, cacheWrites, totalTokens: tokensIn + tokensOut, - cacheHitRate: inputSide > 0 ? cacheReads / inputSide : 0, + // `tokensIn` already counts the cached tokens (the cost helpers store the total input), so it is the + // whole input side. Adding the cache reads again would understate the rate. + cacheHitRate: tokensIn > 0 ? Math.min(1, cacheReads / tokensIn) : 0, taskCount: items.length, byProvider: [...providers.values()].sort(byCostDesc), byModel: [...models.values()].sort(byCostDesc), From 56a7085cbda582264c8189e45f0f678dfb4f95bd Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 10:44:10 -0700 Subject: [PATCH 06/76] fix(usage): reconcile condense cost and record the resolved model --- .../__tests__/usageByModel.spec.ts | 22 +++++++- src/core/task-persistence/usageByModel.ts | 10 ++++ src/core/task/Task.ts | 2 + webview-ui/src/utils/__tests__/usage.spec.ts | 50 +++++++++++++++++++ webview-ui/src/utils/usage.ts | 24 ++++++--- 5 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index 092272ff6d..a30d074d36 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import { providerIdentifiers, type ClineMessage } from "@roo-code/types" -import { summarizeUsageByModel } from "../usageByModel" +import { resolveRecordedModelId, summarizeUsageByModel } from "../usageByModel" const req = (data: Record, ts = 1): ClineMessage => ({ ts, @@ -111,3 +111,23 @@ describe("summarizeUsageByModel", () => { expect(summarizeUsageByModel([])).toEqual([]) }) }) + +describe("resolveRecordedModelId", () => { + it("keeps a model that was recorded when the request started", () => { + expect(resolveRecordedModelId("gpt-5", "gpt-5-mini")).toBe("gpt-5") + }) + + it("uses the model that served the request when the profile named none", () => { + expect(resolveRecordedModelId(undefined, "claude-sonnet-5")).toBe("claude-sonnet-5") + expect(resolveRecordedModelId("", "claude-sonnet-5")).toBe("claude-sonnet-5") + expect(resolveRecordedModelId(null, "claude-sonnet-5")).toBe("claude-sonnet-5") + }) + + it("stays unrecorded when neither is known", () => { + expect(resolveRecordedModelId(undefined, undefined)).toBeUndefined() + }) + + it("ignores a recorded value that is not a string", () => { + expect(resolveRecordedModelId(42, "claude-sonnet-5")).toBe("claude-sonnet-5") + }) +}) diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index 16027c281e..ab2fd72324 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -49,3 +49,13 @@ export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEnt return [...byKey.values()] } + +/** + * The model to record for a finished request. The first write (before the request starts) only knows the + * model the profile names; a profile that names none leaves it empty. By the time the request is done the + * model that actually served it is known, so use that rather than leave the usage unattributed. A model + * recorded earlier is never overwritten. + */ +export function resolveRecordedModelId(recorded: unknown, served: string | undefined): string | undefined { + return typeof recorded === "string" && recorded.length > 0 ? recorded : served +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d5e8cc7b79..7b8e0ded28 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -123,6 +123,7 @@ import { saveTaskMessages, taskMetadata, } from "../task-persistence" +import { resolveRecordedModelId } from "../task-persistence/usageByModel" import { getEnvironmentDetails } from "../environment/getEnvironmentDetails" import { checkContextWindowExceededError } from "../context/context-management/context-error-handling" import { @@ -4071,6 +4072,7 @@ export class Task extends EventEmitter implements TaskLike { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ ...existingData, + modelId: resolveRecordedModelId(existingData.modelId, this.cachedStreamingModel?.id), tokensIn: costResult.totalInputTokens, tokensOut: costResult.totalOutputTokens, cacheWrites: cacheWriteTokens, diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 96254cf033..5858b8a7c8 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -341,6 +341,56 @@ describe("pickMetric", () => { }) }) +describe("spend that has no provider or model (context condensing)", () => { + const entry = (cost: number) => ({ + apiProvider: "anthropic", + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 100, + tokensOut: 10, + cost, + }) + + it("puts the unattributed remainder in the 'not recorded' rows so the rows add up to the total", () => { + // The task cost 5.00, but its recorded requests only account for 4.00: 1.00 was condensing. + const s = summarizeUsage([item({ totalCost: 5, usageByModel: [entry(4)] })], { range: "all", now: NOW }) + + expect(s.totalCost).toBe(5) + expect(s.byProvider.reduce((sum, b) => sum + b.cost, 0)).toBeCloseTo(5) + expect(s.byModel.reduce((sum, b) => sum + b.cost, 0)).toBeCloseTo(5) + expect(s.byProvider.find((b) => b.key === UNRECORDED)?.cost).toBeCloseTo(1) + expect(s.byModel.find((b) => b.key === UNRECORDED)?.cost).toBeCloseTo(1) + }) + + it("does not add a token count or a task to the 'not recorded' rows for that remainder", () => { + const s = summarizeUsage([item({ totalCost: 5, usageByModel: [entry(4)] })], { range: "all", now: NOW }) + const unrecorded = s.byProvider.find((b) => b.key === UNRECORDED)! + expect(unrecorded.tokens).toBe(0) + // The one task is counted under its real provider; the remainder is a cost line, not another task. + expect(s.byProvider.find((b) => b.key === "anthropic")?.tasks).toBe(1) + }) + + it("ignores float noise: no spurious 'not recorded' row when the costs agree", () => { + const s = summarizeUsage([item({ totalCost: 0.1 + 0.2, usageByModel: [entry(0.1), entry(0.2)] })], { + range: "all", + now: NOW, + }) + expect(s.byProvider.find((b) => b.key === UNRECORDED)).toBeUndefined() + expect(s.byModel.find((b) => b.key === UNRECORDED)).toBeUndefined() + }) + + it("adds nothing when the recorded requests cost more than the task total", () => { + const s = summarizeUsage([item({ totalCost: 3, usageByModel: [entry(4)] })], { range: "all", now: NOW }) + expect(s.byProvider.find((b) => b.key === UNRECORDED)).toBeUndefined() + }) + + it("leaves tasks with no usage record exactly as before", () => { + const s = summarizeUsage([item({ totalCost: 2, tokensIn: 50 })], { range: "all", now: NOW }) + expect(s.byProvider).toHaveLength(1) + expect(s.byProvider[0]).toMatchObject({ key: UNRECORDED, cost: 2 }) + }) +}) + describe("modelIdFromKey", () => { it("returns undefined for the unrecorded bucket, the id after the provider separator, or a bare key", () => { expect(modelIdFromKey(UNRECORDED)).toBeUndefined() diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 1d38c5de23..553c7eaa7a 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -15,6 +15,9 @@ import type { HistoryItem } from "@roo-code/types" /** Bucket key for usage whose provider/model was not recorded (older tasks). */ export const UNRECORDED = "__unrecorded__" +/** Cost below this (in dollars) is float noise, not unattributed spend. */ +const REMAINDER_EPSILON = 1e-6 + export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const export type UsageRange = (typeof USAGE_RANGES)[number] @@ -212,13 +215,20 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U // (created before this was tracked) goes to a single "unrecorded" bucket. const seenProviders = new Set() const seenModels = new Set() - const entries = item.usageByModel?.length - ? item.usageByModel - : [ - { requests: 0, tokensIn: tIn, tokensOut: tOut, cost } as NonNullable< - HistoryItem["usageByModel"] - >[number], - ] + type Entry = NonNullable[number] + const entries: Entry[] = item.usageByModel?.length + ? [...item.usageByModel] + : [{ requests: 0, tokensIn: tIn, tokensOut: tOut, cost }] + + // The task's cost also covers work that has no provider or model of its own (condensing the context). + // Without this, the provider and model rows would add up to less than the headline total. The + // remainder goes to the "not recorded" rows. Below a cent-fraction it is rounding noise and is ignored, + // so a float residue cannot create a spurious priced row. + if (item.usageByModel?.length) { + const attributed = item.usageByModel.reduce((sum, e) => sum + (Number.isFinite(e.cost) ? e.cost : 0), 0) + const remainder = cost - attributed + if (remainder > REMAINDER_EPSILON) entries.push({ requests: 0, tokensIn: 0, tokensOut: 0, cost: remainder }) + } for (const e of entries) { const entryTokens = e.tokensIn + e.tokensOut From a2cf471fc86187067d18c99da317bd88df41b728 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 10:55:53 -0700 Subject: [PATCH 07/76] fix(usage): use the provider registry in a test; refresh baselines for the corrected cache rate --- .../__screenshots__/usage-settings-dark.png | Bin 64531 -> 64530 bytes .../usage-settings-degraded.png | Bin 63770 -> 63769 bytes .../usage-settings-high-contrast-light.png | Bin 66509 -> 66513 bytes .../usage-settings-high-contrast.png | Bin 66730 -> 66743 bytes .../__screenshots__/usage-settings-light.png | Bin 66381 -> 66406 bytes webview-ui/src/utils/__tests__/usage.spec.ts | 4 ++-- 6 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-dark.png index 7dde60d668b845079b8c1508f72211cf23ecf728..fdbbc1b168a5a3b89c2d53f78fbc09e7b5e589af 100644 GIT binary patch literal 64530 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 literal 64531 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 TP-NW;-^lE9_jRi#P}qM1g@+Wy diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-degraded.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-degraded.png index 13ff0ecbcda847dcea0d6262a3eb0fc88e370536..8c04e5d3c0b8dfda1d45f5c56458925e0ed3f4a9 100644 GIT binary patch literal 63769 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

CGhHtdK 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

~v#vFfC|%$9i^O<&gp4xcL;G`pNiytb-NKt_@ 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 xh%Lx)8E_cE>_VktNC>!nCwrH$&} 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 NuB@$8rC=TU{{TGLbqD|e diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-light.png index 79d87f53949618703a3b4bd060281fbb1fd4c7a7..dc9e0eddbaf7266f5faa3f705f70ffd33f4919c3 100644 GIT binary patch literal 66406 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 puPGuBYio|-UJy}P| literal 66381 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 jcUO-V!GQ { describe("spend that has no provider or model (context condensing)", () => { const entry = (cost: number) => ({ - apiProvider: "anthropic", + apiProvider: providerIdentifiers.anthropic, modelId: "claude-sonnet-5", requests: 1, tokensIn: 100, @@ -367,7 +367,7 @@ describe("spend that has no provider or model (context condensing)", () => { const unrecorded = s.byProvider.find((b) => b.key === UNRECORDED)! expect(unrecorded.tokens).toBe(0) // The one task is counted under its real provider; the remainder is a cost line, not another task. - expect(s.byProvider.find((b) => b.key === "anthropic")?.tasks).toBe(1) + expect(s.byProvider.find((b) => b.key === providerIdentifiers.anthropic)?.tasks).toBe(1) }) it("ignores float noise: no spurious 'not recorded' row when the costs agree", () => { From dafd502baba37fde516b1a6b35dedac304736e99 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 11:59:54 -0700 Subject: [PATCH 08/76] fix(usage): cancel Copilot quota lookups with the request, unique request ids, close coverage gaps --- src/core/task/__tests__/Task.spec.ts | 103 ++++++++++++++++ .../__tests__/copilotQuota.spec.ts | 86 +++++++++++++ .../__tests__/providerQuotas.spec.ts | 18 +++ src/services/language-models/copilotQuota.ts | 116 ++++++++++++------ .../language-models/providerQuotas.ts | 35 +++--- .../components/settings/ProviderLimits.tsx | 7 +- .../__tests__/ProviderLimits.spec.tsx | 27 +++- webview-ui/src/utils/__tests__/usage.spec.ts | 40 ++++++ 8 files changed, 370 insertions(+), 62 deletions(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index ad07230204..5bca3bdc68 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -2110,6 +2110,39 @@ describe("Cline", () => { await loop await vi.waitFor(() => expect(oldDispose).toHaveBeenCalledOnce()) }) + + it.each([ + { name: "fills in the serving model when the profile named none", profile: undefined, expected: "served-model" }, + { name: "keeps the model the profile named", profile: "profile-model", expected: "profile-model" }, + ])("finalized api_req_started $name", async ({ profile, expected }) => { + const task = await createTaskWithManualRetries() + task.apiConfiguration = { + ...task.apiConfiguration, + apiProvider: providerIdentifiers.anthropic, + apiModelId: profile, + } + disposableApi(task) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: "served-model", + info: { supportsPromptCache: true, contextWindow: 200_000, maxTokens: 4096 } as ModelInfo, + }) + vi.spyOn(getTaskTestAccess(task), "attemptApiRequestWithActiveApi").mockImplementation( + async function* () { + yield { type: "text", text: "a" } + yield { type: "usage", inputTokens: 1, outputTokens: 1 } + }, + ) + realRequestsThenStop(task) + vi.spyOn(console, "error").mockImplementation(() => {}) + + await task.recursivelyMakeClineRequests([{ type: "text", text: "go" }]) + + const started = task.clineMessages.find((m) => m.say === "api_req_started") + const payload = JSON.parse(started?.text ?? "{}") + expect(payload.apiProvider).toBe(providerIdentifiers.anthropic) + expect(payload.modelId).toBe(expected) + expect(payload.tokensIn).toBe(1) + }) }) it("disposes the current handler exactly once even when dispose is called twice", async () => { @@ -3486,6 +3519,76 @@ describe("Cline", () => { }) }) + describe("api_req_started usage attribution", () => { + /** Runs the request setup and returns the JSON the first api_req_started message was written with. */ + const firstApiReqPayload = async ( + apiConfiguration: ProviderSettings, + servedModelId?: string, + ): Promise> => { + const task = new Task({ + provider: mockProvider, + apiConfiguration, + task: "test task", + startTask: false, + }) + + Object.assign(task.api, { ensureModelFetched: vi.fn().mockResolvedValue(undefined) }) + vi.spyOn(task.api, "getModel").mockReturnValue({ + id: servedModelId ?? "served-model", + info: { + supportsImages: false, + supportsPromptCache: true, + contextWindow: 200_000, + maxTokens: 4096, + } as ModelInfo, + }) + vi.mocked(processUserContentMentions).mockResolvedValueOnce({ + content: [{ type: "text", text: "hello" }], + mode: undefined, + }) + vi.spyOn(getTaskTestAccess(task), "saveClineMessages").mockResolvedValue(true) + vi.spyOn(getTaskTestAccess(task), "addToApiConversationHistory").mockResolvedValue(undefined) + vi.spyOn(task.diffViewProvider, "reset").mockResolvedValue(undefined as never) + vi.spyOn(task, "attemptApiRequest").mockImplementation(() => { + throw new Error("stop after the request message is written") + }) + + const payloads: string[] = [] + vi.spyOn(task, "say").mockImplementation(async (type, text) => { + if (type === "api_req_started") payloads.push(String(text)) + return undefined as never + }) + task.clineMessages = [{ ts: Date.now(), type: "say", say: "api_req_started", text: "{}" }] + + await task.recursivelyMakeClineRequests([{ type: "text", text: "hello" }]).catch(() => {}) + return JSON.parse(payloads[0] ?? "{}") + } + + it("records the provider and model that serve the request", async () => { + const payload = await firstApiReqPayload({ + apiProvider: providerIdentifiers.anthropic, + apiKey: "k", + apiModelId: "claude-sonnet-5", + }) + expect(payload).toMatchObject({ + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + }) + }) + + it("leaves the model unset when the profile names none, and records the provider", async () => { + const payload = await firstApiReqPayload({ apiProvider: providerIdentifiers.anthropic, apiKey: "k" }) + expect(payload.apiProvider).toBe(providerIdentifiers.anthropic) + // The first write cannot know the served model; the finalized message fills it in (below). + expect(payload.modelId).toBeUndefined() + }) + + it("records neither provider nor model when the profile sets no provider", async () => { + const payload = await firstApiReqPayload({} as ProviderSettings) + expect(payload.apiProvider).toBeUndefined() + }) + }) + describe("Subtask Rate Limiting", () => { let mockProvider: MockedClineProvider let mockApiConfig: RateLimitedProviderSettings diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index 413532038d..cc1cb910f6 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -216,6 +216,92 @@ describe("getGitHubCopilotQuota", () => { expect(retry).toHaveBeenCalledTimes(1) }) + describe("cancellation", () => { + /** A fetch that never answers on its own and records the signal it was given. */ + const hangingFetch = () => { + let seen: AbortSignal | undefined + const fetchSpy = vi.fn( + (_url: string, init?: RequestInit) => + new Promise((_resolve, reject) => { + seen = init?.signal ?? undefined + seen?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError"))) + }), + ) + return { fetchSpy, signal: () => seen } + } + + it("stops the GitHub request when the only caller goes away", async () => { + const { fetchSpy, signal } = hangingFetch() + const controller = new AbortController() + const result = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + await vi.waitFor(() => expect(signal()).toBeDefined()) + expect(signal()!.aborted).toBe(false) + + controller.abort() + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(signal()!.aborted).toBe(true) + }) + + it("keeps the shared request running until its last caller leaves", async () => { + const { fetchSpy, signal } = hangingFetch() + const leaving = new AbortController() + const staying = new AbortController() + + const a = getGitHubCopilotQuota({ signal: staying.signal }, fetchSpy as never) + const b = getGitHubCopilotQuota({ signal: leaving.signal }, fetchSpy as never) + await vi.waitFor(() => expect(signal()).toBeDefined()) + expect(fetchSpy).toHaveBeenCalledTimes(1) + + leaving.abort() + await expect(b).rejects.toMatchObject({ name: "AbortError" }) + expect(signal()!.aborted).toBe(false) + + staying.abort() + await expect(a).rejects.toMatchObject({ name: "AbortError" }) + expect(signal()!.aborted).toBe(true) + }) + + it("rejects at once with a signal that is already aborted, and makes no request", async () => { + const fetchSpy = vi.fn() + const controller = new AbortController() + controller.abort() + await expect(getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never)).rejects.toMatchObject( + { + name: "AbortError", + }, + ) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("does not cache a cancelled request, so the next call fetches again", async () => { + const { fetchSpy } = hangingFetch() + const controller = new AbortController() + const cancelled = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled()) + controller.abort() + await expect(cancelled).rejects.toBeDefined() + + const good = vi.fn(async () => json(businessPayload)) + await expect(getGitHubCopilotQuota({}, good as never)).resolves.toMatchObject({ ok: true }) + expect(good).toHaveBeenCalledTimes(1) + }) + + it("does not start the GitHub request when it was cancelled while the token was being fetched", async () => { + let release: (session: { accessToken: string }) => void = () => {} + getSession.mockImplementationOnce(() => new Promise((resolve) => (release = resolve))) + const fetchSpy = vi.fn() + const controller = new AbortController() + + const result = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + await vi.waitFor(() => expect(getSession).toHaveBeenCalled()) + controller.abort() + release({ accessToken: "gho_token" }) + + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + }) + it("shares one in-flight request between concurrent callers", async () => { const fetchSpy = vi.fn(async () => json(businessPayload)) const [a, b] = await Promise.all([ diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index a8cfbdbd38..ade24dccc0 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -77,6 +77,13 @@ describe("parseDeepSeekBalance", () => { }) describe("parseDeepSeekBalance – fallbacks", () => { + it("treats a USD or CNY row without a balance as zero when choosing a row", () => { + const result = parseDeepSeekBalance({ + balance_infos: [{ currency: "USD" }, { currency: "CNY" }, { currency: "CNY", total_balance: "5" }], + }) + expect(result).toEqual([{ id: "balance", valueLabel: "¥5.00 left" }]) + }) + it("falls back to a zero USD balance, then to the first row", () => { expect( parseDeepSeekBalance({ @@ -394,6 +401,17 @@ describe("getProviderQuotas – Copilot window and misc", () => { expect(unl).toMatchObject({ ok: true, windows: [{ valueLabel: "Unlimited", resetAt: 7 }] }) }) + it("hands the caller's abort signal to the Copilot lookup", async () => { + const { getGitHubCopilotQuota } = await import("../copilotQuota") + const mocked = vi.mocked(getGitHubCopilotQuota) + mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: true, fetchedAt: 1 } }) + const controller = new AbortController() + await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + signal: controller.signal, + }) + expect(mocked).toHaveBeenLastCalledWith({ force: false, signal: controller.signal }) + }) + it("passes Copilot failures through with their reason and message", async () => { const { getGitHubCopilotQuota } = await import("../copilotQuota") vi.mocked(getGitHubCopilotQuota).mockResolvedValueOnce({ ok: false, reason: "error", message: "boom" }) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index 754edbeca5..4bf821e722 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -78,58 +78,94 @@ async function getGitHubToken(): Promise { } let cache: { at: number; result: GitHubCopilotQuotaResult } | undefined -let inFlight: Promise | undefined +let inFlight: + | { + request: Promise + /** Callers still waiting. The shared fetch is aborted only when the last one leaves. */ + waiters: number + controller: AbortController + } + | undefined + +/** Rejects when `signal` aborts, without cancelling `work` (other callers may still be using it). */ +function untilAborted(work: Promise, signal: AbortSignal | undefined, onAbort: () => void): Promise { + if (!signal) return work + return new Promise((resolve, reject) => { + const abort = () => { + onAbort() + reject(new DOMException("Aborted", "AbortError")) + } + if (signal.aborted) return abort() + signal.addEventListener("abort", abort, { once: true }) + work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) + }) +} /** * Fetches the quota. Concurrent callers share one request; successful results - * are cached briefly unless `force` is set (the Refresh button). + * are cached briefly unless `force` is set (the Refresh button). `signal` lets a caller stop waiting; the + * network work itself stops when the last waiting caller has gone. */ export function getGitHubCopilotQuota( - { force = false }: { force?: boolean } = {}, + { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}, fetchImpl: typeof fetch = fetch, ): Promise { + if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError")) if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) return Promise.resolve(cache.result) - if (inFlight) return inFlight - - const fetchQuota = async (): Promise => { - try { - const token = await getGitHubToken() - if (!token) return { ok: false, reason: "signedOut" } - - const response = await fetchImpl(COPILOT_USER_URL, { - method: "GET", - headers: { - Authorization: `token ${token}`, - Accept: "application/json", - "Editor-Version": `vscode/${vscode.version}`, - "X-Github-Api-Version": "2025-04-01", - }, - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), - }) - - if (response.status === 401) return { ok: false, reason: "signedOut" } - if (response.status === 403 || response.status === 404) return { ok: false, reason: "noCopilot" } - if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } - const quota = parseCopilotQuota(await response.json()) - return quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } - } catch (error) { - return { ok: false, reason: "error", message: safeErrorMessage(error) } + if (!inFlight) { + const controller = new AbortController() + const fetchQuota = async (): Promise => { + try { + const token = await getGitHubToken() + if (controller.signal.aborted) return { ok: false, reason: "error", message: "Request cancelled" } + if (!token) return { ok: false, reason: "signedOut" } + + const response = await fetchImpl(COPILOT_USER_URL, { + method: "GET", + headers: { + Authorization: `token ${token}`, + Accept: "application/json", + "Editor-Version": `vscode/${vscode.version}`, + "X-Github-Api-Version": "2025-04-01", + }, + signal: AbortSignal.any([controller.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]), + }) + + if (response.status === 401) return { ok: false, reason: "signedOut" } + if (response.status === 403 || response.status === 404) return { ok: false, reason: "noCopilot" } + if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } + + const quota = parseCopilotQuota(await response.json()) + return quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } + } catch (error) { + return { ok: false, reason: "error", message: safeErrorMessage(error) } + } } + + const request: Promise = fetchQuota() + .then((result) => { + // One place for every outcome: a success is cached, and anything else (signed out, no Copilot, an + // HTTP error, a thrown or cancelled request) drops an older success so it is not served as current. + cache = result.ok ? { at: Date.now(), result } : undefined + return result + }) + .finally(() => { + if (inFlight?.request === request) inFlight = undefined + }) + inFlight = { request, waiters: 0, controller } } - inFlight = fetchQuota() - .then((result) => { - // One place for every outcome: a success is cached, and anything else (signed out, no Copilot, an - // HTTP error, a thrown error) drops an older success so it is not served as if it were current. - cache = result.ok ? { at: Date.now(), result } : undefined - return result - }) - .finally(() => { - inFlight = undefined - }) - - return inFlight + const shared = inFlight + shared.waiters++ + let left = false + const leave = () => { + if (left) return + left = true + // Last one out cancels the network work; earlier leavers just stop waiting. + if (--shared.waiters === 0) shared.controller.abort() + } + return untilAborted(shared.request, signal, leave).finally(leave) } /** @internal test hook */ diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 433c11278e..70ed6def2b 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -316,12 +316,12 @@ export function parseOpenCodeGoUsage(payload: unknown): ProviderQuotaWindow[] | type Fetch = typeof fetch -async function getJson(fetchImpl: Fetch, url: string, apiKey: string, signal?: AbortSignal): Promise { +async function getJson(fetchImpl: Fetch, url: string, apiKey: string, signal: AbortSignal): Promise { const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) const response = await fetchImpl(url, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, - signal: signal ? AbortSignal.any([signal, timeout]) : timeout, + signal: AbortSignal.any([signal, timeout]), }) if (response.status === 401 || response.status === 403) throw new AuthError() if (!response.ok) throw new QuotaHttpError(response.status) @@ -480,22 +480,20 @@ function unqueryable(provider: KeyedProvider, creds: KeyedCredentials | undefine async function fetchKeyed( provider: KeyedProvider, - creds: KeyedCredentials, + apiKey: string, + urls: string[], fetchImpl: Fetch, - signal?: AbortSignal, + signal: AbortSignal, ): Promise { const now = Date.now() - if (!creds.apiKey || !creds.urls?.length) { - return { providerId: provider.providerId, ok: false, reason: "unsupported", fetchedAt: now } - } try { // Try each URL in order; a failing fallback only matters if no URL yields data. let windows: ProviderQuotaWindow[] | undefined let lastError: unknown - for (const [index, url] of creds.urls.entries()) { + for (const [index, url] of urls.entries()) { try { - windows = provider.parse(await getJson(fetchImpl, url, creds.apiKey, signal), index) + windows = provider.parse(await getJson(fetchImpl, url, apiKey, signal), index) if (windows?.length) break } catch (error) { if (error instanceof AuthError) throw error @@ -517,8 +515,8 @@ async function fetchKeyed( } } -async function fetchCopilot(force: boolean): Promise { - const result = await getGitHubCopilotQuota({ force }) +async function fetchCopilot(force: boolean, signal?: AbortSignal): Promise { + const result = await getGitHubCopilotQuota({ force, signal }) const providerId = providerIdentifiers.githubCopilot if (!result.ok) return { providerId, ok: false, reason: result.reason, message: result.message, fetchedAt: Date.now() } @@ -577,8 +575,8 @@ interface Inflight { const inflight = new Map() /** Identifies a provider and the URLs it is queried at (the credential itself is not part of it). */ -function cacheKey(provider: KeyedProvider, creds: KeyedCredentials): string { - return `${provider.providerId}|${(creds.urls ?? []).join(",")}` +function cacheKey(provider: KeyedProvider, urls: string[]): string { + return `${provider.providerId}|${urls.join(",")}` } /** Rejects as soon as `signal` aborts, without cancelling `work` (which other callers may still be using). */ @@ -606,9 +604,10 @@ function quotaForKeyedProvider( const skipped = unqueryable(provider, creds) if (skipped) return Promise.resolve(skipped) - const resolved = creds as KeyedCredentials - const apiKey = resolved.apiKey as string - const key = cacheKey(provider, resolved) + // `unqueryable` returned early unless there is a key and at least one URL. + const apiKey = creds?.apiKey as string + const urls = creds?.urls as string[] + const key = cacheKey(provider, urls) const hit = cache.get(key) if (!force && hit && hit.apiKey === apiKey && Date.now() - hit.at < CACHE_TTL_MS) return Promise.resolve(hit.result) @@ -616,7 +615,7 @@ function quotaForKeyedProvider( let entry = inflight.get(key) if (!entry || entry.apiKey !== apiKey) { const controller = new AbortController() - const request: Promise = fetchKeyed(provider, resolved, fetchImpl, controller.signal) + const request: Promise = fetchKeyed(provider, apiKey, urls, fetchImpl, controller.signal) .then((result) => { if (result.ok) { cache.set(key, { at: Date.now(), apiKey, result }) @@ -657,7 +656,7 @@ export async function getProviderQuotas( const connected = new Set(profiles.map((p) => p.apiProvider).filter(Boolean) as string[]) const tasks: Promise[] = [] - if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force)) + if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force, signal)) for (const provider of KEYED_PROVIDERS) { if (connected.has(provider.providerId)) { tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl, signal)) diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index 1bab564b08..2a988e6426 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -10,8 +10,11 @@ import i18next from "i18next" import { providerLabel } from "@src/utils/providerLabel" // Module-level, so a component that is unmounted and mounted again never reuses an id that an older, -// still-settling request carries. +// still-settling request carries. The random part separates this webview from any other one (sidebar and +// editor tab each load their own copy of this module), so an id is unique across webviews too. let quotaRequestCounter = 0 +const quotaRequestSession = Math.random().toString(36).slice(2, 10) +const nextQuotaRequestId = () => `provider-quotas-${quotaRequestSession}-${++quotaRequestCounter}` const barColor = (pct: number) => pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" @@ -45,7 +48,7 @@ export const ProviderLimits = () => { const request = useCallback( (force: boolean) => { cancelLatest() - const requestId = `provider-quotas-${++quotaRequestCounter}` + const requestId = nextQuotaRequestId() latestRequestId.current = requestId setLoading(true) vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force, requestId }) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index bf61987640..b11a6d17e9 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -23,6 +23,14 @@ const lastRequestId = () => { } /** Answers the most recent request, or the given id. Pass `null` to send an answer with no id at all. */ +const lastRequestIdAfterRender = (Component: typeof ProviderLimits = ProviderLimits) => { + post.mockClear() + const view = render() + const id = lastRequestId() + view.unmount() + return id +} + const respond = (providerQuotas: ProviderQuota[], id?: string | null) => act(() => { const requestId = id === null ? undefined : (id ?? lastRequestId()) @@ -41,7 +49,7 @@ describe("ProviderLimits", () => { expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: false, - requestId: expect.stringMatching(/^provider-quotas-\d+$/), + requestId: expect.stringMatching(/^provider-quotas-[a-z0-9]+-\d+$/), }) }) @@ -181,7 +189,7 @@ describe("ProviderLimits", () => { expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: true, - requestId: expect.stringMatching(/^provider-quotas-\d+$/), + requestId: expect.stringMatching(/^provider-quotas-[a-z0-9]+-\d+$/), }) }) @@ -304,6 +312,21 @@ describe("ProviderLimits", () => { }) }) + it("makes ids that differ between webviews, not just between requests", async () => { + // Each webview loads its own copy of the module and starts counting again from 1. + vi.resetModules() + const { ProviderLimits: First } = await import("../ProviderLimits") + const first = lastRequestIdAfterRender(First) + vi.resetModules() + const { ProviderLimits: Second } = await import("../ProviderLimits") + const second = lastRequestIdAfterRender(Second) + + expect(first).toMatch(/^provider-quotas-[a-z0-9]+-1$/) + expect(second).toMatch(/^provider-quotas-[a-z0-9]+-1$/) + // Same counter value, still different ids: the session part tells the webviews apart. + expect(first).not.toBe(second) + }) + it("never reuses a request id when the tab is closed and opened again", () => { const first = render() const firstId = lastRequestId() diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 95c08f2437..0fae4293c4 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -542,3 +542,43 @@ describe("usageToCsv", () => { expect(lines[1].endsWith(",0.1235")).toBe(true) }) }) + +describe("defaults and non-finite input", () => { + afterEach(() => { + vi.useRealTimers() + }) + + it("uses the current time when no `now` is given", () => { + vi.useFakeTimers() + vi.setSystemTime(NOW) + const recent = item({ ts: NOW - 1000, totalCost: 1 }) + const old = item({ ts: NOW - 3 * DAY, totalCost: 2 }) + + expect(summarizeUsage([recent, old], { range: "today" }).totalCost).toBe(1) + expect(usageToCsv([recent, old], { range: "today" }).split("\n")).toHaveLength(2) + }) + + it("ignores a non-finite per-model cost when reconciling the task cost", () => { + const summary = summarizeUsage( + [ + item({ + totalCost: 3, + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "m", + requests: 1, + tokensIn: 1, + tokensOut: 1, + cost: Number.NaN, + }, + ], + }), + ], + { range: "all", now: NOW }, + ) + + // The whole task cost is unattributed because the recorded model cost is unusable. + expect(summary.byProvider.find((b) => b.key === UNRECORDED)?.cost).toBe(3) + }) +}) From cda347557a848eea1e89d95c19a5a5824af046eb Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 12:04:00 -0700 Subject: [PATCH 09/76] test(usage): require exactly one quota call, and avoid a racy dynamic import in the test --- .../__tests__/webviewMessageHandler.providerQuotas.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 914b9520a4..dd234a9c95 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -222,6 +222,9 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { type: "requestProviderQuotas", requestId: "provider-quotas-1", }) + // Start them one after the other: two overlapping dynamic imports of the handler's service module can + // resolve to the real module instead of the mock, which would make a real network call. + await vi.waitFor(() => expect(releases).toHaveLength(1)) const second = webviewMessageHandler(editorTab, { type: "requestProviderQuotas", requestId: "provider-quotas-1", @@ -240,7 +243,7 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect.objectContaining({ type: "providerQuotas", requestId: "provider-quotas-1" }), ) // Only the editor tab's request went on to the service; the sidebar's was stopped while loading. - expect(quotas.getProviderQuotas.mock.calls.length).toBeLessThanOrEqual(1) + expect(quotas.getProviderQuotas).toHaveBeenCalledTimes(1) }) it("marks the answer as failed when the quotas could not be loaded, and not when there are simply none", async () => { From f263acfeb27b4a9277c9d599a08106989c19a555 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 12:36:35 -0700 Subject: [PATCH 10/76] style: format Task.spec --- src/core/task/__tests__/Task.spec.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 5bca3bdc68..d7ac471a0b 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -2112,7 +2112,11 @@ describe("Cline", () => { }) it.each([ - { name: "fills in the serving model when the profile named none", profile: undefined, expected: "served-model" }, + { + name: "fills in the serving model when the profile named none", + profile: undefined, + expected: "served-model", + }, { name: "keeps the model the profile named", profile: "profile-model", expected: "profile-model" }, ])("finalized api_req_started $name", async ({ profile, expected }) => { const task = await createTaskWithManualRetries() From ae50b610c63128ddaf4bc26c4c1314864d236eff Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 13:06:59 -0700 Subject: [PATCH 11/76] fix(usage): do not join a cancelled quota request; ignore non-finite model costs; tighten tests --- .../__tests__/copilotQuota.spec.ts | 20 +++++++- .../__tests__/providerQuotas.spec.ts | 48 +++++++++++++++++++ src/services/language-models/copilotQuota.ts | 9 +++- .../language-models/providerQuotas.ts | 19 +++++--- webview-ui/src/utils/__tests__/usage.spec.ts | 6 +++ webview-ui/src/utils/usage.ts | 5 +- 6 files changed, 96 insertions(+), 11 deletions(-) diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index cc1cb910f6..c4ec2eacab 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -279,13 +279,31 @@ describe("getGitHubCopilotQuota", () => { const cancelled = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalled()) controller.abort() - await expect(cancelled).rejects.toBeDefined() + await expect(cancelled).rejects.toMatchObject({ name: "AbortError" }) const good = vi.fn(async () => json(businessPayload)) await expect(getGitHubCopilotQuota({}, good as never)).resolves.toMatchObject({ ok: true }) expect(good).toHaveBeenCalledTimes(1) }) + it("starts a new request instead of joining one whose last caller already left", async () => { + let release: (session: { accessToken: string }) => void = () => {} + getSession.mockImplementationOnce(() => new Promise((resolve) => (release = resolve))) + const controller = new AbortController() + const first = getGitHubCopilotQuota({ signal: controller.signal }, vi.fn() as never) + await vi.waitFor(() => expect(getSession).toHaveBeenCalled()) + controller.abort() + await expect(first).rejects.toMatchObject({ name: "AbortError" }) + + // The cancelled request is still waiting on the token; a new caller must not be handed its result. + const good = vi.fn(async () => json(businessPayload)) + const second = getGitHubCopilotQuota({}, good as never) + release({ accessToken: "gho_token" }) + + await expect(second).resolves.toMatchObject({ ok: true }) + expect(good).toHaveBeenCalledTimes(1) + }) + it("does not start the GitHub request when it was cancelled while the token was being fetched", async () => { let release: (session: { accessToken: string }) => void = () => {} getSession.mockImplementationOnce(() => new Promise((resolve) => (release = resolve))) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index ade24dccc0..b8e59b0895 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -927,6 +927,54 @@ describe("getProviderQuotas", () => { expect(ra).toEqual(rb) }) + it("starts a new request instead of joining one whose last caller already left", async () => { + const body = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + // A host that ignores cancellation: the first request is still pending after its caller left. + let finishFirst: () => void = () => {} + const fetchSpy = vi + .fn() + .mockImplementationOnce(() => new Promise((resolve) => (finishFirst = () => resolve(body())))) + .mockImplementation(async () => body()) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + const controller = new AbortController() + + const first = getProviderQuotas(profiles, { signal: controller.signal }, fetchSpy) + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(1)) + controller.abort() + await expect(first).rejects.toMatchObject({ name: "AbortError" }) + + // A new caller must get its own fetch, not join the cancelled one. + const [q] = await getProviderQuotas(profiles, {}, fetchSpy) + expect(fetchSpy).toHaveBeenCalledTimes(2) + expect(q).toMatchObject({ providerId: providerIdentifiers.openrouter, ok: true }) + finishFirst() + }) + + it("does not let a cancelled request wipe a cached success", async () => { + const ok = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + await getProviderQuotas(profiles, {}, ok) + + const hanging = vi.fn( + (_input: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => + (init?.signal as AbortSignal).addEventListener("abort", () => + reject(new DOMException("Aborted", "AbortError")), + ), + ), + ) + const controller = new AbortController() + const refresh = getProviderQuotas(profiles, { force: true, signal: controller.signal }, hanging) + await vi.waitFor(() => expect(hanging).toHaveBeenCalled()) + controller.abort() + await expect(refresh).rejects.toMatchObject({ name: "AbortError" }) + await new Promise((resolve) => setTimeout(resolve, 0)) + + // The earlier success is still served from the cache. + await getProviderQuotas(profiles, {}, ok) + expect(ok).toHaveBeenCalledTimes(1) + }) + it("caches results for the same set of providers unless forced", async () => { const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index 4bf821e722..f68b6dadee 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -147,7 +147,8 @@ export function getGitHubCopilotQuota( .then((result) => { // One place for every outcome: a success is cached, and anything else (signed out, no Copilot, an // HTTP error, a thrown or cancelled request) drops an older success so it is not served as current. - cache = result.ok ? { at: Date.now(), result } : undefined + // A cancelled request says nothing about the account, so it leaves the cache alone. + if (!controller.signal.aborted) cache = result.ok ? { at: Date.now(), result } : undefined return result }) .finally(() => { @@ -163,7 +164,11 @@ export function getGitHubCopilotQuota( if (left) return left = true // Last one out cancels the network work; earlier leavers just stop waiting. - if (--shared.waiters === 0) shared.controller.abort() + if (--shared.waiters === 0) { + shared.controller.abort() + // Do not let a later caller join a request that is already cancelled (it would only get "cancelled"). + if (inFlight === shared) inFlight = undefined + } } return untilAborted(shared.request, signal, leave).finally(leave) } diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 70ed6def2b..bf4ba6afc7 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -617,11 +617,14 @@ function quotaForKeyedProvider( const controller = new AbortController() const request: Promise = fetchKeyed(provider, apiKey, urls, fetchImpl, controller.signal) .then((result) => { - if (result.ok) { - cache.set(key, { at: Date.now(), apiKey, result }) - } else { - // A failed refresh must not leave an older success to be served as if it were current. - cache.delete(key) + // A cancelled request says nothing about the account, so it leaves the cache as it is. + if (!controller.signal.aborted) { + if (result.ok) { + cache.set(key, { at: Date.now(), apiKey, result }) + } else { + // A failed refresh must not leave an older success to be served as if it were current. + cache.delete(key) + } } return result }) @@ -639,7 +642,11 @@ function quotaForKeyedProvider( if (left) return left = true // Last one out cancels the network work; earlier leavers just stop waiting. - if (--shared.waiters === 0) shared.controller.abort() + if (--shared.waiters === 0) { + shared.controller.abort() + // Do not let a later caller join a request that is already cancelled. + if (inflight.get(key) === shared) inflight.delete(key) + } } return until(shared.request, signal, leave).finally(leave) } diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 0fae4293c4..7a8a9cfc43 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -580,5 +580,11 @@ describe("defaults and non-finite input", () => { // The whole task cost is unattributed because the recorded model cost is unusable. expect(summary.byProvider.find((b) => b.key === UNRECORDED)?.cost).toBe(3) + // The unusable cost never reaches the recorded buckets as NaN. + const recordedProvider = summary.byProvider.find((b) => b.key === providerIdentifiers.anthropic) + expect(recordedProvider).toMatchObject({ cost: 0, tokens: 2, priced: false }) + expect(summary.byModel.find((b) => b.key !== UNRECORDED)).toMatchObject({ cost: 0, tokens: 2, priced: false }) + expect(summary.byProvider.reduce((sum, b) => sum + b.cost, 0)).toBe(3) + expect(summary.byModel.reduce((sum, b) => sum + b.cost, 0)).toBe(3) }) }) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 553c7eaa7a..c87cde7825 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -232,10 +232,11 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U for (const e of entries) { const entryTokens = e.tokensIn + e.tokensOut + const entryCost = Number.isFinite(e.cost) ? e.cost : 0 const providerKey = e.apiProvider || UNRECORDED const modelKey = e.modelId ? `${e.apiProvider ?? ""}\u0000${e.modelId}` : UNRECORDED - addTo(providers, providerKey, e.cost, entryTokens, seenProviders) - addTo(models, modelKey, e.cost, entryTokens, seenModels, { apiProvider: e.apiProvider }) + addTo(providers, providerKey, entryCost, entryTokens, seenProviders) + addTo(models, modelKey, entryCost, entryTokens, seenModels, { apiProvider: e.apiProvider }) } const modeSeen = new Set() From 9191d4a56f5ec6208ca7401ae10bdac8b63685de Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 13:31:26 -0700 Subject: [PATCH 12/76] fix(usage): clear a stale usageByModel when messages are replaced; close coverage partials --- .../TaskHistoryStore.realConcurrency.spec.ts | 26 +++++++++++++++++++ .../taskMetadata.usageByModel.spec.ts | 10 ++++--- src/core/task-persistence/taskMetadata.ts | 7 +++-- ...bviewMessageHandler.providerQuotas.spec.ts | 25 ++++++++++++++++++ src/services/language-models/copilotQuota.ts | 1 - 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts index d35dfc49cf..990519de56 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts @@ -119,6 +119,32 @@ describe("TaskHistoryStore real cross-host locking", () => { } }) + it("clears a stored usageByModel when the same task is saved again without one (e.g. after a rewind)", async () => { + const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), "task-history-usage-clear-")) + const store = new TaskHistoryStore(storagePath) + const usageByModel = [ + { apiProvider: "anthropic", modelId: "m", requests: 1, tokensIn: 5, tokensOut: 5, cost: 1 }, + ] + + try { + await store.initialize() + await store.upsert({ ...item("usage-task"), usageByModel }) + expect(store.get("usage-task")?.usageByModel).toEqual(usageByModel) + + // What taskMetadata emits when the replaced messages record no usage. + await store.upsert({ ...item("usage-task"), usageByModel: undefined }) + expect(store.get("usage-task")?.usageByModel).toBeUndefined() + + // ...and the stored file no longer carries the stale breakdown either. + await store.invalidate("usage-task") + expect(store.get("usage-task")).toBeDefined() + expect(store.get("usage-task")?.usageByModel).toBeUndefined() + } finally { + store.dispose() + await fs.rm(storagePath, { recursive: true, force: true }) + } + }) + it("reports a bounded error when one store never reaches the write barrier", async () => { const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), "task-history-missed-barrier-")) const storeA = new TaskHistoryStore(storagePath) diff --git a/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts index a16e27259d..dc98a88536 100644 --- a/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts @@ -69,14 +69,16 @@ describe("taskMetadata – usageByModel", () => { expect(historyItem.totalCost).toBeCloseTo(0.2) }) - it("omits usageByModel entirely when nothing was recorded, keeping old items' shape", async () => { + it("leaves usageByModel undefined when nothing was recorded, so a stored breakdown is cleared", async () => { const { historyItem } = await taskMetadata({ ...base, messages: [say("text", "hello", 1)] }) - expect(historyItem).not.toHaveProperty("usageByModel") + expect(historyItem).toHaveProperty("usageByModel", undefined) + // Nothing is written for it: the stored shape of older tasks is unchanged. + expect(JSON.parse(JSON.stringify(historyItem))).not.toHaveProperty("usageByModel") }) - it("omits usageByModel for a task with no messages", async () => { + it("leaves usageByModel undefined for a task with no messages", async () => { const { historyItem } = await taskMetadata({ ...base, messages: [] }) - expect(historyItem).not.toHaveProperty("usageByModel") + expect(historyItem).toHaveProperty("usageByModel", undefined) expect(historyItem.tokensIn).toBe(0) }) }) diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index 827095f67a..c5a6af102f 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -110,8 +110,11 @@ export async function taskMetadata({ cacheWrites: tokenUsage.totalCacheWrites, cacheReads: tokenUsage.totalCacheReads, totalCost: tokenUsage.totalCost, - // Only emitted when something was recorded, so older tasks and empty tasks keep their exact shape. - ...(usageByModel.length > 0 ? { usageByModel } : {}), + // Always present, as `undefined` when nothing was recorded: the history store merges onto the stored + // item, so omitting the key would keep a stale breakdown after the messages were replaced (e.g. a + // rewind). `undefined` overrides it and is dropped when the item is written, so the stored shape of + // older and empty tasks is unchanged. + usageByModel: usageByModel.length > 0 ? usageByModel : undefined, size: taskDirSize, workspace, mode, diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index dd234a9c95..457318ae8e 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -246,6 +246,31 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(quotas.getProviderQuotas).toHaveBeenCalledTimes(1) }) + it("lets a newer request with the same id keep its slot when the older one finishes first", async () => { + quotas.getProviderQuotas.mockResolvedValue([]) + const releases: Array<(entries: { id: string }[]) => void> = [] + const provider = makeProvider() + provider.providerSettingsManager.listConfig = vi.fn( + () => new Promise((resolve) => releases.push(resolve)), + ) as never + + // Start them one after the other: see the note in the test above. + const older = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "same-id" }) + await vi.waitFor(() => expect(releases).toHaveLength(1)) + const newer = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "same-id" }) + await vi.waitFor(() => expect(releases).toHaveLength(2)) + + // The older one finishes and cleans up; it must not remove the newer one's entry. + releases[0]([{ id: "a" }]) + await older + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "same-id" }) + releases[1]([{ id: "a" }]) + await newer + + // The cancel reached the newer request, so only the older one was answered. + expect(provider.postMessageToWebview).toHaveBeenCalledTimes(1) + }) + it("marks the answer as failed when the quotas could not be loaded, and not when there are simply none", async () => { const failing = makeProvider() failing.providerSettingsManager.listConfig = vi.fn(async () => { diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index f68b6dadee..f7b5e4f188 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -95,7 +95,6 @@ function untilAborted(work: Promise, signal: AbortSignal | undefined, onAb onAbort() reject(new DOMException("Aborted", "AbortError")) } - if (signal.aborted) return abort() signal.addEventListener("abort", abort, { once: true }) work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) }) From 56d07b15184a3c8060acc29a59ba57d757b5a63b Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 13:36:12 -0700 Subject: [PATCH 13/76] test: use providerIdentifiers in the usageByModel store test --- .../TaskHistoryStore.realConcurrency.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts index 990519de56..54cc1075d5 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts @@ -2,7 +2,7 @@ import * as fs from "fs/promises" import * as os from "os" import * as path from "path" -import type { HistoryItem } from "@roo-code/types" +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" import { TaskHistoryStore } from "../TaskHistoryStore" @@ -123,7 +123,14 @@ describe("TaskHistoryStore real cross-host locking", () => { const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), "task-history-usage-clear-")) const store = new TaskHistoryStore(storagePath) const usageByModel = [ - { apiProvider: "anthropic", modelId: "m", requests: 1, tokensIn: 5, tokensOut: 5, cost: 1 }, + { + apiProvider: providerIdentifiers.anthropic, + modelId: "m", + requests: 1, + tokensIn: 5, + tokensOut: 5, + cost: 1, + }, ] try { From 017237ca93a7c69e6646fd74fb90b8a197cb20aa Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 13:50:41 -0700 Subject: [PATCH 14/76] fix(usage): cancel profile loading and the Copilot token stage; add schema and rewind regression tests --- packages/types/src/__tests__/history.test.ts | 35 +++++++++++++ src/core/task/__tests__/Task.spec.ts | 52 +++++++++++++++++++ ...bviewMessageHandler.providerQuotas.spec.ts | 16 ++++++ src/core/webview/webviewMessageHandler.ts | 12 ++--- .../__tests__/copilotQuota.spec.ts | 16 ++++++ .../__tests__/providerQuotas.spec.ts | 35 +++++++++++++ src/services/language-models/copilotQuota.ts | 22 ++++++-- .../language-models/providerQuotas.ts | 16 ++++-- 8 files changed, 190 insertions(+), 14 deletions(-) diff --git a/packages/types/src/__tests__/history.test.ts b/packages/types/src/__tests__/history.test.ts index 883bb138d2..991a991b33 100644 --- a/packages/types/src/__tests__/history.test.ts +++ b/packages/types/src/__tests__/history.test.ts @@ -64,3 +64,38 @@ describe("historyItemSchema", () => { expect(historyItemSchema.parse(baseItem)).not.toHaveProperty("title") }) }) + +describe("usageByModel on history items", () => { + const base = { id: "task-1", number: 1, ts: 1, task: "t", tokensIn: 1, tokensOut: 1, totalCost: 0 } + + it("preserves every field of a per-model entry", () => { + const entry = { + apiProvider: "provider-a", + modelId: "model-a", + requests: 3, + tokensIn: 100, + tokensOut: 50, + cacheReads: 40, + cacheWrites: 10, + cost: 0.25, + } + expect(historyItemSchema.parse({ ...base, usageByModel: [entry, { ...entry, modelId: "model-b" }] })).toEqual({ + ...base, + usageByModel: [entry, { ...entry, modelId: "model-b" }], + }) + }) + + it("accepts an entry with no provider, model or cache counts, and an item with no breakdown", () => { + const bare = { requests: 1, tokensIn: 2, tokensOut: 3, cost: 0 } + expect(historyItemSchema.parse({ ...base, usageByModel: [bare] }).usageByModel).toEqual([bare]) + expect(historyItemSchema.parse(base).usageByModel).toBeUndefined() + }) + + it.each([ + ["a missing request count", { tokensIn: 1, tokensOut: 1, cost: 0 }], + ["a non-numeric cost", { requests: 1, tokensIn: 1, tokensOut: 1, cost: "free" }], + ["a non-numeric token count", { requests: 1, tokensIn: "1", tokensOut: 1, cost: 0 }], + ])("rejects %s", (_name, entry) => { + expect(historyItemSchema.safeParse({ ...base, usageByModel: [entry] }).success).toBe(false) + }) +}) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index d7ac471a0b..cee47a1fb8 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -7602,6 +7602,58 @@ describe("Cline", () => { } }) + it("overwriteClineMessages sends an explicit empty usageByModel when the replacement records no usage", async () => { + // The history store merges onto the stored item, so a replacement with no recorded usage (for example + // after a rewind) must say so explicitly, or the earlier breakdown would stay on the stored item. + const taskDir = path.join(os.tmpdir(), "test-storage", "tasks", "00000000-0000-7000-8000-000000000000") + fsReal.mkdirSync(taskDir, { recursive: true }) + const historySpy = vi.spyOn(mockProvider, "updateTaskHistory") + try { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + const say = (kind: ClineMessage["say"], text: string, ts: number): ClineMessage => ({ + ts, + type: "say", + say: kind, + text, + }) + const withUsage = [ + say("text", "do it", 1), + say( + "api_req_started", + JSON.stringify({ + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + tokensIn: 10, + tokensOut: 5, + cost: 0.5, + }), + 2, + ), + ] + + await task.overwriteClineMessages(withUsage) + const first = historySpy.mock.calls.at(-1)?.[0] + expect(first?.usageByModel).toEqual([ + expect.objectContaining({ apiProvider: providerIdentifiers.anthropic, requests: 1, cost: 0.5 }), + ]) + + // A rewind to before the request: the replacement records nothing. + await task.overwriteClineMessages([say("text", "do it", 1)]) + const second = historySpy.mock.calls.at(-1)?.[0] + expect(second).toBeDefined() + expect(second).toHaveProperty("usageByModel", undefined) + expect(second?.totalCost).toBe(0) + } finally { + fsReal.rmSync(path.join(taskDir, "ui_messages.json"), { force: true }) + historySpy.mockRestore() + } + }) + it("finalizePartialToolAsk skips the webview update when the message write itself fails", async () => { // Complements the later-stage-failure test above by failing the first save // stage: with the real task directory removed, safeWriteJson's fs.access diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 457318ae8e..a1c9387443 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -203,6 +203,22 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { expect(provider.postMessageToWebview).not.toHaveBeenCalled() }) + it("releases the handler on cancel without waiting for a profile read that never settles", async () => { + const provider = makeProvider() + provider.providerSettingsManager.listConfig = vi.fn(() => new Promise(() => {})) as never + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-stuck" }) + await vi.waitFor(() => expect(provider.providerSettingsManager.listConfig).toHaveBeenCalled()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-stuck" }) + + // Resolves although listConfig never does. + await running + expect(provider.providerSettingsManager.getProfile).not.toHaveBeenCalled() + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + expect(provider.log).not.toHaveBeenCalled() + }) + it("keeps two webviews apart: a cancel from one never aborts the other's request with the same id", async () => { // Each webview has its own provider, and both number their requests from 1. Their loads are held open // at the profile list, so both are pending at once when the cancel arrives. diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 11055fe2e9..247c7f3951 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1599,13 +1599,11 @@ export const webviewMessageHandler = async ( try { const { getProviderQuotas, loadProfiles } = await import("../../services/language-models/providerQuotas") - const profiles = await loadProfiles(provider.providerSettingsManager) - if (!controller.signal.aborted) { - providerQuotas = await getProviderQuotas(profiles, { - force: message.bool === true, - signal: controller.signal, - }) - } + const profiles = await loadProfiles(provider.providerSettingsManager, controller.signal) + providerQuotas = await getProviderQuotas(profiles, { + force: message.bool === true, + signal: controller.signal, + }) } catch (error) { providerQuotasFailed = true if (!controller.signal.aborted) { diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index c4ec2eacab..b8eaa2c394 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -304,6 +304,22 @@ describe("getGitHubCopilotQuota", () => { expect(good).toHaveBeenCalledTimes(1) }) + it("stops waiting for a session request that never settles when the caller goes away", async () => { + getSession.mockImplementationOnce(() => new Promise(() => {})) + const fetchSpy = vi.fn() + const controller = new AbortController() + + const result = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + await vi.waitFor(() => expect(getSession).toHaveBeenCalled()) + controller.abort() + + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(fetchSpy).not.toHaveBeenCalled() + // The stuck lookup is not left holding the slot: the next call starts over. + const good = vi.fn(async () => json(businessPayload)) + await expect(getGitHubCopilotQuota({}, good as never)).resolves.toMatchObject({ ok: true }) + }) + it("does not start the GitHub request when it was cancelled while the token was being fetched", async () => { let release: (session: { accessToken: string }) => void = () => {} getSession.mockImplementationOnce(() => new Promise((resolve) => (release = resolve))) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index b8e59b0895..e26c43e161 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1013,6 +1013,41 @@ describe("loadProfiles", () => { expect(await loadProfiles(source)).toEqual([{ apiProvider: "x" }]) }) + it("stops waiting for the profile list when the signal aborts, and reads no profile", async () => { + const source = { listConfig: vi.fn(() => new Promise(() => {})), getProfile: vi.fn() } + const controller = new AbortController() + const loading = loadProfiles(source, controller.signal) + controller.abort() + await expect(loading).rejects.toMatchObject({ name: "AbortError" }) + expect(source.getProfile).not.toHaveBeenCalled() + }) + + it("stops waiting for profile reads when the signal aborts", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "a" }]), + getProfile: vi.fn(() => new Promise(() => {})), + } + const controller = new AbortController() + const loading = loadProfiles(source, controller.signal) + await vi.waitFor(() => expect(source.getProfile).toHaveBeenCalled()) + controller.abort() + await expect(loading).rejects.toMatchObject({ name: "AbortError" }) + }) + + it("requests no profile once the signal has aborted while the list was loading", async () => { + let release: (entries: { id: string }[]) => void = () => {} + const source = { + listConfig: vi.fn(() => new Promise<{ id: string }[]>((resolve) => (release = resolve))), + getProfile: vi.fn(), + } + const controller = new AbortController() + const loading = loadProfiles(source, controller.signal) + controller.abort() + release([{ id: "a" }]) + await expect(loading).rejects.toMatchObject({ name: "AbortError" }) + expect(source.getProfile).not.toHaveBeenCalled() + }) + it("returns an empty list when there are no profiles", async () => { expect(await loadProfiles({ listConfig: async () => [], getProfile: vi.fn() })).toEqual([]) }) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index f7b5e4f188..5afd2226c2 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -68,10 +68,24 @@ export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCop } } -async function getGitHubToken(): Promise { +/** Rejects as soon as `signal` aborts. The pending work cannot be cancelled, but nobody waits for it any more. */ +function raceAbort(work: Promise, signal: AbortSignal): Promise { + return new Promise((resolve, reject) => { + const abort = () => reject(new DOMException("Aborted", "AbortError")) + if (signal.aborted) return abort() + signal.addEventListener("abort", abort, { once: true }) + work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) + }) +} + +/** Each lookup is bounded by `signal`, so a session request that never settles cannot hold the quota request open. */ +async function getGitHubToken(signal: AbortSignal): Promise { const { authProviderId, authScopeSets } = githubCopilotLanguageModel for (const scopes of authScopeSets) { - const session = await vscode.authentication.getSession(authProviderId, scopes, { silent: true }) + const session = await raceAbort( + Promise.resolve(vscode.authentication.getSession(authProviderId, scopes, { silent: true })), + signal, + ) if (session) return session.accessToken } return undefined @@ -116,7 +130,9 @@ export function getGitHubCopilotQuota( const controller = new AbortController() const fetchQuota = async (): Promise => { try { - const token = await getGitHubToken() + const token = await getGitHubToken( + AbortSignal.any([controller.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]), + ) if (controller.signal.aborted) return { ok: false, reason: "error", message: "Request cancelled" } if (!token) return { ok: false, reason: "signedOut" } diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index bf4ba6afc7..a75c14a5b6 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -552,10 +552,18 @@ export interface ProfileSource { * load is skipped rather than failing the whole refresh. Keys never leave the * extension host: only formatted quota rows are posted to the webview. */ -export async function loadProfiles(source: ProfileSource): Promise { - const entries = await source.listConfig() - const profiles = await Promise.all( - entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined)), +export async function loadProfiles(source: ProfileSource, signal?: AbortSignal): Promise { + // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but no further + // profile is requested once the caller has gone. + const entries = await until(source.listConfig(), signal, () => {}) + const profiles = await until( + Promise.all( + entries.map((entry) => + signal?.aborted ? undefined : source.getProfile({ id: entry.id }).catch(() => undefined), + ), + ), + signal, + () => {}, ) return profiles.filter((p): p is ProviderSettings => !!p) } From ed9657f253b4d1bd356d8a069abc9b83ee337cc9 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 14:06:12 -0700 Subject: [PATCH 15/76] fix(usage): compute metadata from the persisted (merged) messages; assert the same-id response payload --- src/core/task/Task.ts | 8 +++- src/core/task/__tests__/Task.spec.ts | 46 +++++++++++++++++++ ...bviewMessageHandler.providerQuotas.spec.ts | 12 ++++- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7b8e0ded28..a529edf083 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1768,8 +1768,12 @@ export class Task extends EventEmitter implements TaskLike { * to replace the stored messages outright. */ private async saveClineMessages(merge = true): Promise { + // What was actually persisted: with `merge`, newer messages another host wrote are folded in. The metadata + // (including the usage breakdown) is computed from it, so a stale local snapshot cannot clear usage that + // the stored messages still contain. + let persistedMessages: ClineMessage[] | undefined try { - await saveTaskMessages({ + persistedMessages = await saveTaskMessages({ messages: structuredClone(this.clineMessages), taskId: this.taskId, globalStoragePath: this.globalStoragePath, @@ -1790,7 +1794,7 @@ export class Task extends EventEmitter implements TaskLike { rootTaskId: this.rootTaskId, parentTaskId: this.parentTaskId, taskNumber: this.taskNumber, - messages: this.clineMessages, + messages: persistedMessages ?? this.clineMessages, globalStoragePath: this.globalStoragePath, workspace: this.cwd, mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode. diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index cee47a1fb8..9636b434b6 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -7654,6 +7654,52 @@ describe("Cline", () => { } }) + it("computes the usage breakdown from the merged persisted messages, not a stale local snapshot", async () => { + // Another host appended a request that records usage. This host's snapshot does not have that message; + // saving merges the stored one in, and the metadata must follow what was persisted. + const taskDir = path.join(os.tmpdir(), "test-storage", "tasks", "00000000-0000-7000-8000-000000000000") + fsReal.mkdirSync(taskDir, { recursive: true }) + // The mocked fs.readFile serves `mockMessages` as the stored ui_messages.json. + const originalStored = mockMessages.splice(0) + const historySpy = vi.spyOn(mockProvider, "updateTaskHistory") + try { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + const recorded = JSON.stringify({ + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + tokensIn: 10, + tokensOut: 5, + cost: 0.5, + }) + const message = (text: string, messageId: string) => ({ + ts: 2, + type: "say" as const, + say: "api_req_started" as const, + text, + messageId, + }) + const first = { ts: 1, type: "say" as const, say: "text" as const, text: "do it", messageId: "m1" } + mockMessages.push(first, message(recorded, "m2")) + + task.clineMessages = [first] + await getTaskTestAccess(task).saveClineMessages() + + const saved = historySpy.mock.calls.at(-1)?.[0] + expect(saved?.usageByModel).toEqual([ + expect.objectContaining({ apiProvider: providerIdentifiers.anthropic, requests: 1, cost: 0.5 }), + ]) + } finally { + mockMessages.splice(0, mockMessages.length, ...originalStored) + fsReal.rmSync(path.join(taskDir, "ui_messages.json"), { force: true }) + historySpy.mockRestore() + } + }) + it("finalizePartialToolAsk skips the webview update when the message write itself fails", async () => { // Complements the later-stage-failure test above by failing the first save // stage: with the real task directory removed, safeWriteJson's fs.access diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index a1c9387443..8c3ac1421b 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -279,12 +279,22 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { // The older one finishes and cleans up; it must not remove the newer one's entry. releases[0]([{ id: "a" }]) await older + // The older request is answered, with a successful (empty) result. + expect(provider.postMessageToWebview).toHaveBeenCalledTimes(1) + expect(provider.postMessageToWebview).toHaveBeenLastCalledWith({ + type: "providerQuotas", + providerQuotas: [], + providerQuotasFailed: false, + requestId: "same-id", + }) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "same-id" }) releases[1]([{ id: "a" }]) await newer - // The cancel reached the newer request, so only the older one was answered. + // The cancel reached the newer request, so it posts nothing and the count stays at one. expect(provider.postMessageToWebview).toHaveBeenCalledTimes(1) + expect(quotas.getProviderQuotas).toHaveBeenCalledTimes(1) }) it("marks the answer as failed when the quotas could not be loaded, and not when there are simply none", async () => { From f19d08a95edddea77e9c1984565f0b68fcb67beb Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 28 Oct 2026 14:19:43 -0700 Subject: [PATCH 16/76] refactor(usage): remove guards made unreachable by the abortable waits --- src/services/language-models/copilotQuota.ts | 3 +-- src/services/language-models/providerQuotas.ts | 10 +++------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index 5afd2226c2..c9df8aa15e 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -71,8 +71,8 @@ export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCop /** Rejects as soon as `signal` aborts. The pending work cannot be cancelled, but nobody waits for it any more. */ function raceAbort(work: Promise, signal: AbortSignal): Promise { return new Promise((resolve, reject) => { + // The signal is never aborted before this is called (it combines a fresh controller and a timeout). const abort = () => reject(new DOMException("Aborted", "AbortError")) - if (signal.aborted) return abort() signal.addEventListener("abort", abort, { once: true }) work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) }) @@ -133,7 +133,6 @@ export function getGitHubCopilotQuota( const token = await getGitHubToken( AbortSignal.any([controller.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]), ) - if (controller.signal.aborted) return { ok: false, reason: "error", message: "Request cancelled" } if (!token) return { ok: false, reason: "signedOut" } const response = await fetchImpl(COPILOT_USER_URL, { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index a75c14a5b6..362449533a 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -553,15 +553,11 @@ export interface ProfileSource { * extension host: only formatted quota rows are posted to the webview. */ export async function loadProfiles(source: ProfileSource, signal?: AbortSignal): Promise { - // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but no further - // profile is requested once the caller has gone. + // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but the profile reads + // are not started once the list wait has been abandoned. const entries = await until(source.listConfig(), signal, () => {}) const profiles = await until( - Promise.all( - entries.map((entry) => - signal?.aborted ? undefined : source.getProfile({ id: entry.id }).catch(() => undefined), - ), - ), + Promise.all(entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined))), signal, () => {}, ) From ab61e5500772b44b6e3caa684e7128abb0953e49 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 29 Oct 2026 15:33:53 -0700 Subject: [PATCH 17/76] test(usage): pin the cache lifetime and hostile base URLs; report a Copilot payload without premium data as an error --- .../__tests__/copilotQuota.spec.ts | 36 ++++++- .../__tests__/providerQuotas.spec.ts | 93 +++++++++++++++---- src/services/language-models/copilotQuota.ts | 6 +- 3 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index b8eaa2c394..64b196ff67 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) @@ -149,10 +149,17 @@ describe("getGitHubCopilotQuota", () => { expect(result).toEqual({ ok: false, reason: "error", message: "GitHub API error 500" }) }) - it("reports noCopilot when the payload has no premium snapshot", async () => { - const result = await getGitHubCopilotQuota({}, (async () => json({ quota_snapshots: {} })) as never) - expect(result).toMatchObject({ ok: false, reason: "noCopilot" }) - }) + it.each([ + ["an empty quota_snapshots", { quota_snapshots: {} }], + ["a renamed premium field", { copilot_plan: "pro", quota_snapshots: { premium_requests: {} } }], + ["no quota_snapshots", { copilot_plan: "pro" }], + ])( + "reports an error, not noCopilot, when a successful payload has no premium snapshot (%s)", + async (_name, body) => { + const result = await getGitHubCopilotQuota({}, (async () => json(body)) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "GitHub returned no premium request quota" }) + }, + ) it("turns network errors into a generic error result instead of throwing", async () => { const result = await getGitHubCopilotQuota({}, (async () => { @@ -185,6 +192,25 @@ describe("getGitHubCopilotQuota", () => { expect(fetchSpy).toHaveBeenCalledTimes(2) }) + describe("the cache lifetime", () => { + afterEach(() => vi.useRealTimers()) + + it.each([ + [59_999, 1], + [60_000, 2], + ])("serves a cached result %i ms after it was fetched with %i request(s) in total", async (age, requests) => { + vi.useFakeTimers() + vi.setSystemTime(1_000_000) + const fetchSpy = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, fetchSpy as never) + + vi.setSystemTime(1_000_000 + age) + await getGitHubCopilotQuota({}, fetchSpy as never) + + expect(fetchSpy).toHaveBeenCalledTimes(requests) + }) + }) + it("does not cache failures", async () => { const fail = vi.fn(async () => json({}, 500)) await getGitHubCopilotQuota({}, fail as never) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index e26c43e161..52a24b4045 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1,4 +1,4 @@ -import { beforeEach, describe, expect, it, vi } from "vitest" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { providerIdentifiers } from "@roo-code/types" @@ -672,25 +672,62 @@ describe("getProviderQuotas", () => { expect(q).toMatchObject({ ok: true }) }) - it("never sends a key to a custom base URL", async () => { - const fetchSpy = vi.fn(okFetch) - const [q] = await getProviderQuotas( - [ - { - apiProvider: providerIdentifiers.openrouter, - openRouterApiKey: "sk", - openRouterBaseUrl: "https://evil.example/api/v1", - } as never, - ], - {}, - fetchSpy, - ) - expect(q).toMatchObject({ ok: false, reason: "unsupported" }) - expect(fetchSpy).not.toHaveBeenCalled() + describe("never sends a key to a base URL that is not the provider's own host", () => { + const providers = [ + { + id: providerIdentifiers.openrouter, + host: "openrouter.ai", + own: "https://openrouter.ai/api/v1", + settings: { openRouterApiKey: "sk" }, + urlKey: "openRouterBaseUrl", + }, + { + id: providerIdentifiers.deepseek, + host: "api.deepseek.com", + own: "https://api.deepseek.com", + settings: { deepSeekApiKey: "sk" }, + urlKey: "deepSeekBaseUrl", + }, + { + id: providerIdentifiers.moonshot, + host: "api.moonshot.ai", + own: "https://api.moonshot.ai/v1", + settings: { moonshotApiKey: "sk" }, + urlKey: "moonshotBaseUrl", + }, + ] as const + + const hostile = (host: string) => [ + "https://evil.example/api/v1", + `https://${host}.evil.com`, + `https://user@${host}.evil.com`, + `http://${host}`, + `https://${host}:8443`, + ] + + const cases = providers.flatMap((p) => hostile(p.host).map((url) => [p.id, url, p] as const)) + + it.each(cases)("%s: %s", async (_id, url, p) => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [{ apiProvider: p.id, ...p.settings, [p.urlKey]: url } as never], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it.each(providers.map((p) => [p.id, p] as const))("%s: still queries its own host", async (_id, p) => { + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => okFetch()) + await getProviderQuotas([{ apiProvider: p.id, ...p.settings, [p.urlKey]: p.own } as never], {}, fetchSpy) + expect(fetchSpy).toHaveBeenCalled() + for (const [url] of fetchSpy.mock.calls) expect(String(url).startsWith(p.own)).toBe(true) + }) }) it("calls the provider's own host with the saved key", async () => { - const fetchSpy = vi.fn(okFetch) + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, _init?: RequestInit) => okFetch()) const [q] = await getProviderQuotas( [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-test" } as never], {}, @@ -975,6 +1012,28 @@ describe("getProviderQuotas", () => { expect(ok).toHaveBeenCalledTimes(1) }) + describe("the cache lifetime", () => { + afterEach(() => vi.useRealTimers()) + + it.each([ + [59_999, 1], + [60_000, 2], + ])("a result %i ms old gives %i request(s) in total", async (age, requests) => { + vi.useFakeTimers() + vi.setSystemTime(1_000_000) + const fetchSpy = vi.fn( + async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })), + ) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + await getProviderQuotas(profiles, {}, fetchSpy) + + vi.setSystemTime(1_000_000 + age) + await getProviderQuotas(profiles, {}, fetchSpy) + + expect(fetchSpy).toHaveBeenCalledTimes(requests) + }) + }) + it("caches results for the same set of providers unless forced", async () => { const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index c9df8aa15e..be59d1337b 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -151,7 +151,11 @@ export function getGitHubCopilotQuota( if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } const quota = parseCopilotQuota(await response.json()) - return quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } + // A 403 or 404 is how GitHub says there is no Copilot. A success without a premium snapshot is more likely + // a changed payload on this undocumented endpoint, and must not be shown as "no Copilot". + return quota + ? { ok: true, quota } + : { ok: false, reason: "error", message: "GitHub returned no premium request quota" } } catch (error) { return { ok: false, reason: "error", message: safeErrorMessage(error) } } From a6a594cd816135c36ff55d4099b754d340633243 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 29 Oct 2026 15:41:15 -0700 Subject: [PATCH 18/76] feat(usage): translate the names of fixed provider-limit windows --- packages/types/src/providers/vscode-llm.ts | 5 ++ .../__tests__/providerQuotas.spec.ts | 49 +++++++++++++++++++ .../language-models/providerQuotas.ts | 25 +++++++++- .../components/settings/ProviderLimits.tsx | 6 ++- .../__tests__/ProviderLimits.spec.tsx | 30 ++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 10 +++- webview-ui/src/i18n/locales/de/settings.json | 10 +++- webview-ui/src/i18n/locales/en/settings.json | 10 +++- webview-ui/src/i18n/locales/es/settings.json | 10 +++- webview-ui/src/i18n/locales/fr/settings.json | 10 +++- webview-ui/src/i18n/locales/hi/settings.json | 10 +++- webview-ui/src/i18n/locales/id/settings.json | 10 +++- webview-ui/src/i18n/locales/it/settings.json | 10 +++- webview-ui/src/i18n/locales/ja/settings.json | 10 +++- webview-ui/src/i18n/locales/ko/settings.json | 10 +++- webview-ui/src/i18n/locales/nl/settings.json | 10 +++- webview-ui/src/i18n/locales/pl/settings.json | 10 +++- .../src/i18n/locales/pt-BR/settings.json | 10 +++- webview-ui/src/i18n/locales/ru/settings.json | 10 +++- webview-ui/src/i18n/locales/tr/settings.json | 10 +++- webview-ui/src/i18n/locales/vi/settings.json | 10 +++- .../src/i18n/locales/zh-CN/settings.json | 10 +++- .../src/i18n/locales/zh-TW/settings.json | 10 +++- 23 files changed, 274 insertions(+), 21 deletions(-) diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index 91e74e54fd..e130cdbd9d 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -25,6 +25,11 @@ export interface ProviderQuotaWindow { id: string /** Pre-formatted primary figure, e.g. "39,389.3 / 140,000 used", "$12.40 left". */ valueLabel: string + /** + * Set when the label is just the name of a fixed window ("Weekly", "5-hour", "Unlimited"), so the webview can + * show it in the user's language. `valueLabel` stays as the English fallback. + */ + name?: { key: "weekly" | "daily" | "monthly" | "unlimited" | "hours" | "days"; count?: number } /** 0–100 when the provider reports a bounded allowance. */ usedPercent?: number /** Epoch ms. */ diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 52a24b4045..4d11ea9871 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1045,6 +1045,55 @@ describe("getProviderQuotas", () => { }) }) +describe("window names", () => { + beforeEach(() => resetProviderQuotaCache()) + + const quotaFrom = async (profile: Record, body: unknown) => { + const [q] = await getProviderQuotas( + [profile as never], + {}, + vi.fn(async () => new Response(JSON.stringify(body))), + ) + return q + } + + it("gives the fixed windows of OpenCode Go a structured name next to the English text", async () => { + const q = await quotaFrom( + { apiProvider: providerIdentifiers.opencodeGo, opencodeGoApiKey: "sk" }, + { + usage: { + rolling: { percent: 10, resetsAt: 1_800_000_000 }, + weekly: { percent: 20, resetsAt: 1_800_000_000 }, + monthly: { percent: 30, resetsAt: 1_800_000_000 }, + }, + }, + ) + expect(q).toMatchObject({ + ok: true, + windows: [ + { valueLabel: "5-hour", name: { key: "hours", count: 5 } }, + { valueLabel: "Weekly", name: { key: "weekly" } }, + { valueLabel: "Monthly", name: { key: "monthly" } }, + ], + }) + }) + + it("names a Copilot unlimited plan, and leaves a figure without a name", async () => { + const copilot = await import("../copilotQuota") + vi.mocked(copilot.getGitHubCopilotQuota).mockResolvedValueOnce({ + ok: true, + quota: { unlimited: true, resetAt: 7, fetchedAt: 1 }, + }) + const [unlimited] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], {}) + expect(unlimited).toMatchObject({ windows: [{ valueLabel: "Unlimited", name: { key: "unlimited" } }] }) + + resetProviderQuotaCache() + const [counted] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], {}) + expect(counted).toMatchObject({ ok: true, windows: [{ valueLabel: "39,389.3 / 140,000 used" }] }) + expect(counted.ok && counted.windows[0]).not.toHaveProperty("name") + }) +}) + describe("loadProfiles", () => { it("loads every saved profile through the manager, with keys", async () => { const source = { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 362449533a..342445d17e 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -86,6 +86,27 @@ export function windowLabel(seconds: number | undefined, fallback: string): stri return fallback } +const FIXED_WINDOW_NAMES: Record> = { + Weekly: { key: "weekly" }, + Daily: { key: "daily" }, + Monthly: { key: "monthly" }, + Unlimited: { key: "unlimited" }, +} + +/** The structured name of a window whose label is only its name, so it can be translated. Anything else is left as is. */ +function windowName(label: string): ProviderQuotaWindow["name"] { + const fixed = FIXED_WINDOW_NAMES[label] + if (fixed) return fixed + const rolling = /^(\d+)-(hour|day)$/.exec(label) + return rolling ? { key: rolling[2] === "hour" ? "hours" : "days", count: Number(rolling[1]) } : undefined +} + +const withNames = (windows: ProviderQuotaWindow[]): ProviderQuotaWindow[] => + windows.map((w) => { + const name = windowName(w.valueLabel) + return name ? { ...w, name } : w + }) + /** Kimi Code `GET /coding/v1/usages` → 5-hour / weekly / monthly usage. */ export function parseKimiCodeUsage(payload: unknown): ProviderQuotaWindow[] | undefined { const root = asRecord(payload) @@ -502,7 +523,7 @@ async function fetchKeyed( } if (!windows?.length && lastError) throw lastError return windows?.length - ? { providerId: provider.providerId, ok: true, windows, fetchedAt: now } + ? { providerId: provider.providerId, ok: true, windows: withNames(windows), fetchedAt: now } : { providerId: provider.providerId, ok: false, reason: "error", message: "No usage data", fetchedAt: now } } catch (error) { return { @@ -536,7 +557,7 @@ async function fetchCopilot(force: boolean, signal?: AbortSignal): Promise (

- {w.valueLabel} + + {w.name + ? t(`settings:usage.limits.window.${w.name.key}`, { n: w.name.count }) + : w.valueLabel} + {w.usedPercent !== undefined && ( {Math.round(w.usedPercent)}% diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index b11a6d17e9..1466ea1811 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -53,6 +53,36 @@ describe("ProviderLimits", () => { }) }) + describe("window names", () => { + const quota = (windows: Extract["windows"]): ProviderQuota => ({ + providerId: providerIdentifiers.zai, + ok: true, + windows, + fetchedAt: 1, + }) + + it.each([ + [{ key: "weekly" }, "settings:usage.limits.window.weekly"], + [{ key: "unlimited" }, "settings:usage.limits.window.unlimited"], + [{ key: "hours", count: 5 }, "settings:usage.limits.window.hours 5"], + [{ key: "days", count: 3 }, "settings:usage.limits.window.days 3"], + ] as const)("translates a window named %j", (name, expected) => { + render() + respond([quota([{ id: "w", valueLabel: "ENGLISH FALLBACK", name }])]) + + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.zai}`) + expect(row.textContent).toContain(expected) + expect(row.textContent).not.toContain("ENGLISH FALLBACK") + }) + + it("shows the host's text for a window without a fixed name", () => { + render() + respond([quota([{ id: "b", valueLabel: "$4.25 left" }])]) + + expect(screen.getByTestId(`provider-limit-${providerIdentifiers.zai}`).textContent).toContain("$4.25 left") + }) + }) + it("shows a loading row until the first response", () => { render() expect(screen.getByTestId("provider-limits")).toBeTruthy() diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index bb82add8fc..2f4f9f6730 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Sense quota de Copilot", "noKey": "Clau d'API absent o no vàlida", "unsupported": "URL base personalitzada — no comprovat", - "error": "No s'ha pogut carregar" + "error": "No s'ha pogut carregar", + "window": { + "weekly": "Setmanal", + "daily": "Diari", + "monthly": "Mensual", + "unlimited": "Il·limitat", + "hours": "{{n}} hores", + "days": "{{n}} dies" + } }, "byModel": "Per model", "notRecorded": "No registrat", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 7b2ba025ce..712aa2d30f 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Kein Copilot-Kontingent", "noKey": "API-Schlüssel fehlt oder ist ungültig", "unsupported": "Benutzerdefinierte Basis-URL – nicht geprüft", - "error": "Konnte nicht geladen werden" + "error": "Konnte nicht geladen werden", + "window": { + "weekly": "Wöchentlich", + "daily": "Täglich", + "monthly": "Monatlich", + "unlimited": "Unbegrenzt", + "hours": "{{n}} Stunden", + "days": "{{n}} Tage" + } }, "byModel": "Nach Modell", "notRecorded": "Nicht erfasst", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index ad2a3f87cf..860faa87e0 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "No Copilot quota", "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" + "error": "Couldn't load", + "window": { + "weekly": "Weekly", + "daily": "Daily", + "monthly": "Monthly", + "unlimited": "Unlimited", + "hours": "{{n}}-hour", + "days": "{{n}}-day" + } }, "byModel": "By model", "notRecorded": "Not recorded", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 474a5c2437..a4e5dc44c1 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Sin cuota de Copilot", "noKey": "Falta la clave de API o no es válida", "unsupported": "URL base personalizada: no comprobado", - "error": "No se pudo cargar" + "error": "No se pudo cargar", + "window": { + "weekly": "Semanal", + "daily": "Diario", + "monthly": "Mensual", + "unlimited": "Ilimitado", + "hours": "{{n}} horas", + "days": "{{n}} días" + } }, "byModel": "Por modelo", "notRecorded": "No registrado", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 2a8fe1e993..656e250050 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Aucun quota Copilot", "noKey": "Clé API manquante ou invalide", "unsupported": "URL de base personnalisée — non vérifiée", - "error": "Impossible de charger" + "error": "Impossible de charger", + "window": { + "weekly": "Hebdomadaire", + "daily": "Quotidien", + "monthly": "Mensuel", + "unlimited": "Illimité", + "hours": "{{n}} heures", + "days": "{{n}} jours" + } }, "byModel": "Par modèle", "notRecorded": "Non enregistré", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 738875d3b4..af07b366f1 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "कोई Copilot कोटा नहीं", "noKey": "API कुंजी अनुपलब्ध या अमान्य", "unsupported": "कस्टम बेस URL — जाँच नहीं की गई", - "error": "लोड नहीं हो सका" + "error": "लोड नहीं हो सका", + "window": { + "weekly": "साप्ताहिक", + "daily": "दैनिक", + "monthly": "मासिक", + "unlimited": "असीमित", + "hours": "{{n}} घंटे", + "days": "{{n}} दिन" + } }, "byModel": "मॉडल के अनुसार", "notRecorded": "दर्ज नहीं", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index c929956917..acfe059c97 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Tidak ada kuota Copilot", "noKey": "Kunci API hilang atau tidak valid", "unsupported": "URL dasar kustom — tidak diperiksa", - "error": "Tidak dapat dimuat" + "error": "Tidak dapat dimuat", + "window": { + "weekly": "Mingguan", + "daily": "Harian", + "monthly": "Bulanan", + "unlimited": "Tanpa batas", + "hours": "{{n}} jam", + "days": "{{n}} hari" + } }, "byModel": "Menurut model", "notRecorded": "Tidak dicatat", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 46a62a2ed7..3c658c7f48 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Nessuna quota Copilot", "noKey": "Chiave API mancante o non valida", "unsupported": "URL di base personalizzato — non controllato", - "error": "Impossibile caricare" + "error": "Impossibile caricare", + "window": { + "weekly": "Settimanale", + "daily": "Giornaliero", + "monthly": "Mensile", + "unlimited": "Illimitato", + "hours": "{{n}} ore", + "days": "{{n}} giorni" + } }, "byModel": "Per modello", "notRecorded": "Non registrato", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 8d3e18b157..4702c26c66 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Copilot のクォータなし", "noKey": "API キーがないか無効です", "unsupported": "カスタムベース URL — 確認していません", - "error": "読み込めませんでした" + "error": "読み込めませんでした", + "window": { + "weekly": "週間", + "daily": "日次", + "monthly": "月間", + "unlimited": "無制限", + "hours": "{{n}}時間", + "days": "{{n}}日" + } }, "byModel": "モデル別", "notRecorded": "記録なし", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 55adcdf557..19db9f45bc 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Copilot 할당량 없음", "noKey": "API 키가 없거나 올바르지 않음", "unsupported": "사용자 지정 기본 URL — 확인하지 않음", - "error": "불러올 수 없음" + "error": "불러올 수 없음", + "window": { + "weekly": "주간", + "daily": "일간", + "monthly": "월간", + "unlimited": "무제한", + "hours": "{{n}}시간", + "days": "{{n}}일" + } }, "byModel": "모델별", "notRecorded": "기록되지 않음", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index c29cfc42b5..df330c031d 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Geen Copilot-quotum", "noKey": "API-sleutel ontbreekt of is ongeldig", "unsupported": "Aangepaste basis-URL — niet gecontroleerd", - "error": "Kon niet laden" + "error": "Kon niet laden", + "window": { + "weekly": "Wekelijks", + "daily": "Dagelijks", + "monthly": "Maandelijks", + "unlimited": "Onbeperkt", + "hours": "{{n}} uur", + "days": "{{n}} dagen" + } }, "byModel": "Per model", "notRecorded": "Niet vastgelegd", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index f65684ed5b..908436ec20 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Brak limitu Copilot", "noKey": "Brak klucza API lub jest nieprawidłowy", "unsupported": "Niestandardowy bazowy URL — nie sprawdzono", - "error": "Nie udało się wczytać" + "error": "Nie udało się wczytać", + "window": { + "weekly": "Tygodniowo", + "daily": "Dziennie", + "monthly": "Miesięcznie", + "unlimited": "Bez limitu", + "hours": "{{n}} godz.", + "days": "{{n}} dni" + } }, "byModel": "Według modelu", "notRecorded": "Nie zapisano", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index ad47efbbe9..36947565a8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Sem cota do Copilot", "noKey": "Chave de API ausente ou inválida", "unsupported": "URL base personalizada — não verificada", - "error": "Não foi possível carregar" + "error": "Não foi possível carregar", + "window": { + "weekly": "Semanal", + "daily": "Diário", + "monthly": "Mensal", + "unlimited": "Ilimitado", + "hours": "{{n}} horas", + "days": "{{n}} dias" + } }, "byModel": "Por modelo", "notRecorded": "Não registrado", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a2ff469dbb..259921ab43 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Нет квоты Copilot", "noKey": "Ключ API отсутствует или недействителен", "unsupported": "Пользовательский базовый URL — не проверено", - "error": "Не удалось загрузить" + "error": "Не удалось загрузить", + "window": { + "weekly": "Еженедельно", + "daily": "Ежедневно", + "monthly": "Ежемесячно", + "unlimited": "Без ограничений", + "hours": "{{n}} ч", + "days": "{{n}} дн." + } }, "byModel": "По моделям", "notRecorded": "Не записано", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 257cafd6ec..0ab8ca6e38 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Copilot kotası yok", "noKey": "API anahtarı eksik veya geçersiz", "unsupported": "Özel temel URL — kontrol edilmedi", - "error": "Yüklenemedi" + "error": "Yüklenemedi", + "window": { + "weekly": "Haftalık", + "daily": "Günlük", + "monthly": "Aylık", + "unlimited": "Sınırsız", + "hours": "{{n}} saat", + "days": "{{n}} gün" + } }, "byModel": "Modele göre", "notRecorded": "Kaydedilmedi", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 55df86904e..a93b0ea60c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "Không có hạn mức Copilot", "noKey": "Thiếu khóa API hoặc không hợp lệ", "unsupported": "URL cơ sở tùy chỉnh — không kiểm tra", - "error": "Không thể tải" + "error": "Không thể tải", + "window": { + "weekly": "Hằng tuần", + "daily": "Hằng ngày", + "monthly": "Hằng tháng", + "unlimited": "Không giới hạn", + "hours": "{{n}} giờ", + "days": "{{n}} ngày" + } }, "byModel": "Theo mô hình", "notRecorded": "Chưa ghi nhận", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 81cc7bd614..bcbf165d7e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "无 Copilot 配额", "noKey": "API 密钥缺失或无效", "unsupported": "自定义基础 URL — 未检查", - "error": "无法加载" + "error": "无法加载", + "window": { + "weekly": "每周", + "daily": "每日", + "monthly": "每月", + "unlimited": "无限制", + "hours": "{{n}} 小时", + "days": "{{n}} 天" + } }, "byModel": "按模型", "notRecorded": "未记录", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 75b4d2aba6..5f70bdd4ba 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1278,7 +1278,15 @@ "noCopilot": "無 Copilot 配額", "noKey": "API 金鑰遺失或無效", "unsupported": "自訂基底 URL — 未檢查", - "error": "無法載入" + "error": "無法載入", + "window": { + "weekly": "每週", + "daily": "每日", + "monthly": "每月", + "unlimited": "無限制", + "hours": "{{n}} 小時", + "days": "{{n}} 天" + } }, "byModel": "依模型", "notRecorded": "未記錄", From 361be13ea3efe077aaa7378a3149714f45f93a76 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 29 Oct 2026 17:30:49 -0700 Subject: [PATCH 19/76] test(usage): cover the day-count window name --- .../language-models/__tests__/providerQuotas.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 4d11ea9871..0731cec4a5 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1078,6 +1078,14 @@ describe("window names", () => { }) }) + it("names a rolling window of whole days by its count", async () => { + const q = await quotaFrom( + { apiProvider: providerIdentifiers.zai, zaiApiKey: "sk" }, + { code: 200, data: { limits: [{ type: "TOKENS_LIMIT", unit: 6, number: 2, percentage: 10 }] } }, + ) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "14-day", name: { key: "days", count: 14 } }] }) + }) + it("names a Copilot unlimited plan, and leaves a figure without a name", async () => { const copilot = await import("../copilotQuota") vi.mocked(copilot.getGitHubCopilotQuota).mockResolvedValueOnce({ From dc9d128b1e94278d751744eac73cbb17e46931c5 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 30 Oct 2026 22:36:28 -0700 Subject: [PATCH 20/76] fix(usage): record the serving handler per request; OpenRouter total spend; active profile first; fail when no profile loads; name progress bars --- .../__tests__/usageByModel.spec.ts | 39 ++++++++++- src/core/task-persistence/usageByModel.ts | 29 ++++++++ src/core/task/Task.ts | 19 +++++- ...bviewMessageHandler.providerQuotas.spec.ts | 38 +++++++++++ src/core/webview/webviewMessageHandler.ts | 6 +- .../__tests__/providerQuotas.spec.ts | 52 +++++++++++++++ .../language-models/providerQuotas.ts | 35 +++++++--- .../components/settings/ProviderLimits.tsx | 66 ++++++++++--------- .../__tests__/ProviderLimits.spec.tsx | 28 ++++++++ 9 files changed, 269 insertions(+), 43 deletions(-) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index a30d074d36..fffe76a963 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest" import { providerIdentifiers, type ClineMessage } from "@roo-code/types" -import { resolveRecordedModelId, summarizeUsageByModel } from "../usageByModel" +import { bindRequestToHandler, resolveRecordedModelId, summarizeUsageByModel } from "../usageByModel" const req = (data: Record, ts = 1): ClineMessage => ({ ts, @@ -131,3 +131,40 @@ describe("resolveRecordedModelId", () => { expect(resolveRecordedModelId(42, "claude-sonnet-5")).toBe("claude-sonnet-5") }) }) + +describe("bindRequestToHandler", () => { + const placeholder = JSON.stringify({ apiProtocol: "openai", apiProvider: "old-provider", modelId: "old-model" }) + + it("records the provider of the handler that serves the request, over the one the placeholder was written with", () => { + const bound = JSON.parse( + bindRequestToHandler(placeholder, { + apiProvider: "new-provider", + configuredModelId: "new-model", + servedModelId: "served-model", + }), + ) + expect(bound).toEqual({ apiProtocol: "openai", apiProvider: "new-provider", modelId: "new-model" }) + }) + + it("records the model the handler reports when the profile names none", () => { + const bound = JSON.parse( + bindRequestToHandler(placeholder, { + apiProvider: "p", + configuredModelId: undefined, + servedModelId: "served-model", + }), + ) + expect(bound.modelId).toBe("served-model") + }) + + it.each([[undefined], [""], ["not json"]])("starts from nothing when the placeholder is %j", (text) => { + const bound = JSON.parse( + bindRequestToHandler(text as string | undefined, { + apiProvider: "p", + configuredModelId: "m", + servedModelId: undefined, + }), + ) + expect(bound).toEqual({ apiProvider: "p", modelId: "m" }) + }) +}) diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index ab2fd72324..cc33a4c0e4 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -59,3 +59,32 @@ export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEnt export function resolveRecordedModelId(recorded: unknown, served: string | undefined): string | undefined { return typeof recorded === "string" && recorded.length > 0 ? recorded : served } + +/** + * Binds a request's placeholder `api_req_started` to the handler that serves it. The placeholder is written before + * the rate-limit wait and the environment details, and a profile switch in that window replaces the handler, so the + * provider and model written then can differ from the handler whose tokens and cost are recorded at the end. Called + * once the serving handler is known. The configured model is kept when the profile names one; otherwise the model the + * handler reports is recorded. + */ +export function bindRequestToHandler( + text: string | undefined, + handler: { + apiProvider: string | undefined + configuredModelId: string | undefined + servedModelId: string | undefined + }, +): string { + let existing: Record = {} + try { + existing = JSON.parse(text || "{}") + } catch { + // An unreadable placeholder is replaced, not kept. + } + + return JSON.stringify({ + ...existing, + apiProvider: handler.apiProvider, + modelId: resolveRecordedModelId(handler.configuredModelId, handler.servedModelId), + }) +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 6f8ab64f1e..8bfb537889 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -124,7 +124,7 @@ import { saveTaskMessages, taskMetadata, } from "../task-persistence" -import { resolveRecordedModelId } from "../task-persistence/usageByModel" +import { bindRequestToHandler, resolveRecordedModelId } from "../task-persistence/usageByModel" import { getEnvironmentDetails } from "../environment/getEnvironmentDetails" import { checkContextWindowExceededError } from "../context/context-management/context-error-handling" import { @@ -4035,6 +4035,9 @@ export class Task extends EventEmitter implements TaskLike { let inputTokens = 0 let outputTokens = 0 let totalCost: number | undefined + // The model that served this request, captured when it was chosen: a later request replaces + // `cachedStreamingModel` while this one's usage is still being collected in the background. + let servedModelId: string | undefined // We can't use `api_req_finished` anymore since it's a unique case // where it could come after a streaming message (i.e. in the middle @@ -4077,7 +4080,7 @@ export class Task extends EventEmitter implements TaskLike { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ ...existingData, - modelId: resolveRecordedModelId(existingData.modelId, this.cachedStreamingModel?.id), + modelId: resolveRecordedModelId(existingData.modelId, servedModelId), tokensIn: costResult.totalInputTokens, tokensOut: costResult.totalOutputTokens, cacheWrites: cacheWriteTokens, @@ -4147,6 +4150,18 @@ export class Task extends EventEmitter implements TaskLike { // This is especially important for tools and background usage collection this.cachedStreamingModel = this.api.getModel() const streamModelInfo = this.cachedStreamingModel.info + servedModelId = this.cachedStreamingModel.id + + // The handler that serves this request is known now. The placeholder was written before the waits + // above, so bring its provider and model in line with this handler. + this.clineMessages[lastApiReqIndex].text = bindRequestToHandler( + this.clineMessages[lastApiReqIndex].text, + { + apiProvider: this.apiConfiguration.apiProvider, + configuredModelId: getModelId(this.apiConfiguration), + servedModelId, + }, + ) // Yields only if the first chunk is successful, otherwise will // allow the user to retry the request (most likely due to rate diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 8c3ac1421b..f4423755dc 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -23,6 +23,7 @@ const makeProvider = (overrides: Record = {}) => ({ postMessageToWebview: vi.fn(), log: vi.fn(), + contextProxy: { getValue: vi.fn((key: string) => (key === "currentApiConfigName" ? "default" : undefined)) }, providerSettingsManager: { listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), getProfile: vi.fn(async ({ id }: { id: string }) => ({ @@ -48,6 +49,43 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { ]) }) + it("puts the active profile first, so the quota row is the account the requests use", async () => { + const provider = makeProvider({ + contextProxy: { getValue: vi.fn(() => "personal") }, + providerSettingsManager: { + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + ]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: `key-${id}`, + })), + }, + }) + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect( + quotas.getProviderQuotas.mock.calls[0][0].map((p: { openRouterApiKey: string }) => p.openRouterApiKey), + ).toEqual(["key-b", "key-a"]) + }) + + it("reports a failure, not an empty list, when no saved profile can be read", async () => { + const provider = makeProvider({ + providerSettingsManager: { + listConfig: vi.fn(async () => [{ id: "a" }]), + getProfile: vi.fn(async () => { + throw new Error("corrupt secret storage") + }), + }, + }) + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "providerQuotas", providerQuotas: [], providerQuotasFailed: true }), + ) + }) + it("loads every saved profile and posts the formatted quotas back", async () => { const provider = makeProvider() await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 247c7f3951..cbbf66b8cf 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1599,7 +1599,11 @@ export const webviewMessageHandler = async ( try { const { getProviderQuotas, loadProfiles } = await import("../../services/language-models/providerQuotas") - const profiles = await loadProfiles(provider.providerSettingsManager, controller.signal) + const profiles = await loadProfiles( + provider.providerSettingsManager, + controller.signal, + provider.contextProxy.getValue("currentApiConfigName"), + ) providerQuotas = await getProviderQuotas(profiles, { force: message.bool === true, signal: controller.signal, diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 0731cec4a5..72279aee03 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -46,6 +46,18 @@ describe("parseOpenRouterKey", () => { ]) }) + it("labels the all-time usage as total spend, never as this month's", () => { + expect(parseOpenRouterKey({ data: { limit: null, usage: 42 } })).toEqual([ + { id: "spend", valueLabel: "$42.00 spent in total" }, + ]) + }) + + it("prefers the monthly figure when both are reported", () => { + expect(parseOpenRouterKey({ data: { usage: 42, usage_monthly: 3.2 } })).toEqual([ + { id: "spend", valueLabel: "$3.20 spent this month" }, + ]) + }) + it("returns undefined without data", () => { expect(parseOpenRouterKey({})).toBeUndefined() expect(parseOpenRouterKey({ data: {} })).toBeUndefined() @@ -1129,6 +1141,46 @@ describe("loadProfiles", () => { expect(await loadProfiles(source)).toEqual([{ apiProvider: "x" }]) }) + it("fails when none of the saved profiles can be read, instead of reporting no providers", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn(async () => { + throw new Error("corrupt secret storage") + }), + } + await expect(loadProfiles(source)).rejects.toThrow("None of the saved profiles could be read") + }) + + it("returns an empty list when there are no saved profiles", async () => { + const source = { listConfig: vi.fn(async () => []), getProfile: vi.fn() } + expect(await loadProfiles(source)).toEqual([]) + }) + + it("puts the active profile first, so a provider with several profiles shows the account in use", async () => { + const source = { + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + { id: "c", name: "spare" }, + ]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ apiProvider: "x", id }) as never), + } + expect((await loadProfiles(source, undefined, "personal")).map((p) => (p as { id: string }).id)).toEqual([ + "b", + "a", + "c", + ]) + expect((await loadProfiles(source)).map((p) => (p as { id: string }).id)).toEqual(["a", "b", "c"]) + }) + + it("keeps the list order when the active profile is not among the saved ones", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "a", name: "work" }]), + getProfile: vi.fn(async () => ({ apiProvider: "x" }) as never), + } + expect(await loadProfiles(source, undefined, "gone")).toEqual([{ apiProvider: "x" }]) + }) + it("stops waiting for the profile list when the signal aborts, and reads no profile", async () => { const source = { listConfig: vi.fn(() => new Promise(() => {})), getProfile: vi.fn() } const controller = new AbortController() diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 342445d17e..62de799359 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -50,8 +50,12 @@ export function parseOpenRouterKey(payload: unknown): ProviderQuotaWindow[] | un ] } - const usage = toNumber(data.usage_monthly) ?? toNumber(data.usage) - return usage === undefined ? undefined : [{ id: "spend", valueLabel: `${formatMoney(usage)} spent this month` }] + const monthly = toNumber(data.usage_monthly) + if (monthly !== undefined) return [{ id: "spend", valueLabel: `${formatMoney(monthly)} spent this month` }] + + // OpenRouter documents `usage` as all-time credit use, so it must not be shown as this month's spend. + const total = toNumber(data.usage) + return total === undefined ? undefined : [{ id: "spend", valueLabel: `${formatMoney(total)} spent in total` }] } /** DeepSeek `GET /user/balance` → prepaid balance (prefers a positive USD balance). */ @@ -564,25 +568,40 @@ async function fetchCopilot(force: boolean, signal?: AbortSignal): Promise + listConfig(): Promise<{ id: string; name?: string }[]> getProfile(params: { id: string }): Promise } /** * Loads full profiles (including API keys) host-side. A profile that fails to - * load is skipped rather than failing the whole refresh. Keys never leave the - * extension host: only formatted quota rows are posted to the webview. + * load is skipped rather than failing the whole refresh, but if none of the saved + * profiles can be read that is a failure, not "no connected providers". The active + * profile (`activeName`) comes first, so a provider that has several profiles is + * shown with the account the requests use. Keys never leave the extension host: + * only formatted quota rows are posted to the webview. */ -export async function loadProfiles(source: ProfileSource, signal?: AbortSignal): Promise { +export async function loadProfiles( + source: ProfileSource, + signal?: AbortSignal, + activeName?: string, +): Promise { // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but the profile reads // are not started once the list wait has been abandoned. - const entries = await until(source.listConfig(), signal, () => {}) + const listed = await until(source.listConfig(), signal, () => {}) + const entries = activeName + ? [ + ...listed.filter((entry) => entry.name === activeName), + ...listed.filter((entry) => entry.name !== activeName), + ] + : listed const profiles = await until( Promise.all(entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined))), signal, () => {}, ) - return profiles.filter((p): p is ProviderSettings => !!p) + const loaded = profiles.filter((p): p is ProviderSettings => !!p) + if (entries.length > 0 && loaded.length === 0) throw new Error("None of the saved profiles could be read") + return loaded } // Successful results only, per provider and URL set. A missing key, a sign-out or a diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index 31b4df6459..e392e6b465 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -167,40 +167,44 @@ const QuotaRow = ({
{quota.ok && - quota.windows.map((w) => ( -
-
- - {w.name - ? t(`settings:usage.limits.window.${w.name.key}`, { n: w.name.count }) - : w.valueLabel} - + quota.windows.map((w) => { + const windowText = w.name + ? t(`settings:usage.limits.window.${w.name.key}`, { n: w.name.count }) + : w.valueLabel + + return ( +
+
+ {windowText} + {w.usedPercent !== undefined && ( + + {Math.round(w.usedPercent)}% + + )} +
{w.usedPercent !== undefined && ( - - {Math.round(w.usedPercent)}% - +
+
+
+ )} + {w.resetAt && ( +
+ {t("settings:usage.limits.resets", { date: formatReset(w.resetAt) })} +
)}
- {w.usedPercent !== undefined && ( -
-
-
- )} - {w.resetAt && ( -
- {t("settings:usage.limits.resets", { date: formatReset(w.resetAt) })} -
- )} -
- ))} + ) + })}
) } diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index 1466ea1811..74f46d1a37 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -53,6 +53,34 @@ describe("ProviderLimits", () => { }) }) + describe("progress bars", () => { + it("names each bar with its provider and window, so two windows of one provider can be told apart", () => { + render() + respond([ + { + providerId: providerIdentifiers.zai, + ok: true, + fetchedAt: 1, + windows: [ + { id: "a", valueLabel: "5-hour", usedPercent: 40, name: { key: "hours", count: 5 } }, + { id: "b", valueLabel: "Weekly", usedPercent: 10, name: { key: "weekly" } }, + { id: "c", valueLabel: "$4.25 left" }, + ], + }, + ]) + + const bars = screen.getAllByRole("progressbar") + expect(bars).toHaveLength(2) + const labels = bars.map((bar) => bar.getAttribute("aria-label")) + expect(labels[0]).toContain("settings:usage.limits.window.hours 5") + expect(labels[1]).toContain("settings:usage.limits.window.weekly") + expect(new Set(labels).size).toBe(2) + const shown = screen.getByTestId(`provider-limit-${providerIdentifiers.zai}`).querySelector(".font-medium") + expect(shown?.textContent).toBeTruthy() + for (const label of labels) expect(label).toContain(shown?.textContent as string) + }) + }) + describe("window names", () => { const quota = (windows: Extract["windows"]): ProviderQuota => ({ providerId: providerIdentifiers.zai, From f616c703862dd6a2ff83b5be3938ef543934cc12 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 09:25:14 -0700 Subject: [PATCH 21/76] fix(lint): hold the served model id in an object, not a let that is assigned once --- src/core/task/Task.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8bfb537889..a2149d0003 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4037,7 +4037,7 @@ export class Task extends EventEmitter implements TaskLike { let totalCost: number | undefined // The model that served this request, captured when it was chosen: a later request replaces // `cachedStreamingModel` while this one's usage is still being collected in the background. - let servedModelId: string | undefined + const served: { modelId?: string } = {} // We can't use `api_req_finished` anymore since it's a unique case // where it could come after a streaming message (i.e. in the middle @@ -4080,7 +4080,7 @@ export class Task extends EventEmitter implements TaskLike { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ ...existingData, - modelId: resolveRecordedModelId(existingData.modelId, servedModelId), + modelId: resolveRecordedModelId(existingData.modelId, served.modelId), tokensIn: costResult.totalInputTokens, tokensOut: costResult.totalOutputTokens, cacheWrites: cacheWriteTokens, @@ -4150,7 +4150,7 @@ export class Task extends EventEmitter implements TaskLike { // This is especially important for tools and background usage collection this.cachedStreamingModel = this.api.getModel() const streamModelInfo = this.cachedStreamingModel.info - servedModelId = this.cachedStreamingModel.id + served.modelId = this.cachedStreamingModel.id // The handler that serves this request is known now. The placeholder was written before the waits // above, so bring its provider and model in line with this handler. @@ -4159,7 +4159,7 @@ export class Task extends EventEmitter implements TaskLike { { apiProvider: this.apiConfiguration.apiProvider, configuredModelId: getModelId(this.apiConfiguration), - servedModelId, + servedModelId: served.modelId, }, ) From 34335c44e0a43bdf13a55ce6f8e82a8eafe1758f Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 10:42:00 -0700 Subject: [PATCH 22/76] fix(usage): skip non-object stored requests; abort quota requests on dispose; MiniMax 5-hour window only with data --- .../__tests__/usageByModel.spec.ts | 16 ++++++++++ src/core/task-persistence/usageByModel.ts | 3 ++ src/core/webview/ClineProvider.ts | 5 +++- .../webview/__tests__/ClineProvider.spec.ts | 11 +++++++ ...bviewMessageHandler.providerQuotas.spec.ts | 30 ++++++++++++++++++- src/core/webview/webviewMessageHandler.ts | 6 ++++ .../__tests__/providerQuotas.spec.ts | 14 ++++++++- .../language-models/providerQuotas.ts | 18 +++++------ 8 files changed, 91 insertions(+), 12 deletions(-) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index fffe76a963..a75b761d9f 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -112,6 +112,22 @@ describe("summarizeUsageByModel", () => { }) }) +describe("summarizeUsageByModel with stored text that is not a request object", () => { + const msg = (text: string) => + ({ ts: 1, type: "say", say: "api_req_started", text }) as unknown as Parameters< + typeof summarizeUsageByModel + >[0][number] + + it.each(["null", "42", '"text"', "true", "[]"])("skips %s and keeps reading the other messages", (text) => { + const summary = summarizeUsageByModel([ + msg(text), + msg(JSON.stringify({ apiProvider: "p", modelId: "m", tokensIn: 5, tokensOut: 2, cost: 0.5 })), + ]) + + expect(summary).toEqual([{ apiProvider: "p", modelId: "m", requests: 1, tokensIn: 5, tokensOut: 2, cost: 0.5 }]) + }) +}) + describe("resolveRecordedModelId", () => { it("keeps a model that was recorded when the request started", () => { expect(resolveRecordedModelId("gpt-5", "gpt-5-mini")).toBe("gpt-5") diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index cc33a4c0e4..a747e806ab 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -25,6 +25,9 @@ export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEnt continue } + // Valid JSON that is not an object ("null", a number, a string) has no fields to read. + if (typeof parsed !== "object" || parsed === null) continue + // A placeholder (request not finished) has no token counts yet. if (typeof parsed.tokensIn !== "number" && typeof parsed.tokensOut !== "number") continue diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 39c6184d92..38fda05bec 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -116,7 +116,7 @@ import { ProviderSettingsManager } from "../config/ProviderSettingsManager" import { CustomModesManager } from "../config/CustomModesManager" import { PendingActionSettlementError, Task } from "../task/Task" -import { webviewMessageHandler } from "./webviewMessageHandler" +import { abortProviderQuotaRequests, webviewMessageHandler } from "./webviewMessageHandler" import type { WebviewFocusTracker } from "./WebviewFocusTracker" import type { ClineMessage, TodoItem } from "@roo-code/types" import { @@ -869,6 +869,9 @@ export class ClineProvider // hold the event loop after the provider is torn down. this.taskScheduler.cancelQueued() + // Provider-limit requests still running would keep fetching for a view that is gone. + abortProviderQuotaRequests(this) + // Clear all tasks from the stack. The first pop goes through evictCurrentTask() // so an active delegated child is marked interrupted before the extension shuts down, // rather than being left persisted as "active" across the reload. diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index 779313d385..4397b869c7 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -2119,6 +2119,17 @@ describe("ClineProvider", () => { expect(state.allowedReadFiles).toEqual([]) }) + test("dispose stops the provider-limit requests still in flight for this provider", async () => { + const abort = vi.spyOn(await import("../webviewMessageHandler"), "abortProviderQuotaRequests") + try { + await provider.dispose() + + expect(abort).toHaveBeenCalledWith(provider) + } finally { + abort.mockRestore() + } + }) + test("getState returns the saved allowed write files", async () => { await provider.contextProxy.setValue("allowedWriteFiles", ["notes.md"]) diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index f4423755dc..06afd18fd7 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -14,7 +14,7 @@ vi.mock("../../../services/language-models/providerQuotas", async (importOrigina getProviderQuotas: quotas.getProviderQuotas, })) -import { webviewMessageHandler } from "../webviewMessageHandler" +import { abortProviderQuotaRequests, webviewMessageHandler } from "../webviewMessageHandler" import type { ClineProvider } from "../ClineProvider" const SECRET = "sk-super-secret" @@ -86,6 +86,34 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { ) }) + describe("when the provider is disposed", () => { + it("aborts the requests in flight, and posts nothing for them", async () => { + let signal: AbortSignal | undefined + quotas.getProviderQuotas.mockImplementation( + (_profiles: unknown, options: { signal: AbortSignal }) => + new Promise((_resolve, reject) => { + signal = options.signal + options.signal.addEventListener("abort", () => + reject(new DOMException("Aborted", "AbortError")), + ) + }), + ) + const provider = makeProvider() + const handling = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "q1" }) + await vi.waitFor(() => expect(signal).toBeDefined()) + + abortProviderQuotaRequests(provider) + await handling + + expect(signal?.aborted).toBe(true) + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("does nothing for a provider with no requests in flight", () => { + expect(() => abortProviderQuotaRequests(makeProvider())).not.toThrow() + }) + }) + it("loads every saved profile and posts the formatted quotas back", async () => { const provider = makeProvider() await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index cbbf66b8cf..322955f28f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -137,6 +137,12 @@ const handlerFor = (apiConfiguration: ProviderSettings, provider: RouterName): A * request id alone would let one webview cancel, or overwrite, another's request. */ const pendingProviderQuotaRequests = new WeakMap>() +/** Stops the provider-limit requests still in flight for a provider, e.g. when it is disposed. */ +export const abortProviderQuotaRequests = (provider: ClineProvider) => { + for (const controller of pendingProviderQuotaRequests.get(provider)?.values() ?? []) controller.abort() + pendingProviderQuotaRequests.delete(provider) +} + const pendingQuotaFor = (provider: ClineProvider) => { let pending = pendingProviderQuotaRequests.get(provider) if (!pending) pendingProviderQuotaRequests.set(provider, (pending = new Map())) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 72279aee03..4adab5fb3d 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -350,12 +350,24 @@ describe("parser edge cases", () => { expect(byPercent?.[0].usedPercent).toBe(25) const first = parseMiniMaxRemains({ model_remains: [{ model_name: "only" }] }) - expect(first).toEqual([{ id: "5h", valueLabel: "5-hour", usedPercent: undefined, resetAt: undefined }]) + // A row with neither a figure nor a reset time has nothing to show. + expect(first).toBeUndefined() expect( parseMiniMaxRemains({ model_remains: [{ current_interval_remaining_percent: 5 }] })?.[0].usedPercent, ).toBe(95) }) + it("MiniMax: shows the 5-hour window with only a reset time, and with only a figure", () => { + expect( + parseMiniMaxRemains({ model_remains: [{ model_name: "MiniMax-M2", end_time: 1_800_000_000_000 }] }), + ).toEqual([{ id: "5h", valueLabel: "5-hour", usedPercent: undefined, resetAt: 1_800_000_000_000 }]) + expect( + parseMiniMaxRemains({ + model_remains: [{ model_name: "MiniMax-M2", current_interval_remaining_percent: 70 }], + }), + ).toEqual([{ id: "5h", valueLabel: "5-hour", usedPercent: 30, resetAt: undefined }]) + }) + it("MiniMax: skips a weekly window with no data and clamps an out-of-range percent", () => { const [interval, weekly] = parseMiniMaxRemains({ model_remains: [{ model_name: "MiniMax-M2", current_interval_remaining_percent: 150 }], diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 62de799359..b9282a94ad 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -235,14 +235,14 @@ export function parseMiniMaxRemains(payload: unknown, isTokenPlan = true): Provi return clampPercent((used / total) * 100) } - const windows: ProviderQuotaWindow[] = [ - { - id: "5h", - valueLabel: "5-hour", - usedPercent: usedPercentOf("interval"), - resetAt: toEpochMs(model.end_time), - }, - ] + const windows: ProviderQuotaWindow[] = [] + + // A window with neither a figure nor a reset time would show a label with no value. + const intervalPercent = usedPercentOf("interval") + const intervalReset = toEpochMs(model.end_time) + if (intervalPercent !== undefined || intervalReset !== undefined) { + windows.push({ id: "5h", valueLabel: "5-hour", usedPercent: intervalPercent, resetAt: intervalReset }) + } // Status 3 = the window does not apply to this plan tier. const weeklyApplies = toNumber(model.current_weekly_status) !== 3 @@ -255,7 +255,7 @@ export function parseMiniMaxRemains(payload: unknown, isTokenPlan = true): Provi resetAt: toEpochMs(model.weekly_end_time), }) } - return windows + return windows.length ? windows : undefined } /** NanoGPT `GET /api/subscription/v1/usage` → daily / weekly / monthly allowance. */ From a7b0743e264acbfb472243423ba9fd8891a1d780 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 10:44:25 -0700 Subject: [PATCH 23/76] fix(usage): query the active profile's own credentials; deadline for profile reads; look again when the webview regains focus --- ...bviewMessageHandler.providerQuotas.spec.ts | 22 +++ src/core/webview/webviewMessageHandler.ts | 5 +- .../__tests__/providerQuotas.spec.ts | 139 ++++++++++++++++++ .../language-models/providerQuotas.ts | 79 +++++++--- .../components/settings/ProviderLimits.tsx | 4 + .../__tests__/ProviderLimits.spec.tsx | 29 ++++ 6 files changed, 258 insertions(+), 20 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 06afd18fd7..78777b269c 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -70,6 +70,28 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { ).toEqual(["key-b", "key-a"]) }) + it("passes the active profile's id on, so its own credentials are the ones queried", async () => { + const provider = makeProvider({ + contextProxy: { getValue: vi.fn(() => "personal") }, + providerSettingsManager: { + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + ]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ + apiProvider: providerIdentifiers.openrouter, + id, + })), + }, + }) + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(quotas.getProviderQuotas).toHaveBeenCalledWith( + expect.any(Array), + expect.objectContaining({ activeProfileId: "b" }), + ) + }) + it("reports a failure, not an empty list, when no saved profile can be read", async () => { const provider = makeProvider({ providerSettingsManager: { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 322955f28f..1d96b8af8d 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1603,9 +1603,9 @@ export const webviewMessageHandler = async ( let providerQuotas: ProviderQuota[] = [] let providerQuotasFailed = false try { - const { getProviderQuotas, loadProfiles } = + const { getProviderQuotas, loadProfilesWithActive } = await import("../../services/language-models/providerQuotas") - const profiles = await loadProfiles( + const { profiles, activeProfileId } = await loadProfilesWithActive( provider.providerSettingsManager, controller.signal, provider.contextProxy.getValue("currentApiConfigName"), @@ -1613,6 +1613,7 @@ export const webviewMessageHandler = async ( providerQuotas = await getProviderQuotas(profiles, { force: message.bool === true, signal: controller.signal, + activeProfileId, }) } catch (error) { providerQuotasFailed = true diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 4adab5fb3d..7974c9d54a 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -21,6 +21,7 @@ vi.mock("../copilotQuota", () => ({ import { getProviderQuotas, loadProfiles, + loadProfilesWithActive, parseDeepSeekBalance, parseKimiCodeUsage, parseMiniMaxRemains, @@ -1069,6 +1070,86 @@ describe("getProviderQuotas", () => { }) }) +describe("the active profile decides which account is queried", () => { + beforeEach(() => resetProviderQuotaCache()) + + const okSpend = () => vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + + it("reports no key for an active profile without one, instead of using a sibling's key", async () => { + const fetchSpy = okSpend() + const [q] = await getProviderQuotas( + [ + { id: "active", apiProvider: providerIdentifiers.openrouter } as never, + { id: "other", apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sibling-key" } as never, + ], + { activeProfileId: "active" }, + fetchSpy, + ) + + expect(q).toMatchObject({ ok: false, reason: "noKey" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("reports a custom gateway as unsupported when the active profile uses one, not a sibling on the default host", async () => { + const fetchSpy = okSpend() + const [q] = await getProviderQuotas( + [ + { + id: "active", + apiProvider: providerIdentifiers.deepseek, + deepSeekApiKey: "k1", + deepSeekBaseUrl: "https://gateway.example/v1", + } as never, + { id: "other", apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "k2" } as never, + ], + { activeProfileId: "active" }, + fetchSpy, + ) + + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("queries with the active profile's key when a sibling also has one", async () => { + const fetchSpy = okSpend() + await getProviderQuotas( + [ + { id: "other", apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sibling-key" } as never, + { id: "active", apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "active-key" } as never, + ], + { activeProfileId: "active" }, + fetchSpy, + ) + + expect(fetchSpy).toHaveBeenCalledWith( + "https://openrouter.ai/api/v1/key", + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer active-key" }) }), + ) + }) + + it.each([ + ["there is no active profile", undefined], + ["the active profile is for another provider", "elsewhere"], + ["the active profile is not among the loaded ones", "gone"], + ])("keeps the usual choice of profile when %s", async (_name, activeProfileId) => { + const fetchSpy = okSpend() + await getProviderQuotas( + [ + { id: "elsewhere", apiProvider: providerIdentifiers.anthropic } as never, + { id: "keyless", apiProvider: providerIdentifiers.openrouter } as never, + { id: "keyed", apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "the-key" } as never, + ], + { activeProfileId }, + fetchSpy, + ) + + expect(fetchSpy).toHaveBeenCalledWith( + "https://openrouter.ai/api/v1/key", + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer the-key" }) }), + ) + }) +}) + describe("window names", () => { beforeEach(() => resetProviderQuotaCache()) @@ -1193,6 +1274,64 @@ describe("loadProfiles", () => { expect(await loadProfiles(source, undefined, "gone")).toEqual([{ apiProvider: "x" }]) }) + it("returns the id of the active profile with the profiles", async () => { + const source = { + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + ]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ apiProvider: "x", id }) as never), + } + + expect(await loadProfilesWithActive(source, undefined, "personal")).toMatchObject({ activeProfileId: "b" }) + expect((await loadProfilesWithActive(source, undefined, "gone")).activeProfileId).toBeUndefined() + expect((await loadProfilesWithActive(source)).activeProfileId).toBeUndefined() + }) + + describe("a profile read that never settles", () => { + afterEach(() => vi.useRealTimers()) + + it("is skipped after the deadline, so the profiles that were read still produce a response", async () => { + vi.useFakeTimers() + const source = { + listConfig: vi.fn(async () => [{ id: "ok" }, { id: "stuck" }]), + getProfile: vi.fn(({ id }: { id: string }) => + id === "stuck" ? new Promise(() => {}) : Promise.resolve({ apiProvider: "x" } as never), + ), + } + + const loading = loadProfiles(source) + await vi.advanceTimersByTimeAsync(10_000) + + expect(await loading).toEqual([{ apiProvider: "x" }]) + }) + + it("fails when every read is stuck, instead of waiting for ever", async () => { + vi.useFakeTimers() + const source = { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn(() => new Promise(() => {})), + } + + const loading = loadProfiles(source) + const settled = expect(loading).rejects.toThrow("None of the saved profiles could be read") + await vi.advanceTimersByTimeAsync(10_000) + + await settled + }) + + it("does not wait for the deadline when the reads finish", async () => { + vi.useFakeTimers() + const source = { + listConfig: vi.fn(async () => [{ id: "a" }]), + getProfile: vi.fn(async () => ({ apiProvider: "x" }) as never), + } + + expect(await loadProfiles(source)).toEqual([{ apiProvider: "x" }]) + expect(vi.getTimerCount()).toBe(0) + }) + }) + it("stops waiting for the profile list when the signal aborts, and reads no profile", async () => { const source = { listConfig: vi.fn(() => new Promise(() => {})), getProfile: vi.fn() } const controller = new AbortController() diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index b9282a94ad..052f9c9359 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -3,6 +3,7 @@ import { type ProviderQuota, type ProviderQuotaWindow, type ProviderSettings, + type ProviderSettingsWithId, } from "@roo-code/types" import { getGitHubCopilotQuota } from "./copilotQuota" @@ -485,7 +486,20 @@ type KeyedCredentials = ReturnType * one pointed at a custom gateway, which is only a fallback (reported as * unsupported when nothing better exists). */ -function pickCredentials(provider: KeyedProvider, profiles: ProviderSettings[]): KeyedCredentials | undefined { +function pickCredentials( + provider: KeyedProvider, + profiles: ProviderSettings[], + activeProfileId?: string, +): KeyedCredentials | undefined { + // The active profile of this provider is the account the requests use: take its credentials as they are, so a + // keyless one is reported as no key and a custom gateway as unsupported, not replaced by a sibling profile. + const active = activeProfileId + ? (profiles as ProviderSettingsWithId[]).find( + (p) => p.id === activeProfileId && p.apiProvider === provider.providerId, + ) + : undefined + if (active) return provider.credentials(active) + const withKey = profiles .filter((p) => p.apiProvider === provider.providerId) .map(provider.credentials) @@ -572,19 +586,37 @@ export interface ProfileSource { getProfile(params: { id: string }): Promise } +/** How long one profile read may take. A read that never settles is skipped, so the others still produce a response. */ +const PROFILE_READ_TIMEOUT_MS = 10_000 + +function readProfile(source: ProfileSource, id: string): Promise { + return new Promise((resolve) => { + const timer = setTimeout(() => resolve(undefined), PROFILE_READ_TIMEOUT_MS) + source.getProfile({ id }).then( + (profile) => { + clearTimeout(timer) + resolve(profile) + }, + () => { + clearTimeout(timer) + resolve(undefined) + }, + ) + }) +} + /** - * Loads full profiles (including API keys) host-side. A profile that fails to - * load is skipped rather than failing the whole refresh, but if none of the saved - * profiles can be read that is a failure, not "no connected providers". The active - * profile (`activeName`) comes first, so a provider that has several profiles is - * shown with the account the requests use. Keys never leave the extension host: - * only formatted quota rows are posted to the webview. + * Loads full profiles (including API keys) host-side. A profile that fails to load, or takes too long, is skipped + * rather than failing the whole refresh, but if none of the saved profiles can be read that is a failure, not "no + * connected providers". The active profile (`activeName`) comes first, and its id is returned so that its own + * credentials are the ones queried for its provider. Keys never leave the extension host: only formatted quota rows + * are posted to the webview. */ -export async function loadProfiles( +export async function loadProfilesWithActive( source: ProfileSource, signal?: AbortSignal, activeName?: string, -): Promise { +): Promise<{ profiles: ProviderSettings[]; activeProfileId?: string }> { // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but the profile reads // are not started once the list wait has been abandoned. const listed = await until(source.listConfig(), signal, () => {}) @@ -594,14 +626,20 @@ export async function loadProfiles( ...listed.filter((entry) => entry.name !== activeName), ] : listed - const profiles = await until( - Promise.all(entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined))), - signal, - () => {}, - ) + const profiles = await until(Promise.all(entries.map((entry) => readProfile(source, entry.id))), signal, () => {}) const loaded = profiles.filter((p): p is ProviderSettings => !!p) if (entries.length > 0 && loaded.length === 0) throw new Error("None of the saved profiles could be read") - return loaded + + const activeEntry = activeName ? listed.find((entry) => entry.name === activeName) : undefined + return { profiles: loaded, activeProfileId: activeEntry?.id } +} + +export async function loadProfiles( + source: ProfileSource, + signal?: AbortSignal, + activeName?: string, +): Promise { + return (await loadProfilesWithActive(source, signal, activeName)).profiles } // Successful results only, per provider and URL set. A missing key, a sign-out or a @@ -643,8 +681,9 @@ function quotaForKeyedProvider( force: boolean, fetchImpl: Fetch, signal?: AbortSignal, + activeProfileId?: string, ): Promise { - const creds = pickCredentials(provider, profiles) + const creds = pickCredentials(provider, profiles, activeProfileId) const skipped = unqueryable(provider, creds) if (skipped) return Promise.resolve(skipped) @@ -701,7 +740,11 @@ function quotaForKeyedProvider( */ export async function getProviderQuotas( profiles: ProviderSettings[], - { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}, + { + force = false, + signal, + activeProfileId, + }: { force?: boolean; signal?: AbortSignal; activeProfileId?: string } = {}, fetchImpl: Fetch = fetch, ): Promise { const connected = new Set(profiles.map((p) => p.apiProvider).filter(Boolean) as string[]) @@ -710,7 +753,7 @@ export async function getProviderQuotas( if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force, signal)) for (const provider of KEYED_PROVIDERS) { if (connected.has(provider.providerId)) { - tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl, signal)) + tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl, signal, activeProfileId)) } } diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index e392e6b465..43254ab495 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -66,9 +66,13 @@ export const ProviderLimits = () => { setLoading(false) } window.addEventListener("message", onMessage) + // A profile saved in another webview does not reach this one, so look again when it regains focus. + const onFocus = () => request(false) + window.addEventListener("focus", onFocus) request(false) return () => { window.removeEventListener("message", onMessage) + window.removeEventListener("focus", onFocus) // Tell the host nobody is waiting any more so it can stop the profile load and any fetches. cancelLatest() } diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index 74f46d1a37..5b7157687f 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -53,6 +53,35 @@ describe("ProviderLimits", () => { }) }) + describe("when the webview regains focus", () => { + const requests = () => post.mock.calls.map((c) => c[0]).filter((m) => m.type === "requestProviderQuotas") + + it("asks again without forcing, so a profile saved elsewhere is picked up", () => { + render() + expect(requests()).toHaveLength(1) + + act(() => { + window.dispatchEvent(new Event("focus")) + }) + + expect(requests()).toHaveLength(2) + expect(requests()[1]).toMatchObject({ bool: false }) + expect(requests()[1].requestId).not.toBe(requests()[0].requestId) + }) + + it("stops listening once the section is gone", () => { + const { unmount } = render() + unmount() + post.mockClear() + + act(() => { + window.dispatchEvent(new Event("focus")) + }) + + expect(requests()).toHaveLength(0) + }) + }) + describe("progress bars", () => { it("names each bar with its provider and window, so two windows of one provider can be told apart", () => { render() From d66e03d7ced4410978329cb757be24251cca1f99 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 10:59:24 -0700 Subject: [PATCH 24/76] fix(ci): add the new export to the handler mock; drop the unused loadProfiles export --- .../webview/__tests__/ClineProvider.stale-view.spec.ts | 1 + .../language-models/__tests__/providerQuotas.spec.ts | 5 ++++- src/services/language-models/providerQuotas.ts | 8 -------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts b/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts index b570ceb4bf..48321acaa1 100644 --- a/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts @@ -132,6 +132,7 @@ vi.mock("../../../integrations/theme/getTheme", () => ({ vi.mock("../webviewMessageHandler", () => ({ webviewMessageHandler: vi.fn().mockResolvedValue(undefined), + abortProviderQuotaRequests: vi.fn(), })) type ProviderState = Awaited> diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 7974c9d54a..1622f8e63b 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -20,7 +20,6 @@ vi.mock("../copilotQuota", () => ({ import { getProviderQuotas, - loadProfiles, loadProfilesWithActive, parseDeepSeekBalance, parseKimiCodeUsage, @@ -1207,6 +1206,10 @@ describe("window names", () => { }) }) +/** The profiles only: most of these tests do not look at the active profile's id. */ +const loadProfiles = async (...args: Parameters) => + (await loadProfilesWithActive(...args)).profiles + describe("loadProfiles", () => { it("loads every saved profile through the manager, with keys", async () => { const source = { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 052f9c9359..1eef3e866b 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -634,14 +634,6 @@ export async function loadProfilesWithActive( return { profiles: loaded, activeProfileId: activeEntry?.id } } -export async function loadProfiles( - source: ProfileSource, - signal?: AbortSignal, - activeName?: string, -): Promise { - return (await loadProfilesWithActive(source, signal, activeName)).profiles -} - // Successful results only, per provider and URL set. A missing key, a sign-out or a // transient error is never pinned, so fixing it shows up on the next open instead of // after the TTL. An entry is reused only for the exact key it was fetched with, so From e544e9c65ea28e0e24136c60045778e27fe4e60a Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 13:09:39 -0700 Subject: [PATCH 25/76] fix(usage): keep another key's cache entry; abort on sidebar dispose; MiniMax-M rows with a percentage; deadline for the profile list --- src/core/webview/ClineProvider.ts | 2 + .../ClineProvider.stale-view.spec.ts | 16 +++++- .../__tests__/providerQuotas.spec.ts | 54 +++++++++++++++++++ .../language-models/providerQuotas.ts | 35 ++++++++++-- 4 files changed, 101 insertions(+), 6 deletions(-) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 38fda05bec..4230f47329 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1058,6 +1058,8 @@ export class ClineProvider } else { this.log("Clearing webview resources for sidebar view") this.view = undefined + // The view that asked for provider limits is gone: stop fetching them. + abortProviderQuotaRequests(this) this.clearWebviewResources() } }, diff --git a/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts b/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts index 48321acaa1..04c46232bd 100644 --- a/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.stale-view.spec.ts @@ -9,7 +9,7 @@ import type { ExtensionMessage, WebviewMessage } from "@roo-code/types" import { ContextProxy } from "../../config/ContextProxy" import { ClineProvider } from "../ClineProvider" -import { webviewMessageHandler } from "../webviewMessageHandler" +import { abortProviderQuotaRequests, webviewMessageHandler } from "../webviewMessageHandler" // Mock setup vi.mock("fs/promises", () => ({ @@ -415,9 +415,11 @@ describe("ClineProvider - stale view lifecycle", () => { expect(first.postMessage).not.toHaveBeenCalled() expect(second.postMessage).not.toHaveBeenCalled() - // A stale dispose must not clear the current view's listeners. + // A stale dispose must not clear the current view's listeners, nor stop the requests of the current view. + vi.mocked(abortProviderQuotaRequests).mockClear() first.disposed.fire() expect(provider["view"]).toBe(second.view) + expect(abortProviderQuotaRequests).not.toHaveBeenCalled() second.visibility.fire() await flushAsync() expect(second.postMessage).toHaveBeenCalledWith({ type: "action", action: "didBecomeVisible" }) @@ -510,6 +512,16 @@ describe("ClineProvider - stale view lifecycle", () => { expect(ClineProvider.getAllInstances()).toContain(provider) }) + test("disposing the sidebar view stops the provider-limit requests it started", async () => { + const first = createSidebarView() + await provider.resolveWebviewView(first.view) + vi.mocked(abortProviderQuotaRequests).mockClear() + + first.disposed.fire() + + expect(abortProviderQuotaRequests).toHaveBeenCalledWith(provider) + }) + test("the current view keeps dispatching messages, visibility, and theme refreshes", async () => { const first = createSidebarView() await provider.resolveWebviewView(first.view) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 1622f8e63b..65cbdf4d3c 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -357,6 +357,16 @@ describe("parser edge cases", () => { ).toBe(95) }) + it("MiniMax: prefers a MiniMax-M row that reports a percentage over a chat row", () => { + const windows = parseMiniMaxRemains({ + model_remains: [ + { model_name: "chat", current_interval_remaining_percent: 10 }, + { model_name: "MiniMax-M2", current_interval_remaining_percent: 80 }, + ], + }) + expect(windows?.[0].usedPercent).toBe(20) // the M row's 80% left, not the chat row's 10% left + }) + it("MiniMax: shows the 5-hour window with only a reset time, and with only a figure", () => { expect( parseMiniMaxRemains({ model_remains: [{ model_name: "MiniMax-M2", end_time: 1_800_000_000_000 }] }), @@ -1058,6 +1068,25 @@ describe("getProviderQuotas", () => { }) }) + it("a failed request for one key does not remove the cached success of another key", async () => { + const ok = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + const fetchSpy = vi.fn(async (_url: RequestInfo | URL, init?: RequestInit) => + String((init?.headers as Record)?.Authorization).includes("sk-two") + ? new Response("", { status: 500 }) + : ok(), + ) + const one = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never] + const two = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-two" } as never] + + await getProviderQuotas(one, {}, fetchSpy) // cached for sk-one + const [failed] = await getProviderQuotas(two, {}, fetchSpy) // fails for sk-two + expect(failed).toMatchObject({ ok: false }) + const [again] = await getProviderQuotas(one, {}, fetchSpy) + + expect(again).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(2) // the third call came from the cache + }) + it("caches results for the same set of providers unless forced", async () => { const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] @@ -1323,6 +1352,31 @@ describe("loadProfiles", () => { await settled }) + it("fails when the list of saved profiles never arrives, instead of waiting for ever", async () => { + vi.useFakeTimers() + const source = { listConfig: vi.fn(() => new Promise(() => {})), getProfile: vi.fn() } + + const loading = loadProfiles(source) + const settled = expect(loading).rejects.toThrow("Reading the saved profiles timed out") + await vi.advanceTimersByTimeAsync(10_000) + + await settled + expect(source.getProfile).not.toHaveBeenCalled() + }) + + it("passes a failure of the list read on as it is, and clears its timer", async () => { + vi.useFakeTimers() + const source = { + listConfig: vi.fn(async () => { + throw new Error("storage unavailable") + }), + getProfile: vi.fn(), + } + + await expect(loadProfiles(source)).rejects.toThrow("storage unavailable") + expect(vi.getTimerCount()).toBe(0) + }) + it("does not wait for the deadline when the reads finish", async () => { vi.useFakeTimers() const source = { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 1eef3e866b..40994378e5 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -219,7 +219,12 @@ export function parseMiniMaxRemains(payload: unknown, isTokenPlan = true): Provi const rows = remains.map(asRecord).filter((r): r is Record => !!r) const name = (r: Record) => (typeof r.model_name === "string" ? r.model_name : "") const model = - rows.find((r) => /^minimax-m/i.test(name(r)) && (toNumber(r.current_interval_total_count) ?? 0) > 0) ?? + rows.find( + (r) => + /^minimax-m/i.test(name(r)) && + ((toNumber(r.current_interval_total_count) ?? 0) > 0 || + typeof r.current_interval_remaining_percent === "number"), + ) ?? rows.find((r) => ["general", "chat", "text"].includes(name(r).toLowerCase())) ?? rows.find((r) => typeof r.current_interval_remaining_percent === "number") ?? rows[0] @@ -589,6 +594,23 @@ export interface ProfileSource { /** How long one profile read may take. A read that never settles is skipped, so the others still produce a response. */ const PROFILE_READ_TIMEOUT_MS = 10_000 +/** Rejects with `message` if `work` has not settled within `ms`. */ +function withDeadline(work: Promise, ms: number, message: string): Promise { + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(message)), ms) + work.then( + (value) => { + clearTimeout(timer) + resolve(value) + }, + (error: unknown) => { + clearTimeout(timer) + reject(error) + }, + ) + }) +} + function readProfile(source: ProfileSource, id: string): Promise { return new Promise((resolve) => { const timer = setTimeout(() => resolve(undefined), PROFILE_READ_TIMEOUT_MS) @@ -619,7 +641,11 @@ export async function loadProfilesWithActive( ): Promise<{ profiles: ProviderSettings[]; activeProfileId?: string }> { // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but the profile reads // are not started once the list wait has been abandoned. - const listed = await until(source.listConfig(), signal, () => {}) + const listed = await until( + withDeadline(source.listConfig(), PROFILE_READ_TIMEOUT_MS, "Reading the saved profiles timed out"), + signal, + () => {}, + ) const entries = activeName ? [ ...listed.filter((entry) => entry.name === activeName), @@ -696,8 +722,9 @@ function quotaForKeyedProvider( if (!controller.signal.aborted) { if (result.ok) { cache.set(key, { at: Date.now(), apiKey, result }) - } else { - // A failed refresh must not leave an older success to be served as if it were current. + } else if (cache.get(key)?.apiKey === apiKey) { + // A failed refresh must not leave an older success of this key to be served as if it were + // current. A success cached for another key is not this request's to remove. cache.delete(key) } } From c27fe1d6031cc27f228047a10db871823a263f2c Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 15:53:34 -0700 Subject: [PATCH 26/76] fix(usage): inert CSV counters; an explicit usage clear wins over another host's; stop the Copilot scope lookup once cancelled; cover the Usage tab in SettingsView --- .../TaskHistoryStore.realConcurrency.spec.ts | 35 ++++++++++++ .../__tests__/taskStoreConcurrency.spec.ts | 37 ++++++++++++- .../task-persistence/taskStoreConcurrency.ts | 14 ++++- .../__tests__/copilotQuota.spec.ts | 35 ++++++++++++ src/services/language-models/copilotQuota.ts | 7 ++- .../settings/__tests__/SettingsView.spec.tsx | 45 ++++++++++++++++ webview-ui/src/utils/__tests__/usage.spec.ts | 54 +++++++++++++++++++ webview-ui/src/utils/usage.ts | 20 ++++--- 8 files changed, 237 insertions(+), 10 deletions(-) diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts index 54cc1075d5..92d03faeb3 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts @@ -152,6 +152,41 @@ describe("TaskHistoryStore real cross-host locking", () => { } }) + it("clears a usageByModel that another host wrote, when this host saves the task without one", async () => { + const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), "task-history-usage-clear-other-")) + const storeA = new TaskHistoryStore(storagePath) + const storeB = new TaskHistoryStore(storagePath) + const usageByModel = [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "m", + requests: 1, + tokensIn: 5, + tokensOut: 5, + cost: 1, + }, + ] + + try { + await storeA.initialize() + await storeA.upsert(item("shared-usage-task")) // A's cache has no usageByModel + await storeB.initialize() + await storeB.upsert({ ...item("shared-usage-task"), usageByModel }) // another host records usage on disk + + // This host saves the task after its messages were replaced: no usage, so none must remain on disk. + await storeA.upsert({ ...item("shared-usage-task"), usageByModel: undefined }) + + await storeA.invalidate("shared-usage-task") + expect(storeA.get("shared-usage-task")?.usageByModel).toBeUndefined() + await storeB.invalidate("shared-usage-task") + expect(storeB.get("shared-usage-task")?.usageByModel).toBeUndefined() + } finally { + storeA.dispose() + storeB.dispose() + await fs.rm(storagePath, { recursive: true, force: true }) + } + }) + it("reports a bounded error when one store never reaches the write barrier", async () => { const storagePath = await fs.mkdtemp(path.join(os.tmpdir(), "task-history-missed-barrier-")) const storeA = new TaskHistoryStore(storagePath) diff --git a/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts b/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts index 419ba03cb4..b9a173eefa 100644 --- a/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts @@ -1,6 +1,6 @@ import type { HistoryItem } from "@roo-code/types" -import { DeltaRejectedError, mergeHistoryDelta } from "../taskStoreConcurrency" +import { computeHistoryDelta, DeltaRejectedError, mergeHistoryDelta } from "../taskStoreConcurrency" function item(status: HistoryItem["status"]): HistoryItem { return { @@ -34,3 +34,38 @@ describe("mergeHistoryDelta", () => { expect(mergeHistoryDelta(disk, incoming, { id: disk.id, status: undefined }).status).toBe("active") }) }) + +describe("computeHistoryDelta", () => { + it("leaves out a field that equals the cached one", () => { + expect(computeHistoryDelta(item("active"), { mode: "code", tokensIn: 0 } as Partial)).toEqual({ + mode: "code", + }) + }) + + it("always carries an explicit undefined usageByModel, even when the cache has none either", () => { + const delta = computeHistoryDelta(item("active"), { usageByModel: undefined, tokensIn: 0 }) + + expect(Object.keys(delta)).toEqual(["usageByModel"]) + }) + + it("still carries a usageByModel that changed, and leaves out one that did not", () => { + const usage = [{ apiProvider: "p", modelId: "m", requests: 1, tokensIn: 1, tokensOut: 1, cost: 0 }] + const cached = { ...item("active"), usageByModel: usage } + + expect(computeHistoryDelta(cached, { usageByModel: usage })).toEqual({}) + expect(computeHistoryDelta(cached, { usageByModel: [] })).toEqual({ usageByModel: [] }) + }) + + it("does not treat an undefined of any other field as a deletion", () => { + expect(computeHistoryDelta(item("active"), { mode: undefined })).toEqual({}) + }) + + it("lets mergeHistoryDelta clear the usage another host wrote", () => { + const onDisk = { ...item("active"), usageByModel: [{ requests: 1 }] } + const delta = computeHistoryDelta(item("active"), { usageByModel: undefined }) + + const merged = mergeHistoryDelta(onDisk, item("active"), delta) + + expect(JSON.parse(JSON.stringify(merged)).usageByModel).toBeUndefined() + }) +}) diff --git a/src/core/task-persistence/taskStoreConcurrency.ts b/src/core/task-persistence/taskStoreConcurrency.ts index 0f747c4cb2..49ce70efc4 100644 --- a/src/core/task-persistence/taskStoreConcurrency.ts +++ b/src/core/task-persistence/taskStoreConcurrency.ts @@ -14,9 +14,21 @@ export class DeltaRejectedError extends Error { } } +/** + * Fields whose `undefined` is a decision, not an absence: "this task has none". The delta normally leaves out a field + * that equals the cached one, but the cache can lack a value that another host wrote to disk, so an unchanged + * `undefined` would leave that stale value in place. For these fields an explicit `undefined` is always part of the + * delta, and `mergeHistoryDelta` applies it under the file lock (the key is dropped when the item is written). + */ +const EXPLICIT_CLEAR_KEYS: ReadonlySet = new Set(["usageByModel"]) + export function computeHistoryDelta(cached: HistoryItem, incoming: Partial): Partial { return Object.fromEntries( - Object.entries(incoming).filter(([key, value]) => !deepEqual(value, (cached as Record)[key])), + Object.entries(incoming).filter( + ([key, value]) => + (value === undefined && EXPLICIT_CLEAR_KEYS.has(key)) || + !deepEqual(value, (cached as Record)[key]), + ), ) as Partial } diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index 64b196ff67..38a9ecf15f 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -287,6 +287,41 @@ describe("getGitHubCopilotQuota", () => { expect(signal()!.aborted).toBe(true) }) + it("does not look up the second scope once the caller has gone, after the first scope found no session", async () => { + const fetchSpy = vi.fn() + const controller = new AbortController() + getSession.mockReset() + // The first scope answers "no session", and the caller goes away just after that answer, before the + // lookup moves on to the second scope. + getSession.mockImplementationOnce(() => ({ + then(resolve: (value: undefined) => void) { + resolve(undefined) + queueMicrotask(() => controller.abort()) + }, + })) + + const result = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + await new Promise((resolve) => setTimeout(resolve, 0)) // let the lookup loop run on, if it is going to + expect(getSession).toHaveBeenCalledTimes(1) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("rejects a lookup that was cancelled before its session answered, with the cancellation", async () => { + const fetchSpy = vi.fn() + const controller = new AbortController() + getSession.mockReset() + getSession.mockImplementationOnce(() => new Promise(() => {})) // never answers + + const result = getGitHubCopilotQuota({ signal: controller.signal }, fetchSpy as never) + await vi.waitFor(() => expect(getSession).toHaveBeenCalledTimes(1)) + controller.abort() + + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + it("rejects at once with a signal that is already aborted, and makes no request", async () => { const fetchSpy = vi.fn() const controller = new AbortController() diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index be59d1337b..1194baae6e 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -70,9 +70,10 @@ export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCop /** Rejects as soon as `signal` aborts. The pending work cannot be cancelled, but nobody waits for it any more. */ function raceAbort(work: Promise, signal: AbortSignal): Promise { + // The caller has checked that `signal` is not aborted yet (an abort event does not fire again for a signal that + // already is), and nothing can abort it between that check and this call. return new Promise((resolve, reject) => { - // The signal is never aborted before this is called (it combines a fresh controller and a timeout). - const abort = () => reject(new DOMException("Aborted", "AbortError")) + const abort = () => reject(signal.reason) signal.addEventListener("abort", abort, { once: true }) work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) }) @@ -82,6 +83,8 @@ function raceAbort(work: Promise, signal: AbortSignal): Promise { async function getGitHubToken(signal: AbortSignal): Promise { const { authProviderId, authScopeSets } = githubCopilotLanguageModel for (const scopes of authScopeSets) { + // A lookup that has been cancelled is not started: the session request is made before it is raced. + signal.throwIfAborted() const session = await raceAbort( Promise.resolve(vscode.authentication.getSession(authProviderId, scopes, { silent: true })), signal, diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 0835f255f9..661c4a0d84 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -627,6 +627,51 @@ describe("SettingsView - Sound Settings", () => { }) }) +describe("SettingsView - Usage tab", () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it("renders the Usage tab content when it is the target section", () => { + renderWithExtensionState() + + const content = screen.getByTestId("settings-content") + expect(within(content).getByTestId("usage-empty")).toBeInTheDocument() + }) + + it("lists a Usage tab entry, and shows the usage content when it is clicked", () => { + renderWithExtensionState() + expect(within(screen.getByTestId("settings-content")).queryByTestId("usage-empty")).not.toBeInTheDocument() + + fireEvent.click(screen.getByTestId("tab-usage")) + + expect(within(screen.getByTestId("settings-content")).getByTestId("usage-empty")).toBeInTheDocument() + }) + + it("shows the totals and the filters when there is history", () => { + renderWithExtensionState(, { + state: { + taskHistory: [ + { + id: "t1", + number: 1, + ts: Date.now(), + task: "a task", + tokensIn: 100, + tokensOut: 50, + totalCost: 0.5, + }, + ], + }, + }) + + const content = screen.getByTestId("settings-content") + expect(within(content).getByTestId("usage-stats")).toBeInTheDocument() + expect(within(content).getByTestId("usage-export-csv")).toBeInTheDocument() + expect(within(content).queryByTestId("usage-empty")).not.toBeInTheDocument() + }) +}) + describe("SettingsView - API Configuration", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 7a8a9cfc43..e0b712bcf6 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -493,6 +493,60 @@ describe("usageToCsv formula safety", () => { }) }) +describe("usageToCsv numeric columns", () => { + const numericKeys = ["tokensIn", "tokensOut", "cacheReads", "cacheWrites", "totalCost"] as const + const cells = (overrides: Record) => + usageToCsv([item(overrides as never)], { range: "all", now: NOW }) + .split("\n")[1] + .split(",") + .slice(-5) + + it.each(numericKeys)("exports a formula in %s as 0, so no cell can be evaluated", (key) => { + for (const text of ['=HYPERLINK("http://x")', "+1+1", "-2+3", "@SUM(A1)", "\tcmd", "1;2"]) { + const row = cells({ [key]: text }) + const index = numericKeys.indexOf(key) + expect(row[index]).toBe(key === "totalCost" ? "0.0000" : "0") + } + }) + + it.each([[Number.NaN], [Number.POSITIVE_INFINITY], [Number.NEGATIVE_INFINITY], [-5], [null], [{}]])( + "exports %p as 0", + (value) => { + expect( + cells({ tokensIn: value, tokensOut: value, cacheReads: value, cacheWrites: value, totalCost: value }), + ).toEqual(["0", "0", "0", "0", "0.0000"]) + }, + ) + + it("keeps real counters as they are", () => { + expect(cells({ tokensIn: 10, tokensOut: 20, cacheReads: 3, cacheWrites: 4, totalCost: 1.5 })).toEqual([ + "10", + "20", + "3", + "4", + "1.5000", + ]) + }) + + it("leaves the date empty for a time that cannot be written, instead of throwing", () => { + for (const ts of [Number.NaN, Number.POSITIVE_INFINITY, 8.64e15 + 1]) { + const csv = usageToCsv([item({ task: "t", ts })], { range: "all", now: NOW }) + expect(csv.split("\n")[1].startsWith(',"t"')).toBe(true) + } + }) + + it("every cell of a row is digits, a quoted text with no leading trigger, or an ISO date", () => { + const hostile = '=cmd|" /C calc"!A0' + const row = usageToCsv( + [item({ task: hostile, mode: hostile, tokensIn: hostile as never, totalCost: hostile as never })], + { range: "all", now: NOW }, + ).split("\n")[1] + + expect(row).not.toMatch(/(^|,)[=+\-@\t\r]/) + expect(row).not.toMatch(/,"[=+\-@\t\r]/) + }) +}) + describe("usageToCsv", () => { it("sorts oldest first and leaves provider, model and counters blank or zero when nothing was recorded", () => { const csv = usageToCsv( diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index c87cde7825..c5156e6a54 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -325,6 +325,14 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin return `"${safe.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` } + // A counter that is not a finite, non-negative number (corrupted history, or text such as "=1+1" that a + // spreadsheet would evaluate) is exported as 0. What is written is then only ever digits, so every cell is inert. + const counter = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0) + const date = (ts: unknown) => + typeof ts === "number" && Number.isFinite(ts) && !Number.isNaN(new Date(ts).getTime()) + ? new Date(ts).toISOString() + : "" + const rows = history .filter( (item) => @@ -334,16 +342,16 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin .sort((a, b) => a.ts - b.ts) .map((item) => [ - new Date(item.ts).toISOString(), + date(item.ts), escape(item.title || item.task || ""), escape(item.mode || ""), escape(primaryUsage(item)?.apiProvider ?? ""), escape(primaryUsage(item)?.modelId ?? ""), - item.tokensIn || 0, - item.tokensOut || 0, - item.cacheReads || 0, - item.cacheWrites || 0, - (item.totalCost || 0).toFixed(4), + counter(item.tokensIn), + counter(item.tokensOut), + counter(item.cacheReads), + counter(item.cacheWrites), + counter(item.totalCost).toFixed(4), ].join(","), ) From e65ba256fec3acd2368da184956997b26d985ee1 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 16:26:13 -0700 Subject: [PATCH 27/76] fix(usage): one selection for the totals and the CSV; stronger assertions in the usage tests --- .../TaskHistoryStore.realConcurrency.spec.ts | 6 ++++ .../__tests__/taskStoreConcurrency.spec.ts | 14 ++++++++- .../settings/__tests__/SettingsView.spec.tsx | 8 ++++- webview-ui/src/utils/__tests__/usage.spec.ts | 18 +++++++++++ webview-ui/src/utils/usage.ts | 30 +++++++++++-------- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts index 92d03faeb3..03b1bd694c 100644 --- a/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/TaskHistoryStore.realConcurrency.spec.ts @@ -173,9 +173,15 @@ describe("TaskHistoryStore real cross-host locking", () => { await storeB.initialize() await storeB.upsert({ ...item("shared-usage-task"), usageByModel }) // another host records usage on disk + // The setup really happened: the usage is on disk, and this host's cache does not have it yet. + const file = path.join(storagePath, "tasks", "shared-usage-task", "history_item.json") + expect(JSON.parse(await fs.readFile(file, "utf8")).usageByModel).toEqual(usageByModel) + expect(storeA.get("shared-usage-task")?.usageByModel).toBeUndefined() + // This host saves the task after its messages were replaced: no usage, so none must remain on disk. await storeA.upsert({ ...item("shared-usage-task"), usageByModel: undefined }) + expect(JSON.parse(await fs.readFile(file, "utf8")).usageByModel).toBeUndefined() await storeA.invalidate("shared-usage-task") expect(storeA.get("shared-usage-task")?.usageByModel).toBeUndefined() await storeB.invalidate("shared-usage-task") diff --git a/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts b/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts index b9a173eefa..7966bb8e63 100644 --- a/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts +++ b/src/core/task-persistence/__tests__/taskStoreConcurrency.spec.ts @@ -56,10 +56,22 @@ describe("computeHistoryDelta", () => { expect(computeHistoryDelta(cached, { usageByModel: [] })).toEqual({ usageByModel: [] }) }) - it("does not treat an undefined of any other field as a deletion", () => { + it("omits an undefined other field when the cache has no value for it either", () => { expect(computeHistoryDelta(item("active"), { mode: undefined })).toEqual({}) }) + it("carries an undefined other field as an ordinary change when the cache has a value for it", () => { + const cached = { ...item("active"), mode: "code" } + + expect(computeHistoryDelta(cached, { mode: undefined })).toEqual({ mode: undefined }) + }) + + it("does not keep an undefined other field on every save: only usageByModel is a standing decision", () => { + const delta = computeHistoryDelta(item("active"), { mode: undefined, usageByModel: undefined }) + + expect(Object.keys(delta)).toEqual(["usageByModel"]) + }) + it("lets mergeHistoryDelta clear the usage another host wrote", () => { const onDisk = { ...item("active"), usageByModel: [{ requests: 1 }] } const delta = computeHistoryDelta(item("active"), { usageByModel: undefined }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 661c4a0d84..95bd82e513 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -666,7 +666,13 @@ describe("SettingsView - Usage tab", () => { }) const content = screen.getByTestId("settings-content") - expect(within(content).getByTestId("usage-stats")).toBeInTheDocument() + // The totals of the supplied task, not just an element that exists. + const stats = within(content).getByTestId("usage-stats").textContent ?? "" + expect(stats).toContain("$0.50") + expect(stats).toContain("150") // 100 tokens in + 50 out + // The filter controls and the export are there. + expect(within(content).getByTestId("usage-range-7d")).toBeInTheDocument() + expect(within(content).getByTestId("usage-range-all")).toBeInTheDocument() expect(within(content).getByTestId("usage-export-csv")).toBeInTheDocument() expect(within(content).queryByTestId("usage-empty")).not.toBeInTheDocument() }) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index e0b712bcf6..973068dc34 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -493,6 +493,24 @@ describe("usageToCsv formula safety", () => { }) }) +describe("usageToCsv and summarizeUsage count the same tasks", () => { + it("leaves out a task whose time is not a number, in the export as in the totals", () => { + const history = [ + item({ task: "good", ts: NOW, tokensIn: 7, totalCost: 1 }), + item({ task: "no time", ts: null as never, tokensIn: 99, totalCost: 50 }), + item({ task: "text time", ts: "yesterday" as never, tokensIn: 99, totalCost: 50 }), + ] + + const rows = usageToCsv(history, { range: "all", now: NOW }).split("\n").slice(1) + const summary = summarizeUsage(history, { range: "all", now: NOW }) + + expect(rows).toHaveLength(1) + expect(rows[0]).toContain('"good"') + expect(summary.taskCount).toBe(1) + expect(summary.totalCost).toBe(1) + }) +}) + describe("usageToCsv numeric columns", () => { const numericKeys = ["tokensIn", "tokensOut", "cacheReads", "cacheWrites", "totalCost"] as const const cells = (overrides: Record) => diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index c5156e6a54..2ee55755b0 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -176,17 +176,29 @@ function addTo( const byCostDesc = (a: UsageBucket, b: UsageBucket) => b.cost - a.cost || b.tokens - a.tokens -export function summarizeUsage(history: HistoryItem[], options: UsageOptions): UsageSummary { - const now = options.now ?? Date.now() - const start = rangeStart(options.range, now) - const topTaskLimit = options.topTaskLimit ?? 5 +/** + * The tasks the Usage tab counts for these options: the same selection for the totals and for the CSV, so an export + * never has a row the displayed totals leave out. An item whose time is not a number (a corrupted task file) is + * dropped. + */ +function itemsInScope(history: HistoryItem[], options: UsageOptions): HistoryItem[] { + const start = rangeStart(options.range, options.now ?? Date.now()) - const items = history.filter( + return history.filter( (item) => typeof item.ts === "number" && (start === undefined || item.ts >= start) && (!options.workspace || item.workspace === options.workspace), ) +} + +export function summarizeUsage(history: HistoryItem[], options: UsageOptions): UsageSummary { + const now = options.now ?? Date.now() + const start = rangeStart(options.range, now) + const topTaskLimit = options.topTaskLimit ?? 5 + + // The same `now` as above, so the range and the daily series agree on the day. + const items = itemsInScope(history, { ...options, now }) const providers = new Map() const models = new Map() @@ -304,7 +316,6 @@ export function modelIdFromKey(key: string): string | undefined { /** Serialises the filtered usage rows as CSV (one row per task). */ export function usageToCsv(history: HistoryItem[], options: UsageOptions): string { - const start = rangeStart(options.range, options.now ?? Date.now()) const header = [ "date", "task", @@ -333,12 +344,7 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin ? new Date(ts).toISOString() : "" - const rows = history - .filter( - (item) => - (start === undefined || item.ts >= start) && - (!options.workspace || item.workspace === options.workspace), - ) + const rows = itemsInScope(history, options) .sort((a, b) => a.ts - b.ts) .map((item) => [ From e5456851fd537cca7b0b6fa53523b56c57937b02 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sat, 31 Oct 2026 21:40:50 -0700 Subject: [PATCH 28/76] fix(usage): leave out times a date cannot represent; the cost-only remainder counts no task --- webview-ui/src/utils/__tests__/usage.spec.ts | 52 ++++++++++++++++++-- webview-ui/src/utils/usage.ts | 31 ++++++++---- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 973068dc34..0fe8db8d73 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -366,8 +366,29 @@ describe("spend that has no provider or model (context condensing)", () => { const s = summarizeUsage([item({ totalCost: 5, usageByModel: [entry(4)] })], { range: "all", now: NOW }) const unrecorded = s.byProvider.find((b) => b.key === UNRECORDED)! expect(unrecorded.tokens).toBe(0) + expect(unrecorded.cost).toBe(1) // The one task is counted under its real provider; the remainder is a cost line, not another task. expect(s.byProvider.find((b) => b.key === providerIdentifiers.anthropic)?.tasks).toBe(1) + expect(unrecorded.tasks).toBe(0) + // The same in the model rows. + const unrecordedModel = s.byModel.find((b) => b.key === UNRECORDED)! + expect(unrecordedModel.cost).toBe(1) + expect(unrecordedModel.tasks).toBe(0) + expect(unrecordedModel.tokens).toBe(0) + }) + + it("still counts a task under 'not recorded' when it has no usage record at all", () => { + const s = summarizeUsage([item({ totalCost: 2, tokensIn: 50 })], { range: "all", now: NOW }) + expect(s.byProvider.find((b) => b.key === UNRECORDED)?.tasks).toBe(1) + expect(s.byModel.find((b) => b.key === UNRECORDED)?.tasks).toBe(1) + }) + + it("counts a task under 'not recorded' once when a recorded request has no provider, plus the remainder", () => { + const noProvider = { requests: 1, tokensIn: 1, tokensOut: 1, cost: 1 } + const s = summarizeUsage([item({ totalCost: 3, usageByModel: [noProvider] })], { range: "all", now: NOW }) + const unrecorded = s.byProvider.find((b) => b.key === UNRECORDED)! + expect(unrecorded.tasks).toBe(1) + expect(unrecorded.cost).toBe(3) }) it("ignores float noise: no spurious 'not recorded' row when the costs agree", () => { @@ -511,6 +532,31 @@ describe("usageToCsv and summarizeUsage count the same tasks", () => { }) }) +describe("a time that Date cannot represent", () => { + it.each([1e20, -1e20, 8.64e15 + 1, Number.POSITIVE_INFINITY, Number.NaN])( + "is left out of the totals, the daily series and the export (ts = %s)", + (ts) => { + const history = [ + item({ task: "good", ts: NOW, tokensIn: 7, totalCost: 1 }), + item({ task: "bad", ts, totalCost: 50 }), + ] + + const summary = summarizeUsage(history, { range: "all", now: NOW }) + + expect(summary.taskCount).toBe(1) + expect(summary.totalCost).toBe(1) + expect(summary.daily.every((d) => Number.isFinite(d.day))).toBe(true) + expect(summary.daily).toHaveLength(1) + expect(usageToCsv(history, { range: "all", now: NOW }).split("\n")).toHaveLength(2) + }, + ) + + it("keeps the boundary times a date can represent", () => { + const summary = summarizeUsage([item({ task: "edge", ts: 8.64e15, totalCost: 1 })], { range: "all", now: NOW }) + expect(summary.taskCount).toBe(1) + }) +}) + describe("usageToCsv numeric columns", () => { const numericKeys = ["tokensIn", "tokensOut", "cacheReads", "cacheWrites", "totalCost"] as const const cells = (overrides: Record) => @@ -546,10 +592,10 @@ describe("usageToCsv numeric columns", () => { ]) }) - it("leaves the date empty for a time that cannot be written, instead of throwing", () => { - for (const ts of [Number.NaN, Number.POSITIVE_INFINITY, 8.64e15 + 1]) { + it("leaves out a task whose time cannot be written, instead of throwing or writing a blank date", () => { + for (const ts of [Number.NaN, Number.POSITIVE_INFINITY, 8.64e15 + 1, 1e20, -1e20]) { const csv = usageToCsv([item({ task: "t", ts })], { range: "all", now: NOW }) - expect(csv.split("\n")[1].startsWith(',"t"')).toBe(true) + expect(csv.split("\n")).toHaveLength(1) // the header only } }) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 2ee55755b0..e17a2607e4 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -161,13 +161,15 @@ function addTo( tokens: number, countedForTask: Set, extra?: Partial, + /** False for spend that belongs to a task already counted elsewhere (the cost-only remainder). */ + countTask = true, ) { const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0, priced: false, ...extra } bucket.cost += cost bucket.tokens += tokens if (cost > 0) bucket.priced = true // Count the task once per bucket even if several of its requests land here. - if (!countedForTask.has(key)) { + if (countTask && !countedForTask.has(key)) { countedForTask.add(key) bucket.tasks += 1 } @@ -176,17 +178,21 @@ function addTo( const byCostDesc = (a: UsageBucket, b: UsageBucket) => b.cost - a.cost || b.tokens - a.tokens +/** A time that `Date` can represent: a finite number inside the range of a date (±8.64e15 ms). */ +const isRepresentableTime = (ts: unknown): ts is number => + typeof ts === "number" && Number.isFinite(ts) && !Number.isNaN(new Date(ts).getTime()) + /** * The tasks the Usage tab counts for these options: the same selection for the totals and for the CSV, so an export - * never has a row the displayed totals leave out. An item whose time is not a number (a corrupted task file) is - * dropped. + * never has a row the displayed totals leave out. An item whose time is not a time a date can represent (a + * corrupted or hand-edited task file) is dropped. */ function itemsInScope(history: HistoryItem[], options: UsageOptions): HistoryItem[] { const start = rangeStart(options.range, options.now ?? Date.now()) return history.filter( (item) => - typeof item.ts === "number" && + isRepresentableTime(item.ts) && (start === undefined || item.ts >= start) && (!options.workspace || item.workspace === options.workspace), ) @@ -231,6 +237,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U const entries: Entry[] = item.usageByModel?.length ? [...item.usageByModel] : [{ requests: 0, tokensIn: tIn, tokensOut: tOut, cost }] + let remainderCost = 0 // The task's cost also covers work that has no provider or model of its own (condensing the context). // Without this, the provider and model rows would add up to less than the headline total. The @@ -239,7 +246,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U if (item.usageByModel?.length) { const attributed = item.usageByModel.reduce((sum, e) => sum + (Number.isFinite(e.cost) ? e.cost : 0), 0) const remainder = cost - attributed - if (remainder > REMAINDER_EPSILON) entries.push({ requests: 0, tokensIn: 0, tokensOut: 0, cost: remainder }) + if (remainder > REMAINDER_EPSILON) remainderCost = remainder } for (const e of entries) { @@ -251,6 +258,13 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U addTo(models, modelKey, entryCost, entryTokens, seenModels, { apiProvider: e.apiProvider }) } + // The remainder is spend of a task whose recorded requests already count it under their own provider and + // model, so it adds cost to the "not recorded" rows and no task (and no tokens). + if (remainderCost > 0) { + addTo(providers, UNRECORDED, remainderCost, 0, seenProviders, undefined, false) + addTo(models, UNRECORDED, remainderCost, 0, seenModels, { apiProvider: undefined }, false) + } + const modeSeen = new Set() addTo(modes, item.mode || "unknown", cost, tokens, modeSeen) @@ -339,16 +353,13 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin // A counter that is not a finite, non-negative number (corrupted history, or text such as "=1+1" that a // spreadsheet would evaluate) is exported as 0. What is written is then only ever digits, so every cell is inert. const counter = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0) - const date = (ts: unknown) => - typeof ts === "number" && Number.isFinite(ts) && !Number.isNaN(new Date(ts).getTime()) - ? new Date(ts).toISOString() - : "" const rows = itemsInScope(history, options) .sort((a, b) => a.ts - b.ts) .map((item) => [ - date(item.ts), + // The selection above keeps only times a date can represent. + new Date(item.ts).toISOString(), escape(item.title || item.task || ""), escape(item.mode || ""), escape(primaryUsage(item)?.apiProvider ?? ""), From adf020464c28a7292e5061decd96d36944688f90 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sun, 1 Nov 2026 09:57:06 -0800 Subject: [PATCH 29/76] fix(usage): a bounded range ends with today; times whose local day is out of range are left out; neutralise a signed date cell --- webview-ui/src/utils/__tests__/usage.spec.ts | 90 +++++++++++++++++++- webview-ui/src/utils/usage.ts | 28 ++++-- 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 0fe8db8d73..b14cddeef3 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -551,9 +551,95 @@ describe("a time that Date cannot represent", () => { }, ) - it("keeps the boundary times a date can represent", () => { - const summary = summarizeUsage([item({ task: "edge", ts: 8.64e15, totalCost: 1 })], { range: "all", now: NOW }) + it("keeps the upper boundary time, and writes its date so that a spreadsheet cannot read it as a formula", () => { + const history = [item({ task: "edge", ts: 8.64e15, totalCost: 1 })] + const summary = summarizeUsage(history, { range: "all", now: NOW }) expect(summary.taskCount).toBe(1) + + // The ISO form of this date starts with "+" (+275760-09-13T00:00:00.000Z). + const row = usageToCsv(history, { range: "all", now: NOW }).split("\n")[1] + expect(row.startsWith("'+275760-09-13T00:00:00.000Z,")).toBe(true) + expect(row).not.toMatch(/^[=+\-@]/) + }) + + describe("in UTC, where the dates near the ends of the range keep their year", () => { + let originalTz: string | undefined + beforeEach(() => { + originalTz = process.env.TZ + process.env.TZ = "UTC" + }) + afterEach(() => { + if (originalTz === undefined) delete process.env.TZ + else process.env.TZ = originalTz + }) + + it("neutralises a date written with a negative year, and leaves an ordinary date alone", () => { + const ordinary = usageToCsv([item({ task: "t", ts: NOW })], { range: "all", now: NOW }).split("\n")[1] + expect(ordinary.startsWith("2026-10-14T")).toBe(true) + expect(ordinary).not.toMatch(/^'/) + + // A year before 0 is written with a leading "-": the earliest date is -271821-04-20T00:00:00.000Z. + const early = usageToCsv([item({ task: "t", ts: -8.64e15 + 24 * 3600 * 1000 })], { range: "all", now: NOW }) + expect(early.split("\n")[1].startsWith("'-271821-04-21T00:00:00.000Z,")).toBe(true) + }) + }) + + describe("in a time zone ahead of UTC, near the start of the range of dates", () => { + let originalTz: string | undefined + beforeEach(() => { + originalTz = process.env.TZ + process.env.TZ = "Pacific/Kiritimati" // UTC+14 + }) + afterEach(() => { + if (originalTz === undefined) delete process.env.TZ + else process.env.TZ = originalTz + }) + + it("leaves out a time whose local day starts before the range, so no day is NaN and the chart can format every day", () => { + const history = [ + item({ task: "early", ts: -8.64e15 + 3600 * 1000, totalCost: 7 }), // 1 h after the earliest date + item({ task: "ok", ts: NOW, totalCost: 1 }), + ] + + const summary = summarizeUsage(history, { range: "all", now: NOW }) + + expect(summary.daily.every((d) => Number.isFinite(d.day))).toBe(true) + expect(summary.taskCount).toBe(1) + expect(summary.totalCost).toBe(1) + expect(usageToCsv(history, { range: "all", now: NOW }).split("\n")).toHaveLength(2) + for (const d of summary.daily) expect(() => new Intl.DateTimeFormat("en").format(d.day)).not.toThrow() + }) + }) +}) + +describe("a task dated after today", () => { + const DAY = 24 * 3600 * 1000 + const history = () => [ + item({ task: "today", ts: NOW, tokensIn: 10, totalCost: 1 }), + item({ task: "later today", ts: NOW + 2 * 3600 * 1000, tokensIn: 10, totalCost: 2 }), + item({ task: "tomorrow", ts: NOW + DAY, tokensIn: 10, totalCost: 4 }), + item({ task: "next year", ts: NOW + 365 * DAY, tokensIn: 10, totalCost: 8 }), + ] + + it.each(["today", "7d", "30d"] as const)( + "is left out of the totals and the export for %s, so the chart adds up to the headline", + (range) => { + const summary = summarizeUsage(history(), { range, now: NOW }) + + // A task later on the same day is still today's. + expect(summary.taskCount).toBe(2) + expect(summary.totalCost).toBe(3) + expect(summary.daily.reduce((sum, d) => sum + d.cost, 0)).toBe(summary.totalCost) + expect(usageToCsv(history(), { range, now: NOW }).split("\n")).toHaveLength(3) + }, + ) + + it("is kept for all time, where the daily series has a bar for every day with spend", () => { + const summary = summarizeUsage(history(), { range: "all", now: NOW }) + + expect(summary.taskCount).toBe(4) + expect(summary.totalCost).toBe(15) + expect(summary.daily.reduce((sum, d) => sum + d.cost, 0)).toBe(15) }) }) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index e17a2607e4..c5750ea33d 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -178,9 +178,16 @@ function addTo( const byCostDesc = (a: UsageBucket, b: UsageBucket) => b.cost - a.cost || b.tokens - a.tokens -/** A time that `Date` can represent: a finite number inside the range of a date (±8.64e15 ms). */ +/** + * A time the Usage tab can place on a day: a finite number inside the range of a date (±8.64e15 ms) whose local day + * also starts inside that range. Near the ends of the range, in a time zone ahead of UTC, the start of the local day + * falls outside it and would key the task under NaN, which the chart then cannot format. + */ const isRepresentableTime = (ts: unknown): ts is number => - typeof ts === "number" && Number.isFinite(ts) && !Number.isNaN(new Date(ts).getTime()) + typeof ts === "number" && + Number.isFinite(ts) && + !Number.isNaN(new Date(ts).getTime()) && + !Number.isNaN(startOfLocalDay(ts)) /** * The tasks the Usage tab counts for these options: the same selection for the totals and for the CSV, so an export @@ -188,12 +195,17 @@ const isRepresentableTime = (ts: unknown): ts is number => * corrupted or hand-edited task file) is dropped. */ function itemsInScope(history: HistoryItem[], options: UsageOptions): HistoryItem[] { - const start = rangeStart(options.range, options.now ?? Date.now()) + const now = options.now ?? Date.now() + const start = rangeStart(options.range, now) + // A bounded range ends with today: its daily series has no bar after today, so a task dated on a later day would + // be in the headline total without being in the chart. "All time" has no such end and keeps every task. + const lastDay = start === undefined ? undefined : startOfLocalDay(now) return history.filter( (item) => isRepresentableTime(item.ts) && (start === undefined || item.ts >= start) && + (lastDay === undefined || startOfLocalDay(item.ts) <= lastDay) && (!options.workspace || item.workspace === options.workspace), ) } @@ -354,12 +366,18 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin // spreadsheet would evaluate) is exported as 0. What is written is then only ever digits, so every cell is inert. const counter = (value: unknown) => (typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : 0) + // The ISO form of a date near the ends of the range starts with a sign (+275760-09-13…), which a spreadsheet + // could read as a formula: it gets the same leading apostrophe as the text cells. + const dateCell = (ts: number) => { + const iso = new Date(ts).toISOString() + return /^[=+\-@]/.test(iso) ? `'${iso}` : iso + } + const rows = itemsInScope(history, options) .sort((a, b) => a.ts - b.ts) .map((item) => [ - // The selection above keeps only times a date can represent. - new Date(item.ts).toISOString(), + dateCell(item.ts), escape(item.title || item.task || ""), escape(item.mode || ""), escape(primaryUsage(item)?.apiProvider ?? ""), From f4d70a436818c304c97309b919cf73e7d335c03d Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sun, 1 Nov 2026 12:48:47 -0800 Subject: [PATCH 30/76] test(usage): build the ordinary-date fixture from a fixed UTC time --- webview-ui/src/utils/__tests__/usage.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index b14cddeef3..633c0a75ca 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -574,8 +574,10 @@ describe("a time that Date cannot represent", () => { }) it("neutralises a date written with a negative year, and leaves an ordinary date alone", () => { - const ordinary = usageToCsv([item({ task: "t", ts: NOW })], { range: "all", now: NOW }).split("\n")[1] - expect(ordinary.startsWith("2026-10-14T")).toBe(true) + // A fixed UTC time, not NOW (which is built in the machine's own zone, so its UTC date moves with it). + const fixed = Date.UTC(2026, 9, 14, 15, 0, 0) + const ordinary = usageToCsv([item({ task: "t", ts: fixed })], { range: "all", now: fixed }).split("\n")[1] + expect(ordinary.startsWith("2026-10-14T15:00:00.000Z,")).toBe(true) expect(ordinary).not.toMatch(/^'/) // A year before 0 is written with a leading "-": the earliest date is -271821-04-20T00:00:00.000Z. From 466964cd6a2b5c89e47c31baf2b2893c7a31b614 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 3 Nov 2026 06:52:05 -0800 Subject: [PATCH 31/76] fix(usage): the CSV export goes through the host's save dialog, like every other export --- packages/types/src/vscode-extension-host.ts | 1 + .../__tests__/webviewMessageHandler.spec.ts | 25 ++++++ src/core/webview/webviewMessageHandler.ts | 4 + .../misc/__tests__/export-usage-csv.spec.ts | 78 +++++++++++++++++++ src/integrations/misc/export-usage-csv.ts | 29 +++++++ .../src/components/settings/UsageSettings.tsx | 13 ++-- .../__tests__/UsageSettings.midnight.spec.tsx | 7 +- .../settings/__tests__/UsageSettings.spec.tsx | 15 ++-- 8 files changed, 155 insertions(+), 17 deletions(-) create mode 100644 src/integrations/misc/__tests__/export-usage-csv.spec.ts create mode 100644 src/integrations/misc/export-usage-csv.ts diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index ca66c2b1f5..6453c22d22 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -546,6 +546,7 @@ export interface WebviewMessage { | "renameTaskWithId" | "importSettings" | "exportSettings" + | "exportUsageCsv" | "resetState" | typeof RouterModelsMessageType.flushRouterModels | typeof RouterModelsMessageType.requestRouterModels diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 685a7b11c1..973454e2ce 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -773,6 +773,9 @@ describe("webviewMessageHandler - theme fixture probes", () => { }) import { t } from "../../../i18n" +import { exportUsageCsv } from "../../../integrations/misc/export-usage-csv" + +vi.mock("../../../integrations/misc/export-usage-csv", () => ({ exportUsageCsv: vi.fn() })) vi.mock("vscode", () => { const showInformationMessage = vi.fn() @@ -3053,3 +3056,25 @@ describe("webviewMessageHandler - renameTaskWithId", () => { expect(vi.mocked(mockClineProvider.renameTaskWithId).mock.calls).toEqual(expectedCalls) }) }) + +describe("exportUsageCsv", () => { + beforeEach(() => { + vi.mocked(exportUsageCsv).mockReset() + }) + + it("hands the CSV text and the range to the host export", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "exportUsageCsv", + text: "date,cost\n2026-06-01,1.2", + values: { range: "30d" }, + }) + + expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith("date,cost\n2026-06-01,1.2", "30d") + }) + + it("passes nothing for the range when the message has no values", async () => { + await webviewMessageHandler(mockClineProvider, { type: "exportUsageCsv", text: "a" }) + + expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith("a", undefined) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 974656a6e5..bb6ba03a8d 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -73,6 +73,7 @@ import { Terminal } from "../../integrations/terminal/Terminal" import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry" import { openFile } from "../../integrations/misc/open-file" import { openImage, saveImage } from "../../integrations/misc/image-handler" +import { exportUsageCsv } from "../../integrations/misc/export-usage-csv" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import { searchWorkspaceFiles } from "../../services/search/file-search" @@ -1137,6 +1138,9 @@ export const webviewMessageHandler = async ( contextProxy: provider.contextProxy, }) + break + case "exportUsageCsv": + await exportUsageCsv(message.text, message.values?.range) break case "resetState": await provider.resetState() diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts new file mode 100644 index 0000000000..9459024d0e --- /dev/null +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -0,0 +1,78 @@ +import * as vscode from "vscode" + +import { exportUsageCsv } from "../export-usage-csv" + +vi.mock("vscode", () => ({ + Uri: { file: vi.fn((fsPath: string) => ({ fsPath })) }, + workspace: { fs: { writeFile: vi.fn() } }, + window: { showSaveDialog: vi.fn(), showErrorMessage: vi.fn() }, +})) + +const showSaveDialog = vi.mocked(vscode.window.showSaveDialog) +const writeFile = vi.mocked(vscode.workspace.fs.writeFile) +const showErrorMessage = vi.mocked(vscode.window.showErrorMessage) + +describe("exportUsageCsv", () => { + beforeEach(() => { + vi.clearAllMocks() + writeFile.mockResolvedValue(undefined) + }) + + it("writes the CSV to the file the user chose", async () => { + const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri + showSaveDialog.mockResolvedValue(saveUri) + + const saved = await exportUsageCsv("date,cost\n2026-06-01,1.2", "7d") + + expect(saved).toBe(saveUri) + expect(writeFile).toHaveBeenCalledWith(saveUri, Buffer.from("date,cost\n2026-06-01,1.2", "utf-8")) + expect(showErrorMessage).not.toHaveBeenCalled() + }) + + it.each([ + ["today", "zoo-usage-today.csv"], + ["7d", "zoo-usage-7d.csv"], + ["30d", "zoo-usage-30d.csv"], + ["all", "zoo-usage-all.csv"], + ])("suggests %s as %s, in CSV files", async (range, fileName) => { + showSaveDialog.mockResolvedValue(undefined) + + await exportUsageCsv("a", range) + + const options = showSaveDialog.mock.calls[0][0]! + expect(options.defaultUri?.fsPath.endsWith(fileName)).toBe(true) + expect(options.filters).toEqual({ CSV: ["csv"] }) + }) + + it.each([["../../etc/passwd"], ["x.csv"], [""], [42], [undefined], [null]])( + "builds the file name from a known range only: %j gets the default", + async (range) => { + showSaveDialog.mockResolvedValue(undefined) + + await exportUsageCsv("a", range) + + expect(showSaveDialog.mock.calls[0][0]!.defaultUri?.fsPath.endsWith("zoo-usage-all.csv")).toBe(true) + }, + ) + + it("writes nothing if the user cancels the dialog", async () => { + showSaveDialog.mockResolvedValue(undefined) + + expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(writeFile).not.toHaveBeenCalled() + }) + + it.each([[undefined], [null], [""], [42], [{}]])("does nothing for the text %j", async (csv) => { + expect(await exportUsageCsv(csv, "all")).toBeUndefined() + expect(showSaveDialog).not.toHaveBeenCalled() + expect(writeFile).not.toHaveBeenCalled() + }) + + it("tells the user when the file cannot be written, and does not report a saved file", async () => { + showSaveDialog.mockResolvedValue({ fsPath: "/ro/file.csv" } as vscode.Uri) + writeFile.mockRejectedValue(new Error("EACCES: permission denied")) + + expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(showErrorMessage).toHaveBeenCalledWith("EACCES: permission denied") + }) +}) diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts new file mode 100644 index 0000000000..64fad70101 --- /dev/null +++ b/src/integrations/misc/export-usage-csv.ts @@ -0,0 +1,29 @@ +import * as os from "os" +import * as path from "path" +import * as vscode from "vscode" + +const USAGE_RANGES: readonly string[] = ["today", "7d", "30d", "all"] + +/** + * Saves the Usage tab's CSV where the user chooses. A webview cannot reliably download a file, so, like every other + * export, the webview sends the text and the host writes it. The file name is built here from a known range, never + * from the message. Resolves to the saved file, or `undefined` if the user cancelled or there was nothing to save. + */ +export async function exportUsageCsv(csv: unknown, range: unknown): Promise { + if (typeof csv !== "string" || csv.length === 0) return undefined + + const name = `zoo-usage-${typeof range === "string" && USAGE_RANGES.includes(range) ? range : "all"}.csv` + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", name)), + filters: { CSV: ["csv"] }, + }) + if (!saveUri) return undefined + + try { + await vscode.workspace.fs.writeFile(saveUri, Buffer.from(csv, "utf-8")) + } catch (error) { + vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)) + return undefined + } + return saveUri +} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 543bfcff7f..c7f2cf8d80 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -100,14 +100,13 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes { - const csv = usageToCsv(history, { range, workspace, now }) - const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" })) - const a = document.createElement("a") - a.href = url - a.download = `zoo-usage-${range}.csv` - a.click() - URL.revokeObjectURL(url) + vscode.postMessage({ + type: "exportUsageCsv", + text: usageToCsv(history, { range, workspace, now }), + values: { range }, + }) } return ( diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx index f688f68287..b97da248b2 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx @@ -82,9 +82,6 @@ describe("UsageSettings – across local midnight", () => { it("keeps the view and the export on the same day, even if the clock moves past midnight first", () => { render() fireEvent.click(screen.getByTestId("usage-range-today")) - Object.assign(URL, { createObjectURL: vi.fn(() => "blob:x"), revokeObjectURL: vi.fn() }) - const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) - // The wall clock passes midnight but the roll-over timer has not fired yet: the view still shows the // old day, so Export must use that same day rather than a fresh Date.now(). vi.setSystemTime(BEFORE_MIDNIGHT + 20 * MINUTE) @@ -92,7 +89,9 @@ describe("UsageSettings – across local midnight", () => { expect(csvSpy).toHaveBeenCalledTimes(1) expect(csvSpy.mock.calls[0][1]).toMatchObject({ range: "today", now: BEFORE_MIDNIGHT }) - click.mockRestore() + expect(post).toHaveBeenCalledWith( + expect.objectContaining({ type: "exportUsageCsv", values: { range: "today" } }), + ) }) it("does not leave a timer running after unmount", () => { diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx index 4c602677eb..53f5591f30 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -240,19 +240,22 @@ describe("UsageSettings", () => { expect(post).toHaveBeenCalledWith({ type: "showTaskWithId", text: t.id }) }) - it("exports a CSV of the filtered range", () => { + it("asks the host to save a CSV of the filtered range, and does not download it itself", () => { state.taskHistory = [priced(1)] const createObjectURL = vi.fn(() => "blob:x") - const revokeObjectURL = vi.fn() - Object.assign(URL, { createObjectURL, revokeObjectURL }) + Object.assign(URL, { createObjectURL }) const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) render() fireEvent.click(screen.getByTestId("usage-export-csv")) - expect(createObjectURL).toHaveBeenCalledTimes(1) - expect(click).toHaveBeenCalled() - expect(revokeObjectURL).toHaveBeenCalledWith("blob:x") + expect(post).toHaveBeenCalledWith({ + type: "exportUsageCsv", + text: expect.stringContaining("\n"), + values: { range: "7d" }, + }) + expect(createObjectURL).not.toHaveBeenCalled() + expect(click).not.toHaveBeenCalled() click.mockRestore() }) }) From 72be8df8cd29ee0b7dc2d9eedabb1a529f05a3c6 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 3 Nov 2026 07:42:40 -0800 Subject: [PATCH 32/76] fix(usage): range radio group with arrow keys, an empty workspace matches nothing, a failing save dialog is reported; price a request with the protocol it started with --- src/core/task/Task.ts | 20 ++------- src/core/task/__tests__/Task.spec.ts | 45 +++++++++++++++++++ .../misc/__tests__/export-usage-csv.spec.ts | 8 ++++ src/integrations/misc/export-usage-csv.ts | 16 +++---- .../src/components/settings/UsageSettings.tsx | 27 ++++++----- .../settings/__tests__/SettingsView.spec.tsx | 2 + .../settings/__tests__/UsageSettings.spec.tsx | 21 +++++++++ .../__tests__/UsageSettings.visual.tsx | 27 +++++++++++ webview-ui/src/utils/__tests__/usage.spec.ts | 9 ++++ webview-ui/src/utils/usage.ts | 4 +- 10 files changed, 143 insertions(+), 36 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index fb57f3c452..6a5a3165cb 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -4072,14 +4072,8 @@ export class Task extends EventEmitter implements TaskLike { const existingData = JSON.parse(this.clineMessages[lastApiReqIndex].text || "{}") - // Calculate total tokens and cost using provider-aware function - const modelId = getModelId(this.apiConfiguration) - const apiProvider = this.apiConfiguration.apiProvider - const apiProtocol = getApiProtocol( - apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined, - modelId, - ) - + // Calculate total tokens and cost with the protocol this request started with: a profile switch + // while the usage is still being drained must not price it with another provider's formula. const costResult = apiProtocol === "anthropic" ? calculateApiCostAnthropic( @@ -4564,14 +4558,8 @@ export class Task extends EventEmitter implements TaskLike { await this.updateClineMessage(apiReqMessage) } - // Capture telemetry with provider-aware cost calculation - const modelId = getModelId(this.apiConfiguration) - const apiProvider = this.apiConfiguration.apiProvider - const apiProtocol = getApiProtocol( - apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined, - modelId, - ) - + // Capture telemetry with provider-aware cost calculation, using the protocol this request + // started with (see `updateApiReqMsg`). // Use the appropriate cost function based on the API protocol. // The per-TTL split must be passed here too: without it this // telemetry recompute bills every write at the short-TTL rate, diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index c91fda5527..bb0fad55eb 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -585,6 +585,51 @@ describe("Cline", () => { expect(call![1].cost).toBeCloseTo(0.6, 9) expect(call![1].cost).not.toBeCloseTo(0.375, 9) }) + + it("prices a request with the protocol it started with when the profile is switched while its usage is drained", async () => { + const task = await createTaskWithManualRetries() + const captureLlmCompletion = vi.spyOn(TelemetryService.instance, "captureLlmCompletion") + + vi.spyOn(task, "ask").mockResolvedValue({ response: "yesButtonClicked" } satisfies TaskAskResult) + vi.spyOn(task, "attemptApiRequest") + .mockImplementationOnce(() => + (async function* (): AsyncGenerator { + yield { type: "text", text: "partial response" } + // The user switches to a profile whose provider prices requests the OpenAI way, while the + // usage of this request has not been collected yet. + task.apiConfiguration = { + ...mockApiConfig, + apiProvider: providerIdentifiers.openai, + apiModelId: "gpt-4o", + } + yield { + type: "usage", + inputTokens: 0, + outputTokens: 0, + cacheWriteTokens: 100_000, + cacheWrite1hTokens: 100_000, + cacheReadTokens: 0, + } + })(), + ) + .mockImplementation(() => { + throw new Error("stop after usage capture") + }) + + await task.recursivelyMakeClineRequests([{ type: "text", text: "original user request" }]) + + await vi.waitFor(() => + expect( + captureLlmCompletion.mock.calls.some(([, payload]) => payload?.cacheWriteTokens === 100_000), + ).toBe(true), + ) + const call = captureLlmCompletion.mock.calls.find(([, payload]) => payload?.cacheWriteTokens === 100_000) + + // Still the Anthropic accounting the request started with: its input tokens add the cache writes back in + // (100k), where the OpenAI accounting of the new profile would report the 0 input tokens as they are. + expect(call![1].inputTokens).toBe(100_000) + expect(call![1].cost).toBeCloseTo(0.6, 9) + }) }) describe("request continuation", () => { diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index 9459024d0e..bae3ab782a 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -68,6 +68,14 @@ describe("exportUsageCsv", () => { expect(writeFile).not.toHaveBeenCalled() }) + it("tells the user when the save dialog itself fails, and writes nothing", async () => { + showSaveDialog.mockRejectedValue(new Error("dialog unavailable")) + + expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(showErrorMessage).toHaveBeenCalledWith("dialog unavailable") + expect(writeFile).not.toHaveBeenCalled() + }) + it("tells the user when the file cannot be written, and does not report a saved file", async () => { showSaveDialog.mockResolvedValue({ fsPath: "/ro/file.csv" } as vscode.Uri) writeFile.mockRejectedValue(new Error("EACCES: permission denied")) diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts index 64fad70101..ededdc32ad 100644 --- a/src/integrations/misc/export-usage-csv.ts +++ b/src/integrations/misc/export-usage-csv.ts @@ -7,23 +7,23 @@ const USAGE_RANGES: readonly string[] = ["today", "7d", "30d", "all"] /** * Saves the Usage tab's CSV where the user chooses. A webview cannot reliably download a file, so, like every other * export, the webview sends the text and the host writes it. The file name is built here from a known range, never - * from the message. Resolves to the saved file, or `undefined` if the user cancelled or there was nothing to save. + * from the message. Resolves to the saved file, or `undefined` if the user cancelled, there was nothing to save, or the dialog or the write failed (the user is told). */ export async function exportUsageCsv(csv: unknown, range: unknown): Promise { if (typeof csv !== "string" || csv.length === 0) return undefined const name = `zoo-usage-${typeof range === "string" && USAGE_RANGES.includes(range) ? range : "all"}.csv` - const saveUri = await vscode.window.showSaveDialog({ - defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", name)), - filters: { CSV: ["csv"] }, - }) - if (!saveUri) return undefined - try { + const saveUri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file(path.join(os.homedir(), "Downloads", name)), + filters: { CSV: ["csv"] }, + }) + if (!saveUri) return undefined + await vscode.workspace.fs.writeFile(saveUri, Buffer.from(csv, "utf-8")) + return saveUri } catch (error) { vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)) return undefined } - return saveUri } diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index c7f2cf8d80..3833ef5026 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -1,5 +1,6 @@ import { HTMLAttributes, useEffect, useMemo, useState } from "react" import { Download } from "lucide-react" +import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import { useAppTranslation } from "@/i18n/TranslationContext" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -138,28 +139,34 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes
-
setRange(value as UsageRange)} + orientation="horizontal" aria-label={t("settings:usage.range.label")}> {USAGE_RANGES.map((r) => ( - + ))} -
+ exercises the same handler. @@ -94,6 +100,7 @@ const withUsage = (overrides: Partial & { cost?: number; cacheReads describe("UsageSettings – branches", () => { beforeEach(() => { + translation.language = undefined // Only the date is frozen: the view reads "now" from it, the timers stay real. vi.useFakeTimers({ toFake: ["Date"] }) vi.setSystemTime(NOW) @@ -140,6 +147,49 @@ describe("UsageSettings – branches", () => { expect(monthChart.nextElementSibling).toBeNull() // too many bars to label }) + describe("the language of the dates", () => { + const weekdayLabels = (locale?: string) => lastDays(7, new Intl.DateTimeFormat(locale, { weekday: "narrow" })) + const lastBarTitle = () => { + const chart = screen.getByTestId("usage-daily-chart") + return chart.children[chart.children.length - 1].getAttribute("title") + } + + it("formats the labels and the tooltips of the chart in the language of the application, not the browser's", () => { + state.taskHistory = [withUsage()] + translation.language = "de" + render() + + expect(weekdayLabels("de")).not.toEqual(weekdayLabels()) // the two differ, so this tells them apart + expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels("de")) + expect(lastBarTitle()).toMatch(new RegExp(`^${new Intl.DateTimeFormat("de").format(NOW)} · `)) + }) + + it("follows a change of the language", () => { + state.taskHistory = [withUsage()] + translation.language = "de" + const { rerender } = render() + expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels("de")) + + translation.language = "ja" + rerender() + + expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels("ja")) + expect(lastBarTitle()).toMatch(new RegExp(`^${new Intl.DateTimeFormat("ja").format(NOW)} · `)) + }) + + it.each([["cimode"], ["en_US"], [""], [undefined]])( + "falls back to the browser's formats for the language %j, which the platform cannot use", + (language) => { + state.taskHistory = [withUsage()] + translation.language = language + render() + + expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels()) + expect(lastBarTitle()).toMatch(new RegExp(`^${new Intl.DateTimeFormat().format(NOW)} · `)) + }, + ) + }) + it("shows an unpriced top task by tokens and a priced one by cost when metrics are mixed", () => { state.taskHistory = [ withUsage({ cost: 2, task: "priced task" }), diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx index d1b52b948d..be4377299e 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -11,6 +11,29 @@ vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) +// Radix Select does not open in jsdom; a native onValueChange(e.target.value)}> + {children} + + ), + SelectTrigger: ({ children }: { children: React.ReactNode }) => <>{children}, + SelectValue: () => null, + SelectContent: ({ children }: { children: React.ReactNode }) => <>{children}, + SelectItem: ({ value, children }: { value: string; children: React.ReactNode }) => ( + + ), +})) + // The provider-limits section talks to the extension host; it has its own spec. vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
})) @@ -199,11 +222,18 @@ describe("UsageSettings", () => { }) describe("filters", () => { - it("defaults to this workspace and can widen to all", () => { + it("defaults to this workspace, widens to all workspaces and narrows again", () => { state.taskHistory = [priced(1), priced(2, { workspace: "/ws/other" })] render() expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") + + fireEvent.change(screen.getByTestId("scope-select"), { target: { value: "all" } }) + expect(screen.getByTestId("usage-stats").textContent).toContain("$3.00") + + fireEvent.change(screen.getByTestId("scope-select"), { target: { value: "workspace" } }) + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") }) it("shows nothing for This workspace when no folder is open, and everything for All workspaces", () => { @@ -273,19 +303,47 @@ describe("UsageSettings", () => { }) it("asks the host to save a CSV of the filtered range, and does not download it itself", () => { - state.taskHistory = [priced(1)] + const daysAgo = (days: number) => { + const day = new Date() + day.setDate(day.getDate() - days) + return day.getTime() + } + state.taskHistory = [ + priced(1, { task: "Included task" }), + priced(2, { task: "Old task", ts: daysAgo(20) }), // outside 7 days, inside 30 + priced(4, { task: "Other workspace task", workspace: "/ws/other" }), + ] const createObjectURL = vi.fn(() => "blob:x") Object.assign(URL, { createObjectURL }) const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) + const exported = () => + post.mock.calls.map((c) => c[0]).filter((m) => m.type === "exportUsageCsv") as Array<{ + text: string + values: { range: string } + }> render() + + // This workspace, 7 days: only the task that is in both. + fireEvent.click(screen.getByTestId("usage-export-csv")) + expect(exported().at(-1)?.values).toEqual({ range: "7d" }) + expect(exported().at(-1)?.text).toContain("Included task") + expect(exported().at(-1)?.text).not.toContain("Old task") + expect(exported().at(-1)?.text).not.toContain("Other workspace task") + + // The wider range brings in the old task, and the wider scope the other workspace's: the export follows. + fireEvent.click(screen.getByTestId("usage-range-30d")) + fireEvent.click(screen.getByTestId("usage-export-csv")) + expect(exported().at(-1)?.values).toEqual({ range: "30d" }) + expect(exported().at(-1)?.text).toContain("Old task") + expect(exported().at(-1)?.text).not.toContain("Other workspace task") + + fireEvent.change(screen.getByTestId("scope-select"), { target: { value: "all" } }) fireEvent.click(screen.getByTestId("usage-export-csv")) + expect(exported().at(-1)?.text).toContain("Included task") + expect(exported().at(-1)?.text).toContain("Old task") + expect(exported().at(-1)?.text).toContain("Other workspace task") - expect(post).toHaveBeenCalledWith({ - type: "exportUsageCsv", - text: expect.stringContaining("\n"), - values: { range: "7d" }, - }) expect(createObjectURL).not.toHaveBeenCalled() expect(click).not.toHaveBeenCalled() click.mockRestore() diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index a493d65926..248a83b25a 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1169,8 +1169,8 @@ "daily": "Diari", "monthly": "Mensual", "unlimited": "Il·limitat", - "hours": "{{n}} hores", - "days": "{{n}} dies" + "hours": "{{n}} h", + "days": "{{n}} d" } }, "byModel": "Per model", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index f0beb20202..2614b8ed21 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1169,8 +1169,8 @@ "daily": "Täglich", "monthly": "Monatlich", "unlimited": "Unbegrenzt", - "hours": "{{n}} Stunden", - "days": "{{n}} Tage" + "hours": "{{n}} Std.", + "days": "{{n}} T" } }, "byModel": "Nach Modell", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 21f1027163..df6b0778bc 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1162,15 +1162,15 @@ "signedOut": "Sesión no iniciada", "noCopilot": "Sin cuota de Copilot", "noKey": "Falta la clave de API o no es válida", - "unsupported": "URL base personalizada: no comprobado", + "unsupported": "URL base personalizada: no comprobada", "error": "No se pudo cargar", "window": { "weekly": "Semanal", "daily": "Diario", "monthly": "Mensual", "unlimited": "Ilimitado", - "hours": "{{n}} horas", - "days": "{{n}} días" + "hours": "{{n}} h", + "days": "{{n}} d" } }, "byModel": "Por modelo", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 10b0d66a45..6f8eac8d26 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1158,7 +1158,7 @@ "refresh": "Actualiser", "updated": "Mis à jour {{time}}", "loading": "Vérification des fournisseurs connectés…", - "resets": "Réinitialisation {{date}}", + "resets": "Réinitialisation le {{date}}", "signedOut": "Non connecté", "noCopilot": "Aucun quota Copilot", "noKey": "Clé API manquante ou invalide", @@ -1169,8 +1169,8 @@ "daily": "Quotidien", "monthly": "Mensuel", "unlimited": "Illimité", - "hours": "{{n}} heures", - "days": "{{n}} jours" + "hours": "{{n}} h", + "days": "{{n}} j" } }, "byModel": "Par modèle", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index af29baa008..3cfde620e7 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1127,7 +1127,7 @@ "today": "Oggi", "7d": "7 giorni", "30d": "30 giorni", - "all": "Sempre" + "all": "Tutto il periodo" }, "scope": { "label": "Mostra", @@ -1169,8 +1169,8 @@ "daily": "Giornaliero", "monthly": "Mensile", "unlimited": "Illimitato", - "hours": "{{n}} ore", - "days": "{{n}} giorni" + "hours": "{{n}} h", + "days": "{{n}} g" } }, "byModel": "Per modello", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index dba98d06c1..f146fb4aee 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1139,7 +1139,7 @@ "cost": "비용", "tokens": "토큰", "tasks": "작업", - "cacheHit": "{{percent}}% 캐시에서", + "cacheHit": "캐시 적중률 {{percent}}%", "inOut": "입력 {{input}} · 출력 {{output}}", "models": "모델" }, diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 174c3df087..a3dc2219f2 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1158,7 +1158,7 @@ "refresh": "Vernieuwen", "updated": "Bijgewerkt {{time}}", "loading": "Verbonden providers controleren…", - "resets": "Reset {{date}}", + "resets": "Reset op {{date}}", "signedOut": "Niet aangemeld", "noCopilot": "Geen Copilot-quotum", "noKey": "API-sleutel ontbreekt of is ongeldig", @@ -1170,7 +1170,7 @@ "monthly": "Maandelijks", "unlimited": "Onbeperkt", "hours": "{{n}} uur", - "days": "{{n}} dagen" + "days": "{{n}} d" } }, "byModel": "Per model", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index fba30475a0..43e49faeeb 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1140,7 +1140,7 @@ "tokens": "tokeny", "tasks": "zadania", "cacheHit": "{{percent}}% z pamięci podręcznej", - "inOut": "{{input}} wejście · {{output}} wyjście", + "inOut": "Wejście: {{input}} · wyjście: {{output}}", "models": "modele" }, "dailyCost": "Koszt dzienny", @@ -1158,7 +1158,7 @@ "refresh": "Odśwież", "updated": "Zaktualizowano {{time}}", "loading": "Sprawdzanie połączonych dostawców…", - "resets": "Reset {{date}}", + "resets": "Reset: {{date}}", "signedOut": "Nie zalogowano", "noCopilot": "Brak limitu Copilot", "noKey": "Brak klucza API lub jest nieprawidłowy", @@ -1170,7 +1170,7 @@ "monthly": "Miesięcznie", "unlimited": "Bez limitu", "hours": "{{n}} godz.", - "days": "{{n}} dni" + "days": "{{n}} dn." } }, "byModel": "Według modelu", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 1c33b65e8c..aa83db004e 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1169,8 +1169,8 @@ "daily": "Diário", "monthly": "Mensal", "unlimited": "Ilimitado", - "hours": "{{n}} horas", - "days": "{{n}} dias" + "hours": "{{n}} h", + "days": "{{n}} d" } }, "byModel": "Por modelo", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index e4b165ac51..db371c3bb5 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1140,7 +1140,7 @@ "tokens": "токены", "tasks": "задачи", "cacheHit": "{{percent}}% из кэша", - "inOut": "{{input}} вход · {{output}} выход", + "inOut": "Вход: {{input}} · выход: {{output}}", "models": "модели" }, "dailyCost": "Стоимость по дням", @@ -1158,7 +1158,7 @@ "refresh": "Обновить", "updated": "Обновлено {{time}}", "loading": "Проверка подключённых провайдеров…", - "resets": "Сброс {{date}}", + "resets": "Сброс: {{date}}", "signedOut": "Вход не выполнен", "noCopilot": "Нет квоты Copilot", "noKey": "Ключ API отсутствует или недействителен", diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index eea5c36c90..0edf9c88f5 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -623,6 +623,35 @@ describe("a time that Date cannot represent", () => { }) }) +describe("in a time zone where the day of a change of daylight saving time begins at 01:00", () => { + let originalTz: string | undefined + beforeEach(() => { + originalTz = process.env.TZ + process.env.TZ = "America/Santiago" // on Sunday 6 September 2026 the clocks go from 00:00 to 01:00 + }) + afterEach(() => { + if (originalTz === undefined) delete process.env.TZ + else process.env.TZ = originalTz + }) + + it("starts the range at the start of its first day, not at 01:00, and gives every day a bar of its own", () => { + const now = new Date(2026, 8, 6, 12, 0, 0).getTime() // the day that starts at 01:00 + const history = [ + item({ task: "just after midnight", ts: new Date(2026, 8, 0, 0, 30).getTime(), totalCost: 3 }), // 31 August 00:30 + item({ task: "today", ts: now, totalCost: 1 }), + ] + + const summary = summarizeUsage(history, { range: "7d", now }) + + expect(summary.taskCount).toBe(2) // the task at 00:30 on the first day is in the range + expect(summary.totalCost).toBe(4) + expect(summary.daily).toHaveLength(7) + expect(summary.daily[0]).toMatchObject({ cost: 3 }) // and in the bar of that day + expect(summary.daily[6]).toMatchObject({ cost: 1 }) + expect(usageToCsv(history, { range: "7d", now }).split("\n")).toHaveLength(3) + }) +}) + describe("a task dated after today", () => { const DAY = 24 * 3600 * 1000 const history = () => [ diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 409f36021d..d571d058fb 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -132,12 +132,14 @@ function startOfLocalDay(ts: number): number { return d.getTime() } -/** Local midnight `days` calendar days before `ts` (not `days * 24h`, which is off by an hour across a DST change). */ +/** + * The start of the local day `days` calendar days before `ts` (not `days * 24h`, which is off by an hour across a DST + * change). It is built from the calendar fields, so that the day has its own start: where a day begins at 01:00 + * because midnight does not exist on it, an earlier day is not moved to 01:00 too. + */ function startOfLocalDayMinus(ts: number, days: number): number { const d = new Date(ts) - d.setHours(0, 0, 0, 0) - d.setDate(d.getDate() - days) - return d.getTime() + return new Date(d.getFullYear(), d.getMonth(), d.getDate() - days).getTime() } /** Inclusive lower bound for a range, or `undefined` for "all". */ From 8109f45ab609d5ad282c4605eedd523250bceca9 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 4 Nov 2026 18:14:35 -0800 Subject: [PATCH 56/76] test: wait for the indexing to finish instead of a fixed delay; the language test does not assume the runtime locale --- .../SettingsView.providerLimits.spec.tsx | 16 ++++++++-------- .../__tests__/UsageSettings.branches.spec.tsx | 14 ++++++++++---- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.providerLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.providerLimits.spec.tsx index 349398b09e..c6f434a5db 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.providerLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.providerLimits.spec.tsx @@ -16,6 +16,12 @@ const postMessage = vi.mocked(vscode.postMessage) const posted = (type: string) => postMessage.mock.calls.map((call) => call[0] as { type: string; requestId?: string }).filter((m) => m.type === type) +/** Settings hides its content (opacity-0) while it mounts every tab once to index it: wait until that is over. */ +const waitForIndexing = async () => { + await waitFor(() => expect(screen.getByTestId("settings-content")).not.toHaveClass("opacity-0")) + await act(async () => {}) // let the effects of the tab that is shown run +} + describe("SettingsView – provider limits", () => { beforeEach(() => postMessage.mockClear()) @@ -25,11 +31,8 @@ describe("SettingsView – provider limits", () => { }) // Settings mounts every tab once to index it (the Usage tab included) before it shows the one that was asked for. + await waitForIndexing() await waitFor(() => expect(screen.getByTestId("provider-limits")).toBeTruthy()) - await waitFor(() => expect(posted("requestProviderQuotas")).toHaveLength(1)) - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)) - }) // One request, from the tab the user is looking at; the indexing pass sent none (and so cancelled none). expect(posted("requestProviderQuotas")).toHaveLength(1) @@ -41,10 +44,7 @@ describe("SettingsView – provider limits", () => { queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }), }) - await waitFor(() => expect(screen.getByTestId("save-button")).toBeTruthy()) - await act(async () => { - await new Promise((resolve) => setTimeout(resolve, 50)) - }) + await waitForIndexing() expect(posted("requestProviderQuotas")).toHaveLength(0) expect(posted("cancelProviderQuotas")).toHaveLength(0) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx index 8fd286f59b..62dc29ea27 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx @@ -156,12 +156,18 @@ describe("UsageSettings – branches", () => { it("formats the labels and the tooltips of the chart in the language of the application, not the browser's", () => { state.taskHistory = [withUsage()] - translation.language = "de" + // A language that the runtime's own locale does not share, whichever that is: the labels and the dates differ. + const language = ["de", "ja"].find( + (candidate) => + weekdayLabels(candidate).join() !== weekdayLabels().join() && + new Intl.DateTimeFormat(candidate).format(NOW) !== new Intl.DateTimeFormat().format(NOW), + ) + expect(language).toBeDefined() + translation.language = language render() - expect(weekdayLabels("de")).not.toEqual(weekdayLabels()) // the two differ, so this tells them apart - expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels("de")) - expect(lastBarTitle()).toMatch(new RegExp(`^${new Intl.DateTimeFormat("de").format(NOW)} · `)) + expect(labelsUnder(screen.getByTestId("usage-daily-chart"))).toEqual(weekdayLabels(language)) + expect(lastBarTitle()).toMatch(new RegExp(`^${new Intl.DateTimeFormat(language).format(NOW)} · `)) }) it("follows a change of the language", () => { From 0883ccf483373170e84da0c17e132c923133cda8 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 4 Nov 2026 19:20:48 -0800 Subject: [PATCH 57/76] test(usage): the rendered top tasks are checked in order, from the largest down --- .../__tests__/UsageSettings.branches.spec.tsx | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx index 62dc29ea27..498c94e34a 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx @@ -1,4 +1,4 @@ -import { fireEvent, render, screen } from "@testing-library/react" +import { fireEvent, render, screen, within } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { providerIdentifiers, type HistoryItem } from "@roo-code/types" @@ -278,26 +278,49 @@ describe("UsageSettings – branches", () => { expect(screen.getByTestId("usage-top-tasks").textContent).toContain("bare") }) - it("treats a task with no cost as zero when ranking by tokens", () => { - state.taskHistory = [ + it("treats a task with no cost as zero when ranking by tokens, and lists the tasks from the largest down", () => { + const unpriced = (title: string, tokensIn: number, tokensOut: number) => task({ - task: "tokens only", + task: title, totalCost: undefined as never, + tokensIn, + tokensOut, usageByModel: [ { apiProvider: providerIdentifiers.githubCopilot, modelId: "gpt-5", requests: 1, - tokensIn: 100, - tokensOut: 10, + tokensIn, + tokensOut, cost: 0, }, ], - }), + }) + // Not in the order of their sizes, so that only the ranking can put them in order. + state.taskHistory = [ + unpriced("medium task", 1000, 100), + unpriced("small task", 100, 10), + unpriced("large task", 5000, 500), ] + render() - expect(screen.getByTestId("usage-top-tasks").textContent).toContain("tokens only") + + const rows = within(screen.getByTestId("usage-top-tasks")).getAllByRole("button") + expect(rows).toHaveLength(3) + expect(rows.map((row) => /(large|medium|small) task/.exec(row.textContent ?? "")?.[1])).toEqual([ + "large", + "medium", + "small", + ]) + // Each is shown by its tokens (input and output together): there is no price to show. + expect(rows.map((row) => /gpt-5(5\.5|1\.1|110)/.exec(row.textContent ?? "")?.[1])).toEqual([ + "5.5", + "1.1", + "110", + ]) + expect(screen.getByTestId("usage-top-tasks").textContent).not.toContain("$") }) + it("shows the cost for a priced task whose token counters are missing", () => { state.taskHistory = [ task({ From 419199f09da51d4ee522247b84d63e5e604e83ed Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Wed, 4 Nov 2026 20:19:55 -0800 Subject: [PATCH 58/76] test(usage): the rendered top tasks are checked with their complete token values, suffix included --- .../__tests__/UsageSettings.branches.spec.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx index 498c94e34a..36a8b17188 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx @@ -1,4 +1,5 @@ import { fireEvent, render, screen, within } from "@testing-library/react" +import i18next from "i18next" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { providerIdentifiers, type HistoryItem } from "@roo-code/types" @@ -303,7 +304,16 @@ describe("UsageSettings – branches", () => { unpriced("large task", 5000, 500), ] + // The magnitude suffixes come from the translations, which this spec does not load: give them as in English. + const suffixes: Record = { + "common:number_format.thousand_suffix": "k", + "common:number_format.million_suffix": "M", + "common:number_format.billion_suffix": "B", + } + const translate = vi.spyOn(i18next, "t").mockImplementation(((key: string) => suffixes[key] ?? key) as never) + render() + translate.mockRestore() const rows = within(screen.getByTestId("usage-top-tasks")).getAllByRole("button") expect(rows).toHaveLength(3) @@ -313,9 +323,9 @@ describe("UsageSettings – branches", () => { "small", ]) // Each is shown by its tokens (input and output together): there is no price to show. - expect(rows.map((row) => /gpt-5(5\.5|1\.1|110)/.exec(row.textContent ?? "")?.[1])).toEqual([ - "5.5", - "1.1", + expect(rows.map((row) => /gpt-5(.+?)common:time_ago/.exec(row.textContent ?? "")?.[1])).toEqual([ + "5.5k", + "1.1k", "110", ]) expect(screen.getByTestId("usage-top-tasks").textContent).not.toContain("$") From e7a877795bcd2af81f40c30a1fd263b6d2895151 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 07:37:22 -0800 Subject: [PATCH 59/76] fix(usage): the profile reads take the request's abort signal at the source boundary, so a request that was cancelled while it waited for the profile lock reads nothing --- src/core/config/ProviderSettingsManager.ts | 10 ++- .../__tests__/ProviderSettingsManager.spec.ts | 71 +++++++++++++++++++ .../__tests__/providerQuotas.spec.ts | 16 ++++- .../language-models/providerQuotas.ts | 9 +-- 4 files changed, 100 insertions(+), 6 deletions(-) diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 01695d7994..6a1afc1ad9 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -393,10 +393,15 @@ export class ProviderSettingsManager { /** * List all available configs with metadata. + * + * With `signal`, a call that is aborted by the time its turn comes (it waited behind other operations) reads + * nothing from storage. A read that has started cannot be stopped: it holds the lock until it settles, because + * a second `load` (which may migrate and write) must not run beside it. */ - public async listConfig(): Promise { + public async listConfig(options?: { signal?: AbortSignal }): Promise { try { return await this.lock(async () => { + options?.signal?.throwIfAborted() const providerProfiles = await this.load() return Object.entries(providerProfiles.apiConfigs).map(([name, apiConfig]) => ({ @@ -443,11 +448,14 @@ export class ProviderSettingsManager { } } + /** With `signal`, a call that is aborted by the time its turn comes reads nothing from storage (see `listConfig`). */ public async getProfile( params: { name: string } | { id: string }, + options?: { signal?: AbortSignal }, ): Promise { try { return await this.lock(async () => { + options?.signal?.throwIfAborted() const providerProfiles = await this.load() let name: string let providerSettings: ProviderSettingsWithId diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index a1fc9ac7de..327f2f4cef 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -578,6 +578,77 @@ describe("ProviderSettingsManager", () => { }) }) + describe("reads that are aborted before their turn", () => { + const stored: ProviderProfiles = { + currentApiConfigName: "default", + apiConfigs: { default: { id: "default-id" }, other: { id: "other-id" } }, + modeApiConfigs: {}, + migrations: { rateLimitSecondsMigrated: true }, + } + const aborted = () => { + const controller = new AbortController() + controller.abort() + return controller.signal + } + + it("lists nothing, and reads nothing from storage, when the signal is already aborted", async () => { + mockSecrets.get.mockClear() + mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) + + await expect(providerSettingsManager.listConfig({ signal: aborted() })).rejects.toThrow( + "Failed to list configs", + ) + + expect(mockSecrets.get).not.toHaveBeenCalled() + }) + + it("reads no profile from storage when the signal is already aborted", async () => { + mockSecrets.get.mockClear() + mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) + + await expect(providerSettingsManager.getProfile({ id: "other-id" }, { signal: aborted() })).rejects.toThrow( + "Failed to get profile", + ) + + expect(mockSecrets.get).not.toHaveBeenCalled() + }) + + it("reads nothing for a call that is aborted while it waits behind another one, and the others are not held up", async () => { + mockSecrets.get.mockClear() + let finishFirst: (value: string) => void = () => {} + mockSecrets.get + .mockImplementationOnce(() => new Promise((resolve) => (finishFirst = resolve))) + .mockResolvedValue(JSON.stringify(stored)) + const load = vi.spyOn(providerSettingsManager as unknown as { load(): Promise }, "load") + const controller = new AbortController() + + const first = providerSettingsManager.listConfig() // holds the lock: its read has started + await vi.waitFor(() => expect(mockSecrets.get).toHaveBeenCalledTimes(1)) + const waiting = providerSettingsManager.getProfile({ id: "other-id" }, { signal: controller.signal }) + const waitingFailed = expect(waiting).rejects.toThrow("Failed to get profile") + const after = providerSettingsManager.listConfig() // queued behind both, and not aborted + controller.abort() + finishFirst(JSON.stringify(stored)) + + await first + await waitingFailed + await expect(after).resolves.toHaveLength(2) + // Only the first call and the one that was not aborted loaded the profiles: the aborted one never did. + expect(load).toHaveBeenCalledTimes(2) + load.mockRestore() + }) + + it("reads as before when the signal is not aborted", async () => { + mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) + const controller = new AbortController() + + await expect(providerSettingsManager.listConfig({ signal: controller.signal })).resolves.toHaveLength(2) + await expect( + providerSettingsManager.getProfile({ id: "other-id" }, { signal: controller.signal }), + ).resolves.toMatchObject({ name: "other" }) + }) + }) + describe("ListConfig", () => { it("should list all available configs", async () => { const existingConfig: ProviderProfiles = { diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 45fd6fea34..cbdaee067c 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1372,7 +1372,21 @@ describe("loadProfiles", () => { { apiProvider: "x", openRouterApiKey: "key-a" }, { apiProvider: "x", openRouterApiKey: "key-b" }, ]) - expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }) + expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }, { signal: undefined }) + }) + + it("hands the request's abort signal to the source, for the list and for every profile", async () => { + const controller = new AbortController() + const source = { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn(async () => ({ apiProvider: "x" }) as never), + } + + await loadProfilesWithActive(source, controller.signal) + + expect(source.listConfig).toHaveBeenCalledExactlyOnceWith({ signal: controller.signal }) + expect(source.getProfile).toHaveBeenCalledTimes(2) + for (const call of source.getProfile.mock.calls) expect(call[1]).toEqual({ signal: controller.signal }) }) it("skips a profile that fails to load instead of failing the whole refresh", async () => { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 7ff909efa2..cddc5423b1 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -600,8 +600,9 @@ async function fetchCopilot(force: boolean, signal?: AbortSignal): Promise - getProfile(params: { id: string }): Promise + /** With `signal`, a read that is aborted before it starts reads nothing: it cannot be stopped once it has. */ + listConfig(options?: { signal?: AbortSignal }): Promise<{ id: string; name?: string }[]> + getProfile(params: { id: string }, options?: { signal?: AbortSignal }): Promise } /** How long one profile read may take. A read that never settles is skipped, so the others still produce a response. */ @@ -654,7 +655,7 @@ function withDeadline(start: () => PromiseLike, ms: number, message: strin /** Reads one profile; one that fails, takes too long, or is abandoned (`signal` aborts) reads as missing. */ function readProfile(source: ProfileSource, id: string, signal?: AbortSignal): Promise { return withDeadline( - () => source.getProfile({ id }), + () => source.getProfile({ id }, { signal }), PROFILE_READ_TIMEOUT_MS, "Reading a profile timed out", signal, @@ -677,7 +678,7 @@ export async function loadProfilesWithActive( // request that was already cancelled, and the profile reads are not started once the list wait has been abandoned. const listed = await until( withDeadline( - () => source.listConfig(), + () => source.listConfig({ signal }), PROFILE_READ_TIMEOUT_MS, "Reading the saved profiles timed out", signal, From 823d8e5afb85b5dcaa5ea22906f4272ed9e25fa7 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 09:12:10 -0800 Subject: [PATCH 60/76] fix(usage): a Kimi Code profile with no recorded method is an OAuth one in the quota path too; corrupted numbers in the usage records are covered, and non-finite cache counters are left out --- .../__tests__/usageByModel.spec.ts | 79 +++++++++++++++++++ src/core/task-persistence/usageByModel.ts | 9 ++- .../__tests__/providerQuotas.spec.ts | 32 +++++++- .../language-models/providerQuotas.ts | 5 +- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index a7b7b1c4b7..3e642132cc 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -126,6 +126,85 @@ describe("summarizeUsageByModel with stored text that is not a request object", }) }) +describe("summarizeUsageByModel with corrupted numbers", () => { + const raw = (fields: string): ClineMessage => ({ ts: 1, type: "say", say: "api_req_started", text: `{${fields}}` }) + const head = '"apiProvider":"p","modelId":"m"' + + // JSON has no NaN or Infinity: JSON.stringify writes NaN as null, and a number too big for a double is read back + // as Infinity (1e999). + it.each([ + ["a number too big to be finite (Infinity)", "1e999"], + ["a number too small to be finite (-Infinity)", "-1e999"], + ["NaN, written as null", "null"], + ["a string", '"100"'], + ["a boolean", "true"], + ["an object", "{}"], + ["an array", "[5]"], + ])("counts the request and reads %s as 0 in a token field, keeping the numeric ones", (_name, value) => { + const summary = summarizeUsageByModel([raw(`${head},"tokensIn":${value},"tokensOut":7,"cost":0.5`)]) + + expect(summary).toEqual([{ apiProvider: "p", modelId: "m", requests: 1, tokensIn: 0, tokensOut: 7, cost: 0.5 }]) + }) + + it.each([ + ["Infinity", "1e999"], + ["-Infinity", "-1e999"], + ["NaN, written as null", "null"], + ["a string", '"0.5"'], + ])("reads %s as 0 in the cost, and keeps the tokens", (_name, value) => { + const summary = summarizeUsageByModel([raw(`${head},"tokensIn":3,"tokensOut":4,"cost":${value}`)]) + + expect(summary).toEqual([{ apiProvider: "p", modelId: "m", requests: 1, tokensIn: 3, tokensOut: 4, cost: 0 }]) + }) + + it("does not let a corrupted request spoil the sums of the others of the same model", () => { + const summary = summarizeUsageByModel([ + raw(`${head},"tokensIn":100,"tokensOut":10,"cost":1`), + raw(`${head},"tokensIn":1e999,"tokensOut":-1e999,"cost":null`), + raw(`${head},"tokensIn":200,"tokensOut":20,"cost":2`), + ]) + + expect(summary).toEqual([ + { apiProvider: "p", modelId: "m", requests: 3, tokensIn: 300, tokensOut: 30, cost: 3 }, + ]) + expect(Object.values(summary[0]).every((value) => typeof value !== "number" || Number.isFinite(value))).toBe( + true, + ) + }) + + it.each([ + ["both token fields are not numbers", '"tokensIn":"1","tokensOut":null,"cost":5'], + ["both token fields are missing", '"cost":5'], + ])("does not count a request whose %s: it has not finished", (_name, fields) => { + expect(summarizeUsageByModel([raw(`${head},${fields}`)])).toEqual([]) + }) + + it.each([ + ["Infinity", "1e999"], + ["-Infinity", "-1e999"], + ["NaN, written as null", "null"], + ["a string", '"5"'], + ])("leaves a cache counter that is %s out, so the sums stay finite", (_name, value) => { + const summary = summarizeUsageByModel([ + raw(`${head},"tokensIn":1,"tokensOut":1,"cost":0,"cacheReads":${value},"cacheWrites":${value}`), + raw(`${head},"tokensIn":1,"tokensOut":1,"cost":0,"cacheReads":30,"cacheWrites":4`), + ]) + + expect(summary).toEqual([ + { + apiProvider: "p", + modelId: "m", + requests: 2, + tokensIn: 2, + tokensOut: 2, + cost: 0, + cacheReads: 30, + cacheWrites: 4, + }, + ]) + }) +}) + describe("resolveRecordedModelId", () => { it("keeps a model that was recorded when the request started", () => { expect(resolveRecordedModelId("gpt-5", "gpt-5-mini")).toBe("gpt-5") diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index a747e806ab..0572f98898 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -46,8 +46,13 @@ export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEnt entry.tokensIn += num(parsed.tokensIn) entry.tokensOut += num(parsed.tokensOut) entry.cost += num(parsed.cost) - if (typeof parsed.cacheReads === "number") entry.cacheReads = (entry.cacheReads ?? 0) + parsed.cacheReads - if (typeof parsed.cacheWrites === "number") entry.cacheWrites = (entry.cacheWrites ?? 0) + parsed.cacheWrites + // A counter that is not a finite number (a corrupted file) is not a report of the cache: it is left out. + if (typeof parsed.cacheReads === "number" && Number.isFinite(parsed.cacheReads)) { + entry.cacheReads = (entry.cacheReads ?? 0) + parsed.cacheReads + } + if (typeof parsed.cacheWrites === "number" && Number.isFinite(parsed.cacheWrites)) { + entry.cacheWrites = (entry.cacheWrites ?? 0) + parsed.cacheWrites + } } return [...byKey.values()] diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index cbdaee067c..903234dcc5 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -574,13 +574,35 @@ describe("Kimi Code authentication", () => { expect((fetchSpy.mock.calls[0][1]?.headers as Record).Authorization).toBe("Bearer real-key") }) - it("treats a profile with no recorded method as an API-key profile", async () => { + it("treats a profile with no recorded method as an OAuth profile, as the requests do, and does not query its key", async () => { const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ usage: { limit: 100, used: 10 } }))) - await getProviderQuotas( + + const [q] = await getProviderQuotas( [{ apiProvider: providerIdentifiers.kimiCode, kimiCodeApiKey: "k" } as never], {}, fetchSpy, ) + + expect(q).toMatchObject({ ok: false, reason: "noKey" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("queries a profile that names the API-key method, and no other", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ usage: { limit: 100, used: 10 } }))) + resetProviderQuotaCache() + + await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.kimiCode, + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "k", + } as never, + ], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledTimes(1) }) }) @@ -677,7 +699,11 @@ describe("getProviderQuotas – coding-plan providers", () => { await getProviderQuotas( [ { apiProvider: providerIdentifiers.nanogpt, nanoGptApiKey: "k" } as never, - { apiProvider: providerIdentifiers.kimiCode, kimiCodeApiKey: "k" } as never, + { + apiProvider: providerIdentifiers.kimiCode, + kimiCodeAuthMethod: "api-key", + kimiCodeApiKey: "k", + } as never, { apiProvider: providerIdentifiers.opencodeGo, opencodeGoApiKey: "k" } as never, ], {}, diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index cddc5423b1..87feaabee5 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -436,8 +436,9 @@ const KEYED_PROVIDERS: KeyedProvider[] = [ // Only API-key profiles are queried; OAuth sign-ins have no key to send. providerId: providerIdentifiers.kimiCode, credentials: (s) => ({ - // An OAuth sign-in has no API key to send, even if an old key is still in the profile. - apiKey: s.kimiCodeAuthMethod === "oauth" ? undefined : s.kimiCodeApiKey, + // An OAuth sign-in has no API key to send, even if an old key is still in the profile. A profile with no + // recorded method is an OAuth one, as everywhere else (the requests, the validation, the sign-in check). + apiKey: (s.kimiCodeAuthMethod ?? "oauth") === "api-key" ? s.kimiCodeApiKey : undefined, urls: ["https://api.kimi.com/coding/v1/usages"], }), parse: parseKimiCodeUsage, From a8e5ffe1b81fe9726c9af2c02db8a1480f0650ef Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 09:30:31 -0800 Subject: [PATCH 61/76] test(usage): the mock of the profile read declares its parameters, so the recorded calls type-check --- .../language-models/__tests__/providerQuotas.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 903234dcc5..d691cd14c7 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1405,7 +1405,10 @@ describe("loadProfiles", () => { const controller = new AbortController() const source = { listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), - getProfile: vi.fn(async () => ({ apiProvider: "x" }) as never), + // The parameters are declared, so that the calls it records are typed with them. + getProfile: vi.fn( + async (_params: { id: string }, _options?: { signal?: AbortSignal }) => ({ apiProvider: "x" }) as never, + ), } await loadProfilesWithActive(source, controller.signal) From 15c508145d04dafd796183833c2128a32a44dd43 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 09:49:26 -0800 Subject: [PATCH 62/76] fix(usage): a request is priced with the protocol of the handler that serves it, also when the profile is switched before it starts --- .../__tests__/usageByModel.spec.ts | 21 ++++ src/core/task-persistence/usageByModel.ts | 3 + src/core/task/Task.ts | 17 ++- src/core/task/__tests__/Task.spec.ts | 102 ++++++++++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index 3e642132cc..ba302c4db5 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -239,6 +239,27 @@ describe("bindRequestToHandler", () => { expect(bound).toEqual({ apiProtocol: "openai", apiProvider: "new-provider", modelId: "new-model" }) }) + it("records the pricing protocol of the handler too, over the one the placeholder was written with", () => { + const bound = JSON.parse( + bindRequestToHandler(placeholder, { + apiProvider: "new-provider", + configuredModelId: "new-model", + servedModelId: undefined, + apiProtocol: "anthropic", + }), + ) + + expect(bound).toEqual({ apiProtocol: "anthropic", apiProvider: "new-provider", modelId: "new-model" }) + }) + + it("keeps the protocol of the placeholder when the handler's is not given", () => { + const bound = JSON.parse( + bindRequestToHandler(placeholder, { apiProvider: "p", configuredModelId: "m", servedModelId: undefined }), + ) + + expect(bound.apiProtocol).toBe("openai") + }) + it("records the model the handler reports when the profile names none", () => { const bound = JSON.parse( bindRequestToHandler(placeholder, { diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index 0572f98898..46927d2688 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -81,6 +81,8 @@ export function bindRequestToHandler( apiProvider: string | undefined configuredModelId: string | undefined servedModelId: string | undefined + /** The pricing protocol of that handler, when it is known: it replaces the one the placeholder was written with. */ + apiProtocol?: string }, ): string { let existing: Record = {} @@ -92,6 +94,7 @@ export function bindRequestToHandler( return JSON.stringify({ ...existing, + ...(handler.apiProtocol ? { apiProtocol: handler.apiProtocol } : {}), apiProvider: handler.apiProvider, modelId: resolveRecordedModelId(handler.configuredModelId, handler.servedModelId), }) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c75da97663..64b9221083 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3966,7 +3966,9 @@ export class Task extends EventEmitter implements TaskLike { // Determine API protocol based on provider and model const modelId = getModelId(this.apiConfiguration) const apiProvider = this.apiConfiguration.apiProvider - const apiProtocol = getApiProtocol( + // A placeholder: it is replaced below by the protocol of the handler that serves the request, which may be + // another one if the profile is switched during the waits that come first. + let apiProtocol = getApiProtocol( apiProvider && !isRetiredProvider(apiProvider) ? apiProvider : undefined, modelId, ) @@ -4198,13 +4200,22 @@ export class Task extends EventEmitter implements TaskLike { served.modelId = this.cachedStreamingModel.id // The handler that serves this request is known now. The placeholder was written before the waits - // above, so bring its provider and model in line with this handler. + // above, so bring its provider, model and pricing protocol in line with this handler: the cost is + // worked out with the model info of this handler, so it needs the protocol of the same one. (The + // protocol is fixed from here on: a profile switch while the usage is still being collected does not + // change how this request is priced.) + const servingProvider = this.apiConfiguration.apiProvider + apiProtocol = getApiProtocol( + servingProvider && !isRetiredProvider(servingProvider) ? servingProvider : undefined, + resolveRecordedModelId(getModelId(this.apiConfiguration), served.modelId), + ) this.clineMessages[lastApiReqIndex].text = bindRequestToHandler( this.clineMessages[lastApiReqIndex].text, { - apiProvider: this.apiConfiguration.apiProvider, + apiProvider: servingProvider, configuredModelId: getModelId(this.apiConfiguration), servedModelId: served.modelId, + apiProtocol, }, ) diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 76583dd6fe..713c811fe5 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -64,6 +64,7 @@ type TaskTestAccess = { messages: ApiMessage[], requestModelInfo: ModelInfo, ) => Array<{ role: string; content: unknown } | { type: "reasoning"; encrypted_content: string }> + maybeWaitForProviderRateLimit: (retryAttempt: number) => Promise attemptApiRequestWithActiveApi: () => AsyncGenerator condenseContextWithActiveApi: () => Promise retiredApis: Set @@ -632,6 +633,107 @@ describe("Cline", () => { }) }) + describe("the protocol that prices a request follows the handler that serves it", () => { + const openAiModelInfo = { + contextWindow: 128_000, + supportsPromptCache: true, + inputPrice: 3, + outputPrice: 15, + cacheWritesPrice: 3.75, + cacheWritesPrice1h: 6, + cacheReadsPrice: 0.3, + } as ModelInfo + + // The handler is shared by the tests of this file, so what is replaced on it is put back after each test. + const spies: Array<{ mockRestore: () => void }> = [] + afterEach(() => { + for (const spy of spies.splice(0)) spy.mockRestore() + }) + + const run = async (switchProfileBeforeTheRequestStarts: boolean) => { + const task = await createTaskWithManualRetries() + const captureLlmCompletion = vi.spyOn(TelemetryService.instance, "captureLlmCompletion") + captureLlmCompletion.mockClear() // earlier tests spied on it too: only the calls of this one are wanted + vi.spyOn(task, "ask").mockResolvedValue({ response: "yesButtonClicked" } satisfies TaskAskResult) + if (switchProfileBeforeTheRequestStarts) { + // The profile is switched while the request waits to start, so the handler that serves it is the new one, + // which prices requests the OpenAI way. + spies.push( + vi.spyOn(getTaskTestAccess(task), "maybeWaitForProviderRateLimit").mockImplementation(async () => { + task.apiConfiguration = { + ...mockApiConfig, + apiProvider: providerIdentifiers.openai, + apiModelId: "gpt-4o", + } + }), + vi.spyOn(task.api, "getModel").mockReturnValue({ id: "gpt-4o", info: openAiModelInfo }), + ) + } + const lastRequestRecord = () => + JSON.parse([...task.clineMessages].reverse().find((m) => m.say === "api_req_started")?.text ?? "{}") + let recordedAtStart: Record = {} + vi.spyOn(task, "attemptApiRequest") + .mockImplementationOnce(() => + (async function* (): AsyncGenerator { + // The request has been bound to its handler by now, and is not finished. + recordedAtStart = lastRequestRecord() + yield { type: "text", text: "partial response" } + yield { + type: "usage", + inputTokens: 0, + outputTokens: 0, + cacheWriteTokens: 200_000, + cacheWrite1hTokens: 200_000, + cacheReadTokens: 0, + } + })(), + ) + .mockImplementation(() => { + throw new Error("stop after usage capture") + }) + + await task.recursivelyMakeClineRequests([{ type: "text", text: "original user request" }]) + + await vi.waitFor(() => + expect( + captureLlmCompletion.mock.calls.some(([, payload]) => payload?.cacheWriteTokens === 200_000), + ).toBe(true), + ) + const call = captureLlmCompletion.mock.calls.find(([, payload]) => payload?.cacheWriteTokens === 200_000) + return { payload: call![1], recorded: lastRequestRecord(), recordedAtStart } + } + + it("prices the request with the OpenAI formula when the profile was switched to an OpenAI one before it started", async () => { + const { payload, recorded, recordedAtStart } = await run(true) + + // OpenAI accounting: the input tokens are what was reported (none), where the Anthropic accounting of the profile the + // request was written with would add the cache writes back in (200k). + expect(payload.inputTokens).toBe(0) + expect(payload.cost).toBeCloseTo(1.2, 9) // 200k tokens written for an hour, at 6 per million + // The record says the same, from the moment the request is bound: the provider, the model and the protocol of + // the handler that serves it, and not those of the placeholder that was written before the switch. + expect(recordedAtStart).toMatchObject({ + apiProtocol: "openai", + apiProvider: providerIdentifiers.openai, + modelId: "gpt-4o", + }) + expect(recorded).toMatchObject({ + apiProtocol: "openai", + apiProvider: providerIdentifiers.openai, + modelId: "gpt-4o", + }) + }) + + it("still prices an unswitched request with the formula of the profile it started with", async () => { + const { payload, recorded, recordedAtStart } = await run(false) + + // Anthropic accounting: the cache writes are added to the input tokens. + expect(payload.inputTokens).toBe(200_000) + expect(recordedAtStart).toMatchObject({ apiProtocol: "anthropic", apiProvider: mockApiConfig.apiProvider }) + expect(recorded).toMatchObject({ apiProtocol: "anthropic", apiProvider: mockApiConfig.apiProvider }) + }) + }) + describe("request continuation", () => { it("sends the no-tool-use reminder in a second request when the first response has no tool use", async () => { const task = await createTaskWithManualRetries() From f62f9c385b8bdaadbaa77c0310be7b1f25b19981 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 11:03:06 -0800 Subject: [PATCH 63/76] fix(usage): the provider limits request applies the organization allow list to the profiles it queries --- ...bviewMessageHandler.providerQuotas.spec.ts | 84 ++++++++++++++++++- src/core/webview/webviewMessageHandler.ts | 5 ++ .../__tests__/providerQuotas.spec.ts | 38 +++++++++ .../language-models/providerQuotas.ts | 13 ++- 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 703a70ee12..0e2e1f0617 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -2,7 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest" -import { providerIdentifiers } from "@roo-code/types" +import { providerIdentifiers, type OrganizationAllowList } from "@roo-code/types" const quotas = vi.hoisted(() => ({ getProviderQuotas: vi.fn(), @@ -19,10 +19,13 @@ import type { ClineProvider } from "../ClineProvider" const SECRET = "sk-super-secret" +const ALLOW_ALL: OrganizationAllowList = { allowAll: true, providers: {} } + const makeProvider = (overrides: Record = {}) => ({ postMessageToWebview: vi.fn(), log: vi.fn(), + getState: vi.fn(async () => ({ organizationAllowList: ALLOW_ALL })), contextProxy: { getValue: vi.fn((key: string) => (key === "currentApiConfigName" ? "default" : undefined)) }, providerSettingsManager: { listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), @@ -92,6 +95,85 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { ) }) + describe("the organisation allow list", () => { + const onlyOpenRouter: OrganizationAllowList = { + allowAll: false, + providers: { [providerIdentifiers.openrouter]: { allowAll: true } }, + } + const profileBy = (providerForId: Record) => ({ + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + ]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ + apiProvider: providerForId[id], + apiKey: `key-${id}`, + id, + })), + }) + const request = async ( + organizationAllowList: OrganizationAllowList, + active: string, + providers: Record, + ) => { + const provider = makeProvider({ + getState: vi.fn(async () => ({ organizationAllowList })), + contextProxy: { getValue: vi.fn(() => active) }, + providerSettingsManager: profileBy(providers), + }) + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + return provider + } + const providers = { a: providerIdentifiers.openrouter, b: providerIdentifiers.anthropic } + const queried = () => quotas.getProviderQuotas.mock.calls[0][0].map((p: { id: string }) => p.id) as string[] + + it("keeps a profile it refuses out of the quota request, with its credentials", async () => { + await request(onlyOpenRouter, "work", providers) + + expect(queried()).toEqual(["a"]) + expect(JSON.stringify(quotas.getProviderQuotas.mock.calls)).not.toContain("key-b") + }) + + it("does not name a refused profile as the active one, and still names an allowed one", async () => { + await request(onlyOpenRouter, "personal", providers) // the active profile is the refused one + expect(quotas.getProviderQuotas.mock.calls[0][1].activeProfileId).toBeUndefined() + + quotas.getProviderQuotas.mockClear() + await request(onlyOpenRouter, "work", providers) + expect(quotas.getProviderQuotas.mock.calls[0][1].activeProfileId).toBe("a") + }) + + it("queries nothing for a profile of a provider the list does not name, and posts no failure for it", async () => { + const provider = await request(onlyOpenRouter, "personal", { a: providers.b, b: providers.b }) + + expect(queried()).toEqual([]) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "providerQuotas", providerQuotasFailed: false }), + ) + }) + + it("queries every profile when the list allows all", async () => { + await request(ALLOW_ALL, "personal", providers) + + expect(queried()).toEqual(["b", "a"]) + expect(quotas.getProviderQuotas.mock.calls[0][1].activeProfileId).toBe("b") + }) + + it("reports a failure, and queries nothing, when the allow list cannot be read", async () => { + const provider = makeProvider({ + getState: vi.fn(async () => { + throw new Error("state unavailable") + }), + }) + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ providerQuotas: [], providerQuotasFailed: true }), + ) + }) + }) + it("reports a failure, not an empty list, when no saved profile can be read", async () => { const provider = makeProvider({ providerSettingsManager: { diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 08e438a587..f62666aa49 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -63,6 +63,7 @@ import { } from "./rulesMessageHandler" import { changeLanguage, t } from "../../i18n" import { Package } from "../../shared/package" +import { ProfileValidator } from "../../shared/ProfileValidator" import { type RouterName, toRouterName } from "../../shared/api" import { MessageEnhancer } from "./messageEnhancer" @@ -1659,10 +1660,14 @@ export const webviewMessageHandler = async ( try { const { getProviderQuotas, loadProfilesWithActive } = await import("../../services/language-models/providerQuotas") + // The organisation allow list applies here as it does to a request: a profile it refuses is not read for its + // credentials, and no quota is fetched with them. + const { organizationAllowList } = await provider.getState() const { profiles, activeProfileId } = await loadProfilesWithActive( provider.providerSettingsManager, controller.signal, provider.contextProxy.getValue("currentApiConfigName"), + (profile) => ProfileValidator.isProfileAllowed(profile, organizationAllowList), ) providerQuotas = await getProviderQuotas(profiles, { force: message.bool === true, diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index d691cd14c7..ab6379a892 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1630,6 +1630,44 @@ describe("loadProfiles", () => { expect((await loadProfilesWithActive(source)).activeProfileId).toBeUndefined() }) + describe("the profiles that the organisation allow list refuses", () => { + const source = { + listConfig: vi.fn(async () => [ + { id: "a", name: "work" }, + { id: "b", name: "personal" }, + { id: "c", name: "spare" }, + ]), + getProfile: vi.fn( + async ({ id }: { id: string }) => ({ apiProvider: id === "b" ? "refused" : "x", apiKey: id }) as never, + ), + } + const allowedExceptRefused = (profile: { apiProvider?: string }) => profile.apiProvider !== "refused" + + it("are left out, and every profile is allowed when no predicate is given", async () => { + const filtered = await loadProfilesWithActive(source, undefined, undefined, allowedExceptRefused) + const unfiltered = await loadProfilesWithActive(source) + + expect(filtered.profiles.map((p) => p.apiKey)).toEqual(["a", "c"]) + expect(unfiltered.profiles.map((p) => p.apiKey)).toEqual(["a", "b", "c"]) + }) + + it("are not reported as the active profile, while an allowed active profile still is", async () => { + const refusedActive = await loadProfilesWithActive(source, undefined, "personal", allowedExceptRefused) + const allowedActive = await loadProfilesWithActive(source, undefined, "spare", allowedExceptRefused) + + expect(refusedActive.activeProfileId).toBeUndefined() + expect(refusedActive.profiles.map((p) => p.apiKey)).toEqual(["a", "c"]) + expect(allowedActive.activeProfileId).toBe("c") + expect(allowedActive.profiles.map((p) => p.apiKey)).toEqual(["c", "a"]) // the active one comes first + }) + + it("do not turn into a failure: the profiles were read, the list is just empty", async () => { + const result = await loadProfilesWithActive(source, undefined, "work", () => false) + + expect(result).toEqual({ profiles: [], activeProfileId: undefined }) + }) + }) + describe("a profile read that never settles", () => { afterEach(() => vi.useRealTimers()) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 87feaabee5..99f2be705e 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -668,12 +668,14 @@ function readProfile(source: ProfileSource, id: string, signal?: AbortSignal): P * rather than failing the whole refresh, but if none of the saved profiles can be read that is a failure, not "no * connected providers". The active profile (`activeName`) comes first, and its id is returned so that its own * credentials are the ones queried for its provider. Keys never leave the extension host: only formatted quota rows - * are posted to the webview. + * are posted to the webview. A profile that `isAllowed` refuses (the organisation allow list) is left out, and is not + * reported as the active one: no credential of it is queried. */ export async function loadProfilesWithActive( source: ProfileSource, signal?: AbortSignal, activeName?: string, + isAllowed: (profile: ProviderSettings) => boolean = () => true, ): Promise<{ profiles: ProviderSettings[]; activeProfileId?: string }> { // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but none is started for a // request that was already cancelled, and the profile reads are not started once the list wait has been abandoned. @@ -701,8 +703,15 @@ export async function loadProfilesWithActive( const loaded = profiles.filter((p): p is ProviderSettings => !!p) if (entries.length > 0 && loaded.length === 0) throw new Error("None of the saved profiles could be read") + const allowed = entries.flatMap((entry, index) => { + const profile = profiles[index] + return profile && isAllowed(profile) ? [{ entry, profile }] : [] + }) const activeEntry = activeName ? listed.find((entry) => entry.name === activeName) : undefined - return { profiles: loaded, activeProfileId: activeEntry?.id } + return { + profiles: allowed.map(({ profile }) => profile), + activeProfileId: allowed.find(({ entry }) => entry.id === activeEntry?.id)?.entry.id, + } } // Successful results only, per provider and URL set. A missing key, a sign-out or a From 261349cfdb0e0bcacb9430fb654c730804e20be7 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Thu, 5 Nov 2026 14:01:02 -0800 Subject: [PATCH 64/76] perf(usage): the provider limits request reads the allow list without the task history --- .../__tests__/webviewMessageHandler.providerQuotas.spec.ts | 6 ++++++ src/core/webview/webviewMessageHandler.ts | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 0e2e1f0617..bd42c8460a 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -152,6 +152,12 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { ) }) + it("reads the allow list without the task history, which it does not use", async () => { + const provider = await request(onlyOpenRouter, "work", providers) + + expect(provider.getState).toHaveBeenCalledExactlyOnceWith({ includeTaskHistory: false }) + }) + it("queries every profile when the list allows all", async () => { await request(ALLOW_ALL, "personal", providers) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index f62666aa49..a9a0cadc4a 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1661,8 +1661,8 @@ export const webviewMessageHandler = async ( const { getProviderQuotas, loadProfilesWithActive } = await import("../../services/language-models/providerQuotas") // The organisation allow list applies here as it does to a request: a profile it refuses is not read for its - // credentials, and no quota is fetched with them. - const { organizationAllowList } = await provider.getState() + // credentials, and no quota is fetched with them. Only the allow list is wanted: the task history is not. + const { organizationAllowList } = await provider.getState({ includeTaskHistory: false }) const { profiles, activeProfileId } = await loadProfilesWithActive( provider.providerSettingsManager, controller.signal, From a42498ed20aa7c0a7d21ac3165ae94dbe38c07be Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 10:34:53 -0800 Subject: [PATCH 65/76] fix(history): an edit that cannot be applied after a checkpoint restore is reported and handed back, and the cut is undone in memory; pin what queued message saves write --- .../task/__tests__/Task.persistence.spec.ts | 68 ++++++++++++++ src/core/webview/ClineProvider.ts | 31 +++++++ .../checkpointRestoreHandler.spec.ts | 88 +++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 36367ccd06..037d6f9bd4 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -1276,6 +1276,74 @@ describe("Task persistence", () => { expect(await second).toBe(true) }) + describe("what each save writes when the messages change while a write waits", () => { + const text = (value: string, ts: number): ClineMessage => ({ ts, type: "say", say: "text", text: value }) + const newTask = () => + new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + /** Every call of the write, with the messages it was given and how, in the order they were made. */ + const writes = () => + mockSaveTaskMessages.mock.calls.map(([call]) => ({ + texts: (call.messages as ClineMessage[]).map((message) => message.text), + merge: call.merge, + })) + + it("writes the messages as they are when its turn comes, with the flag it was asked with, and the overwrite last", async () => { + const release = createDeferred() + mockSaveTaskMessages.mockImplementationOnce(async () => { + await release.promise // the first write is slow to reach the disk + return [] + }) + const task = newTask() + const save = () => getTaskPersistenceAccess(task).saveClineMessages() + task.clineMessages.push(text("first", 1)) + + const first = save() // blocked on the disk, with the messages it was started with + await vi.waitFor(() => expect(mockSaveTaskMessages).toHaveBeenCalledTimes(1)) + task.clineMessages.push(text("second", 2)) + const second = save() // queued behind it, as an ordinary save (merged with what is on the disk) + const overwrite = task.overwriteClineMessages([text("replacement", 3)]) // queued behind both, authoritative + task.clineMessages.push(text("after the replacement", 4)) // a message that comes in while both wait + + release.resolve(undefined) + await Promise.all([first, second, overwrite]) + + expect(writes()).toEqual([ + // Started before any change: it has what the task had then, and is not touched by what came after. + { texts: ["first"], merge: true }, + // Its turn came after the task was given the replacement, so that is what it writes, still merged. + { texts: ["replacement", "after the replacement"], merge: true }, + // The authoritative save replaces what is stored, and keeps the message that came in meanwhile. + { texts: ["replacement", "after the replacement"], merge: false }, + ]) + }) + + it("does not give a save started earlier the messages that a later overwrite removed from the task", async () => { + const release = createDeferred() + mockSaveTaskMessages.mockImplementationOnce(async () => { + await release.promise + return [] + }) + const task = newTask() + task.clineMessages.push(text("kept", 1), text("removed", 2)) + + const first = getTaskPersistenceAccess(task).saveClineMessages() + await vi.waitFor(() => expect(mockSaveTaskMessages).toHaveBeenCalledTimes(1)) + const overwrite = task.overwriteClineMessages([text("kept", 1)]) // the edit removes the second message + release.resolve(undefined) + await Promise.all([first, overwrite]) + + const [started, authoritative] = writes() + expect(started.texts).toEqual(["kept", "removed"]) // what the task had when that write began + expect(authoritative).toEqual({ texts: ["kept"], merge: false }) // and the last word is the replacement + }) + }) + it("keeps saves in the order they were asked for", async () => { const order: number[] = [] let n = 0 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index a828c87ce1..b4b5ebcfd5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1669,6 +1669,10 @@ export class ClineProvider // Process the pending edit after a short delay to ensure the task is fully initialized setTimeout(async () => { + // What the task held before the edit truncated it, and whether the truncation reached the disk: a write + // that failed leaves the disk as it was, and the task must be left the same. + const messagesBeforeEdit = task.clineMessages + let messagesPersisted = false try { // Find the message index in the restored state const { messageIndex, apiConversationHistoryIndex } = (() => { @@ -1682,6 +1686,7 @@ export class ClineProvider if (messageIndex !== -1) { // Remove the target message and all subsequent messages await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex)) + messagesPersisted = true if (apiConversationHistoryIndex !== -1) { await task.overwriteApiConversationHistory( @@ -1698,6 +1703,32 @@ export class ClineProvider } } catch (error) { this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`) + // The pending edit was taken out of the store when this began, so it is handed back to the user, not + // lost: the failure is reported and the edited message is put back in the chat box to be sent again. + // The messages that were cut in memory are put back too when the write of the cut failed, so the + // task is what the disk says it is. + if (!messagesPersisted) { + try { + await task.overwriteClineMessages(messagesBeforeEdit, false) + } catch { + // Nothing is written by this call; there is nothing more to do for the task. + } + } + void vscode.window.showErrorMessage( + t("common:errors.checkpoint_restore_failed", { + error: error instanceof Error ? error.message : String(error), + }), + ) + try { + await this.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: pendingEdit.editedContent, + images: pendingEdit.images, + }) + } catch { + // The failure is reported above; the edit cannot be shown if the webview is gone. + } } }, 100) // Small delay to ensure task is fully ready } diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index 6195be5572..f85f4751f4 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -80,6 +80,7 @@ function createFixture() { taskEventListeners: new Map void>>(), performPreparationTasks: vi.fn<(task: Task) => Promise>(), log: vi.fn<(message: string) => void>(), + postMessageToWebview: vi.fn().mockResolvedValue(undefined), getCurrentTask: vi.fn(() => task), getTaskWithId: vi.fn().mockResolvedValue({ historyItem, @@ -409,4 +410,91 @@ describe("checkpointRestoreHandler", () => { expect(fixture.taskDouble.handleWebviewAskResponse).toHaveBeenCalledWith("messageResponse", "Accepted edit", []) expect(fixture.store.get(`task-${fixture.task.taskId}`)).toBeUndefined() }) + + describe("a pending edit that cannot be applied after the restore", () => { + const stageAndRehydrate = async () => { + vi.useFakeTimers() + await handleCheckpointRestoreOperation({ + ...fixture.config, + operation: "edit", + editData: { + editedContent: "Edited prompt", + images: ["data:image/png;base64,AAAA"], + apiConversationHistoryIndex: 0, + }, + }) + await ClineProvider.prototype.createTaskWithHistoryItem.call(fixture.provider, fixture.historyItem, { + startTask: false, + }) + await vi.advanceTimersByTimeAsync(100) + } + const handedBack = () => + expect(fixture.providerDouble.postMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "setChatBoxMessage", + text: "Edited prompt", + images: ["data:image/png;base64,AAAA"], + }) + const reported = () => + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "common:errors.checkpoint_restore_failed: Disk full", + ) + + it("reports a message write that failed, hands the edit back, and leaves the task as the disk has it", async () => { + const messagesBefore = fixture.task.clineMessages + fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce(fixture.failure) + + await stageAndRehydrate() + + reported() + handedBack() + // The cut was not stored, so it is undone in memory (and not written again). + expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenLastCalledWith(messagesBefore, false) + expect(fixture.taskDouble.overwriteApiConversationHistory).not.toHaveBeenCalled() + expect(fixture.taskDouble.handleWebviewAskResponse).not.toHaveBeenCalled() + expect(fixture.store.get(`task-${fixture.task.taskId}`)).toBeUndefined() // and it is not applied by a later start + }) + + it("reports an API history write that failed after the messages were stored, and hands the edit back", async () => { + fixture.taskDouble.overwriteApiConversationHistory.mockRejectedValueOnce(fixture.failure) + + await stageAndRehydrate() + + reported() + handedBack() + // The messages were cut on the disk already: they are not put back in memory. + expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenCalledTimes(1) + expect(fixture.taskDouble.handleWebviewAskResponse).not.toHaveBeenCalled() + }) + + it("reports an edit that could not be sent, and hands it back", async () => { + fixture.taskDouble.handleWebviewAskResponse.mockRejectedValueOnce(fixture.failure) + + await stageAndRehydrate() + + reported() + handedBack() + }) + + it("still reports it when the chat box cannot be filled", async () => { + fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce(fixture.failure) + fixture.providerDouble.postMessageToWebview.mockRejectedValue(new Error("webview gone")) + + await stageAndRehydrate() + + reported() + }) + + it("says nothing when the edit was applied", async () => { + await stageAndRehydrate() + + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + expect(fixture.providerDouble.postMessageToWebview).not.toHaveBeenCalled() + expect(fixture.taskDouble.handleWebviewAskResponse).toHaveBeenCalledWith( + "messageResponse", + "Edited prompt", + ["data:image/png;base64,AAAA"], + ) + }) + }) }) From dbe6cacd575c647575c36c9b6e844d0327beeaae Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 11:20:43 -0800 Subject: [PATCH 66/76] fix(usage,history): the CSV export is inert for semicolon and tab imports; a lost message write restores the replaced messages, with what came in meanwhile --- src/core/task/Task.ts | 19 +++- .../task/__tests__/Task.persistence.spec.ts | 89 ++++++++++++++++++- src/core/webview/ClineProvider.ts | 14 --- .../checkpointRestoreHandler.spec.ts | 20 +++-- .../misc/__tests__/export-usage-csv.spec.ts | 79 ++++++++++++++++ src/integrations/misc/export-usage-csv.ts | 13 ++- 6 files changed, 210 insertions(+), 24 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 12b8e7cdce..ea5dbfaf86 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1733,15 +1733,32 @@ export class Task extends EventEmitter implements TaskLike { * Also resets cloud sync tracking to avoid re-syncing previously synced messages. */ public async overwriteClineMessages(newMessages: ClineMessage[], persist = true) { + const replaced = this.clineMessages this.hydrateClineMessages(newMessages) + const replacement = new Set(this.clineMessages) if (persist) { // A failed write is reported to the caller: the delete, edit and checkpoint flows go on to save the // messages again, and carrying on after a lost write would leave the history entry (and its usage // breakdown) describing messages that are no longer the ones on disk. - if (!(await this.saveClineMessages(false))) throw new Error("Failed to save the messages") + if (!(await this.saveClineMessages(false))) { + // The rewind flows cut the API history only after this, so a rejection leaves it as it was: the messages + // are put back, with what came in while the write was on its way, and the two stay consistent. + this.restoreReplacedMessages(replaced, replacement) + throw new Error("Failed to save the messages") + } } } + /** + * Undoes a replacement whose write was lost: the messages that were replaced come back, and so does every message + * that was added to the task after the replacement (an update made to a message that both share is in the message + * itself), so nothing that happened while the write was on its way is dropped. + */ + private restoreReplacedMessages(replaced: ClineMessage[], replacement: ReadonlySet) { + const addedSince = this.clineMessages.filter((message) => !replacement.has(message)) + this.hydrateClineMessages([...replaced, ...addedSince]) + } + private hydrateClineMessages(messages: ClineMessage[]) { this.clineMessages = ensureMessageIdentifiers(messages) restoreTodoListForTask(this) diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 037d6f9bd4..7ecfcdcb1d 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -1013,15 +1013,100 @@ describe("Task persistence", () => { mockSaveTaskMessages.mockRejectedValueOnce(new Error("disk full")) const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) const task = newTask() + task.clineMessages.push({ ts: 1, type: "say", say: "text", text: "before" }) await expect(task.overwriteClineMessages(replacement)).rejects.toThrow("Failed to save the messages") consoleError.mockRestore() - // The messages are replaced in memory all the same, and the history entry was not written for them. - expect(task.clineMessages.map((message) => message.text)).toEqual(["replacement"]) + // The history entry was not written for them, and the messages are the ones that were on the disk. + expect(task.clineMessages.map((message) => message.text)).toEqual(["before"]) expect(mockTaskMetadata).not.toHaveBeenCalled() }) + describe("when the write is lost", () => { + const text = (value: string, ts: number): ClineMessage => ({ + ts, + type: "say", + say: "text", + text: value, + }) + /** A write that is slow to fail: it settles, as a failure, when the test says so. */ + const slowFailingWrite = () => { + let fail!: (error: Error) => void + const promise = new Promise((_resolve, reject) => (fail = reject)) + mockSaveTaskMessages.mockImplementationOnce(() => promise) + return () => fail(new Error("disk full")) + } + const texts = (task: Task) => task.clineMessages.map((message) => message.text) + + it("puts the replaced messages back, and keeps the ones that came in while the write was on its way", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + const failTheWrite = slowFailingWrite() + const task = newTask() + task.clineMessages.push(text("kept", 1), text("cut", 2)) + + const overwriting = task.overwriteClineMessages([text("kept", 1)]) // a rewind cuts the second message + const failed = expect(overwriting).rejects.toThrow("Failed to save the messages") + await vi.waitFor(() => expect(mockSaveTaskMessages).toHaveBeenCalledTimes(1)) + expect(texts(task)).toEqual(["kept"]) // while it is written, the task has the replacement + task.clineMessages.push(text("arrived meanwhile", 3)) + failTheWrite() + await failed + + expect(texts(task)).toEqual(["kept", "cut", "arrived meanwhile"]) + consoleError.mockRestore() + }) + + it("keeps an update made to a message while the write was on its way", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + const failTheWrite = slowFailingWrite() + const task = newTask() + task.clineMessages.push(text("kept", 1), text("cut", 2)) + + const failed = expect(task.overwriteClineMessages([task.clineMessages[0]])).rejects.toThrow( + "Failed to save the messages", + ) + await vi.waitFor(() => expect(mockSaveTaskMessages).toHaveBeenCalledTimes(1)) + task.clineMessages[0].text = "kept, and then updated" // the message is shared by both arrays + failTheWrite() + await failed + + expect(texts(task)).toEqual(["kept, and then updated", "cut"]) + consoleError.mockRestore() + }) + + it("leaves the messages and the API history of a rewind consistent: both as they were", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + mockSaveTaskMessages.mockRejectedValueOnce(new Error("disk full")) + const task = newTask() + task.clineMessages.push(text("first", 1), text("second", 2), text("third", 3)) + const history = [ + { ts: 1, role: "user" as const, content: [{ type: "text" as const, text: "first" }] }, + { ts: 2, role: "assistant" as const, content: [{ type: "text" as const, text: "second" }] }, + { ts: 3, role: "user" as const, content: [{ type: "text" as const, text: "third" }] }, + ] + task.apiConversationHistory.push(...history) + + await expect(task.messageManager.rewindToTimestamp(2)).rejects.toThrow( + "Failed to save the messages", + ) + + expect(texts(task)).toEqual(["first", "second", "third"]) + expect(task.apiConversationHistory.map((message) => message.ts)).toEqual([1, 2, 3]) + consoleError.mockRestore() + }) + + it("does not touch the messages when the write was stored", async () => { + mockSaveTaskMessages.mockResolvedValueOnce([text("kept", 1)]) + const task = newTask() + task.clineMessages.push(text("kept", 1), text("cut", 2)) + + await task.overwriteClineMessages([task.clineMessages[0]]) + + expect(texts(task)).toEqual(["kept"]) + }) + }) + it("resolves once the messages and the history entry are written", async () => { mockSaveTaskMessages.mockResolvedValueOnce(replacement) const task = newTask() diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b4b5ebcfd5..0b76c76f83 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1669,10 +1669,6 @@ export class ClineProvider // Process the pending edit after a short delay to ensure the task is fully initialized setTimeout(async () => { - // What the task held before the edit truncated it, and whether the truncation reached the disk: a write - // that failed leaves the disk as it was, and the task must be left the same. - const messagesBeforeEdit = task.clineMessages - let messagesPersisted = false try { // Find the message index in the restored state const { messageIndex, apiConversationHistoryIndex } = (() => { @@ -1686,7 +1682,6 @@ export class ClineProvider if (messageIndex !== -1) { // Remove the target message and all subsequent messages await task.overwriteClineMessages(task.clineMessages.slice(0, messageIndex)) - messagesPersisted = true if (apiConversationHistoryIndex !== -1) { await task.overwriteApiConversationHistory( @@ -1705,15 +1700,6 @@ export class ClineProvider this.log(`[createTaskWithHistoryItem] Error processing pending edit: ${error}`) // The pending edit was taken out of the store when this began, so it is handed back to the user, not // lost: the failure is reported and the edited message is put back in the chat box to be sent again. - // The messages that were cut in memory are put back too when the write of the cut failed, so the - // task is what the disk says it is. - if (!messagesPersisted) { - try { - await task.overwriteClineMessages(messagesBeforeEdit, false) - } catch { - // Nothing is written by this call; there is nothing more to do for the task. - } - } void vscode.window.showErrorMessage( t("common:errors.checkpoint_restore_failed", { error: error instanceof Error ? error.message : String(error), diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index f85f4751f4..5b3839ea3e 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -440,16 +440,16 @@ describe("checkpointRestoreHandler", () => { "common:errors.checkpoint_restore_failed: Disk full", ) - it("reports a message write that failed, hands the edit back, and leaves the task as the disk has it", async () => { - const messagesBefore = fixture.task.clineMessages + it("reports a message write that failed, and hands the edit back", async () => { fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce(fixture.failure) await stageAndRehydrate() reported() handedBack() - // The cut was not stored, so it is undone in memory (and not written again). - expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenLastCalledWith(messagesBefore, false) + // Undoing the cut in memory is the task's own business (`overwriteClineMessages` does it when its write is lost): + // the provider does not write, or replace, the messages again. + expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenCalledTimes(1) expect(fixture.taskDouble.overwriteApiConversationHistory).not.toHaveBeenCalled() expect(fixture.taskDouble.handleWebviewAskResponse).not.toHaveBeenCalled() expect(fixture.store.get(`task-${fixture.task.taskId}`)).toBeUndefined() // and it is not applied by a later start @@ -462,7 +462,6 @@ describe("checkpointRestoreHandler", () => { reported() handedBack() - // The messages were cut on the disk already: they are not put back in memory. expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenCalledTimes(1) expect(fixture.taskDouble.handleWebviewAskResponse).not.toHaveBeenCalled() }) @@ -476,6 +475,17 @@ describe("checkpointRestoreHandler", () => { handedBack() }) + it("reports a failure that is not an Error as the text it is", async () => { + fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce("the disk is full") + + await stageAndRehydrate() + + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith( + "common:errors.checkpoint_restore_failed: the disk is full", + ) + handedBack() + }) + it("still reports it when the chat box cannot be filled", async () => { fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce(fixture.failure) fixture.providerDouble.postMessageToWebview.mockRejectedValue(new Error("webview gone")) diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index 497b4104f0..7ea18c229b 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -164,6 +164,85 @@ describe("neutralizeCsvFormulas", () => { expect(neutralizeCsvFormulas(`a,"${cell}",b`)).toBe(`a,"'${cell}",b`) }) + // A spreadsheet that does not split on commas: it reads the file with its own list separator, and recognises a quote only + // at the start of a field. This reads a row that way. + const fieldsSplitOn = (delimiter: string, csv: string) => { + const fields: string[] = [] + for (const line of csv.split("\n")) { + let field = "" + let inQuotes = false + let atStart = true + for (let i = 0; i < line.length; i++) { + const char = line[i] + if (inQuotes) { + if (char !== '"') field += char + else if (line[i + 1] === '"') ((field += '"'), i++) + else inQuotes = false + } else if (char === '"' && atStart) { + inQuotes = true + atStart = false + } else if (char === delimiter) { + fields.push(field) + field = "" + atStart = true + } else { + field += char + atStart = false + } + } + fields.push(field) + } + return fields + } + const startsAFormula = (field: string) => /^(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r]/u.test(field) + + describe("for a spreadsheet that splits on a semicolon or a tab", () => { + it.each([ + ["a formula after a semicolon in a cell", 'task,"x;=HYPERLINK(""http://e"",""go"")",1'], + ["a formula after a semicolon in a cell that is not quoted", "task,x;=1+1,1"], + ["a formula after a semicolon in a cell, with the quote in the middle of the field", 'a,"b;=1+1",c'], + ["a sign after a semicolon", "task,x;-2+3,1"], + ["an at sign after a semicolon", "task,x;@SUM(1),1"], + ["a formula after a tab in a cell", "task,x\t=1+1,1"], + ["a formula after several separators", "task,x;;\t=1+1,1"], + ["a formula after a semicolon and an invisible character", "task,x;\u0000=1+1,1"], + ["a cell that is a semicolon and a formula", "task,;=1+1,1"], + ])("leaves no field that is a formula, whatever the separator: %s", (_name, csv) => { + const safe = neutralizeCsvFormulas(csv)! + + for (const delimiter of [",", ";", "\t"]) { + for (const field of fieldsSplitOn(delimiter, safe)) { + expect( + startsAFormula(field), + `${JSON.stringify(field)} split on ${JSON.stringify(delimiter)}`, + ).toBe(false) + } + } + }) + + it("shows that the same text is a formula for a semicolon import when it is left as it was written", () => { + const written = "task,x;=1+1,1" + + expect(fieldsSplitOn(";", written).some(startsAFormula)).toBe(true) + }) + + it("changes nothing for a semicolon or a tab that is not followed by a formula start", () => { + const csv = 'task,"a; b;c\td",1' + + expect(neutralizeCsvFormulas(csv)).toBe(csv) + }) + + it("writes the text that follows a semicolon as inert text from exportUsageCsv, whatever the webview sent", async () => { + const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri + showSaveDialog.mockResolvedValue(saveUri) + + await exportUsageCsv("date,task\n2026-06-01,\"x;=cmd|' /c calc'!A0\"", "all") + + const written = writeFile.mock.calls[0][1].toString() + expect(fieldsSplitOn(";", written).some(startsAFormula)).toBe(false) + }) + }) + it("neutralises every row and every column, not only the first row or the task text", () => { const out = neutralizeCsvFormulas('h1,h2\n1,"=HYPERLINK(""http://x"")"\n"@a",2\n=3,4') diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts index 64784d6ea2..ce7c25634a 100644 --- a/src/integrations/misc/export-usage-csv.ts +++ b/src/integrations/misc/export-usage-csv.ts @@ -11,12 +11,20 @@ const USAGE_RANGES: readonly string[] = ["today", "7d", "30d", "all"] */ const FORMULA_START = /^(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r]/u +/** + * A spreadsheet may split a row on a semicolon (the list separator of many locales) or a tab as well as on a comma, and + * it recognises a quote only at the start of a field: so the text after a semicolon or a tab inside a cell can start a + * field of its own, whatever quotes surround the cell. A formula start there is broken as well. + */ +const FORMULA_AFTER_FIELD_SEPARATOR = /([;\t])(?=(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r])/gu + /** The most text the host writes for an export: ten years of daily tasks is far below it, a runaway message is not. */ const MAX_USAGE_CSV_LENGTH = 20_000_000 /** * Makes every cell of a CSV inert for a spreadsheet: a cell that starts with = + - @ (or a tab or carriage return) is - * evaluated as a formula by a spreadsheet, so it gets a leading apostrophe. The text is parsed and written again, so + * evaluated as a formula by a spreadsheet, so it gets a leading apostrophe, and so does the text that follows a + * semicolon or a tab inside a cell (where a spreadsheet that splits on those starts a new field). The text is parsed and written again, so * a cell is judged by its value, however it was quoted. Returns `undefined` for text that is not well-formed CSV * (a quote that is never closed). Text that is already inert comes back unchanged. */ @@ -72,7 +80,8 @@ export function neutralizeCsvFormulas(csv: string): string | undefined { .map((cells) => cells .map((cell) => { - const safe = FORMULA_START.test(cell.value) ? `'${cell.value}` : cell.value + const started = FORMULA_START.test(cell.value) ? `'${cell.value}` : cell.value + const safe = started.replace(FORMULA_AFTER_FIELD_SEPARATOR, "$1'") return cell.quoted || /[",\n\r]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe }) .join(","), From fc6ca5c4acb9065c422ab315a9911be66fb3f07d Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 12:48:32 -0800 Subject: [PATCH 67/76] test: no comma expression in the CSV parser of the tests (lint) --- src/integrations/misc/__tests__/export-usage-csv.spec.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index 7ea18c229b..cc67889e30 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -176,8 +176,10 @@ describe("neutralizeCsvFormulas", () => { const char = line[i] if (inQuotes) { if (char !== '"') field += char - else if (line[i + 1] === '"') ((field += '"'), i++) - else inQuotes = false + else if (line[i + 1] === '"') { + field += '"' + i++ + } else inQuotes = false } else if (char === '"' && atStart) { inQuotes = true atStart = false From 07a612d8a6278ca77e1fb12153d92efbbdf05c04 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 13:27:16 -0800 Subject: [PATCH 68/76] fix(usage): neutralize formulas after line breaks inside CSV cells in linear time; restore messages inside the lost write before the queue advances --- src/core/task/Task.ts | 19 +++-- .../task/__tests__/Task.persistence.spec.ts | 26 +++++++ src/core/task/__tests__/Task.spec.ts | 2 + .../misc/__tests__/export-usage-csv.spec.ts | 73 ++++++++++++++++++- src/integrations/misc/export-usage-csv.ts | 25 ++++--- 5 files changed, 126 insertions(+), 19 deletions(-) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index ea5dbfaf86..c40c350179 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1740,10 +1740,11 @@ export class Task extends EventEmitter implements TaskLike { // A failed write is reported to the caller: the delete, edit and checkpoint flows go on to save the // messages again, and carrying on after a lost write would leave the history entry (and its usage // breakdown) describing messages that are no longer the ones on disk. - if (!(await this.saveClineMessages(false))) { - // The rewind flows cut the API history only after this, so a rejection leaves it as it was: the messages - // are put back, with what came in while the write was on its way, and the two stay consistent. - this.restoreReplacedMessages(replaced, replacement) + // The rewind flows cut the API history only after this, so a rejection leaves it as it was: the messages are put + // back, with what came in while the write was on its way, and the two stay consistent. They are put back by the + // write itself, before the next save in the line starts: one behind this would otherwise copy the cut messages, + // and a message that was never on the disk would be lost to it. + if (!(await this.saveClineMessages(false, () => this.restoreReplacedMessages(replaced, replacement)))) { throw new Error("Failed to save the messages") } } @@ -1874,7 +1875,7 @@ export class Task extends EventEmitter implements TaskLike { * snapshot is merged with the on-disk record. `overwriteClineMessages` passes `false` * to replace the stored messages outright. */ - private async saveClineMessages(merge = true): Promise { + private async saveClineMessages(merge = true, onWriteLost?: () => void): Promise { // Two queues per task, shared by every instance of the task. The message writes go one at a time, and a write // never waits for the metadata of the save before it: the messages are what must reach the disk, so a slow // history write cannot delay them. The metadata stage (the history entry, derived from the messages that save @@ -1882,7 +1883,9 @@ export class Task extends EventEmitter implements TaskLike { // were asked for: callers that do not wait for a save (the fire-and-forget ones) would otherwise let an older // save's metadata land after a newer save's and replace it, an explicit clear of the usage breakdown included. // A save that fails does not hold up the next one. - const result = await enqueueTaskSave(messageWriteQueues, this.taskId, () => this.writeClineMessages(merge)) + const result = await enqueueTaskSave(messageWriteQueues, this.taskId, () => + this.writeClineMessages(merge, onWriteLost), + ) if (!result.saved) return false await enqueueTaskSave(taskMetadataQueues, this.taskId, () => this.writeTaskMetadata(result.persistedMessages)) @@ -1892,6 +1895,7 @@ export class Task extends EventEmitter implements TaskLike { /** The message write: `saved` is whether it reached the disk, and `persistedMessages` what was actually stored. */ private async writeClineMessages( merge: boolean, + onWriteLost?: () => void, ): Promise<{ saved: false } | { saved: true; persistedMessages: ClineMessage[] | undefined }> { // What was actually persisted: with `merge`, newer messages another host wrote are folded in. The metadata // (including the usage breakdown) is computed from it, so a stale local snapshot cannot clear usage that @@ -1906,6 +1910,9 @@ export class Task extends EventEmitter implements TaskLike { return { saved: true, persistedMessages } } catch (error) { console.error("Failed to save Roo messages:", error) + // Still inside the queued work: what the save asked to put right is put right before the next save in the line + // takes its copy of the messages. + onWriteLost?.() return { saved: false } } } diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 7ecfcdcb1d..b816cb0305 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -1057,6 +1057,32 @@ describe("Task persistence", () => { consoleError.mockRestore() }) + it("restores the messages before the save queued behind the lost write starts, so it does not write the cut ones", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + const failTheWrite = slowFailingWrite() + mockSaveTaskMessages.mockResolvedValue([]) + const task = newTask() + task.clineMessages.push(text("on the disk", 1), text("not on the disk yet", 2)) // the second was never saved + + const overwriting = task.overwriteClineMessages([task.clineMessages[0]]) // a rewind cuts it + const failed = expect(overwriting).rejects.toThrow("Failed to save the messages") + await vi.waitFor(() => expect(mockSaveTaskMessages).toHaveBeenCalledTimes(1)) + const queued = getTaskPersistenceAccess(task).saveClineMessages() // an ordinary save, behind the lost write + failTheWrite() + await failed + await queued + + const [, queuedWrite] = mockSaveTaskMessages.mock.calls + // Nothing on the disk has that message: the write behind the lost one must not be the last to have a say + // with the messages cut. + expect((queuedWrite[0].messages as ClineMessage[]).map((message) => message.text)).toEqual([ + "on the disk", + "not on the disk yet", + ]) + expect(texts(task)).toEqual(["on the disk", "not on the disk yet"]) + consoleError.mockRestore() + }) + it("keeps an update made to a message while the write was on its way", async () => { const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) const failTheWrite = slowFailingWrite() diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 881826ac82..90bc49a8e8 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -590,6 +590,8 @@ describe("Cline", () => { it("prices a request with the protocol it started with when the profile is switched while its usage is drained", async () => { const task = await createTaskWithManualRetries() const captureLlmCompletion = vi.spyOn(TelemetryService.instance, "captureLlmCompletion") + // The spy is shared with the tests before this one: their calls (this token count included) are not this test's. + captureLlmCompletion.mockClear() vi.spyOn(task, "ask").mockResolvedValue({ response: "yesButtonClicked" } satisfies TaskAskResult) vi.spyOn(task, "attemptApiRequest") diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index cc67889e30..1c600a6f15 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -154,8 +154,8 @@ describe("neutralizeCsvFormulas", () => { ["1-2"], ["\uFEFFtext"], ["\u0000text"], // invisible characters alone do not make a formula - ["\n=1"], // a line feed is text: it is not skipped like a control character - ["\u0000\n=1"], + ["line\ntext"], // a line break inside a cell is text, as long as what follows it is not a formula + ["\nnot a formula"], ])("leaves the cell %j alone", (cell) => { expect(neutralizeCsvFormulas(`a,"${cell}",b`)).toBe(`a,"${cell}",b`) }) @@ -198,6 +198,72 @@ describe("neutralizeCsvFormulas", () => { } const startsAFormula = (field: string) => /^(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r]/u.test(field) + describe("for a line break inside a cell", () => { + it.each([ + ["the crafted message", 'date,task\n2026,"note\n=1+1\nend"'], + ["after a carriage return", 'date,task\n2026,"note\r=1+1\rend"'], + ["after a Windows line ending", 'date,task\n2026,"note\r\n@SUM(1)\r\nend"'], + ["after several line breaks and an invisible character", 'date,task\n2026,"a\n\n\u0000-2+3\nend"'], + ["in a cell that is not quoted by the webview but is multiline once written", 'date,task\n2026,"x\n+1"'], + ])("is inert in every line, for comma, semicolon and tab imports: %s", (_name, csv) => { + const safe = neutralizeCsvFormulas(csv)! + + // An application that ends a row at every line break whatever quotes are open reads each line as a row. + for (const delimiter of [",", ";", "\t"]) { + for (const field of fieldsSplitOn(delimiter, safe.replace(/\r(?!\n)/g, "\n").replace(/\r\n/g, "\n"))) { + expect( + startsAFormula(field), + `${JSON.stringify(field)} split on ${JSON.stringify(delimiter)}`, + ).toBe(false) + } + } + }) + + it("shows that the crafted message was a formula on its second line when it was written as it came", () => { + const written = 'date,task\n2026,"note\n=1+1\nend"' + + expect(fieldsSplitOn(",", written).some(startsAFormula)).toBe(true) // a line that starts with = + }) + + it("writes the crafted message as inert text from exportUsageCsv", async () => { + const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri + showSaveDialog.mockResolvedValue(saveUri) + + await exportUsageCsv('date,task\n2026,"note\n=1+1\nend"', "all") + + const written = writeFile.mock.calls[0][1].toString() + for (const delimiter of [",", ";", "\t"]) { + expect(fieldsSplitOn(delimiter, written).some(startsAFormula)).toBe(false) + } + }) + }) + + describe("on text that is built to be slow", () => { + const secondsFor = (csv: string) => { + const started = performance.now() + neutralizeCsvFormulas(csv) + return (performance.now() - started) / 1000 + } + + it.each([ + ["a long run of tabs in a cell", `a,"${"\t".repeat(200_000)}",b`], + ["a long run of tabs and semicolons", `a,"${";\t".repeat(100_000)}x",b`], + ["a long run of line breaks", `a,"${"\n".repeat(200_000)}",b`], + ["a long run of invisible characters between separators", `a,"${";\u0000".repeat(100_000)}x",b`], + ["a long run of tabs followed by a formula", `a,"${"\t".repeat(200_000)}=1",b`], + ])("takes a pass over the text, not a pass for every separator: %s", (_name, csv) => { + expect(secondsFor(csv)).toBeLessThan(2) // quadratic scanning of 200 000 separators takes minutes + }) + + it("still breaks every formula start in such a run", () => { + const safe = neutralizeCsvFormulas(`a,"${"\t".repeat(1000)}=1",b`)! + + for (const delimiter of [",", ";", "\t"]) { + expect(fieldsSplitOn(delimiter, safe).some(startsAFormula)).toBe(false) + } + }) + }) + describe("for a spreadsheet that splits on a semicolon or a tab", () => { it.each([ ["a formula after a semicolon in a cell", 'task,"x;=HYPERLINK(""http://e"",""go"")",1'], @@ -253,7 +319,8 @@ describe("neutralizeCsvFormulas", () => { it("keeps commas, doubled quotes and newlines inside a quoted cell, and judges the cell by its value", () => { expect(neutralizeCsvFormulas('"a,b","say ""hi""\nthen go"')).toBe('"a,b","say ""hi""\nthen go"') - expect(neutralizeCsvFormulas('"x\n=1+1"')).toBe('"x\n=1+1"') // a formula only starts a cell + // A line break inside a cell can end a row for an application that ignores the quotes: what follows it is a start too. + expect(neutralizeCsvFormulas('"x\n=1+1"')).toBe('"x\n\'=1+1"') }) it("reads a Windows line ending as one row end", () => { diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts index ce7c25634a..575600724d 100644 --- a/src/integrations/misc/export-usage-csv.ts +++ b/src/integrations/misc/export-usage-csv.ts @@ -5,18 +5,23 @@ import * as vscode from "vscode" const USAGE_RANGES: readonly string[] = ["today", "7d", "30d", "all"] /** - * What starts a cell that a spreadsheet may evaluate: = + - @, a tab or a carriage return, and the same after - * invisible characters that an application may skip: control characters such as NUL (a line feed is text, not - * skipped) and format characters such as a byte order mark or the zero-width ones. + * The characters that an application may skip before it reads a formula: control characters such as NUL and format + * characters such as a byte order mark or the zero-width ones. A tab, a carriage return and a line feed are not among + * them: the first two start a formula themselves, and the line feed ends a line. Keeping them out also keeps the scans + * below from overlapping, so they cost a pass over the text, not a pass for every separator. */ -const FORMULA_START = /^(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r]/u +const SKIPPED = String.raw`(?![\t\r\n])[\p{Cc}\p{Cf}]` + +/** What starts a cell that a spreadsheet may evaluate: = + - @, a tab or a carriage return, after the characters it skips. */ +const FORMULA_START = new RegExp(`^(?:${SKIPPED})*[=+\\-@\\t\\r]`, "u") /** - * A spreadsheet may split a row on a semicolon (the list separator of many locales) or a tab as well as on a comma, and - * it recognises a quote only at the start of a field: so the text after a semicolon or a tab inside a cell can start a - * field of its own, whatever quotes surround the cell. A formula start there is broken as well. + * A spreadsheet may split a row on a semicolon (the list separator of many locales) or a tab as well as on a comma, may + * end a row at every line break whatever quotes are open, and recognises a quote only at the start of a field: so the + * text after a semicolon, a tab or a line break inside a cell can start a field or a row of its own, whatever quotes + * surround the cell. A formula start there is broken as well. */ -const FORMULA_AFTER_FIELD_SEPARATOR = /([;\t])(?=(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r])/gu +const FORMULA_AFTER_SEPARATOR = new RegExp(`([;\\t\\r\\n])(?=(?:${SKIPPED})*[=+\\-@\\t\\r])`, "gu") /** The most text the host writes for an export: ten years of daily tasks is far below it, a runaway message is not. */ const MAX_USAGE_CSV_LENGTH = 20_000_000 @@ -24,7 +29,7 @@ const MAX_USAGE_CSV_LENGTH = 20_000_000 /** * Makes every cell of a CSV inert for a spreadsheet: a cell that starts with = + - @ (or a tab or carriage return) is * evaluated as a formula by a spreadsheet, so it gets a leading apostrophe, and so does the text that follows a - * semicolon or a tab inside a cell (where a spreadsheet that splits on those starts a new field). The text is parsed and written again, so + * semicolon, a tab or a line break inside a cell (where a spreadsheet that splits on those starts a new field or row). The text is parsed and written again, so * a cell is judged by its value, however it was quoted. Returns `undefined` for text that is not well-formed CSV * (a quote that is never closed). Text that is already inert comes back unchanged. */ @@ -81,7 +86,7 @@ export function neutralizeCsvFormulas(csv: string): string | undefined { cells .map((cell) => { const started = FORMULA_START.test(cell.value) ? `'${cell.value}` : cell.value - const safe = started.replace(FORMULA_AFTER_FIELD_SEPARATOR, "$1'") + const safe = started.replace(FORMULA_AFTER_SEPARATOR, "$1'") return cell.quoted || /[",\n\r]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe }) .join(","), From 4da346b7c10b0da970b1ad245ce6de0cc383b775 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 13:29:23 -0800 Subject: [PATCH 69/76] fix(usage): read all profiles for the provider limits in one locked read, with one deadline --- src/core/config/ProviderSettingsManager.ts | 21 +++++ .../__tests__/ProviderSettingsManager.spec.ts | 31 +++++++ .../__tests__/providerQuotas.spec.ts | 81 +++++++++++++++++++ .../language-models/providerQuotas.ts | 72 ++++++++++++----- 4 files changed, 186 insertions(+), 19 deletions(-) diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 6a1afc1ad9..7a1adc2cb1 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -416,6 +416,27 @@ export class ProviderSettingsManager { } } + /** + * Every profile with its name, read in one locked load (a `getProfile` per profile would load and parse all of them + * again for each, one lock turn after the other). A profile without an id is left out: it cannot be asked for by id. + * With `signal`, a call that is aborted by the time its turn comes reads nothing from storage (see `listConfig`). + */ + public async getAllProfiles(options?: { + signal?: AbortSignal + }): Promise> { + try { + return await this.lock(async () => { + options?.signal?.throwIfAborted() + const providerProfiles = await this.load() + return Object.entries(providerProfiles.apiConfigs).flatMap(([name, apiConfig]) => + apiConfig.id ? [{ ...apiConfig, id: apiConfig.id, name }] : [], + ) + }) + } catch (error) { + throw new Error(`Failed to get profiles: ${error instanceof Error ? error.message : error}`) + } + } + /** * Save a config with the given name. * Preserves the ID from the input 'config' object if it exists, diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 327f2f4cef..825b0ccd74 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -638,6 +638,37 @@ describe("ProviderSettingsManager", () => { load.mockRestore() }) + it("reads every profile in one locked load, with its name and id", async () => { + mockSecrets.get.mockClear() + mockSecrets.get.mockResolvedValue( + JSON.stringify({ + ...stored, + apiConfigs: { ...stored.apiConfigs, "no id": { apiProvider: providerIdentifiers.anthropic } }, + }), + ) + const load = vi.spyOn(providerSettingsManager as unknown as { load(): Promise }, "load") + + const all = await providerSettingsManager.getAllProfiles() + + expect(load).toHaveBeenCalledTimes(1) // not one load, and one lock turn, per profile + expect(all).toEqual([ + { name: "default", id: "default-id" }, + { name: "other", id: "other-id" }, + ]) // a profile without an id cannot be asked for by id, so it is left out + load.mockRestore() + }) + + it("reads no profiles from storage when the signal is already aborted", async () => { + mockSecrets.get.mockClear() + mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) + + await expect(providerSettingsManager.getAllProfiles({ signal: aborted() })).rejects.toThrow( + "Failed to get profiles", + ) + + expect(mockSecrets.get).not.toHaveBeenCalled() + }) + it("reads as before when the signal is not aborted", async () => { mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) const controller = new AbortController() diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index ab6379a892..16aa194526 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1586,6 +1586,87 @@ describe("loadProfiles", () => { }) }) + describe("with a source that reads every profile at once", () => { + beforeEach(() => vi.useFakeTimers()) + afterEach(() => vi.useRealTimers()) + + const bulk = (rows: Array<{ id: string; name?: string; apiProvider?: string }>) => ({ + listConfig: vi.fn(async () => []), + getProfile: vi.fn(), + getAllProfiles: vi.fn(async () => rows as never), + }) + + it("reads once, not once per profile, and does not list or read the profiles one by one", async () => { + const source = bulk([ + { id: "a", name: "work", apiProvider: "x" }, + { id: "b", name: "personal", apiProvider: "y" }, + { id: "c", name: "spare", apiProvider: "z" }, + ]) + const controller = new AbortController() + + const result = await loadProfilesWithActive(source, controller.signal, "personal") + + expect(source.getAllProfiles).toHaveBeenCalledExactlyOnceWith({ signal: controller.signal }) + expect(source.listConfig).not.toHaveBeenCalled() + expect(source.getProfile).not.toHaveBeenCalled() + expect(result.profiles.map((profile) => profile.apiProvider)).toEqual(["y", "x", "z"]) // the active first + expect(result.activeProfileId).toBe("b") + expect(vi.getTimerCount()).toBe(0) + }) + + it("applies the allow list: a refused profile is left out and is not the active one", async () => { + const source = bulk([ + { id: "a", name: "work", apiProvider: "openrouter" }, + { id: "b", name: "personal", apiProvider: "deepseek" }, + ]) + + const result = await loadProfilesWithActive( + source, + undefined, + "personal", + (profile) => profile.apiProvider !== "deepseek", + ) + + expect(result.profiles.map((profile) => profile.apiProvider)).toEqual(["openrouter"]) + expect(result.activeProfileId).toBeUndefined() + }) + + it("fails when the one read fails, and when it takes too long", async () => { + const failing = bulk([]) + failing.getAllProfiles.mockRejectedValue(new Error("secret storage unavailable")) + await expect(loadProfilesWithActive(failing)).rejects.toThrow("secret storage unavailable") + + const hanging = bulk([]) + hanging.getAllProfiles.mockImplementation(() => new Promise(() => {})) + const loading = loadProfilesWithActive(hanging) + const failed = expect(loading).rejects.toThrow("Reading the saved profiles timed out") + await vi.advanceTimersByTimeAsync(10_000) + await failed + expect(vi.getTimerCount()).toBe(0) + }) + + it("stops waiting as soon as the request is cancelled, and starts nothing for one that was", async () => { + const controller = new AbortController() + const hanging = bulk([]) + hanging.getAllProfiles.mockImplementation(() => new Promise(() => {})) + const loading = loadProfilesWithActive(hanging, controller.signal) + const rejected = expect(loading).rejects.toMatchObject({ name: "AbortError" }) + controller.abort() + await rejected + expect(vi.getTimerCount()).toBe(0) + + const cancelled = bulk([{ id: "a", name: "work" }]) + await expect(loadProfilesWithActive(cancelled, controller.signal)).rejects.toMatchObject({ + name: "AbortError", + }) + expect(cancelled.getAllProfiles).not.toHaveBeenCalled() + }) + + it("returns an empty list when there are no saved profiles", async () => { + expect(await loadProfilesWithActive(bulk([]))).toEqual({ profiles: [], activeProfileId: undefined }) + }) + }) + it("returns an empty list when there are no saved profiles", async () => { const source = { listConfig: vi.fn(async () => []), getProfile: vi.fn() } expect(await loadProfiles(source)).toEqual([]) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 99f2be705e..fefee72bf5 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -604,6 +604,14 @@ export interface ProfileSource { /** With `signal`, a read that is aborted before it starts reads nothing: it cannot be stopped once it has. */ listConfig(options?: { signal?: AbortSignal }): Promise<{ id: string; name?: string }[]> getProfile(params: { id: string }, options?: { signal?: AbortSignal }): Promise + /** + * Every profile, read in one turn of the settings lock. Without it, each profile is read on its own (a source that + * loads everything for every read then takes one lock turn per profile, and a later read can time out behind the + * earlier ones). + */ + getAllProfiles?(options?: { + signal?: AbortSignal + }): Promise> } /** How long one profile read may take. A read that never settles is skipped, so the others still produce a response. */ @@ -679,6 +687,49 @@ export async function loadProfilesWithActive( ): Promise<{ profiles: ProviderSettings[]; activeProfileId?: string }> { // Stops waiting as soon as `signal` aborts. The reads themselves cannot be cancelled, but none is started for a // request that was already cancelled, and the profile reads are not started once the list wait has been abandoned. + const readAll = source.getAllProfiles?.bind(source) + const rows = await (readAll ? readAllProfiles(readAll, signal) : readEachProfile(source, signal)) + const entries = activeName + ? [...rows.filter((row) => row.name === activeName), ...rows.filter((row) => row.name !== activeName)] + : rows + const loaded = entries.filter((row) => !!row.profile) + if (entries.length > 0 && loaded.length === 0) throw new Error("None of the saved profiles could be read") + + const allowed = entries.flatMap((entry) => (entry.profile && isAllowed(entry.profile) ? [entry] : [])) + const activeEntry = activeName ? rows.find((row) => row.name === activeName) : undefined + return { + profiles: allowed.map(({ profile }) => profile as ProviderSettings), + activeProfileId: allowed.find((entry) => entry.id === activeEntry?.id)?.id, + } +} + +interface ProfileRow { + id: string + name?: string + /** Undefined when the profile could not be read. */ + profile?: ProviderSettings +} + +/** All the profiles in one read: one lock turn and one deadline, so the number of profiles does not add up. */ +async function readAllProfiles( + readAll: NonNullable, + signal?: AbortSignal, +): Promise { + const all = await until( + withDeadline( + () => readAll({ signal }), + PROFILE_READ_TIMEOUT_MS, + "Reading the saved profiles timed out", + signal, + ), + signal, + () => {}, + ) + return all.map((profile) => ({ id: profile.id, name: profile.name, profile })) +} + +/** For a source without a bulk read: the list, then a read for each profile. */ +async function readEachProfile(source: ProfileSource, signal?: AbortSignal): Promise { const listed = await until( withDeadline( () => source.listConfig({ signal }), @@ -689,29 +740,12 @@ export async function loadProfilesWithActive( signal, () => {}, ) - const entries = activeName - ? [ - ...listed.filter((entry) => entry.name === activeName), - ...listed.filter((entry) => entry.name !== activeName), - ] - : listed const profiles = await until( - Promise.all(entries.map((entry) => readProfile(source, entry.id, signal))), + Promise.all(listed.map((entry) => readProfile(source, entry.id, signal))), signal, () => {}, ) - const loaded = profiles.filter((p): p is ProviderSettings => !!p) - if (entries.length > 0 && loaded.length === 0) throw new Error("None of the saved profiles could be read") - - const allowed = entries.flatMap((entry, index) => { - const profile = profiles[index] - return profile && isAllowed(profile) ? [{ entry, profile }] : [] - }) - const activeEntry = activeName ? listed.find((entry) => entry.name === activeName) : undefined - return { - profiles: allowed.map(({ profile }) => profile), - activeProfileId: allowed.find(({ entry }) => entry.id === activeEntry?.id)?.entry.id, - } + return listed.map((entry, index) => ({ id: entry.id, name: entry.name, profile: profiles[index] })) } // Successful results only, per provider and URL set. A missing key, a sign-out or a From a73732ba73d95d201afc4518530cb4b9a6e6ee24 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 13:50:11 -0800 Subject: [PATCH 70/76] fix(usage): remember the folder of the last usage export; start the task again when a checkpoint restore cannot rewind the conversation; guard the provider-limit date locale; test the bulk profile read through the handler --- packages/types/src/global-settings.ts | 1 + .../checkpoints/ConversationRewindError.ts | 11 ++ .../checkpoints/__tests__/checkpoint.test.ts | 36 ++++++ src/core/checkpoints/index.ts | 59 +++++---- .../task/__tests__/Task.persistence.spec.ts | 32 +++++ .../checkpointRestoreHandler.spec.ts | 57 ++++++--- ...bviewMessageHandler.providerQuotas.spec.ts | 120 ++++++++++++++++++ .../__tests__/webviewMessageHandler.spec.ts | 10 +- src/core/webview/checkpointRestoreHandler.ts | 8 +- src/core/webview/webviewMessageHandler.ts | 2 +- .../misc/__tests__/export-usage-csv.spec.ts | 85 ++++++++++--- src/integrations/misc/export-usage-csv.ts | 19 ++- .../components/settings/ProviderLimits.tsx | 7 +- .../src/components/settings/UsageSettings.tsx | 13 +- .../__tests__/ProviderLimits.spec.tsx | 31 +++++ .../src/utils/__tests__/dateLocale.spec.ts | 21 +++ webview-ui/src/utils/dateLocale.ts | 12 ++ 17 files changed, 442 insertions(+), 82 deletions(-) create mode 100644 src/core/checkpoints/ConversationRewindError.ts create mode 100644 webview-ui/src/utils/__tests__/dateLocale.spec.ts create mode 100644 webview-ui/src/utils/dateLocale.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index f5757a51c7..2a6654b78f 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -309,6 +309,7 @@ export const globalSettingsSchema = z.object({ lastSettingsExportPath: z.string().optional(), lastTaskExportPath: z.string().optional(), lastImageSavePath: z.string().optional(), + lastUsageExportPath: z.string().optional(), /** * Path to worktree to auto-open after switching workspaces. diff --git a/src/core/checkpoints/ConversationRewindError.ts b/src/core/checkpoints/ConversationRewindError.ts new file mode 100644 index 0000000000..a7279438f0 --- /dev/null +++ b/src/core/checkpoints/ConversationRewindError.ts @@ -0,0 +1,11 @@ +/** + * The files were put back to the checkpoint, but the conversation could not be rewound to it (the messages could not be + * saved, and were left as they were). The task is started again from what is saved, so the workspace and the task are + * not left apart with the task stopped; the caller still has to tell the user, and may keep an edit for it. + */ +export class ConversationRewindError extends Error { + constructor(override readonly cause: unknown) { + super(cause instanceof Error ? cause.message : String(cause)) + this.name = "ConversationRewindError" + } +} diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 1d56126f0a..7a842d84a9 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" import { checkpointSave, checkpointRestore, checkpointDiff, getCheckpointService } from "../index" +import { ConversationRewindError } from "../ConversationRewindError" import { MessageManager } from "../../message-manager" import { RepoPerTaskCheckpointService } from "../../../services/checkpoints" import * as vscode from "vscode" @@ -257,6 +258,41 @@ describe("Checkpoint functionality", () => { expect(mockProvider.cancelTask).toHaveBeenCalled() }) + it.each(["delete", "edit"] as const)( + "starts the task again, and reports the failure, when the files were restored but the messages could not be saved (%s)", + async (operation) => { + const failure = new Error("Failed to save the messages") + mockTask.overwriteClineMessages.mockRejectedValueOnce(failure) + + const restoring = checkpointRestore(mockTask, { + ts: 2, + commitHash: RESTORE_HASH, + mode: "restore", + operation, + }) + + await expect(restoring).rejects.toBeInstanceOf(ConversationRewindError) + await expect(restoring).rejects.toMatchObject({ + message: "Failed to save the messages", + cause: failure, + }) + expect(mockCheckpointService.restoreCheckpoint).toHaveBeenCalledWith(RESTORE_HASH) // the files are back + // The messages were left as they were, so the history is not cut, and the task is started again (once). + expect(mockTask.overwriteApiConversationHistory).not.toHaveBeenCalled() + expect(mockProvider.cancelTask).toHaveBeenCalledTimes(1) + }, + ) + + it("does not start the task again when the files could not be restored", async () => { + mockCheckpointService.restoreCheckpoint.mockRejectedValueOnce(new Error("no such commit")) + + await expect( + checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "restore", operation: "edit" }), + ).rejects.toThrow("no such commit") + + expect(mockProvider.cancelTask).not.toHaveBeenCalled() + }) + it("should handle preview mode without modifying messages", async () => { await checkpointRestore(mockTask, { ts: 2, diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 0bf73a726e..6005a6969c 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -17,6 +17,7 @@ import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints" import { assertRecordedCheckpoint } from "./recordedCheckpoint" +import { ConversationRewindError } from "./ConversationRewindError" const WARNING_THRESHOLD_MS = 5000 @@ -262,30 +263,13 @@ export async function checkpointRestore( await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash }) if (mode === "restore") { - // Calculate metrics from messages that will be deleted (must be done before rewind) - const deletedMessages = task.clineMessages.slice(index + 1) - - const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics( - task.combineMessages(deletedMessages), - ) - - // Use MessageManager to properly handle context-management events - // This ensures orphaned Summary messages and truncation markers are cleaned up - await task.messageManager.rewindToTimestamp(ts, { - includeTargetMessage: operation === "edit", - }) - - // Report the deleted API request metrics - await task.say( - "api_req_deleted", - JSON.stringify({ - tokensIn: totalTokensIn, - tokensOut: totalTokensOut, - cacheWrites: totalCacheWrites, - cacheReads: totalCacheReads, - cost: totalCost, - } satisfies ClineApiReqInfo), - ) + try { + await rewindConversation(task, ts, index, operation) + } catch (error) { + // The workspace is already restored: the task still has to be started again, as after a rewind that worked. + provider?.cancelTask() + throw new ConversationRewindError(error) + } } // The task is already cancelled by the provider beforehand, but we @@ -305,6 +289,33 @@ export async function checkpointRestore( } } +async function rewindConversation(task: Task, ts: number, index: number, operation: "delete" | "edit") { + // Calculate metrics from messages that will be deleted (must be done before rewind) + const deletedMessages = task.clineMessages.slice(index + 1) + + const { totalTokensIn, totalTokensOut, totalCacheWrites, totalCacheReads, totalCost } = getApiMetrics( + task.combineMessages(deletedMessages), + ) + + // Use MessageManager to properly handle context-management events + // This ensures orphaned Summary messages and truncation markers are cleaned up + await task.messageManager.rewindToTimestamp(ts, { + includeTargetMessage: operation === "edit", + }) + + // Report the deleted API request metrics + await task.say( + "api_req_deleted", + JSON.stringify({ + tokensIn: totalTokensIn, + tokensOut: totalTokensOut, + cacheWrites: totalCacheWrites, + cacheReads: totalCacheReads, + cost: totalCost, + } satisfies ClineApiReqInfo), + ) +} + export type CheckpointDiffOptions = { ts?: number previousCommitHash?: string diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index b816cb0305..76890d8ee1 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -354,6 +354,38 @@ describe("Task persistence", () => { expect(mockSaveApiMessages).not.toHaveBeenCalled() }) + it("keeps the cut API history in memory, without rejecting, when its write fails, and the next save writes it", async () => { + const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + await task.overwriteApiConversationHistory( + [ + { role: "user", content: "first", ts: 1 }, + { role: "user", content: "second", ts: 2 }, + ], + false, + ) + mockSaveApiMessages.mockClear() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + + // The failure is the task's own to retry: a caller (the edit after a checkpoint restore) is not told of it. + await expect( + task.overwriteApiConversationHistory([task.apiConversationHistory[0]]), + ).resolves.toBeUndefined() + expect(task.apiConversationHistory.map((message) => message.content)).toEqual(["first"]) + + mockSaveApiMessages.mockClear() + expect(await task.retrySaveApiConversationHistory()).toBe(true) + expect( + mockSaveApiMessages.mock.calls[0][0].messages.map((message: { content: unknown }) => message.content), + ).toEqual(["first"]) + consoleError.mockRestore() + }) + it("returns false on failure", async () => { vi.useFakeTimers() diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index 5b3839ea3e..2ced8f3344 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -3,6 +3,7 @@ import type { HistoryItem } from "@roo-code/types" import { Task } from "../../task/Task" import { ClineProvider } from "../ClineProvider" +import { ConversationRewindError } from "../../checkpoints/ConversationRewindError" import { handleCheckpointRestoreOperation, type CheckpointRestoreConfig } from "../checkpointRestoreHandler" import { PendingEditOperationStore } from "../PendingEditOperationStore" import { saveTaskMessages } from "../../task-persistence" @@ -345,6 +346,40 @@ describe("checkpointRestoreHandler", () => { expect(saveTaskMessages).not.toHaveBeenCalled() }) + describe("when the files were restored but the conversation could not be rewound", () => { + const rewindFailure = () => new ConversationRewindError(new Error("Failed to save the messages")) + + it("keeps the pending edit for the task that is started again, and offers no restart", async () => { + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(rewindFailure()) + + await expect( + handleCheckpointRestoreOperation({ + ...fixture.config, + operation: "edit", + editData: { editedContent: "Edited prompt", apiConversationHistoryIndex: 0 }, + }), + ).rejects.toBeInstanceOf(ConversationRewindError) + + expect(fixture.store.get(`task-${fixture.task.taskId}`)?.editedContent).toBe("Edited prompt") + expect(fixture.providerDouble.clearPendingEditOperation).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledExactlyOnceWith( + "common:errors.checkpoint_restore_failed: Failed to save the messages", + ) + }) + + it("does not offer to restart a deleted task, which was started again by the restore", async () => { + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(rewindFailure()) + + await expect(handleCheckpointRestoreOperation(fixture.config)).rejects.toBeInstanceOf( + ConversationRewindError, + ) + + expect(showWarningMessage).not.toHaveBeenCalled() + expect(fixture.providerDouble.restartTaskIfCurrent).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledOnce() + }) + }) + it("a failed older edit keeps a newer edit staged during its restore", async () => { const operationId = `task-${fixture.task.taskId}` fixture.taskDouble.checkpointRestore.mockImplementationOnce(async () => { @@ -455,25 +490,9 @@ describe("checkpointRestoreHandler", () => { expect(fixture.store.get(`task-${fixture.task.taskId}`)).toBeUndefined() // and it is not applied by a later start }) - it("reports an API history write that failed after the messages were stored, and hands the edit back", async () => { - fixture.taskDouble.overwriteApiConversationHistory.mockRejectedValueOnce(fixture.failure) - - await stageAndRehydrate() - - reported() - handedBack() - expect(fixture.taskDouble.overwriteClineMessages).toHaveBeenCalledTimes(1) - expect(fixture.taskDouble.handleWebviewAskResponse).not.toHaveBeenCalled() - }) - - it("reports an edit that could not be sent, and hands it back", async () => { - fixture.taskDouble.handleWebviewAskResponse.mockRejectedValueOnce(fixture.failure) - - await stageAndRehydrate() - - reported() - handedBack() - }) + // Not covered here: a failed API-history write and a failed send. `Task.overwriteApiConversationHistory` keeps the + // cut history in memory and does not reject when its write fails (tested in `Task.persistence.spec`), and + // `Task.handleWebviewAskResponse` returns nothing to reject: neither can reach this path. it("reports a failure that is not an Error as the text it is", async () => { fixture.taskDouble.overwriteClineMessages.mockRejectedValueOnce("the disk is full") diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index bd42c8460a..ae3949c0b8 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -180,6 +180,126 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { }) }) + // The manager reads every profile in one locked read; the doubles above are sources without it. + describe("with a source that reads every profile at once", () => { + const rows = (providerForId: Record) => + Object.entries(providerForId).map(([id, apiProvider]) => ({ + id, + name: id === "a" ? "work" : "personal", + apiProvider, + apiKey: `key-${id}`, + })) + const bulkProvider = (getAllProfiles: () => Promise, overrides: Record = {}) => { + const manager = { + getAllProfiles: vi.fn(getAllProfiles), + listConfig: vi.fn(), + getProfile: vi.fn(), + } + return { + manager, + provider: makeProvider({ providerSettingsManager: manager, ...overrides }), + } + } + const queried = () => quotas.getProviderQuotas.mock.calls[0][0].map((p: { id: string }) => p.id) as string[] + + it("reads once, puts the active profile first and names it", async () => { + const { provider, manager } = bulkProvider( + async () => rows({ a: providerIdentifiers.openrouter, b: providerIdentifiers.openrouter }), + { contextProxy: { getValue: vi.fn(() => "personal") } }, + ) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(manager.getAllProfiles).toHaveBeenCalledTimes(1) + expect(manager.listConfig).not.toHaveBeenCalled() + expect(manager.getProfile).not.toHaveBeenCalled() + expect(queried()).toEqual(["b", "a"]) + expect(quotas.getProviderQuotas.mock.calls[0][1].activeProfileId).toBe("b") + }) + + it("keeps a profile the allow list refuses out of the request, and does not name it as the active one", async () => { + const onlyOpenRouter: OrganizationAllowList = { + allowAll: false, + providers: { [providerIdentifiers.openrouter]: { allowAll: true } }, + } + const { provider } = bulkProvider( + async () => rows({ a: providerIdentifiers.openrouter, b: providerIdentifiers.anthropic }), + { + getState: vi.fn(async () => ({ organizationAllowList: onlyOpenRouter })), + contextProxy: { getValue: vi.fn(() => "personal") }, + }, + ) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(queried()).toEqual(["a"]) + expect(JSON.stringify(quotas.getProviderQuotas.mock.calls)).not.toContain("key-b") + expect(quotas.getProviderQuotas.mock.calls[0][1].activeProfileId).toBeUndefined() + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ providerQuotasFailed: false }), + ) + }) + + it("answers with a failure, and logs it, when the read fails", async () => { + const { provider } = bulkProvider(async () => { + throw new Error("secret storage unavailable") + }) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "bulk-1" }) + + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + providerQuotasFailed: true, + requestId: "bulk-1", + }) + expect(provider.log).toHaveBeenCalledWith(expect.stringContaining("secret storage unavailable")) + }) + + it("answers with an empty list when there are no profiles", async () => { + quotas.getProviderQuotas.mockResolvedValue([]) + const { provider } = bulkProvider(async () => []) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ providerQuotas: [], providerQuotasFailed: false }), + ) + }) + + it("is released by a cancel without waiting for a read that never settles, and posts nothing", async () => { + const { provider, manager } = bulkProvider(() => new Promise(() => {})) + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "bulk-2" }) + await vi.waitFor(() => expect(manager.getAllProfiles).toHaveBeenCalled()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "bulk-2" }) + + await running + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + expect(provider.log).not.toHaveBeenCalled() + }) + + it("starts no read for a request that was cancelled while the allow list was read", async () => { + const { provider, manager } = bulkProvider(async () => rows({ a: providerIdentifiers.openrouter })) + // The cancel arrives while the allow list is read, before any profile read is started. + let releaseState: () => void = () => {} + provider.getState = vi.fn( + () => new Promise((resolve) => (releaseState = () => resolve({ organizationAllowList: ALLOW_ALL }))), + ) as never + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "bulk-3" }) + await vi.waitFor(() => expect(provider.getState).toHaveBeenCalled()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "bulk-3" }) + releaseState() + await running + + expect(manager.getAllProfiles).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + }) + it("reports a failure, not an empty list, when no saved profile can be read", async () => { const provider = makeProvider({ providerSettingsManager: { diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 433c40a43b..f6aefa8908 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -3063,20 +3063,24 @@ describe("exportUsageCsv", () => { vi.mocked(exportUsageCsv).mockReset() }) - it("hands the CSV text and the range to the host export", async () => { + it("hands the CSV text, the range and the settings store (for the folder of the last export) to the host export", async () => { await webviewMessageHandler(mockClineProvider, { type: "exportUsageCsv", text: "date,cost\n2026-06-01,1.2", values: { range: "30d" }, }) - expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith("date,cost\n2026-06-01,1.2", "30d") + expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith( + "date,cost\n2026-06-01,1.2", + "30d", + mockClineProvider.contextProxy, + ) }) it("passes nothing for the range when the message has no values", async () => { await webviewMessageHandler(mockClineProvider, { type: "exportUsageCsv", text: "a" }) - expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith("a", undefined) + expect(exportUsageCsv).toHaveBeenCalledExactlyOnceWith("a", undefined, mockClineProvider.contextProxy) }) }) diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index d8b1acb4b3..4d765ec03a 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -1,4 +1,5 @@ import { Task } from "../task/Task" +import { ConversationRewindError } from "../checkpoints/ConversationRewindError" import { ClineProvider } from "./ClineProvider" import { saveTaskMessages } from "../task-persistence" import * as vscode from "vscode" @@ -85,7 +86,10 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore // For edit operations, the task cancellation in checkpointRestore // will trigger reinitialization, which will process pendingEditAfterRestore } catch (error) { - if (operation === "edit" && !checkpointRestored && pendingEditOwner) { + // The files were restored and the task is started again (see `ConversationRewindError`): the starting task + // takes the pending edit, and hands it back to the user if it cannot apply it, and there is nothing to restart. + const taskRestarted = error instanceof ConversationRewindError + if (operation === "edit" && !checkpointRestored && pendingEditOwner && !taskRestarted) { provider.clearPendingEditOperation(`task-${currentCline.taskId}`, pendingEditOwner) } console.error(`Error in checkpoint restore (${operation}):`, error) @@ -94,7 +98,7 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore error: error instanceof Error ? error.message : String(error), }), ) - if (operation === "delete" && !checkpointRestored && currentCline.abort) { + if (operation === "delete" && !checkpointRestored && !taskRestarted && currentCline.abort) { try { const restart = t("common:checkpoints.restart") const response = await vscode.window.showWarningMessage( diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index a9a0cadc4a..06651add57 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1178,7 +1178,7 @@ export const webviewMessageHandler = async ( break } case "exportUsageCsv": - await exportUsageCsv(message.text, message.values?.range) + await exportUsageCsv(message.text, message.values?.range, provider.contextProxy) break case "exportSettings": await exportSettings({ diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index 1c600a6f15..1fc74cee55 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -1,3 +1,5 @@ +import * as os from "os" +import * as path from "path" import * as vscode from "vscode" import { exportUsageCsv, neutralizeCsvFormulas } from "../export-usage-csv" @@ -12,17 +14,68 @@ const showSaveDialog = vi.mocked(vscode.window.showSaveDialog) const writeFile = vi.mocked(vscode.workspace.fs.writeFile) const showErrorMessage = vi.mocked(vscode.window.showErrorMessage) +const stored = new Map() +const context = { + getValue: vi.fn((key: string) => stored.get(key)), + setValue: vi.fn(async (key: string, value: unknown) => void stored.set(key, value)), +} + describe("exportUsageCsv", () => { beforeEach(() => { vi.clearAllMocks() + stored.clear() writeFile.mockResolvedValue(undefined) }) + describe("the folder of the dialog", () => { + it("opens in Downloads at first, and remembers the folder the file was saved in", async () => { + showSaveDialog.mockResolvedValue({ fsPath: "/work/reports/chosen.csv" } as vscode.Uri) + + await exportUsageCsv("a", "30d", context) + + expect(showSaveDialog.mock.calls[0][0]!.defaultUri?.fsPath).toBe( + path.join(os.homedir(), "Downloads", "zoo-usage-30d.csv"), + ) + expect(stored.get("lastUsageExportPath")).toBe("/work/reports/chosen.csv") + }) + + it("opens in the folder of the last export, with the name of this range", async () => { + stored.set("lastUsageExportPath", "/work/reports/chosen.csv") + showSaveDialog.mockResolvedValue(undefined) + + await exportUsageCsv("a", "7d", context) + + expect(showSaveDialog.mock.calls[0][0]!.defaultUri?.fsPath).toBe( + path.join("/work/reports", "zoo-usage-7d.csv"), + ) + }) + + it("remembers nothing when the user cancels, or when nothing was written", async () => { + showSaveDialog.mockResolvedValue(undefined) + await exportUsageCsv("a", "all", context) + + showSaveDialog.mockResolvedValue({ fsPath: "/x/y.csv" } as vscode.Uri) + writeFile.mockRejectedValue(new Error("disk full")) + await exportUsageCsv("a", "all", context) + + expect(context.setValue).not.toHaveBeenCalled() + }) + + it("still reports the saved file when the folder cannot be remembered", async () => { + const saveUri = { fsPath: "/x/y.csv" } as vscode.Uri + showSaveDialog.mockResolvedValue(saveUri) + context.setValue.mockRejectedValueOnce(new Error("state unavailable")) + + expect(await exportUsageCsv("a", "all", context)).toBe(saveUri) + expect(showErrorMessage).not.toHaveBeenCalled() + }) + }) + it("writes the CSV to the file the user chose", async () => { const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri showSaveDialog.mockResolvedValue(saveUri) - const saved = await exportUsageCsv("date,cost\n2026-06-01,1.2", "7d") + const saved = await exportUsageCsv("date,cost\n2026-06-01,1.2", "7d", context) expect(saved).toBe(saveUri) expect(writeFile).toHaveBeenCalledWith(saveUri, Buffer.from("date,cost\n2026-06-01,1.2", "utf-8")) @@ -37,7 +90,7 @@ describe("exportUsageCsv", () => { ])("suggests %s as %s, in CSV files", async (range, fileName) => { showSaveDialog.mockResolvedValue(undefined) - await exportUsageCsv("a", range) + await exportUsageCsv("a", range, context) const options = showSaveDialog.mock.calls[0][0]! expect(options.defaultUri?.fsPath.endsWith(fileName)).toBe(true) @@ -49,7 +102,7 @@ describe("exportUsageCsv", () => { async (range) => { showSaveDialog.mockResolvedValue(undefined) - await exportUsageCsv("a", range) + await exportUsageCsv("a", range, context) expect(showSaveDialog.mock.calls[0][0]!.defaultUri?.fsPath.endsWith("zoo-usage-all.csv")).toBe(true) }, @@ -58,12 +111,12 @@ describe("exportUsageCsv", () => { it("writes nothing if the user cancels the dialog", async () => { showSaveDialog.mockResolvedValue(undefined) - expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(await exportUsageCsv("a", "all", context)).toBeUndefined() expect(writeFile).not.toHaveBeenCalled() }) it.each([[undefined], [null], [""], [42], [{}]])("does nothing for the text %j", async (csv) => { - expect(await exportUsageCsv(csv, "all")).toBeUndefined() + expect(await exportUsageCsv(csv, "all", context)).toBeUndefined() expect(showSaveDialog).not.toHaveBeenCalled() expect(writeFile).not.toHaveBeenCalled() }) @@ -71,7 +124,7 @@ describe("exportUsageCsv", () => { it("tells the user when the save dialog itself fails, and writes nothing", async () => { showSaveDialog.mockRejectedValue(new Error("dialog unavailable")) - expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(await exportUsageCsv("a", "all", context)).toBeUndefined() expect(showErrorMessage).toHaveBeenCalledWith("dialog unavailable") expect(writeFile).not.toHaveBeenCalled() }) @@ -80,11 +133,11 @@ describe("exportUsageCsv", () => { const saveUri = { fsPath: "/tmp/big.csv" } as vscode.Uri showSaveDialog.mockResolvedValue(saveUri) - expect(await exportUsageCsv("a".repeat(20_000_001), "all")).toBeUndefined() + expect(await exportUsageCsv("a".repeat(20_000_001), "all", context)).toBeUndefined() expect(showSaveDialog).not.toHaveBeenCalled() expect(writeFile).not.toHaveBeenCalled() - expect(await exportUsageCsv("a".repeat(20_000_000), "all")).toBe(saveUri) + expect(await exportUsageCsv("a".repeat(20_000_000), "all", context)).toBe(saveUri) expect(writeFile).toHaveBeenCalledTimes(1) const [target, bytes] = writeFile.mock.calls[0] expect(target).toBe(saveUri) @@ -95,7 +148,7 @@ describe("exportUsageCsv", () => { showSaveDialog.mockResolvedValue({ fsPath: "/ro/file.csv" } as vscode.Uri) writeFile.mockRejectedValue("disk full") - expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(await exportUsageCsv("a", "all", context)).toBeUndefined() expect(showErrorMessage).toHaveBeenCalledWith("disk full") }) @@ -103,7 +156,7 @@ describe("exportUsageCsv", () => { showSaveDialog.mockResolvedValue({ fsPath: "/ro/file.csv" } as vscode.Uri) writeFile.mockRejectedValue(new Error("EACCES: permission denied")) - expect(await exportUsageCsv("a", "all")).toBeUndefined() + expect(await exportUsageCsv("a", "all", context)).toBeUndefined() expect(showErrorMessage).toHaveBeenCalledWith("EACCES: permission denied") }) }) @@ -229,7 +282,7 @@ describe("neutralizeCsvFormulas", () => { const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri showSaveDialog.mockResolvedValue(saveUri) - await exportUsageCsv('date,task\n2026,"note\n=1+1\nend"', "all") + await exportUsageCsv('date,task\n2026,"note\n=1+1\nend"', "all", context) const written = writeFile.mock.calls[0][1].toString() for (const delimiter of [",", ";", "\t"]) { @@ -304,7 +357,7 @@ describe("neutralizeCsvFormulas", () => { const saveUri = { fsPath: "/tmp/chosen.csv" } as vscode.Uri showSaveDialog.mockResolvedValue(saveUri) - await exportUsageCsv("date,task\n2026-06-01,\"x;=cmd|' /c calc'!A0\"", "all") + await exportUsageCsv("date,task\n2026-06-01,\"x;=cmd|' /c calc'!A0\"", "all", context) const written = writeFile.mock.calls[0][1].toString() expect(fieldsSplitOn(";", written).some(startsAFormula)).toBe(false) @@ -335,7 +388,7 @@ describe("neutralizeCsvFormulas", () => { showSaveDialog.mockResolvedValue({ fsPath: "/tmp/out.csv" } as vscode.Uri) writeFile.mockResolvedValue(undefined) - await exportUsageCsv("date,task\n2026-06-01,\"=cmd|' /C calc'!A0\"\n+1,@x", "all") + await exportUsageCsv("date,task\n2026-06-01,\"=cmd|' /C calc'!A0\"\n+1,@x", "all", context) expect(writeFile).toHaveBeenCalledWith( expect.anything(), @@ -347,7 +400,7 @@ describe("neutralizeCsvFormulas", () => { showSaveDialog.mockResolvedValue({ fsPath: "/tmp/out.csv" } as vscode.Uri) writeFile.mockResolvedValue(undefined) - await exportUsageCsv('h\n"\uFEFF=1+1"', "all") + await exportUsageCsv('h\n"\uFEFF=1+1"', "all", context) expect(writeFile).toHaveBeenCalledWith(expect.anything(), Buffer.from('h\n"\'\uFEFF=1+1"', "utf-8")) }) @@ -356,7 +409,7 @@ describe("neutralizeCsvFormulas", () => { showSaveDialog.mockResolvedValue({ fsPath: "/tmp/out.csv" } as vscode.Uri) writeFile.mockResolvedValue(undefined) - await exportUsageCsv('h\n"\u0000=1+1"\n\u0000@x', "all") + await exportUsageCsv('h\n"\u0000=1+1"\n\u0000@x', "all", context) const [, bytes] = writeFile.mock.calls[0] expect(Buffer.from(bytes).toString("utf-8")).toBe("h\n\"'\u0000=1+1\"\n'\u0000@x") @@ -366,7 +419,7 @@ describe("neutralizeCsvFormulas", () => { showSaveDialog.mockResolvedValue({ fsPath: "/tmp/out.csv" } as vscode.Uri) writeFile.mockClear() - expect(await exportUsageCsv('a,"open', "all")).toBeUndefined() + expect(await exportUsageCsv('a,"open', "all", context)).toBeUndefined() expect(showSaveDialog).not.toHaveBeenCalled() expect(writeFile).not.toHaveBeenCalled() }) diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts index 575600724d..b54797db2a 100644 --- a/src/integrations/misc/export-usage-csv.ts +++ b/src/integrations/misc/export-usage-csv.ts @@ -2,6 +2,10 @@ import * as os from "os" import * as path from "path" import * as vscode from "vscode" +import { resolveDefaultSaveUri, saveLastExportPath, type ExportContext } from "../../utils/export" + +const LAST_EXPORT_PATH_KEY = "lastUsageExportPath" + const USAGE_RANGES: readonly string[] = ["today", "7d", "30d", "all"] /** @@ -97,9 +101,13 @@ export function neutralizeCsvFormulas(csv: string): string | undefined { /** * Saves the Usage tab's CSV where the user chooses. A webview cannot reliably download a file, so, like every other * export, the webview sends the text and the host writes it. The file name is built here from a known range, never - * from the message. Resolves to the saved file, or `undefined` if the user cancelled, there was nothing to save, or the dialog or the write failed (the user is told). + * from the message. The dialog opens in the folder of the last export (Downloads for the first). Resolves to the saved file, or `undefined` if the user cancelled, there was nothing to save, or the dialog or the write failed (the user is told). */ -export async function exportUsageCsv(csv: unknown, range: unknown): Promise { +export async function exportUsageCsv( + csv: unknown, + range: unknown, + context: ExportContext, +): Promise { if (typeof csv !== "string" || csv.length === 0 || csv.length > MAX_USAGE_CSV_LENGTH) return undefined // The text comes from a webview: whatever it holds, no cell of the saved file is a formula. const safeCsv = neutralizeCsvFormulas(csv) @@ -108,12 +116,17 @@ export async function exportUsageCsv(csv: unknown, range: unknown): Promise undefined) return saveUri } catch (error) { vscode.window.showErrorMessage(error instanceof Error ? error.message : String(error)) diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index ba269754ff..6f3e510629 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -7,6 +7,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import i18next from "i18next" +import { dateLocale } from "@src/utils/dateLocale" import { providerLabel } from "@src/utils/providerLabel" // Module-level, so a component that is unmounted and mounted again never reuses an id that an older, @@ -117,7 +118,7 @@ export const ProviderLimits = ({ enabled = true }: { enabled?: boolean }) => { {fetchedAt ? ( {t("settings:usage.limits.updated", { - time: new Date(fetchedAt).toLocaleTimeString(i18next.language || "en", { + time: new Date(fetchedAt).toLocaleTimeString(dateLocale(i18next.language) ?? "en", { hour: "numeric", minute: "2-digit", }), @@ -147,7 +148,9 @@ export const ProviderLimits = ({ enabled = true }: { enabled?: boolean }) => { quota={q} name={providerLabel(q.providerId)} formatReset={(ts) => - new Date(ts).toLocaleDateString(i18next.language || "en", { dateStyle: "medium" }) + new Date(ts).toLocaleDateString(dateLocale(i18next.language) ?? "en", { + dateStyle: "medium", + }) } /> ))} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 6318c0a8c7..d57fa88eeb 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -22,6 +22,7 @@ import { usageToCsv, } from "@src/utils/usage" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" +import { dateLocale } from "@src/utils/dateLocale" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" @@ -39,18 +40,6 @@ const PALETTE = [ "var(--vscode-charts-red)", ] -/** - * The locale for the dates of the chart: the application language, else the browser's. A tag the platform does not - * know falls back by itself; one that is not a language tag at all would throw, and is left out here. - */ -const dateLocale = (language: string | undefined): string | undefined => { - try { - return language ? Intl.getCanonicalLocales(language)[0] : undefined - } catch { - return undefined - } -} - /** * Usage tab. * diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index 3216530854..217f4528f3 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -3,6 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import { providerIdentifiers, type ProviderQuota } from "@roo-code/types" +import i18next from "i18next" + import { ProviderLimits } from "../ProviderLimits" const post = vi.hoisted(() => vi.fn()) @@ -251,6 +253,35 @@ describe("ProviderLimits", () => { ) }) + it.each([["not a tag"], ["en_US"], [""]])( + "still renders the reset date and the updated time when the language is %j, which is not a language tag", + (language) => { + const previous = i18next.language + Object.defineProperty(i18next, "language", { value: language, configurable: true }) + try { + render() + const resetAt = new Date(2026, 10, 1, 12, 0).getTime() + const fetchedAt = new Date(2026, 10, 1, 14, 41).getTime() + respond([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt, + windows: [{ id: "key_limit", valueLabel: "x", usedPercent: 10, resetAt }], + }, + ]) + + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + expect(row.textContent).toContain("settings:usage.limits.resets") + expect(screen.getByTestId("provider-limits-refresh").textContent).toContain( + "settings:usage.limits.updated", + ) + } finally { + Object.defineProperty(i18next, "language", { value: previous, configurable: true }) + } + }, + ) + it.each(["noKey", "noCopilot", "signedOut", "unsupported", "error"] as const)( "shows the quiet status text for %s", (reason) => { diff --git a/webview-ui/src/utils/__tests__/dateLocale.spec.ts b/webview-ui/src/utils/__tests__/dateLocale.spec.ts new file mode 100644 index 0000000000..fa43bb31d7 --- /dev/null +++ b/webview-ui/src/utils/__tests__/dateLocale.spec.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest" + +import { dateLocale } from "../dateLocale" + +describe("dateLocale", () => { + it.each([ + ["en", "en"], + ["pt-BR", "pt-BR"], + ["zh-cn", "zh-CN"], + ["EN-us", "en-US"], + ])("keeps the language tag %j as %j", (language, expected) => { + expect(dateLocale(language)).toBe(expected) + }) + + it.each([[undefined], [""], ["not a tag"], ["en_US"], ["-"], ["x".repeat(100)]])( + "leaves out %j, which is not a language tag, instead of throwing", + (language) => { + expect(dateLocale(language)).toBeUndefined() + }, + ) +}) diff --git a/webview-ui/src/utils/dateLocale.ts b/webview-ui/src/utils/dateLocale.ts new file mode 100644 index 0000000000..61e68795de --- /dev/null +++ b/webview-ui/src/utils/dateLocale.ts @@ -0,0 +1,12 @@ +/** + * The locale for the dates and times of the display: the application language, else the platform's. A tag the platform + * does not know falls back by itself; one that is not a language tag at all would throw a RangeError and abort the + * render, and is left out here. + */ +export const dateLocale = (language: string | undefined): string | undefined => { + try { + return language ? Intl.getCanonicalLocales(language)[0] : undefined + } catch { + return undefined + } +} From 2377fe1190da31cb21e14f6cc569951561019ae0 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 13:57:27 -0800 Subject: [PATCH 71/76] test(usage): use providerIdentifiers in the bulk profile read test (lint) --- .../language-models/__tests__/providerQuotas.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 16aa194526..bca4828cff 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1616,18 +1616,18 @@ describe("loadProfiles", () => { it("applies the allow list: a refused profile is left out and is not the active one", async () => { const source = bulk([ - { id: "a", name: "work", apiProvider: "openrouter" }, - { id: "b", name: "personal", apiProvider: "deepseek" }, + { id: "a", name: "work", apiProvider: providerIdentifiers.openrouter }, + { id: "b", name: "personal", apiProvider: providerIdentifiers.deepseek }, ]) const result = await loadProfilesWithActive( source, undefined, "personal", - (profile) => profile.apiProvider !== "deepseek", + (profile) => profile.apiProvider !== providerIdentifiers.deepseek, ) - expect(result.profiles.map((profile) => profile.apiProvider)).toEqual(["openrouter"]) + expect(result.profiles.map((profile) => profile.apiProvider)).toEqual([providerIdentifiers.openrouter]) expect(result.activeProfileId).toBeUndefined() }) From cdad28d824c851e62e8ad134a64e6d772d085150 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 15:30:37 -0800 Subject: [PATCH 72/76] fix(usage): provider-limit figures are data the webview translates in every locale; break formulas behind quotes after a separator in CSV cells; cover the rewind error --- packages/types/src/providers/vscode-llm.ts | 24 ++- .../checkpoints/__tests__/checkpoint.test.ts | 12 ++ .../misc/__tests__/export-usage-csv.spec.ts | 48 ++++++ src/integrations/misc/export-usage-csv.ts | 6 +- .../__tests__/providerQuotas.spec.ts | 107 ++++++++++--- .../language-models/providerQuotas.ts | 65 +++++++- .../components/settings/ProviderLimits.tsx | 48 +++++- .../__tests__/ProviderLimits.locale.spec.tsx | 143 ++++++++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 15 +- webview-ui/src/i18n/locales/de/settings.json | 15 +- webview-ui/src/i18n/locales/en/settings.json | 15 +- webview-ui/src/i18n/locales/es/settings.json | 15 +- webview-ui/src/i18n/locales/fr/settings.json | 15 +- webview-ui/src/i18n/locales/hi/settings.json | 15 +- webview-ui/src/i18n/locales/id/settings.json | 15 +- webview-ui/src/i18n/locales/it/settings.json | 15 +- webview-ui/src/i18n/locales/ja/settings.json | 15 +- webview-ui/src/i18n/locales/ko/settings.json | 15 +- webview-ui/src/i18n/locales/nl/settings.json | 15 +- webview-ui/src/i18n/locales/pl/settings.json | 15 +- .../src/i18n/locales/pt-BR/settings.json | 15 +- webview-ui/src/i18n/locales/ru/settings.json | 15 +- webview-ui/src/i18n/locales/tr/settings.json | 15 +- webview-ui/src/i18n/locales/vi/settings.json | 15 +- .../src/i18n/locales/zh-CN/settings.json | 15 +- .../src/i18n/locales/zh-TW/settings.json | 15 +- 26 files changed, 670 insertions(+), 53 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index 3ccb610f76..5d8ff8efdd 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -16,6 +16,26 @@ export const vsCodeLmModelsMessageTypes = [ "providerQuotas", ] as const +/** The fixed name of a window, so the webview can show it in the user's language. */ +export type ProviderQuotaWindowName = { + key: "weekly" | "daily" | "monthly" | "unlimited" | "hours" | "days" | "credits" | "tokens" + count?: number +} + +/** + * The figure of a window, as data, so the webview writes the words around it in the user's language. The amounts are + * already formatted (a currency, a count): they are not translated. `valueLabel` stays as the English fallback. + */ +export type ProviderQuotaValue = + | { kind: "usedOf"; used: string; limit: string } + | { kind: "percentUsed"; percent: number } + | { kind: "spentMonth" | "spentTotal" | "left"; amount: string } + | { kind: "notReported" } + /** `window` is the English name of the window, for one that has no `name`. */ + | { kind: "creditsOf"; window: string; name?: ProviderQuotaWindowName; used: string; total: string } + | { kind: "windowState"; window: string; name?: ProviderQuotaWindowName; state: string } + | { kind: "windowDegraded"; window: string; name?: ProviderQuotaWindowName } + /** * One limit/balance a provider reports (e.g. Copilot premium credits for the * month, an OpenRouter key limit, a DeepSeek prepaid balance). @@ -29,7 +49,9 @@ export interface ProviderQuotaWindow { * Set when the label is just the name of a fixed window ("Weekly", "5-hour", "Unlimited"), so the webview can * show it in the user's language. `valueLabel` stays as the English fallback. */ - name?: { key: "weekly" | "daily" | "monthly" | "unlimited" | "hours" | "days"; count?: number } + name?: ProviderQuotaWindowName + /** Set when the figure has words around it ("$5.00 left", "12 / 20 used"): the webview writes them in the user's language. */ + value?: ProviderQuotaValue /** 0–100 when the provider reports a bounded allowance. */ usedPercent?: number /** Epoch ms. */ diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 7a842d84a9..053c46045b 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -283,6 +283,18 @@ describe("Checkpoint functionality", () => { }, ) + it.each([ + ["an Error", new Error("disk full"), "disk full"], + ["a plain value", "disk full", "disk full"], + ])("says the failure of the rewind as it is, for %s", (_name, cause, message) => { + const error = new ConversationRewindError(cause) + + expect(error).toBeInstanceOf(Error) + expect(error.name).toBe("ConversationRewindError") + expect(error.message).toBe(message) + expect(error.cause).toBe(cause) + }) + it("does not start the task again when the files could not be restored", async () => { mockCheckpointService.restoreCheckpoint.mockRejectedValueOnce(new Error("no such commit")) diff --git a/src/integrations/misc/__tests__/export-usage-csv.spec.ts b/src/integrations/misc/__tests__/export-usage-csv.spec.ts index 1fc74cee55..7bbcc681e8 100644 --- a/src/integrations/misc/__tests__/export-usage-csv.spec.ts +++ b/src/integrations/misc/__tests__/export-usage-csv.spec.ts @@ -251,6 +251,54 @@ describe("neutralizeCsvFormulas", () => { } const startsAFormula = (field: string) => /^(?:(?!\n)[\p{Cc}\p{Cf}])*[=+\-@\t\r]/u.test(field) + describe("for a quote after a separator inside a cell", () => { + // What is written when a cell has a formula behind quotes: a separator, quotes (doubled in the file), a sign. + const formulaBehindQuotes = /[;\t\r\n](?:""|\p{Cc}|\p{Cf})*[=+\-@]/u + + it.each([ + ["a semicolon", 'a,"x;""=1+1;end"', ";"], + ["a tab", 'a,"x\t""=1+1\tend"', "\t"], + ["several quotes", 'a,"x;""""""@SUM(1)"', ";"], + ["quotes and an invisible character", 'a,"x;""\u0000""-2+3"', ";"], + ["a line break", 'a,"x\n""=1+1"', "\n"], + ])("breaks the formula start behind the quotes after %s", (_name, csv, delimiter) => { + const safe = neutralizeCsvFormulas(csv)! + + expect(safe, JSON.stringify(safe)).not.toMatch(formulaBehindQuotes) + expect(fieldsSplitOn(delimiter, safe).some(startsAFormula), JSON.stringify(safe)).toBe(false) + expect(safe).toContain(`${delimiter}'`) // the apostrophe is in front of the quotes + }) + + it("shows that the crafted cell was a formula behind its quote when it was written as it came", () => { + const crafted = 'a,"x;""=1+1;end"' + + expect(crafted).toMatch(formulaBehindQuotes) + expect(fieldsSplitOn(";", crafted).some(startsAFormula)).toBe(true) + }) + + it("writes the crafted cell as inert text from exportUsageCsv, for comma, semicolon and tab imports", async () => { + showSaveDialog.mockResolvedValue({ fsPath: "/tmp/chosen.csv" } as vscode.Uri) + + await exportUsageCsv('date,task\n2026,"x;""=1+1;end"', "all", context) + + const written = writeFile.mock.calls[0][1].toString() + expect(written).not.toMatch(formulaBehindQuotes) + for (const delimiter of [",", ";", "\t"]) { + expect(fieldsSplitOn(delimiter, written).some(startsAFormula), JSON.stringify(written)).toBe(false) + } + }) + + it("leaves a quote after a separator alone when no formula follows it", () => { + expect(neutralizeCsvFormulas('a,"x;""quoted"" text"')).toBe('a,"x;""quoted"" text"') + }) + + it("still takes a pass over the text for a long run of separators and quotes", () => { + const started = performance.now() + neutralizeCsvFormulas(`a,"${';""'.repeat(100_000)}x",b`) + expect((performance.now() - started) / 1000).toBeLessThan(2) + }) + }) + describe("for a line break inside a cell", () => { it.each([ ["the crafted message", 'date,task\n2026,"note\n=1+1\nend"'], diff --git a/src/integrations/misc/export-usage-csv.ts b/src/integrations/misc/export-usage-csv.ts index b54797db2a..0ec830c845 100644 --- a/src/integrations/misc/export-usage-csv.ts +++ b/src/integrations/misc/export-usage-csv.ts @@ -23,9 +23,11 @@ const FORMULA_START = new RegExp(`^(?:${SKIPPED})*[=+\\-@\\t\\r]`, "u") * A spreadsheet may split a row on a semicolon (the list separator of many locales) or a tab as well as on a comma, may * end a row at every line break whatever quotes are open, and recognises a quote only at the start of a field: so the * text after a semicolon, a tab or a line break inside a cell can start a field or a row of its own, whatever quotes - * surround the cell. A formula start there is broken as well. + * surround the cell. A formula start there is broken as well, also behind quotes: a field of such a spreadsheet that + * starts with a quote is a quoted field, and what is in it is evaluated all the same. A quote is not skipped by the other + * scan (it is not an invisible character), so the two stay disjoint and linear. */ -const FORMULA_AFTER_SEPARATOR = new RegExp(`([;\\t\\r\\n])(?=(?:${SKIPPED})*[=+\\-@\\t\\r])`, "gu") +const FORMULA_AFTER_SEPARATOR = new RegExp(`([;\\t\\r\\n])(?=(?:${SKIPPED}|")*[=+\\-@\\t\\r])`, "gu") /** The most text the host writes for an export: ten years of daily tasks is far below it, a runaway message is not. */ const MAX_USAGE_CSV_LENGTH = 20_000_000 diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index bca4828cff..0ff6d657c5 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -36,25 +36,30 @@ import { describe("parseOpenRouterKey", () => { it("reports a bounded key limit as used / limit with a percent", () => { expect(parseOpenRouterKey({ data: { limit: 50, limit_remaining: 37.6 } })).toEqual([ - { id: "key_limit", valueLabel: "$12.40 / $50.00 used", usedPercent: expect.closeTo(24.8, 5) }, + { + id: "key_limit", + valueLabel: "$12.40 / $50.00 used", + value: { kind: "usedOf", used: "$12.40", limit: "$50.00" }, + usedPercent: expect.closeTo(24.8, 5), + }, ]) }) it("falls back to monthly spend for unlimited keys", () => { expect(parseOpenRouterKey({ data: { limit: null, usage_monthly: 3.2 } })).toEqual([ - { id: "spend", valueLabel: "$3.20 spent this month" }, + { id: "spend", valueLabel: "$3.20 spent this month", value: { kind: "spentMonth", amount: "$3.20" } }, ]) }) it("labels the all-time usage as total spend, never as this month's", () => { expect(parseOpenRouterKey({ data: { limit: null, usage: 42 } })).toEqual([ - { id: "spend", valueLabel: "$42.00 spent in total" }, + { id: "spend", valueLabel: "$42.00 spent in total", value: { kind: "spentTotal", amount: "$42.00" } }, ]) }) it("prefers the monthly figure when both are reported", () => { expect(parseOpenRouterKey({ data: { usage: 42, usage_monthly: 3.2 } })).toEqual([ - { id: "spend", valueLabel: "$3.20 spent this month" }, + { id: "spend", valueLabel: "$3.20 spent this month", value: { kind: "spentMonth", amount: "$3.20" } }, ]) }) @@ -73,12 +78,12 @@ describe("parseDeepSeekBalance", () => { { currency: "USD", total_balance: "4.25" }, ], }), - ).toEqual([{ id: "balance", valueLabel: "$4.25 left" }]) + ).toEqual([{ id: "balance", valueLabel: "$4.25 left", value: { kind: "left", amount: "$4.25" } }]) }) it("uses ¥ for a CNY-only balance", () => { expect(parseDeepSeekBalance({ balance_infos: [{ currency: "CNY", total_balance: "8" }] })).toEqual([ - { id: "balance", valueLabel: "¥8.00 left" }, + { id: "balance", valueLabel: "¥8.00 left", value: { kind: "left", amount: "¥8.00" } }, ]) }) @@ -93,7 +98,7 @@ describe("parseDeepSeekBalance – fallbacks", () => { const result = parseDeepSeekBalance({ balance_infos: [{ currency: "USD" }, { currency: "CNY" }, { currency: "CNY", total_balance: "5" }], }) - expect(result).toEqual([{ id: "balance", valueLabel: "¥5.00 left" }]) + expect(result).toEqual([{ id: "balance", valueLabel: "¥5.00 left", value: { kind: "left", amount: "¥5.00" } }]) }) it("falls back to a zero USD balance, then to the first row", () => { @@ -104,9 +109,9 @@ describe("parseDeepSeekBalance – fallbacks", () => { { currency: "USD", total_balance: "0" }, ], }), - ).toEqual([{ id: "balance", valueLabel: "$0.00 left" }]) + ).toEqual([{ id: "balance", valueLabel: "$0.00 left", value: { kind: "left", amount: "$0.00" } }]) expect(parseDeepSeekBalance({ balance_infos: [{ currency: "EUR", total_balance: "3" }] })).toEqual([ - { id: "balance", valueLabel: "$3.00 left" }, + { id: "balance", valueLabel: "$3.00 left", value: { kind: "left", amount: "$3.00" } }, ]) }) }) @@ -114,7 +119,7 @@ describe("parseDeepSeekBalance – fallbacks", () => { describe("parseMoonshotBalance", () => { it("reads available_balance", () => { expect(parseMoonshotBalance({ data: { available_balance: 19.5 } })).toEqual([ - { id: "balance", valueLabel: "$19.50 left" }, + { id: "balance", valueLabel: "$19.50 left", value: { kind: "left", amount: "$19.50" } }, ]) }) }) @@ -177,7 +182,13 @@ describe("parseZaiQuota", () => { }), ).toEqual([ { id: "limit_0", valueLabel: "5-hour", usedPercent: 40, resetAt: 1_800_000_000_000 }, - { id: "limit_1", valueLabel: "Weekly: 500 / 2,000 credits", usedPercent: 25, resetAt: undefined }, + { + id: "limit_1", + valueLabel: "Weekly: 500 / 2,000 credits", + value: { kind: "creditsOf", window: "Weekly", name: { key: "weekly" }, used: "500", total: "2,000" }, + usedPercent: 25, + resetAt: undefined, + }, ]) }) @@ -269,14 +280,32 @@ describe("parseNanoGptUsage", () => { monthly: { percentUsed: 0.4, resetAt: 1_800_000_000 }, }), ).toEqual([ - { id: "daily", valueLabel: "Daily (grace)", usedPercent: 25, resetAt: undefined }, - { id: "monthly", valueLabel: "Monthly (grace)", usedPercent: 40, resetAt: 1_800_000_000_000 }, + { + id: "daily", + valueLabel: "Daily (grace)", + value: { kind: "windowState", window: "Daily", name: { key: "daily" }, state: "grace" }, + usedPercent: 25, + resetAt: undefined, + }, + { + id: "monthly", + valueLabel: "Monthly (grace)", + value: { kind: "windowState", window: "Monthly", name: { key: "monthly" }, state: "grace" }, + usedPercent: 40, + resetAt: 1_800_000_000_000, + }, ]) }) it("keeps an empty window only when its label carries a state, and a window that has a reset time", () => { expect(parseNanoGptUsage({ state: "grace", monthly: {} })).toEqual([ - { id: "monthly", valueLabel: "Monthly (grace)", usedPercent: undefined, resetAt: undefined }, + { + id: "monthly", + valueLabel: "Monthly (grace)", + value: { kind: "windowState", window: "Monthly", name: { key: "monthly" }, state: "grace" }, + usedPercent: undefined, + resetAt: undefined, + }, ]) expect(parseNanoGptUsage({ monthly: { resetAt: 1_800_000_000 } })).toEqual([ { id: "monthly", valueLabel: "Monthly", usedPercent: undefined, resetAt: 1_800_000_000_000 }, @@ -291,6 +320,7 @@ describe("parseNanoGptUsage", () => { const daily = parseNanoGptUsage({ dailyInputTokens: { percentUsed: 0.9, degraded: true } })![0] expect(daily.usedPercent).toBeUndefined() expect(daily.valueLabel).toBe("Daily (degraded)") + expect(daily.value).toEqual({ kind: "windowDegraded", window: "Daily", name: { key: "daily" } }) }) it("names the plan state rather than 'degraded' when both apply, and leaves a healthy window's label alone", () => { @@ -334,7 +364,7 @@ describe("balance parser edge cases", () => { { currency: "CNY", total_balance: "12" }, ], }), - ).toEqual([{ id: "balance", valueLabel: "¥12.00 left" }]) + ).toEqual([{ id: "balance", valueLabel: "¥12.00 left", value: { kind: "left", amount: "¥12.00" } }]) expect( parseDeepSeekBalance({ balance_infos: [ @@ -342,9 +372,9 @@ describe("balance parser edge cases", () => { { currency: "USD", total_balance: "0" }, ], }), - ).toEqual([{ id: "balance", valueLabel: "$0.00 left" }]) + ).toEqual([{ id: "balance", valueLabel: "$0.00 left", value: { kind: "left", amount: "$0.00" } }]) expect(parseDeepSeekBalance({ balance_infos: [{ currency: "EUR", total_balance: "3" }] })).toEqual([ - { id: "balance", valueLabel: "$3.00 left" }, + { id: "balance", valueLabel: "$3.00 left", value: { kind: "left", amount: "$3.00" } }, ]) }) @@ -373,7 +403,13 @@ describe("parser edge cases", () => { }), ).toEqual([{ id: "limit_0", valueLabel: "Tokens", usedPercent: 25, resetAt: undefined }]) expect(parseZaiQuota({ data: { limits: [{ type: "CREDIT_LIMIT", usage: 0, currentValue: 0 }] } })).toEqual([ - { id: "limit_0", valueLabel: "Credits: 0 / 0 credits", usedPercent: undefined, resetAt: undefined }, + { + id: "limit_0", + valueLabel: "Credits: 0 / 0 credits", + value: { kind: "creditsOf", window: "Credits", name: { key: "credits" }, used: "0", total: "0" }, + usedPercent: undefined, + resetAt: undefined, + }, ]) }) @@ -465,13 +501,19 @@ describe("getProviderQuotas – Copilot window and misc", () => { mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: false, usedPercent: 41.6, fetchedAt: 1 } }) const [pct] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) - expect(pct).toMatchObject({ ok: true, windows: [{ valueLabel: "42% used", usedPercent: 41.6 }] }) + expect(pct).toMatchObject({ + ok: true, + windows: [{ valueLabel: "42% used", value: { kind: "percentUsed", percent: 42 }, usedPercent: 41.6 }], + }) resetProviderQuotaCache() mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: false, fetchedAt: 1 } }) const [none] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) // Unknown usage is reported as unknown (no bar), never as a measured 0%. - expect(none).toMatchObject({ ok: true, windows: [{ valueLabel: "Usage not reported" }] }) + expect(none).toMatchObject({ + ok: true, + windows: [{ valueLabel: "Usage not reported", value: { kind: "notReported" } }], + }) expect((none as { windows: Array<{ usedPercent?: number }> }).windows[0].usedPercent).toBeUndefined() resetProviderQuotaCache() @@ -734,7 +776,15 @@ describe("getProviderQuotas", () => { providerId: providerIdentifiers.githubCopilot, ok: true, plan: "business", - windows: [{ id: "premium", valueLabel: "39,389.3 / 140,000 used", usedPercent: 28.1, resetAt: 1 }], + windows: [ + { + id: "premium", + valueLabel: "39,389.3 / 140,000 used", + value: { kind: "usedOf", used: "39,389.3", limit: "140,000" }, + usedPercent: 28.1, + resetAt: 1, + }, + ], }) }) @@ -842,7 +892,10 @@ describe("getProviderQuotas", () => { "https://openrouter.ai/api/v1/key", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer sk-test" }) }), ) - expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "$5.00 / $10.00 used" }] }) + expect(q).toMatchObject({ + ok: true, + windows: [{ valueLabel: "$5.00 / $10.00 used", value: { kind: "usedOf", used: "$5.00", limit: "$10.00" } }], + }) }) it("maps 401 to noKey", async () => { @@ -870,7 +923,10 @@ describe("getProviderQuotas", () => { {}, fetchSpy, ) - expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "$7.50 left" }] }) + expect(q).toMatchObject({ + ok: true, + windows: [{ valueLabel: "$7.50 left", value: { kind: "left", amount: "$7.50" } }], + }) expect(String(fetchSpy.mock.calls[0][0])).toContain("api.deepseek.com") // The gateway profile's key is never sent anywhere. expect(JSON.stringify(fetchSpy.mock.calls)).not.toContain("gateway-key") @@ -1921,6 +1977,9 @@ describe("error handling never leaks credentials", () => { const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { force: true, }) - expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "60% used", usedPercent: 60 }] }) + expect(q).toMatchObject({ + ok: true, + windows: [{ valueLabel: "60% used", value: { kind: "percentUsed", percent: 60 }, usedPercent: 60 }], + }) }) }) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index fefee72bf5..2472a90b67 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -1,6 +1,7 @@ import { providerIdentifiers, type ProviderQuota, + type ProviderQuotaValue, type ProviderQuotaWindow, type ProviderSettings, type ProviderSettingsWithId, @@ -46,17 +47,34 @@ export function parseOpenRouterKey(payload: unknown): ProviderQuotaWindow[] | un { id: "key_limit", valueLabel: `${formatMoney(used)} / ${formatMoney(limit)} used`, + value: { kind: "usedOf", used: formatMoney(used), limit: formatMoney(limit) }, usedPercent: clampPercent((used / limit) * 100), }, ] } const monthly = toNumber(data.usage_monthly) - if (monthly !== undefined) return [{ id: "spend", valueLabel: `${formatMoney(monthly)} spent this month` }] + if (monthly !== undefined) { + return [ + { + id: "spend", + valueLabel: `${formatMoney(monthly)} spent this month`, + value: { kind: "spentMonth", amount: formatMoney(monthly) }, + }, + ] + } // OpenRouter documents `usage` as all-time credit use, so it must not be shown as this month's spend. const total = toNumber(data.usage) - return total === undefined ? undefined : [{ id: "spend", valueLabel: `${formatMoney(total)} spent in total` }] + return total === undefined + ? undefined + : [ + { + id: "spend", + valueLabel: `${formatMoney(total)} spent in total`, + value: { kind: "spentTotal", amount: formatMoney(total) }, + }, + ] } /** DeepSeek `GET /user/balance` → prepaid balance (prefers a positive USD balance). */ @@ -73,13 +91,22 @@ export function parseDeepSeekBalance(payload: unknown): ProviderQuotaWindow[] | const balance = toNumber(pick?.total_balance) if (balance === undefined) return undefined - return [{ id: "balance", valueLabel: `${formatMoney(balance, pick?.currency === "CNY" ? "¥" : "$")} left` }] + const amount = formatMoney(balance, pick?.currency === "CNY" ? "¥" : "$") + return [{ id: "balance", valueLabel: `${amount} left`, value: { kind: "left", amount } }] } /** Moonshot `GET /v1/users/me/balance` → available balance. */ export function parseMoonshotBalance(payload: unknown): ProviderQuotaWindow[] | undefined { const balance = toNumber(asRecord(asRecord(payload)?.data)?.available_balance) - return balance === undefined ? undefined : [{ id: "balance", valueLabel: `${formatMoney(balance)} left` }] + return balance === undefined + ? undefined + : [ + { + id: "balance", + valueLabel: `${formatMoney(balance)} left`, + value: { kind: "left", amount: formatMoney(balance) }, + }, + ] } /** "5-hour", "Weekly", "3d", … for a rolling window of the given length. */ @@ -96,6 +123,8 @@ const FIXED_WINDOW_NAMES: Record { const result = await getGitHubCopilotQuota({ force, signal }) const providerId = providerIdentifiers.githubCopilot @@ -591,6 +647,7 @@ async function fetchCopilot(force: boolean, signal?: AbortSignal): Promise { ) } +type Translate = ReturnType["t"] + +/** + * The figure of a window in the user's language. The host sends the amounts already formatted, and the words around + * them as data (`value`, `name`): a window from an older host (or one it has no data for) shows `valueLabel`, which is English. + */ +const quotaValueText = (w: ProviderQuotaWindow, t: Translate): string => { + const windowName = (name: ProviderQuotaWindow["name"], fallback: string) => + name ? t(`settings:usage.limits.window.${name.key}`, { n: name.count }) : fallback + const { value } = w + if (value) { + switch (value.kind) { + case "usedOf": + return t("settings:usage.limits.value.usedOf", { used: value.used, limit: value.limit }) + case "percentUsed": + return t("settings:usage.limits.value.percentUsed", { percent: value.percent }) + case "spentMonth": + case "spentTotal": + case "left": + return t(`settings:usage.limits.value.${value.kind}`, { amount: value.amount }) + case "notReported": + return t("settings:usage.limits.value.notReported") + case "creditsOf": + return t("settings:usage.limits.value.creditsOf", { + window: windowName(value.name, value.window), + used: value.used, + total: value.total, + }) + case "windowState": + return t("settings:usage.limits.value.windowState", { + window: windowName(value.name, value.window), + state: value.state, + }) + case "windowDegraded": + return t("settings:usage.limits.value.windowDegraded", { + window: windowName(value.name, value.window), + }) + } + } + return windowName(w.name, w.valueLabel) +} + const QuotaRow = ({ quota, name, @@ -192,9 +234,7 @@ const QuotaRow = ({ {quota.ok && quota.windows.map((w) => { - const windowText = w.name - ? t(`settings:usage.limits.window.${w.name.key}`, { n: w.name.count }) - : w.valueLabel + const windowText = quotaValueText(w, t) return (
diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx new file mode 100644 index 0000000000..7ea728c568 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx @@ -0,0 +1,143 @@ +import { readFileSync, readdirSync } from "node:fs" +import path from "node:path" + +import { act, render, screen } from "@testing-library/react" +import { createInstance } from "i18next" +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type ProviderQuota, type ProviderQuotaWindow } from "@roo-code/types" + +import { ProviderLimits } from "../ProviderLimits" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const localesDir = path.resolve(__dirname, "../../../i18n/locales") +const locales = readdirSync(localesDir) +const settingsOf = (locale: string) => JSON.parse(readFileSync(path.join(localesDir, locale, "settings.json"), "utf8")) + +// The real translations, as the application loads them (it does not escape values: React already does). +const i18n = createInstance() +const translator = vi.hoisted(() => ({ + t: (key: string, options?: Record) => `${key}${options ?? ""}`, +})) +vi.mock("@/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ t: translator.t }) })) + +beforeAll(async () => { + await i18n.init({ + lng: "en", + fallbackLng: "en", + interpolation: { escapeValue: false }, + resources: Object.fromEntries(locales.map((locale) => [locale, { settings: settingsOf(locale) }])), + defaultNS: "settings", + ns: ["settings"], + }) + translator.t = (key, options) => i18n.t(key.replace(/^settings:/, ""), { ...options, ns: "settings" }) +}) + +const respond = (windows: ProviderQuotaWindow[]) => { + const requestId = post.mock.calls + .map((call) => call[0]) + .filter((message) => message.type === "requestProviderQuotas") + .at(-1)?.requestId + const providerQuotas: ProviderQuota[] = [ + { providerId: providerIdentifiers.openrouter, ok: true, fetchedAt: 1, windows }, + ] + act(() => { + window.dispatchEvent( + new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas, requestId } }), + ) + }) +} + +const rowText = () => screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`).textContent + +describe("ProviderLimits in another language", () => { + beforeEach(() => post.mockClear()) + + it.each([ + ["de", { kind: "left", amount: "$4.25" }, "$4.25 übrig"], + ["de", { kind: "spentMonth", amount: "$3.20" }, "$3.20 diesen Monat ausgegeben"], + ["de", { kind: "spentTotal", amount: "$42.00" }, "$42.00 insgesamt ausgegeben"], + ["de", { kind: "usedOf", used: "$12.40", limit: "$50.00" }, "$12.40 / $50.00 verbraucht"], + ["ja", { kind: "left", amount: "¥8.00" }, "残り ¥8.00"], + ["ja", { kind: "spentMonth", amount: "$3.20" }, "今月の支出 $3.20"], + ["fr", { kind: "left", amount: "$4.25" }, "$4.25 restants"], + ["fr", { kind: "percentUsed", percent: 42 }, "42 % utilisé"], + ["zh-CN", { kind: "spentTotal", amount: "$42.00" }, "累计已花费 $42.00"], + ["ru", { kind: "notReported" }, "Использование не сообщается"], + [ + "es", + { kind: "creditsOf", window: "Weekly", name: { key: "weekly" }, used: "500", total: "2,000" }, + "Semanal: 500 / 2,000 créditos", + ], + ["it", { kind: "windowDegraded", window: "Daily", name: { key: "daily" } }, "Giornaliero (degradato)"], + [ + "pt-BR", + { kind: "windowState", window: "Monthly", name: { key: "monthly" }, state: "grace" }, + "Mensal (grace)", + ], + ] as const)("writes the %s label for %j as %j, not in English", async (locale, value, expected) => { + await i18n.changeLanguage(locale) + render() + + respond([{ id: "w", valueLabel: "ENGLISH FALLBACK", value }]) + + expect(rowText()).toContain(expected) + expect(rowText()).not.toContain("ENGLISH FALLBACK") + expect(rowText()).not.toContain("settings:") // no key is shown instead of a word + }) + + it("shows the English label of a window that has no data for the figure (an older host)", async () => { + await i18n.changeLanguage("de") + render() + + respond([{ id: "w", valueLabel: "$4.25 left" }]) + + expect(rowText()).toContain("$4.25 left") + }) + + it("keeps a window name that has no structured name as the host wrote it", async () => { + await i18n.changeLanguage("de") + render() + + respond([ + { + id: "w", + valueLabel: "Daily (grace)", + value: { kind: "windowState", window: "Daily (x)", state: "grace" }, + }, + ]) + + expect(rowText()).toContain("Daily (x) (grace)") + }) + + describe("every language that the application has", () => { + const valueKeys = [ + "usedOf", + "percentUsed", + "spentMonth", + "spentTotal", + "left", + "notReported", + "creditsOf", + "windowState", + "windowDegraded", + ] + const placeholders = (text: string) => [...text.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]).sort() + + it.each(locales)("%s has every figure and window name, with the same placeholders as English", (locale) => { + const english = settingsOf("en").usage.limits + const limits = settingsOf(locale).usage.limits + + for (const key of valueKeys) { + expect(typeof limits.value[key], `${locale} value.${key}`).toBe("string") + expect(placeholders(limits.value[key]), `${locale} value.${key}`).toEqual( + placeholders(english.value[key]), + ) + } + for (const key of ["credits", "tokens"]) + expect(typeof limits.window[key], `${locale} window.${key}`).toBe("string") + }) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 005cd742c8..91d9d339b6 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Mensual", "unlimited": "Il·limitat", "hours": "{{n}} h", - "days": "{{n}} d" + "days": "{{n}} d", + "credits": "Crèdits", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} utilitzats", + "percentUsed": "{{percent}}% utilitzat", + "spentMonth": "{{amount}} gastats aquest mes", + "spentTotal": "{{amount}} gastats en total", + "left": "{{amount}} restants", + "notReported": "Ús no informat", + "creditsOf": "{{window}}: {{used}} / {{total}} crèdits", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (degradat)" } }, "byModel": "Per model", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index de94aef833..0e539b55a6 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Monatlich", "unlimited": "Unbegrenzt", "hours": "{{n}} Std.", - "days": "{{n}} T" + "days": "{{n}} T", + "credits": "Credits", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} verbraucht", + "percentUsed": "{{percent}} % verbraucht", + "spentMonth": "{{amount}} diesen Monat ausgegeben", + "spentTotal": "{{amount}} insgesamt ausgegeben", + "left": "{{amount}} übrig", + "notReported": "Verbrauch nicht gemeldet", + "creditsOf": "{{window}}: {{used}} / {{total}} Credits", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (eingeschränkt)" } }, "byModel": "Nach Modell", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 710e7e86af..8be10a5fa7 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Monthly", "unlimited": "Unlimited", "hours": "{{n}}-hour", - "days": "{{n}}-day" + "days": "{{n}}-day", + "credits": "Credits", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} used", + "percentUsed": "{{percent}}% used", + "spentMonth": "{{amount}} spent this month", + "spentTotal": "{{amount}} spent in total", + "left": "{{amount}} left", + "notReported": "Usage not reported", + "creditsOf": "{{window}}: {{used}} / {{total}} credits", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (degraded)" } }, "byModel": "By model", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index e36590dab7..40bf49e47c 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Mensual", "unlimited": "Ilimitado", "hours": "{{n}} h", - "days": "{{n}} d" + "days": "{{n}} d", + "credits": "Créditos", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} usados", + "percentUsed": "{{percent}}% usado", + "spentMonth": "{{amount}} gastados este mes", + "spentTotal": "{{amount}} gastados en total", + "left": "{{amount}} restantes", + "notReported": "Uso no informado", + "creditsOf": "{{window}}: {{used}} / {{total}} créditos", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (degradado)" } }, "byModel": "Por modelo", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 483cb2e7de..6aa77e36dd 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Mensuel", "unlimited": "Illimité", "hours": "{{n}} h", - "days": "{{n}} j" + "days": "{{n}} j", + "credits": "Crédits", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} utilisés", + "percentUsed": "{{percent}} % utilisé", + "spentMonth": "{{amount}} dépensés ce mois-ci", + "spentTotal": "{{amount}} dépensés au total", + "left": "{{amount}} restants", + "notReported": "Utilisation non communiquée", + "creditsOf": "{{window}} : {{used}} / {{total}} crédits", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (dégradé)" } }, "byModel": "Par modèle", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 256ce043ee..0cd28dc195 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1170,7 +1170,20 @@ "monthly": "मासिक", "unlimited": "असीमित", "hours": "{{n}} घंटे", - "days": "{{n}} दिन" + "days": "{{n}} दिन", + "credits": "क्रेडिट", + "tokens": "टोकन" + }, + "value": { + "usedOf": "{{used}} / {{limit}} उपयोग हुआ", + "percentUsed": "{{percent}}% उपयोग हुआ", + "spentMonth": "इस महीने {{amount}} खर्च हुए", + "spentTotal": "कुल {{amount}} खर्च हुए", + "left": "{{amount}} शेष", + "notReported": "उपयोग की जानकारी नहीं मिली", + "creditsOf": "{{window}}: {{used}} / {{total}} क्रेडिट", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (सीमित)" } }, "byModel": "मॉडल के अनुसार", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 263f23a293..5db48909a3 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Bulanan", "unlimited": "Tanpa batas", "hours": "{{n}} jam", - "days": "{{n}} hari" + "days": "{{n}} hari", + "credits": "Kredit", + "tokens": "Token" + }, + "value": { + "usedOf": "{{used}} / {{limit}} terpakai", + "percentUsed": "{{percent}}% terpakai", + "spentMonth": "{{amount}} dibelanjakan bulan ini", + "spentTotal": "{{amount}} dibelanjakan secara total", + "left": "{{amount}} tersisa", + "notReported": "Penggunaan tidak dilaporkan", + "creditsOf": "{{window}}: {{used}} / {{total}} kredit", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (menurun)" } }, "byModel": "Menurut model", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 5ffa486613..ce4035e89f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Mensile", "unlimited": "Illimitato", "hours": "{{n}} h", - "days": "{{n}} g" + "days": "{{n}} g", + "credits": "Crediti", + "tokens": "Token" + }, + "value": { + "usedOf": "{{used}} / {{limit}} usati", + "percentUsed": "{{percent}}% usato", + "spentMonth": "{{amount}} spesi questo mese", + "spentTotal": "{{amount}} spesi in totale", + "left": "{{amount}} rimanenti", + "notReported": "Utilizzo non comunicato", + "creditsOf": "{{window}}: {{used}} / {{total}} crediti", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (degradato)" } }, "byModel": "Per modello", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d0c74f9901..b187f12450 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1170,7 +1170,20 @@ "monthly": "月間", "unlimited": "無制限", "hours": "{{n}}時間", - "days": "{{n}}日" + "days": "{{n}}日", + "credits": "クレジット", + "tokens": "トークン" + }, + "value": { + "usedOf": "{{used}} / {{limit}} 使用", + "percentUsed": "{{percent}}% 使用", + "spentMonth": "今月の支出 {{amount}}", + "spentTotal": "累計の支出 {{amount}}", + "left": "残り {{amount}}", + "notReported": "使用量は報告されていません", + "creditsOf": "{{window}}: {{used}} / {{total}} クレジット", + "windowState": "{{window}}({{state}})", + "windowDegraded": "{{window}}(低下)" } }, "byModel": "モデル別", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 9080605d9c..6e5b99bd14 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1170,7 +1170,20 @@ "monthly": "월간", "unlimited": "무제한", "hours": "{{n}}시간", - "days": "{{n}}일" + "days": "{{n}}일", + "credits": "크레딧", + "tokens": "토큰" + }, + "value": { + "usedOf": "{{used}} / {{limit}} 사용", + "percentUsed": "{{percent}}% 사용", + "spentMonth": "이번 달 {{amount}} 사용", + "spentTotal": "총 {{amount}} 사용", + "left": "{{amount}} 남음", + "notReported": "사용량이 보고되지 않음", + "creditsOf": "{{window}}: {{used}} / {{total}} 크레딧", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (저하됨)" } }, "byModel": "모델별", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index be9d2e48bf..ffa75bdade 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Maandelijks", "unlimited": "Onbeperkt", "hours": "{{n}} uur", - "days": "{{n}} d" + "days": "{{n}} d", + "credits": "Credits", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} gebruikt", + "percentUsed": "{{percent}}% gebruikt", + "spentMonth": "{{amount}} uitgegeven deze maand", + "spentTotal": "{{amount}} in totaal uitgegeven", + "left": "{{amount}} over", + "notReported": "Gebruik niet gemeld", + "creditsOf": "{{window}}: {{used}} / {{total}} credits", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (verminderd)" } }, "byModel": "Per model", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 96d7fd9bc4..e07c1481d7 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Miesięcznie", "unlimited": "Bez limitu", "hours": "{{n}} godz.", - "days": "{{n}} dn." + "days": "{{n}} dn.", + "credits": "Kredyty", + "tokens": "Tokeny" + }, + "value": { + "usedOf": "{{used}} / {{limit}} wykorzystano", + "percentUsed": "{{percent}}% wykorzystano", + "spentMonth": "{{amount}} wydano w tym miesiącu", + "spentTotal": "{{amount}} wydano łącznie", + "left": "Pozostało {{amount}}", + "notReported": "Zużycie nie zostało zgłoszone", + "creditsOf": "{{window}}: {{used}} / {{total}} kredytów", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (ograniczone)" } }, "byModel": "Według modelu", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 5e6781abad..5c61c350b5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Mensal", "unlimited": "Ilimitado", "hours": "{{n}} h", - "days": "{{n}} d" + "days": "{{n}} d", + "credits": "Créditos", + "tokens": "Tokens" + }, + "value": { + "usedOf": "{{used}} / {{limit}} usados", + "percentUsed": "{{percent}}% usado", + "spentMonth": "{{amount}} gastos neste mês", + "spentTotal": "{{amount}} gastos no total", + "left": "{{amount}} restantes", + "notReported": "Uso não informado", + "creditsOf": "{{window}}: {{used}} / {{total}} créditos", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (degradado)" } }, "byModel": "Por modelo", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 90d71d3d16..ef2ee739c7 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Ежемесячно", "unlimited": "Без ограничений", "hours": "{{n}} ч", - "days": "{{n}} дн." + "days": "{{n}} дн.", + "credits": "Кредиты", + "tokens": "Токены" + }, + "value": { + "usedOf": "{{used}} / {{limit}} использовано", + "percentUsed": "{{percent}}% использовано", + "spentMonth": "{{amount}} потрачено в этом месяце", + "spentTotal": "{{amount}} потрачено всего", + "left": "Осталось {{amount}}", + "notReported": "Использование не сообщается", + "creditsOf": "{{window}}: {{used}} / {{total}} кредитов", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (ограничено)" } }, "byModel": "По моделям", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 44a9373a8a..5e5da0561b 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Aylık", "unlimited": "Sınırsız", "hours": "{{n}} saat", - "days": "{{n}} gün" + "days": "{{n}} gün", + "credits": "Krediler", + "tokens": "Token" + }, + "value": { + "usedOf": "{{used}} / {{limit}} kullanıldı", + "percentUsed": "%{{percent}} kullanıldı", + "spentMonth": "Bu ay {{amount}} harcandı", + "spentTotal": "Toplam {{amount}} harcandı", + "left": "{{amount}} kaldı", + "notReported": "Kullanım bildirilmedi", + "creditsOf": "{{window}}: {{used}} / {{total}} kredi", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (düşük performans)" } }, "byModel": "Modele göre", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index d61a3d0e37..750ea88774 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1170,7 +1170,20 @@ "monthly": "Hằng tháng", "unlimited": "Không giới hạn", "hours": "{{n}} giờ", - "days": "{{n}} ngày" + "days": "{{n}} ngày", + "credits": "Tín dụng", + "tokens": "Token" + }, + "value": { + "usedOf": "Đã dùng {{used}} / {{limit}}", + "percentUsed": "Đã dùng {{percent}}%", + "spentMonth": "Đã chi {{amount}} trong tháng này", + "spentTotal": "Đã chi tổng cộng {{amount}}", + "left": "Còn {{amount}}", + "notReported": "Chưa báo cáo mức sử dụng", + "creditsOf": "{{window}}: {{used}} / {{total}} tín dụng", + "windowState": "{{window}} ({{state}})", + "windowDegraded": "{{window}} (suy giảm)" } }, "byModel": "Theo mô hình", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 1f518c2600..c1e3434cfa 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1170,7 +1170,20 @@ "monthly": "每月", "unlimited": "无限制", "hours": "{{n}} 小时", - "days": "{{n}} 天" + "days": "{{n}} 天", + "credits": "积分", + "tokens": "令牌" + }, + "value": { + "usedOf": "已使用 {{used}} / {{limit}}", + "percentUsed": "已使用 {{percent}}%", + "spentMonth": "本月已花费 {{amount}}", + "spentTotal": "累计已花费 {{amount}}", + "left": "剩余 {{amount}}", + "notReported": "未报告用量", + "creditsOf": "{{window}}:{{used}} / {{total}} 积分", + "windowState": "{{window}}({{state}})", + "windowDegraded": "{{window}}(降级)" } }, "byModel": "按模型", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 19f969c1a5..cebcb39d7c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1170,7 +1170,20 @@ "monthly": "每月", "unlimited": "無限制", "hours": "{{n}} 小時", - "days": "{{n}} 天" + "days": "{{n}} 天", + "credits": "點數", + "tokens": "權杖" + }, + "value": { + "usedOf": "已使用 {{used}} / {{limit}}", + "percentUsed": "已使用 {{percent}}%", + "spentMonth": "本月已花費 {{amount}}", + "spentTotal": "累計已花費 {{amount}}", + "left": "剩餘 {{amount}}", + "notReported": "未回報用量", + "creditsOf": "{{window}}:{{used}} / {{total}} 點數", + "windowState": "{{window}}({{state}})", + "windowDegraded": "{{window}}(降級)" } }, "byModel": "依模型", From 2c7ac88f86307a720ae3acb8c36e2171979c9cbc Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 16:01:58 -0800 Subject: [PATCH 73/76] fix(usage): abort queued profile reads when their deadline is up; tell the user the files changed when a checkpoint restore cannot rewind the conversation; bind the request message by identity --- .../checkpoints/ConversationRewindError.ts | 16 +++++ .../__tests__/ConversationRewindError.spec.ts | 26 +++++++ .../checkpoints/__tests__/checkpoint.test.ts | 21 ++++++ .../__tests__/ProviderSettingsManager.spec.ts | 65 ++++++++++++++++++ .../__tests__/usageByModel.spec.ts | 37 +++++++++- src/core/task-persistence/usageByModel.ts | 16 +++++ src/core/task/Task.ts | 28 +++++--- .../checkpointRestoreHandler.spec.ts | 25 ++++++- src/core/webview/checkpointRestoreHandler.ts | 8 +-- src/core/webview/webviewMessageHandler.ts | 13 +--- src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/id/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/nl/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + .../__tests__/providerQuotas.spec.ts | 67 +++++++++++++++++-- .../language-models/providerQuotas.ts | 25 +++++-- 30 files changed, 325 insertions(+), 40 deletions(-) create mode 100644 src/core/checkpoints/__tests__/ConversationRewindError.spec.ts diff --git a/src/core/checkpoints/ConversationRewindError.ts b/src/core/checkpoints/ConversationRewindError.ts index a7279438f0..e5aa8001ca 100644 --- a/src/core/checkpoints/ConversationRewindError.ts +++ b/src/core/checkpoints/ConversationRewindError.ts @@ -1,3 +1,5 @@ +import { t } from "../../i18n" + /** * The files were put back to the checkpoint, but the conversation could not be rewound to it (the messages could not be * saved, and were left as they were). The task is started again from what is saved, so the workspace and the task are @@ -9,3 +11,17 @@ export class ConversationRewindError extends Error { this.name = "ConversationRewindError" } } + +/** + * What the user is told when a checkpoint restore failed. A restore that put the files back but could not rewind the + * conversation says so: the files did change, and the task was started again with its previous conversation. + */ +export function checkpointRestoreFailureMessage(error: unknown): string { + const reason = error instanceof Error ? error.message : String(error) + return t( + error instanceof ConversationRewindError + ? "common:errors.checkpoint_rewind_failed" + : "common:errors.checkpoint_restore_failed", + { error: reason }, + ) +} diff --git a/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts b/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts new file mode 100644 index 0000000000..ebf9d9f121 --- /dev/null +++ b/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts @@ -0,0 +1,26 @@ +import { checkpointRestoreFailureMessage, ConversationRewindError } from "../ConversationRewindError" + +vi.mock("../../../i18n", () => ({ + t: vi.fn((key: string, options?: { error?: string }) => `${key}: ${options?.error}`), +})) + +describe("checkpointRestoreFailureMessage", () => { + it.each([ + ["an Error", new Error("Missing commit"), "Missing commit"], + ["a plain value", "Invalid reference", "Invalid reference"], + ])("says a failed restore as such, for %s", (_name, error, reason) => { + expect(checkpointRestoreFailureMessage(error)).toBe(`common:errors.checkpoint_restore_failed: ${reason}`) + }) + + it.each([ + ["an Error", new Error("Failed to save the messages"), "Failed to save the messages"], + ["a plain value", "disk full", "disk full"], + ])( + "says that the files were restored, and the conversation was not rewound, when the cause is %s", + (_name, cause, reason) => { + expect(checkpointRestoreFailureMessage(new ConversationRewindError(cause))).toBe( + `common:errors.checkpoint_rewind_failed: ${reason}`, + ) + }, + ) +}) diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 053c46045b..1f394bc4e8 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -283,6 +283,27 @@ describe("Checkpoint functionality", () => { }, ) + it("when only the closing message fails after the rewind was saved: reports it as a rewind error, keeps the rewound conversation, and starts the task again once", async () => { + const failure = new Error("could not say") + mockTask.say.mockRejectedValueOnce(failure) + + const restoring = checkpointRestore(mockTask, { + ts: 2, + commitHash: RESTORE_HASH, + mode: "restore", + operation: "delete", + }) + + await expect(restoring).rejects.toBeInstanceOf(ConversationRewindError) + await expect(restoring).rejects.toMatchObject({ message: "could not say", cause: failure }) + // The conversation was rewound and saved before the message failed: both writes were made and are kept. + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([{ ts: 1, say: "user", text: "Message 1" }]) + expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ + { ts: 1, role: "user", content: [{ type: "text", text: "Message 1" }] }, + ]) + expect(mockProvider.cancelTask).toHaveBeenCalledTimes(1) + }) + it.each([ ["an Error", new Error("disk full"), "disk full"], ["a plain value", "disk full", "disk full"], diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 825b0ccd74..605ad73bd5 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -1,5 +1,6 @@ // npx vitest src/core/config/__tests__/ProviderSettingsManager.spec.ts +import { loadProfilesWithActive } from "../../../services/language-models/providerQuotas" import { OPEN_AI_CODEX_SERVICE_TIER_KEY, OpenAiCodexServiceTier, @@ -669,6 +670,70 @@ describe("ProviderSettingsManager", () => { expect(mockSecrets.get).not.toHaveBeenCalled() }) + it("reads nothing for a provider-limits load whose deadline expired while it waited behind another read, and frees the lock for the next", async () => { + vi.useFakeTimers() + try { + mockSecrets.get.mockClear() + let finishFirst: (value: string) => void = () => {} + mockSecrets.get + .mockImplementationOnce(() => new Promise((resolve) => (finishFirst = resolve))) + .mockResolvedValue(JSON.stringify(stored)) + const load = vi.spyOn(providerSettingsManager as unknown as { load(): Promise }, "load") + + const first = providerSettingsManager.listConfig() // holds the lock: its read has started and hangs + await vi.waitFor(() => expect(mockSecrets.get).toHaveBeenCalledTimes(1)) + const loading = loadProfilesWithActive(providerSettingsManager) // queued behind it, with the real manager + const failed = expect(loading).rejects.toThrow("Reading the saved profiles timed out") + const after = providerSettingsManager.listConfig() // queued behind both, and not abandoned + + await vi.advanceTimersByTimeAsync(10_000) // the deadline of the load + await failed + finishFirst(JSON.stringify(stored)) + await first + await expect(after).resolves.toHaveLength(2) + + // The first call and the one that was not abandoned loaded the profiles: the queued load never did. + expect(load).toHaveBeenCalledTimes(2) + expect(mockSecrets.get).toHaveBeenCalledTimes(2) + load.mockRestore() + } finally { + vi.useRealTimers() + } + }) + + it("also reads nothing for the per-profile reads of a source that has no bulk read, once their deadline expired", async () => { + vi.useFakeTimers() + try { + mockSecrets.get.mockClear() + let finishFirst: (value: string) => void = () => {} + mockSecrets.get + .mockImplementationOnce(() => new Promise((resolve) => (finishFirst = resolve))) + .mockResolvedValue(JSON.stringify(stored)) + const load = vi.spyOn(providerSettingsManager as unknown as { load(): Promise }, "load") + const perProfile = { + listConfig: (options?: { signal?: AbortSignal }) => providerSettingsManager.listConfig(options), + getProfile: (params: { id: string }, options?: { signal?: AbortSignal }) => + providerSettingsManager.getProfile(params, options), + } + + const first = providerSettingsManager.listConfig() + await vi.waitFor(() => expect(mockSecrets.get).toHaveBeenCalledTimes(1)) + const loading = loadProfilesWithActive(perProfile) + const failed = expect(loading).rejects.toThrow("Reading the saved profiles timed out") + + await vi.advanceTimersByTimeAsync(10_000) + await failed + finishFirst(JSON.stringify(stored)) + await first + await vi.advanceTimersByTimeAsync(0) + + expect(load).toHaveBeenCalledTimes(1) // the queued list never read anything + load.mockRestore() + } finally { + vi.useRealTimers() + } + }) + it("reads as before when the signal is not aborted", async () => { mockSecrets.get.mockResolvedValue(JSON.stringify(stored)) const controller = new AbortController() diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts index ba302c4db5..c0845d1b28 100644 --- a/src/core/task-persistence/__tests__/usageByModel.spec.ts +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -2,7 +2,12 @@ import { describe, expect, it } from "vitest" import { providerIdentifiers, type ClineMessage } from "@roo-code/types" -import { bindRequestToHandler, resolveRecordedModelId, summarizeUsageByModel } from "../usageByModel" +import { + bindRequestMessage, + bindRequestToHandler, + resolveRecordedModelId, + summarizeUsageByModel, +} from "../usageByModel" const req = (data: Record, ts = 1): ClineMessage => ({ ts, @@ -282,3 +287,33 @@ describe("bindRequestToHandler", () => { expect(bound).toEqual({ apiProvider: "p", modelId: "m" }) }) }) + +describe("bindRequestMessage", () => { + const handler = { apiProvider: "new-provider", configuredModelId: "new-model", servedModelId: undefined } + const message = (text: string, ts: number): ClineMessage => ({ ts, type: "say", say: "api_req_started", text }) + + it("binds the placeholder where it is now, though the messages before it were removed meanwhile", () => { + const placeholder = message(JSON.stringify({ apiProvider: "old" }), 3) + const messages = [message("a", 1), message("b", 2), placeholder] + messages.splice(0, 2) // a rewind moved it to index 0 + + expect(bindRequestMessage(messages, placeholder, handler)).toBe(true) + + expect(JSON.parse(placeholder.text!)).toMatchObject({ apiProvider: "new-provider", modelId: "new-model" }) + }) + + it("changes nothing, and says so, when the placeholder was removed, and does not touch the message at its old index", () => { + const placeholder = message(JSON.stringify({ apiProvider: "old" }), 3) + const other = message(JSON.stringify({ apiProvider: "other" }), 4) + const messages = [message("a", 1), message("b", 2), other] // the placeholder was deleted, `other` took its place + + expect(bindRequestMessage(messages, placeholder, handler)).toBe(false) + + expect(other.text).toBe(JSON.stringify({ apiProvider: "other" })) + expect(placeholder.text).toBe(JSON.stringify({ apiProvider: "old" })) + }) + + it("says it was not bound when there is no placeholder at all", () => { + expect(bindRequestMessage([message("a", 1)], undefined, handler)).toBe(false) + }) +}) diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts index 46927d2688..dad4d65878 100644 --- a/src/core/task-persistence/usageByModel.ts +++ b/src/core/task-persistence/usageByModel.ts @@ -68,6 +68,22 @@ export function resolveRecordedModelId(recorded: unknown, served: string | undef return typeof recorded === "string" && recorded.length > 0 ? recorded : served } +/** + * Binds the placeholder message of a request to its serving handler (see `bindRequestToHandler`), found by identity: + * the messages can be rewound, deleted or edited while the request is being prepared, which moves the placeholder or + * removes it, so an index taken earlier may point at another message or at none. Resolves to `false`, and changes + * nothing, when the placeholder is no longer among the messages. + */ +export function bindRequestMessage( + messages: ClineMessage[], + placeholder: ClineMessage | undefined, + handler: Parameters[1], +): boolean { + if (!placeholder || !messages.includes(placeholder)) return false + placeholder.text = bindRequestToHandler(placeholder.text, handler) + return true +} + /** * Binds a request's placeholder `api_req_started` to the handler that serves it. The placeholder is written before * the rate-limit wait and the environment details, and a profile switch in that window replaces the handler, so the diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index c40c350179..33ad7ad0e3 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -125,7 +125,7 @@ import { taskMetadata, } from "../task-persistence" import { createTaskSaveQueues, enqueueTaskSave } from "./taskSaveQueue" -import { bindRequestToHandler, resolveRecordedModelId } from "../task-persistence/usageByModel" +import { bindRequestMessage, resolveRecordedModelId } from "../task-persistence/usageByModel" import { getEnvironmentDetails } from "../environment/getEnvironmentDetails" import { checkContextWindowExceededError } from "../context/context-management/context-error-handling" import { @@ -4143,6 +4143,9 @@ export class Task extends EventEmitter implements TaskLike { // potential details for example), we need to update the text of that // message. const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started") + // The placeholder itself: the messages can be rewound or deleted during the waits below, which moves it or + // removes it, so the handler is bound to this message and not to an index taken now. + const requestMessage = this.clineMessages[lastApiReqIndex] this.clineMessages[lastApiReqIndex].text = JSON.stringify({ apiProtocol, @@ -4287,15 +4290,20 @@ export class Task extends EventEmitter implements TaskLike { servingProvider && !isRetiredProvider(servingProvider) ? servingProvider : undefined, resolveRecordedModelId(getModelId(this.apiConfiguration), served.modelId), ) - this.clineMessages[lastApiReqIndex].text = bindRequestToHandler( - this.clineMessages[lastApiReqIndex].text, - { - apiProvider: servingProvider, - configuredModelId: getModelId(this.apiConfiguration), - servedModelId: served.modelId, - apiProtocol, - }, - ) + const bound = bindRequestMessage(this.clineMessages, requestMessage, { + apiProvider: servingProvider, + configuredModelId: getModelId(this.apiConfiguration), + servedModelId: served.modelId, + apiProtocol, + }) + if (!bound) { + // The conversation was rewound or edited while this request was being prepared: the message that stands + // for it is gone, and so is the turn it belonged to. Nothing has been sent yet, so it is stopped here, as + // an abort is, and no other message is changed in its place. + throw new Error( + `[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} request was removed from the conversation`, + ) + } // Yields only if the first chunk is successful, otherwise will // allow the user to retry the request (most likely due to rate diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index 2ced8f3344..f95660862f 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -362,8 +362,9 @@ describe("checkpointRestoreHandler", () => { expect(fixture.store.get(`task-${fixture.task.taskId}`)?.editedContent).toBe("Edited prompt") expect(fixture.providerDouble.clearPendingEditOperation).not.toHaveBeenCalled() + // The user is told that the files did change, and what to do: not a plain restore failure. expect(vscode.window.showErrorMessage).toHaveBeenCalledExactlyOnceWith( - "common:errors.checkpoint_restore_failed: Failed to save the messages", + "common:errors.checkpoint_rewind_failed: Failed to save the messages", ) }) @@ -376,7 +377,27 @@ describe("checkpointRestoreHandler", () => { expect(showWarningMessage).not.toHaveBeenCalled() expect(fixture.providerDouble.restartTaskIfCurrent).not.toHaveBeenCalled() - expect(vscode.window.showErrorMessage).toHaveBeenCalledOnce() + expect(vscode.window.showErrorMessage).toHaveBeenCalledExactlyOnceWith( + "common:errors.checkpoint_rewind_failed: Failed to save the messages", + ) + }) + + it("for a deleted task that was aborted, tells the user the files changed, and neither saves nor rehydrates it again", async () => { + fixture.taskDouble.abort = true // the task was aborted before the restore, as a delete does + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(rewindFailure()) + + await expect(handleCheckpointRestoreOperation(fixture.config)).rejects.toMatchObject({ + name: "ConversationRewindError", + message: "Failed to save the messages", + }) + + // `checkpointRestore` started the task again itself (tested there): the handler only reports. + expect(saveTaskMessages).not.toHaveBeenCalled() + expect(fixture.providerDouble.createTaskWithHistoryItem).not.toHaveBeenCalled() + expect(showWarningMessage).not.toHaveBeenCalled() + expect(vscode.window.showErrorMessage).toHaveBeenCalledExactlyOnceWith( + "common:errors.checkpoint_rewind_failed: Failed to save the messages", + ) }) }) diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index 4d765ec03a..0ec85ee850 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -1,5 +1,5 @@ import { Task } from "../task/Task" -import { ConversationRewindError } from "../checkpoints/ConversationRewindError" +import { checkpointRestoreFailureMessage, ConversationRewindError } from "../checkpoints/ConversationRewindError" import { ClineProvider } from "./ClineProvider" import { saveTaskMessages } from "../task-persistence" import * as vscode from "vscode" @@ -93,11 +93,7 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore provider.clearPendingEditOperation(`task-${currentCline.taskId}`, pendingEditOwner) } console.error(`Error in checkpoint restore (${operation}):`, error) - void vscode.window.showErrorMessage( - t("common:errors.checkpoint_restore_failed", { - error: error instanceof Error ? error.message : String(error), - }), - ) + void vscode.window.showErrorMessage(checkpointRestoreFailureMessage(error)) if (operation === "delete" && !checkpointRestored && !taskRestarted && currentCline.abort) { try { const restart = t("common:checkpoints.restart") diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 06651add57..237a6e1964 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -44,6 +44,7 @@ import { importRooTaskHistory } from "../task-persistence/importRooTaskHistory" import { ClineProvider } from "./ClineProvider" import { findOriginalContent } from "./stripOriginalContent" import { handleCheckpointRestoreOperation } from "./checkpointRestoreHandler" +import { checkpointRestoreFailureMessage } from "../checkpoints/ConversationRewindError" import { assertRecordedCheckpoint } from "../checkpoints/recordedCheckpoint" import { generateErrorDiagnostics } from "./diagnosticsHandler" import { @@ -1932,11 +1933,7 @@ export const webviewMessageHandler = async ( await provider.getCurrentTask()?.checkpointRestore(result.data) } catch (error) { console.error("[checkpointRestore] checkpointRestore failed:", error) - void vscode.window.showErrorMessage( - t("common:errors.checkpoint_restore_failed", { - error: error instanceof Error ? error.message : String(error), - }), - ) + void vscode.window.showErrorMessage(checkpointRestoreFailureMessage(error)) } } @@ -1986,11 +1983,7 @@ export const webviewMessageHandler = async ( }) } catch (error) { console.error("[completionCheckpointRestore] checkpointRestore failed:", error) - void vscode.window.showErrorMessage( - t("common:errors.checkpoint_restore_failed", { - error: error instanceof Error ? error.message : String(error), - }), - ) + void vscode.window.showErrorMessage(checkpointRestoreFailureMessage(error)) } } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index d414d697f2..7b60a52180 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -22,6 +22,7 @@ "checkpoint_timeout": "S'ha esgotat el temps en intentar restaurar el punt de control.", "checkpoint_failed": "Ha fallat la restauració del punt de control.", "checkpoint_restore_failed": "Error durant la restauració del punt de control: {{error}}", + "checkpoint_rewind_failed": "Els fitxers s'han restaurat al punt de control, però no s'ha pogut rebobinar la conversa: {{error}}. La tasca s'ha reiniciat amb la conversa anterior; torna a restaurar el punt de control per rebobinar-la.", "checkpoint_missing": "No es pot restaurar el punt de control {{commitHash}}: aquest punt de control ja no existeix al repositori de punts de control. No s'ha modificat cap fitxer.", "checkpoint_invalid_hash": "No es pot restaurar ni comparar el punt de control: la referència no correspon a cap punt de control vàlid d'aquesta tasca. No s'ha modificat cap fitxer.", "git_not_installed": "Git és necessari per a la funció de punts de control. Si us plau, instal·la Git per activar els punts de control.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 8361221019..1ea17f8464 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Zeitüberschreitung beim Versuch, den Checkpoint wiederherzustellen.", "checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.", "checkpoint_restore_failed": "Fehler beim Wiederherstellen des Checkpoints: {{error}}", + "checkpoint_rewind_failed": "Die Dateien wurden auf den Checkpoint zurückgesetzt, aber die Unterhaltung konnte nicht zurückgespult werden: {{error}}. Die Aufgabe wurde mit der bisherigen Unterhaltung neu gestartet; stelle den Checkpoint erneut wieder her, um sie zurückzuspulen.", "checkpoint_missing": "Checkpoint {{commitHash}} kann nicht wiederhergestellt werden: Dieser Checkpoint existiert nicht mehr im Checkpoint-Repository. Es wurden keine Dateien geändert.", "checkpoint_invalid_hash": "Checkpoint kann nicht wiederhergestellt oder verglichen werden: Die Referenz ist kein gültiger Checkpoint dieser Aufgabe. Es wurden keine Dateien geändert.", "git_not_installed": "Git ist für die Checkpoint-Funktion erforderlich. Bitte installiere Git, um Checkpoints zu aktivieren.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 21cb77dd5f..a79fe333b1 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Timed out when attempting to restore checkpoint.", "checkpoint_failed": "Failed to restore checkpoint.", "checkpoint_restore_failed": "Error during checkpoint restore: {{error}}", + "checkpoint_rewind_failed": "The files were restored to the checkpoint, but the conversation could not be rewound: {{error}}. The task was restarted with its previous conversation; restore the checkpoint again to rewind it.", "checkpoint_missing": "Cannot restore checkpoint {{commitHash}}: this checkpoint no longer exists in the checkpoint repository. No files were changed.", "checkpoint_invalid_hash": "Cannot restore or compare checkpoint: the checkpoint reference is not a valid checkpoint of this task. No files were changed.", "git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 5b959f6e2f..0e6238fd4c 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Se agotó el tiempo al intentar restaurar el punto de control.", "checkpoint_failed": "Error al restaurar el punto de control.", "checkpoint_restore_failed": "Error durante la restauración del punto de control: {{error}}", + "checkpoint_rewind_failed": "Los archivos se restauraron al punto de control, pero no se pudo retroceder la conversación: {{error}}. La tarea se reinició con su conversación anterior; restaura el punto de control de nuevo para retroceder la conversación.", "checkpoint_missing": "No se puede restaurar el punto de control {{commitHash}}: este punto de control ya no existe en el repositorio de puntos de control. No se ha modificado ningún archivo.", "checkpoint_invalid_hash": "No se puede restaurar ni comparar el punto de control: la referencia no es un punto de control válido de esta tarea. No se ha modificado ningún archivo.", "git_not_installed": "Git es necesario para la función de puntos de control. Por favor, instala Git para activar los puntos de control.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 03e911de49..ae5dde51e3 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Expiration du délai lors de la tentative de rétablissement du checkpoint.", "checkpoint_failed": "Échec du rétablissement du checkpoint.", "checkpoint_restore_failed": "Erreur lors du rétablissement du checkpoint : {{error}}", + "checkpoint_rewind_failed": "Les fichiers ont été restaurés au point de contrôle, mais la conversation n'a pas pu être rembobinée : {{error}}. La tâche a été redémarrée avec sa conversation précédente ; restaurez à nouveau le point de contrôle pour la rembobiner.", "checkpoint_missing": "Impossible de rétablir le checkpoint {{commitHash}} : ce checkpoint n'existe plus dans le dépôt de checkpoints. Aucun fichier n'a été modifié.", "checkpoint_invalid_hash": "Impossible de rétablir ou de comparer le checkpoint : la référence ne correspond à aucun checkpoint valide de cette tâche. Aucun fichier n'a été modifié.", "git_not_installed": "Git est requis pour la fonctionnalité des points de contrôle. Veuillez installer Git pour activer les points de contrôle.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index 36479f1060..bbfbe569df 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "चेकपॉइंट को पुनर्स्थापित करने का प्रयास करते समय टाइमआउट हो गया।", "checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।", "checkpoint_restore_failed": "चेकपॉइंट पुनर्स्थापित करते समय त्रुटि: {{error}}", + "checkpoint_rewind_failed": "फ़ाइलें चेकपॉइंट पर बहाल हो गईं, लेकिन बातचीत को पीछे नहीं किया जा सका: {{error}}। कार्य को उसकी पिछली बातचीत के साथ पुनः आरंभ किया गया; बातचीत को पीछे करने के लिए चेकपॉइंट को दोबारा बहाल करें।", "checkpoint_missing": "चेकपॉइंट {{commitHash}} पुनर्स्थापित नहीं किया जा सकता: यह चेकपॉइंट अब चेकपॉइंट रिपॉज़िटरी में मौजूद नहीं है। किसी भी फ़ाइल में बदलाव नहीं हुआ।", "checkpoint_invalid_hash": "चेकपॉइंट को पुनर्स्थापित या तुलना नहीं किया जा सकता: यह संदर्भ इस कार्य का मान्य चेकपॉइंट नहीं है। किसी भी फ़ाइल में बदलाव नहीं हुआ।", "git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट सक्षम करने के लिए Git इंस्टॉल करें।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index d047cf3cbf..dd2a8da867 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Timeout saat mencoba memulihkan checkpoint.", "checkpoint_failed": "Gagal memulihkan checkpoint.", "checkpoint_restore_failed": "Terjadi kesalahan saat memulihkan checkpoint: {{error}}", + "checkpoint_rewind_failed": "File telah dipulihkan ke checkpoint, tetapi percakapan tidak dapat diputar mundur: {{error}}. Tugas dimulai ulang dengan percakapan sebelumnya; pulihkan checkpoint lagi untuk memutar mundur percakapan.", "checkpoint_missing": "Tidak dapat memulihkan checkpoint {{commitHash}}: checkpoint ini sudah tidak ada di repositori checkpoint. Tidak ada file yang diubah.", "checkpoint_invalid_hash": "Tidak dapat memulihkan atau membandingkan checkpoint: referensi ini bukan checkpoint yang valid untuk tugas ini. Tidak ada file yang diubah.", "git_not_installed": "Git diperlukan untuk fitur checkpoint. Silakan instal Git untuk mengaktifkan checkpoint.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index ecdf443a3d..b2c4185ac9 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Timeout durante il tentativo di ripristinare il checkpoint.", "checkpoint_failed": "Impossibile ripristinare il checkpoint.", "checkpoint_restore_failed": "Errore durante il ripristino del checkpoint: {{error}}", + "checkpoint_rewind_failed": "I file sono stati ripristinati al checkpoint, ma non è stato possibile riavvolgere la conversazione: {{error}}. L'attività è stata riavviata con la conversazione precedente; ripristina di nuovo il checkpoint per riavvolgerla.", "checkpoint_missing": "Impossibile ripristinare il checkpoint {{commitHash}}: questo checkpoint non esiste più nel repository dei checkpoint. Nessun file è stato modificato.", "checkpoint_invalid_hash": "Impossibile ripristinare o confrontare il checkpoint: il riferimento non è un checkpoint valido di questa attività. Nessun file è stato modificato.", "git_not_installed": "Git è richiesto per la funzione di checkpoint. Per favore, installa Git per abilitare i checkpoint.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 2220a608d6..12d4b521ca 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "チェックポイントの復元を試みる際にタイムアウトしました。", "checkpoint_failed": "チェックポイントの復元に失敗しました。", "checkpoint_restore_failed": "チェックポイントの復元中にエラーが発生しました: {{error}}", + "checkpoint_rewind_failed": "ファイルはチェックポイントに復元されましたが、会話を巻き戻せませんでした: {{error}}。タスクは以前の会話のまま再開されました。会話を巻き戻すには、もう一度チェックポイントを復元してください。", "checkpoint_missing": "チェックポイント {{commitHash}} を復元できません。このチェックポイントはチェックポイントリポジトリに存在しなくなっています。ファイルは変更されていません。", "checkpoint_invalid_hash": "チェックポイントを復元または比較できません。この参照はこのタスクの有効なチェックポイントではありません。ファイルは変更されていません。", "git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index b462dc8056..9a80ee964a 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "체크포인트 복원을 시도하는 중 시간 초과되었습니다.", "checkpoint_failed": "체크포인트 복원에 실패했습니다.", "checkpoint_restore_failed": "체크포인트 복원 중 오류가 발생했어: {{error}}", + "checkpoint_rewind_failed": "파일은 체크포인트로 복원되었지만 대화를 되돌리지 못했습니다: {{error}}. 작업은 이전 대화로 다시 시작되었습니다. 대화를 되돌리려면 체크포인트를 다시 복원하세요.", "checkpoint_missing": "체크포인트 {{commitHash}}을(를) 복원할 수 없어. 이 체크포인트는 체크포인트 저장소에 더 이상 존재하지 않아. 변경된 파일은 없어.", "checkpoint_invalid_hash": "체크포인트를 복원하거나 비교할 수 없어. 이 참조는 이 작업의 유효한 체크포인트가 아니야. 변경된 파일은 없어.", "git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 01523541d9..0ceaea8516 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Time-out bij het herstellen van checkpoint.", "checkpoint_failed": "Herstellen van checkpoint mislukt.", "checkpoint_restore_failed": "Fout bij het herstellen van checkpoint: {{error}}", + "checkpoint_rewind_failed": "De bestanden zijn hersteld naar het checkpoint, maar het gesprek kon niet worden teruggespoeld: {{error}}. De taak is opnieuw gestart met het vorige gesprek; herstel het checkpoint opnieuw om het terug te spoelen.", "checkpoint_missing": "Checkpoint {{commitHash}} kan niet worden hersteld: dit checkpoint bestaat niet meer in de checkpoint-repository. Er zijn geen bestanden gewijzigd.", "checkpoint_invalid_hash": "Checkpoint kan niet worden hersteld of vergeleken: de verwijzing is geen geldig checkpoint van deze taak. Er zijn geen bestanden gewijzigd.", "git_not_installed": "Git is vereist voor de checkpoint-functie. Installeer Git om checkpoints in te schakelen.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index d2b6412257..e71797d8c4 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Upłynął limit czasu podczas próby przywrócenia punktu kontrolnego.", "checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.", "checkpoint_restore_failed": "Błąd podczas przywracania punktu kontrolnego: {{error}}", + "checkpoint_rewind_failed": "Pliki zostały przywrócone do punktu kontrolnego, ale nie udało się cofnąć rozmowy: {{error}}. Zadanie uruchomiono ponownie z poprzednią rozmową; przywróć punkt kontrolny jeszcze raz, aby ją cofnąć.", "checkpoint_missing": "Nie można przywrócić punktu kontrolnego {{commitHash}}: ten punkt kontrolny nie istnieje już w repozytorium punktów kontrolnych. Żadne pliki nie zostały zmienione.", "checkpoint_invalid_hash": "Nie można przywrócić ani porównać punktu kontrolnego: odwołanie nie jest prawidłowym punktem kontrolnym tego zadania. Żadne pliki nie zostały zmienione.", "git_not_installed": "Funkcja punktów kontrolnych wymaga oprogramowania Git. Zainstaluj Git, aby włączyć punkty kontrolne.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 02e65d5ab9..2820a06cdf 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -22,6 +22,7 @@ "checkpoint_timeout": "Tempo esgotado ao tentar restaurar o ponto de verificação.", "checkpoint_failed": "Falha ao restaurar o ponto de verificação.", "checkpoint_restore_failed": "Erro durante a restauração do ponto de verificação: {{error}}", + "checkpoint_rewind_failed": "Os arquivos foram restaurados para o checkpoint, mas não foi possível retroceder a conversa: {{error}}. A tarefa foi reiniciada com a conversa anterior; restaure o checkpoint novamente para retroceder a conversa.", "checkpoint_missing": "Não é possível restaurar o ponto de verificação {{commitHash}}: este ponto de verificação não existe mais no repositório de pontos de verificação. Nenhum arquivo foi alterado.", "checkpoint_invalid_hash": "Não é possível restaurar ou comparar o ponto de verificação: a referência não é um ponto de verificação válido desta tarefa. Nenhum arquivo foi alterado.", "git_not_installed": "O Git é necessário para o recurso de checkpoints. Por favor, instale o Git para habilitar os checkpoints.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 64e60ab2ff..b539ae8fe4 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Превышено время ожидания при попытке восстановления контрольной точки.", "checkpoint_failed": "Не удалось восстановить контрольную точку.", "checkpoint_restore_failed": "Ошибка при восстановлении контрольной точки: {{error}}", + "checkpoint_rewind_failed": "Файлы восстановлены до контрольной точки, но переписку не удалось откатить: {{error}}. Задача перезапущена с прежней перепиской; восстановите контрольную точку ещё раз, чтобы откатить её.", "checkpoint_missing": "Невозможно восстановить контрольную точку {{commitHash}}: этой контрольной точки больше нет в репозитории контрольных точек. Ни один файл не был изменён.", "checkpoint_invalid_hash": "Невозможно восстановить или сравнить контрольную точку: ссылка не является действительной контрольной точкой этой задачи. Ни один файл не был изменён.", "git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 992e03a419..0a396a20d5 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Kontrol noktasını geri yüklemeye çalışırken zaman aşımına uğradı.", "checkpoint_failed": "Kontrol noktası geri yüklenemedi.", "checkpoint_restore_failed": "Kontrol noktası geri yüklenirken hata oluştu: {{error}}", + "checkpoint_rewind_failed": "Dosyalar kontrol noktasına geri yüklendi ancak konuşma geri sarılamadı: {{error}}. Görev önceki konuşmasıyla yeniden başlatıldı; konuşmayı geri sarmak için kontrol noktasını yeniden geri yükleyin.", "checkpoint_missing": "{{commitHash}} kontrol noktası geri yüklenemiyor: bu kontrol noktası artık kontrol noktası deposunda mevcut değil. Hiçbir dosya değiştirilmedi.", "checkpoint_invalid_hash": "Kontrol noktası geri yüklenemiyor veya karşılaştırılamıyor: bu başvuru, bu görevin geçerli bir kontrol noktası değil. Hiçbir dosya değiştirilmedi.", "git_not_installed": "Kontrol noktaları özelliği için Git gereklidir. Kontrol noktalarını etkinleştirmek için lütfen Git'i yükleyin.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 94c61bdaae..51ca6358e7 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "Đã hết thời gian khi cố gắng khôi phục điểm kiểm tra.", "checkpoint_failed": "Không thể khôi phục điểm kiểm tra.", "checkpoint_restore_failed": "Lỗi khi khôi phục điểm kiểm tra: {{error}}", + "checkpoint_rewind_failed": "Các tệp đã được khôi phục về điểm kiểm tra, nhưng không thể tua lại cuộc trò chuyện: {{error}}. Tác vụ đã được khởi động lại với cuộc trò chuyện trước đó; hãy khôi phục điểm kiểm tra lần nữa để tua lại.", "checkpoint_missing": "Không thể khôi phục điểm kiểm tra {{commitHash}}: điểm kiểm tra này không còn tồn tại trong kho lưu trữ điểm kiểm tra. Không có tệp nào bị thay đổi.", "checkpoint_invalid_hash": "Không thể khôi phục hoặc so sánh điểm kiểm tra: tham chiếu này không phải là điểm kiểm tra hợp lệ của tác vụ này. Không có tệp nào bị thay đổi.", "git_not_installed": "Yêu cầu Git cho tính năng điểm kiểm tra. Vui lòng cài đặt Git để bật điểm kiểm tra.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 2c39a92571..f27fc63cf4 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -23,6 +23,7 @@ "checkpoint_timeout": "尝试恢复检查点时超时。", "checkpoint_failed": "恢复检查点失败。", "checkpoint_restore_failed": "恢复存档点时出错:{{error}}", + "checkpoint_rewind_failed": "文件已恢复到检查点,但无法回退对话:{{error}}。任务已使用之前的对话重新启动;请再次恢复检查点以回退对话。", "checkpoint_missing": "无法恢复存档点 {{commitHash}}:此存档点已不存在于存档点仓库中。未更改任何文件。", "checkpoint_invalid_hash": "无法恢复或比较存档点:该引用不是此任务的有效存档点。未更改任何文件。", "git_not_installed": "检查点功能需要 Git。请安装 Git 以启用检查点。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index a96493284b..16710bc1b5 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -18,6 +18,7 @@ "checkpoint_timeout": "嘗試恢復檢查點時超時。", "checkpoint_failed": "恢復檢查點失敗。", "checkpoint_restore_failed": "還原存檔點時發生錯誤:{{error}}", + "checkpoint_rewind_failed": "檔案已還原至檢查點,但無法回溯對話:{{error}}。工作已使用先前的對話重新啟動;請再次還原檢查點以回溯對話。", "checkpoint_missing": "無法還原存檔點 {{commitHash}}:此存檔點已不存在於存檔點儲存庫中。未變更任何檔案。", "checkpoint_invalid_hash": "無法還原或比較存檔點:此參照不是此工作的有效存檔點。未變更任何檔案。", "git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。", diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 0ff6d657c5..dd9b1a256c 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -1454,7 +1454,7 @@ describe("loadProfiles", () => { { apiProvider: "x", openRouterApiKey: "key-a" }, { apiProvider: "x", openRouterApiKey: "key-b" }, ]) - expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }, { signal: undefined }) + expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }, { signal: expect.any(AbortSignal) }) }) it("hands the request's abort signal to the source, for the list and for every profile", async () => { @@ -1469,9 +1469,10 @@ describe("loadProfiles", () => { await loadProfilesWithActive(source, controller.signal) - expect(source.listConfig).toHaveBeenCalledExactlyOnceWith({ signal: controller.signal }) + // Each read gets a signal of its own, which the request's abort reaches (see the deadline tests below). + expect(source.listConfig).toHaveBeenCalledExactlyOnceWith({ signal: expect.any(AbortSignal) }) expect(source.getProfile).toHaveBeenCalledTimes(2) - for (const call of source.getProfile.mock.calls) expect(call[1]).toEqual({ signal: controller.signal }) + for (const call of source.getProfile.mock.calls) expect(call[1]).toEqual({ signal: expect.any(AbortSignal) }) }) it("skips a profile that fails to load instead of failing the whole refresh", async () => { @@ -1662,7 +1663,7 @@ describe("loadProfiles", () => { const result = await loadProfilesWithActive(source, controller.signal, "personal") - expect(source.getAllProfiles).toHaveBeenCalledExactlyOnceWith({ signal: controller.signal }) + expect(source.getAllProfiles).toHaveBeenCalledExactlyOnceWith({ signal: expect.any(AbortSignal) }) expect(source.listConfig).not.toHaveBeenCalled() expect(source.getProfile).not.toHaveBeenCalled() expect(result.profiles.map((profile) => profile.apiProvider)).toEqual(["y", "x", "z"]) // the active first @@ -1718,6 +1719,64 @@ describe("loadProfiles", () => { expect(cancelled.getAllProfiles).not.toHaveBeenCalled() }) + describe("the signal that each read is given", () => { + const signalOf = (read: { mock: { calls: unknown[][] } }) => + (read.mock.calls[0][0] as { signal: AbortSignal }).signal + + it("is aborted when the deadline is up, so a read that is still queued reads nothing", async () => { + const source = bulk([]) + source.getAllProfiles.mockImplementation(() => new Promise(() => {})) + + const loading = loadProfilesWithActive(source) + const failed = expect(loading).rejects.toThrow("Reading the saved profiles timed out") + await vi.advanceTimersByTimeAsync(0) + expect(signalOf(source.getAllProfiles).aborted).toBe(false) // while it waits + await vi.advanceTimersByTimeAsync(10_000) + + await failed + expect(signalOf(source.getAllProfiles).aborted).toBe(true) + }) + + it("is aborted when the request is cancelled", async () => { + const controller = new AbortController() + const source = bulk([]) + source.getAllProfiles.mockImplementation(() => new Promise(() => {})) + + const loading = loadProfilesWithActive(source, controller.signal) + const rejected = expect(loading).rejects.toMatchObject({ name: "AbortError" }) + await vi.advanceTimersByTimeAsync(0) + controller.abort() + + await rejected + expect(signalOf(source.getAllProfiles).aborted).toBe(true) + }) + + it("is not aborted by a read that finishes in time", async () => { + const source = bulk([{ id: "a", name: "work", apiProvider: providerIdentifiers.openrouter }]) + + await loadProfilesWithActive(source) + + expect(signalOf(source.getAllProfiles).aborted).toBe(false) + }) + + it("is aborted for the per-profile reads too, when their deadline is up", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "slow" }]), + getProfile: vi.fn(() => new Promise(() => {})), + } + + const loading = loadProfilesWithActive(source) + const failed = expect(loading).rejects.toThrow("None of the saved profiles could be read") + await vi.advanceTimersByTimeAsync(10_000) + + await failed + expect( + (source.getProfile.mock.calls[0] as unknown as [unknown, { signal: AbortSignal }])[1].signal + .aborted, + ).toBe(true) + }) + }) + it("returns an empty list when there are no saved profiles", async () => { expect(await loadProfilesWithActive(bulk([]))).toEqual({ profiles: [], activeProfileId: undefined }) }) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 2472a90b67..13be56a27b 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -680,32 +680,43 @@ const PROFILE_READ_TIMEOUT_MS = 10_000 * released whichever way it ends, so a request that is abandoned leaves nothing behind while the work (which cannot be * cancelled) is still pending. */ -function withDeadline(start: () => PromiseLike, ms: number, message: string, signal?: AbortSignal): Promise { +function withDeadline( + start: (signal: AbortSignal) => PromiseLike, + ms: number, + message: string, + signal?: AbortSignal, +): Promise { return new Promise((resolve, reject) => { if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError")) + // The read is given a signal of its own: it is aborted when the deadline is up as well as when the request is, + // so a read that is still queued behind another one (it has not started) reads nothing when its turn comes. + // One that has started cannot be stopped, and runs to its end. + const work = new AbortController() const done = () => { clearTimeout(timer) signal?.removeEventListener("abort", onAbort) } const onAbort = () => { done() + work.abort() reject(new DOMException("Aborted", "AbortError")) } const timer = setTimeout(() => { done() + work.abort() reject(new Error(message)) }, ms) signal?.addEventListener("abort", onAbort, { once: true }) - let work: PromiseLike + let running: PromiseLike try { - work = start() + running = start(work.signal) } catch (error) { done() return reject(error) } - work.then( + running.then( (value) => { done() resolve(value) @@ -721,7 +732,7 @@ function withDeadline(start: () => PromiseLike, ms: number, message: strin /** Reads one profile; one that fails, takes too long, or is abandoned (`signal` aborts) reads as missing. */ function readProfile(source: ProfileSource, id: string, signal?: AbortSignal): Promise { return withDeadline( - () => source.getProfile({ id }, { signal }), + (readSignal) => source.getProfile({ id }, { signal: readSignal }), PROFILE_READ_TIMEOUT_MS, "Reading a profile timed out", signal, @@ -774,7 +785,7 @@ async function readAllProfiles( ): Promise { const all = await until( withDeadline( - () => readAll({ signal }), + (readSignal) => readAll({ signal: readSignal }), PROFILE_READ_TIMEOUT_MS, "Reading the saved profiles timed out", signal, @@ -789,7 +800,7 @@ async function readAllProfiles( async function readEachProfile(source: ProfileSource, signal?: AbortSignal): Promise { const listed = await until( withDeadline( - () => source.listConfig({ signal }), + (readSignal) => source.listConfig({ signal: readSignal }), PROFILE_READ_TIMEOUT_MS, "Reading the saved profiles timed out", signal, From 2398387d2ef250658130ed69762206b7cf2169af Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 16:33:28 -0800 Subject: [PATCH 74/76] fix(usage): a closing-note failure is not a failed rewind; a failed overwrite of the API history makes the next saves authoritative; CSV text is judged after its line breaks are replaced; exact day-boundary tests --- .../checkpoints/__tests__/checkpoint.test.ts | 50 ++++--- src/core/checkpoints/index.ts | 42 ++++-- src/core/task/Task.ts | 12 +- .../task/__tests__/Task.persistence.spec.ts | 126 ++++++++++++++---- .../src/utils/__tests__/usage.dst.spec.ts | 58 ++++++++ webview-ui/src/utils/__tests__/usage.spec.ts | 63 +++++++++ webview-ui/src/utils/usage.ts | 7 +- 7 files changed, 299 insertions(+), 59 deletions(-) diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 1f394bc4e8..01894781c5 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -283,24 +283,42 @@ describe("Checkpoint functionality", () => { }, ) - it("when only the closing message fails after the rewind was saved: reports it as a rewind error, keeps the rewound conversation, and starts the task again once", async () => { - const failure = new Error("could not say") - mockTask.say.mockRejectedValueOnce(failure) + it.each(["delete", "edit"] as const)( + "is not a failed rewind when only the closing message cannot be said after the rewind was saved (%s): the task is started again, as after any rewind that worked", + async (operation) => { + mockTask.say.mockRejectedValueOnce(new Error("task aborted")) - const restoring = checkpointRestore(mockTask, { - ts: 2, - commitHash: RESTORE_HASH, - mode: "restore", - operation: "delete", - }) + await expect( + checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "restore", operation }), + ).resolves.toBeUndefined() - await expect(restoring).rejects.toBeInstanceOf(ConversationRewindError) - await expect(restoring).rejects.toMatchObject({ message: "could not say", cause: failure }) - // The conversation was rewound and saved before the message failed: both writes were made and are kept. - expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([{ ts: 1, say: "user", text: "Message 1" }]) - expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ - { ts: 1, role: "user", content: [{ type: "text", text: "Message 1" }] }, - ]) + // The conversation was rewound and saved: both writes were made and are kept. + expect(mockTask.overwriteClineMessages).toHaveBeenCalledTimes(1) + expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledTimes(1) + expect(mockProvider.cancelTask).toHaveBeenCalledTimes(1) + // The note is skipped, and the reason is logged for whoever looks. + expect(mockProvider.log).toHaveBeenCalledWith( + expect.stringContaining("could not be noted: task aborted"), + ) + }, + ) + + it("says the deleted requests when the task can, with their usage", async () => { + mockTask.combineMessages.mockReturnValue([]) + + await checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "restore", operation: "delete" }) + + expect(mockTask.say).toHaveBeenCalledExactlyOnceWith("api_req_deleted", expect.stringContaining("tokensIn")) + }) + + it("still reports a rewind that failed, whatever the closing message would have done", async () => { + mockTask.overwriteClineMessages.mockRejectedValueOnce(new Error("Failed to save the messages")) + + await expect( + checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "restore", operation: "delete" }), + ).rejects.toBeInstanceOf(ConversationRewindError) + + expect(mockTask.say).not.toHaveBeenCalled() expect(mockProvider.cancelTask).toHaveBeenCalledTimes(1) }) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index 6005a6969c..b56f597002 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -263,13 +263,27 @@ export async function checkpointRestore( await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash }) if (mode === "restore") { + let deletedRequests: ClineApiReqInfo try { - await rewindConversation(task, ts, index, operation) + deletedRequests = await rewindConversation(task, ts, index, operation) } catch (error) { // The workspace is already restored: the task still has to be started again, as after a rewind that worked. provider?.cancelTask() throw new ConversationRewindError(error) } + + // The rewind is saved. The row that reports the deleted requests is only a note: a task that cannot say it + // (it was aborted, as a delete does) has been rewound all the same, so this is not a failed rewind, and the + // task is started again, and a pending edit applied, as after any rewind that worked. + try { + await task.say("api_req_deleted", JSON.stringify(deletedRequests)) + } catch (error) { + provider?.log( + `[checkpointRestore] the conversation was rewound, but the deleted requests could not be noted: ${ + error instanceof Error ? error.message : String(error) + }`, + ) + } } // The task is already cancelled by the provider beforehand, but we @@ -289,7 +303,13 @@ export async function checkpointRestore( } } -async function rewindConversation(task: Task, ts: number, index: number, operation: "delete" | "edit") { +/** Rewinds the conversation to the checkpoint, and gives the usage of the requests that it deleted. */ +async function rewindConversation( + task: Task, + ts: number, + index: number, + operation: "delete" | "edit", +): Promise { // Calculate metrics from messages that will be deleted (must be done before rewind) const deletedMessages = task.clineMessages.slice(index + 1) @@ -303,17 +323,13 @@ async function rewindConversation(task: Task, ts: number, index: number, operati includeTargetMessage: operation === "edit", }) - // Report the deleted API request metrics - await task.say( - "api_req_deleted", - JSON.stringify({ - tokensIn: totalTokensIn, - tokensOut: totalTokensOut, - cacheWrites: totalCacheWrites, - cacheReads: totalCacheReads, - cost: totalCost, - } satisfies ClineApiReqInfo), - ) + return { + tokensIn: totalTokensIn, + tokensOut: totalTokensOut, + cacheWrites: totalCacheWrites, + cacheReads: totalCacheReads, + cost: totalCost, + } satisfies ClineApiReqInfo } export type CheckpointDiffOptions = { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 33ad7ad0e3..aa71cd3ca7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1615,18 +1615,28 @@ export class Task extends EventEmitter implements TaskLike { return saved } + /** + * An authoritative write of the API history (an overwrite: a rewind or an edit cut it) failed, so the disk still has + * the messages that were cut. A merging save (every ordinary save) would bring them back: it keeps what is on disk and + * not in memory. Until an authoritative write succeeds, the saves are authoritative too. + */ + private apiHistoryOverwriteNotStored = false + /** Persists the current API conversation history to disk, returning false on I/O errors. */ private async saveApiConversationHistory(merge = true): Promise { + const authoritative = !merge || this.apiHistoryOverwriteNotStored try { await saveApiMessages({ messages: structuredClone(this.apiConversationHistory), taskId: this.taskId, globalStoragePath: this.globalStoragePath, - merge, + merge: !authoritative, }) + if (authoritative) this.apiHistoryOverwriteNotStored = false return true } catch (error) { console.error("Failed to save API conversation history:", error) + if (!merge) this.apiHistoryOverwriteNotStored = true return false } } diff --git a/src/core/task/__tests__/Task.persistence.spec.ts b/src/core/task/__tests__/Task.persistence.spec.ts index 76890d8ee1..ca80382fbd 100644 --- a/src/core/task/__tests__/Task.persistence.spec.ts +++ b/src/core/task/__tests__/Task.persistence.spec.ts @@ -18,6 +18,8 @@ import type { Anthropic } from "@anthropic-ai/sdk" import { PendingActionSettlementError, Task } from "../Task" import { ClineProvider } from "../../webview/ClineProvider" import { ContextProxy } from "../../config/ContextProxy" +import { mergeApiMessageSnapshots } from "../../task-persistence/mergeMessageSnapshots" +import type { ApiMessage } from "../../task-persistence/apiMessages" import { providerIdentifiers } from "@roo-code/types/provider-identifiers" import { attemptCompletionTool, type AttemptCompletionCallbacks } from "../../tools/AttemptCompletionTool" import type { ToolUse } from "../../../shared/tools" @@ -354,36 +356,106 @@ describe("Task persistence", () => { expect(mockSaveApiMessages).not.toHaveBeenCalled() }) - it("keeps the cut API history in memory, without rejecting, when its write fails, and the next save writes it", async () => { - const consoleError = vi.spyOn(console, "error").mockImplementation(() => {}) - const task = new Task({ - provider: mockProvider, - apiConfiguration: mockApiConfig, - task: "test task", - startTask: false, + describe("after an overwrite of the API history whose write failed", () => { + /** The file, with what the real write does to it: authoritative for an overwrite, merging for every other save. */ + let disk: ApiMessage[] + const useFile = () => { + mockSaveApiMessages.mockImplementation( + async ({ messages, merge }: { messages: ApiMessage[]; merge?: boolean }) => { + disk = merge ? (mergeApiMessageSnapshots(disk, messages) as ApiMessage[]) : messages + return disk + }, + ) + } + const contents = (messages: ApiMessage[]) => messages.map((message) => message.content) + const newTaskWithHistory = async () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + await task.overwriteApiConversationHistory( + [ + { role: "user", content: "first", ts: 1, messageId: "m1" }, + { role: "user", content: "second", ts: 2, messageId: "m2" }, + ], + false, + ) + disk = structuredClone(task.apiConversationHistory) // both are stored + return task + } + + beforeEach(() => { + mockSaveApiMessages.mockReset() + useFile() + vi.spyOn(console, "error").mockImplementation(() => {}) }) - await task.overwriteApiConversationHistory( - [ - { role: "user", content: "first", ts: 1 }, - { role: "user", content: "second", ts: 2 }, - ], - false, - ) - mockSaveApiMessages.mockClear() - mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + afterEach(() => vi.restoreAllMocks()) - // The failure is the task's own to retry: a caller (the edit after a checkpoint restore) is not told of it. - await expect( - task.overwriteApiConversationHistory([task.apiConversationHistory[0]]), - ).resolves.toBeUndefined() - expect(task.apiConversationHistory.map((message) => message.content)).toEqual(["first"]) + it("keeps the cut history in memory, without rejecting, and the next save stores only the cut history", async () => { + const task = await newTaskWithHistory() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) - mockSaveApiMessages.mockClear() - expect(await task.retrySaveApiConversationHistory()).toBe(true) - expect( - mockSaveApiMessages.mock.calls[0][0].messages.map((message: { content: unknown }) => message.content), - ).toEqual(["first"]) - consoleError.mockRestore() + // The failure is the task's own to retry: a caller (the edit after a checkpoint restore) is not told of it. + await expect( + task.overwriteApiConversationHistory([task.apiConversationHistory[0]]), + ).resolves.toBeUndefined() + expect(contents(task.apiConversationHistory)).toEqual(["first"]) + expect(contents(disk)).toEqual(["first", "second"]) // the write was lost: the file still has both + + expect(await task.retrySaveApiConversationHistory()).toBe(true) + + // A merging save would have kept "second", which is on disk and not in memory. + expect(contents(disk)).toEqual(["first"]) + }) + + it("also for an ordinary save that comes before any retry", async () => { + const task = await newTaskWithHistory() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + await task.overwriteApiConversationHistory([task.apiConversationHistory[0]]) + + const flushed = await task["saveApiConversationHistory"]() + + expect(flushed).toBe(true) + expect(contents(disk)).toEqual(["first"]) + }) + + it("goes back to merging saves once the authoritative write has been stored", async () => { + const task = await newTaskWithHistory() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + await task.overwriteApiConversationHistory([task.apiConversationHistory[0]]) + await task["saveApiConversationHistory"]() // authoritative, and stored + mockSaveApiMessages.mockClear() + + await task["saveApiConversationHistory"]() + + expect(mockSaveApiMessages).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ merge: true })) + }) + + it("stays authoritative while the saves keep failing, and is not made so by a merging save that failed", async () => { + const task = await newTaskWithHistory() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + await task.overwriteApiConversationHistory([task.apiConversationHistory[0]]) + mockSaveApiMessages.mockRejectedValueOnce(new Error("still full")) + expect(await task["saveApiConversationHistory"]()).toBe(false) + mockSaveApiMessages.mockClear() + + await task["saveApiConversationHistory"]() + + expect(mockSaveApiMessages).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ merge: false })) + }) + + it("does not turn the merging saves of a task that never lost an overwrite into authoritative ones", async () => { + const task = await newTaskWithHistory() + mockSaveApiMessages.mockRejectedValueOnce(new Error("disk full")) + await task["saveApiConversationHistory"]() // a merging save that failed + mockSaveApiMessages.mockClear() + + await task["saveApiConversationHistory"]() + + expect(mockSaveApiMessages).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({ merge: true })) + }) }) it("returns false on failure", async () => { diff --git a/webview-ui/src/utils/__tests__/usage.dst.spec.ts b/webview-ui/src/utils/__tests__/usage.dst.spec.ts index 4b2a58c027..d2f047c8b5 100644 --- a/webview-ui/src/utils/__tests__/usage.dst.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.dst.spec.ts @@ -107,3 +107,61 @@ describe("a 30-day window across a clock change", () => { expect(new Set(daily.map((d) => d.day)).size).toBe(30) }) }) + +// The ends of a day that is 23 or 25 hours long, to the millisecond, built from local calendar fields (a fixed 24-hour +// step would put them an hour off, or on the wrong day). +describe.each([ + ["the 23-hour day of spring forward", { y: 2026, m: 2, d: 8, hours: 23 }], + ["the 25-hour day of fall back", { y: 2026, m: 10, d: 1, hours: 25 }], +])("the ends of %s", (_name, day) => { + const first = new Date(day.y, day.m, day.d, 0, 0, 0, 0).getTime() + const last = new Date(day.y, day.m, day.d, 23, 59, 59, 999).getTime() + const nextFirst = new Date(day.y, day.m, day.d + 1, 0, 0, 0, 0).getTime() + const previousLast = new Date(day.y, day.m, day.d - 1, 23, 59, 59, 999).getTime() + const now = new Date(day.y, day.m, day.d, 12, 0).getTime() + const tasks = () => [ + item({ task: "previous-last-ms", ts: previousLast }), + item({ task: "first-ms", ts: first }), + item({ task: "last-ms", ts: last }), + item({ task: "next-first-ms", ts: nextFirst }), + ] + const written = (range: "today" | "7d" | "30d") => + usageToCsv(tasks(), { range, now }) + .split("\n") + .slice(1) + .map((row) => /"(task [^"]*|[a-z-]+ms)"/.exec(row)?.[1]) + + it("really is a day that is not 24 hours long in this runner", () => { + expect((nextFirst - first) / 3_600_000).toBe(day.hours) + expect(last - first).toBe(day.hours * 3_600_000 - 1) + }) + + it.each(["today", "7d", "30d"] as const)( + "includes its first and its last millisecond, and excludes the millisecond before and the one after, in the export for %s", + (range) => { + const rows = written(range) + + expect(rows).toContain("first-ms") + expect(rows).toContain("last-ms") + expect(rows).not.toContain("next-first-ms") // the first millisecond of tomorrow is after today + if (range === "today") expect(rows).not.toContain("previous-last-ms") + }, + ) + + it("counts the same tasks in the totals as in the export, for today", () => { + const summary = summarizeUsage(tasks(), { range: "today", now }) + + expect(summary.taskCount).toBe(2) + expect(summary.daily).toHaveLength(1) + expect(summary.daily[0]).toMatchObject({ day: first }) + }) + + it("for a window that begins on this day, includes the first millisecond and excludes the one before", () => { + const windowNow = new Date(day.y, day.m, day.d + 6, 12, 0).getTime() // this day is the first of its 7 days + + expect(rangeStart("7d", windowNow)).toBe(first) + const rows = usageToCsv(tasks(), { range: "7d", now: windowNow }) + expect(rows).toContain("first-ms") + expect(rows).not.toContain("previous-last-ms") + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 0edf9c88f5..6271a307a1 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -511,6 +511,31 @@ describe("usageToCsv formula safety", () => { }, ) + // A line break in front of a sign is written as a space: the cell is then read with its leading spaces trimmed. + it.each([ + ["\n=1+1", "' =1+1"], + ["\r\n+SUM(A1)", "' +SUM(A1)"], + ["\n\n@x", "' @x"], + [" =1+1", "' =1+1"], + ["\n\u0000=1+1", "' \u0000=1+1"], + ["\u200B\n-2+3", "'\u200B -2+3"], + ["\n\tcmd", "' \tcmd"], + ])("neutralises a cell whose sign comes after line breaks or spaces: %j", (text, written) => { + const csv = usageToCsv([item({ task: text })], { range: "all", now: NOW }) + + const cell = csv.split("\n")[1].split(",")[1] + expect(cell).toBe(`"${written}"`) + }) + + it.each(["line\n=1+1", "a = b", "x\n- list item", "\nplain text"])( + "leaves %j alone: no sign starts the cell", + (text) => { + const csv = usageToCsv([item({ task: text })], { range: "all", now: NOW }) + + expect(csv.split("\n")[1].split(",")[1]).toBe(`"${text.replace(/\n/g, " ")}"`) + }, + ) + it("leaves ordinary text and the numeric columns alone", () => { const csv = usageToCsv([item({ task: "Fix the = sign", totalCost: 1.5, tokensIn: 10 })], { range: "all", @@ -652,6 +677,44 @@ describe("in a time zone where the day of a change of daylight saving time begin }) }) +describe("the ends of today, to the millisecond", () => { + const today = new Date(NOW) + const at = (dayOffset: number, h: number, min: number, sec: number, ms: number) => + new Date(today.getFullYear(), today.getMonth(), today.getDate() + dayOffset, h, min, sec, ms).getTime() + const tasks = () => [ + item({ task: "yesterday-last-ms", ts: at(-1, 23, 59, 59, 999), tokensIn: 1, totalCost: 1 }), + item({ task: "today-first-ms", ts: at(0, 0, 0, 0, 0), tokensIn: 1, totalCost: 2 }), + item({ task: "today-last-ms", ts: at(0, 23, 59, 59, 999), tokensIn: 1, totalCost: 4 }), + item({ task: "tomorrow-first-ms", ts: at(1, 0, 0, 0, 0), tokensIn: 1, totalCost: 8 }), + ] + + it("includes the first and the last millisecond of today, and excludes the first millisecond of tomorrow, in every bounded range", () => { + for (const range of ["today", "7d", "30d"] as const) { + const csv = usageToCsv(tasks(), { range, now: NOW }) + expect(csv, range).toContain("today-first-ms") + expect(csv, range).toContain("today-last-ms") + expect(csv, range).not.toContain("tomorrow-first-ms") + + const summary = summarizeUsage(tasks(), { range, now: NOW }) + // The export and the headline agree, and the chart adds up to it. + expect( + summary.daily.reduce((sum, d) => sum + d.cost, 0), + range, + ).toBe(summary.totalCost) + expect(summary.totalCost, range).toBe(range === "today" ? 6 : 7) // yesterday's last millisecond is in the longer ranges + } + }) + + it("excludes the last millisecond of yesterday from today alone", () => { + expect(usageToCsv(tasks(), { range: "today", now: NOW })).not.toContain("yesterday-last-ms") + expect(usageToCsv(tasks(), { range: "7d", now: NOW })).toContain("yesterday-last-ms") + }) + + it("keeps a task of tomorrow in all time, which has no end", () => { + expect(usageToCsv(tasks(), { range: "all", now: NOW })).toContain("tomorrow-first-ms") + }) +}) + describe("a task dated after today", () => { const DAY = 24 * 3600 * 1000 const history = () => [ diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index d571d058fb..599f61b306 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -359,9 +359,12 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin // Spreadsheet apps evaluate a quoted cell that starts with = + - @ (or tab/CR) as a formula, and the // task text is user- and model-controlled. A leading apostrophe makes the cell inert text. + // The text is made one line first, and judged as it is written: a line break in front of a sign becomes a space, and + // an application that trims spaces (or skips invisible characters) would read the sign as the start of the cell. const escape = (value: string) => { - const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value - return `"${safe.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` + const flat = value.replace(/\r?\n/g, " ") + const safe = /^[\s\p{Cc}\p{Cf}]*[=+\-@\t\r]/u.test(flat) ? `'${flat}` : flat + return `"${safe.replace(/"/g, '""')}"` } // A counter that is not a finite, non-negative number (corrupted history, or text such as "=1+1" that a From 28825429f891af06e633cf4bcf06768c1193cb8d Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Fri, 6 Nov 2026 16:59:15 -0800 Subject: [PATCH 75/76] test(usage): no message argument to expect in the webview tests (tsc); same coverage --- .../__tests__/ProviderLimits.locale.spec.tsx | 18 ++++++++++----- webview-ui/src/utils/__tests__/usage.spec.ts | 22 +++++++++---------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx index 7ea728c568..addf38dbd1 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.locale.spec.tsx @@ -130,14 +130,20 @@ describe("ProviderLimits in another language", () => { const english = settingsOf("en").usage.limits const limits = settingsOf(locale).usage.limits + // What is wrong, by key, so that a failure says which key of which locale it is. + const problems: string[] = [] for (const key of valueKeys) { - expect(typeof limits.value[key], `${locale} value.${key}`).toBe("string") - expect(placeholders(limits.value[key]), `${locale} value.${key}`).toEqual( - placeholders(english.value[key]), - ) + const text = limits.value[key] + if (typeof text !== "string") problems.push(`value.${key} is missing`) + else if (placeholders(text).join() !== placeholders(english.value[key]).join()) { + problems.push(`value.${key} has other placeholders than English: ${text}`) + } } - for (const key of ["credits", "tokens"]) - expect(typeof limits.window[key], `${locale} window.${key}`).toBe("string") + for (const key of ["credits", "tokens"]) { + if (typeof limits.window[key] !== "string") problems.push(`window.${key} is missing`) + } + + expect(problems).toEqual([]) }) }) }) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 6271a307a1..a033787510 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -688,22 +688,20 @@ describe("the ends of today, to the millisecond", () => { item({ task: "tomorrow-first-ms", ts: at(1, 0, 0, 0, 0), tokensIn: 1, totalCost: 8 }), ] - it("includes the first and the last millisecond of today, and excludes the first millisecond of tomorrow, in every bounded range", () => { - for (const range of ["today", "7d", "30d"] as const) { + it.each(["today", "7d", "30d"] as const)( + "includes the first and the last millisecond of today, and excludes the first millisecond of tomorrow, in the %s range", + (range) => { const csv = usageToCsv(tasks(), { range, now: NOW }) - expect(csv, range).toContain("today-first-ms") - expect(csv, range).toContain("today-last-ms") - expect(csv, range).not.toContain("tomorrow-first-ms") + expect(csv).toContain("today-first-ms") + expect(csv).toContain("today-last-ms") + expect(csv).not.toContain("tomorrow-first-ms") const summary = summarizeUsage(tasks(), { range, now: NOW }) // The export and the headline agree, and the chart adds up to it. - expect( - summary.daily.reduce((sum, d) => sum + d.cost, 0), - range, - ).toBe(summary.totalCost) - expect(summary.totalCost, range).toBe(range === "today" ? 6 : 7) // yesterday's last millisecond is in the longer ranges - } - }) + expect(summary.daily.reduce((sum, d) => sum + d.cost, 0)).toBe(summary.totalCost) + expect(summary.totalCost).toBe(range === "today" ? 6 : 7) // yesterday's last millisecond is in the longer ranges + }, + ) it("excludes the last millisecond of yesterday from today alone", () => { expect(usageToCsv(tasks(), { range: "today", now: NOW })).not.toContain("yesterday-last-ms") From c189fa29ec5fe8ebf05172ae8f26c1871ccb0861 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Sun, 8 Nov 2026 17:55:18 -0800 Subject: [PATCH 76/76] fix(checkpoints): await the restart after a failed rewind, tell the user when it failed too, hand the edit back; log a failed restart after a rewind that worked --- .../checkpoints/ConversationRewindError.ts | 19 ++++-- .../__tests__/ConversationRewindError.spec.ts | 17 +++++ .../checkpoints/__tests__/checkpoint.test.ts | 58 ++++++++++++++++ src/core/checkpoints/index.ts | 21 +++++- .../checkpointRestoreHandler.spec.ts | 67 +++++++++++++++++++ src/core/webview/checkpointRestoreHandler.ts | 16 ++++- src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/id/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/nl/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + 24 files changed, 207 insertions(+), 9 deletions(-) diff --git a/src/core/checkpoints/ConversationRewindError.ts b/src/core/checkpoints/ConversationRewindError.ts index e5aa8001ca..91d9e2c6b6 100644 --- a/src/core/checkpoints/ConversationRewindError.ts +++ b/src/core/checkpoints/ConversationRewindError.ts @@ -6,22 +6,31 @@ import { t } from "../../i18n" * not left apart with the task stopped; the caller still has to tell the user, and may keep an edit for it. */ export class ConversationRewindError extends Error { - constructor(override readonly cause: unknown) { + /** Whether the task was started again. When it was not, nothing will take a pending edit: the caller hands it back. */ + readonly restarted: boolean + + /** `restartFailure` is given when starting the task again failed too (what failed is in its `error`). */ + constructor( + override readonly cause: unknown, + readonly restartFailure?: { error: unknown }, + ) { super(cause instanceof Error ? cause.message : String(cause)) this.name = "ConversationRewindError" + this.restarted = restartFailure === undefined } } /** * What the user is told when a checkpoint restore failed. A restore that put the files back but could not rewind the - * conversation says so: the files did change, and the task was started again with its previous conversation. + * conversation says so: the files did change, and the task was started again with its previous conversation (or, when + * that failed too, it has to be started by the user). */ export function checkpointRestoreFailureMessage(error: unknown): string { const reason = error instanceof Error ? error.message : String(error) + if (!(error instanceof ConversationRewindError)) + return t("common:errors.checkpoint_restore_failed", { error: reason }) return t( - error instanceof ConversationRewindError - ? "common:errors.checkpoint_rewind_failed" - : "common:errors.checkpoint_restore_failed", + error.restarted ? "common:errors.checkpoint_rewind_failed" : "common:errors.checkpoint_rewind_restart_failed", { error: reason }, ) } diff --git a/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts b/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts index ebf9d9f121..489891fb38 100644 --- a/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts +++ b/src/core/checkpoints/__tests__/ConversationRewindError.spec.ts @@ -4,6 +4,15 @@ vi.mock("../../../i18n", () => ({ t: vi.fn((key: string, options?: { error?: string }) => `${key}: ${options?.error}`), })) +describe("ConversationRewindError", () => { + it("is restarted unless the restart failed too", () => { + expect(new ConversationRewindError(new Error("x")).restarted).toBe(true) + const failed = new ConversationRewindError(new Error("x"), { error: new Error("y") }) + expect(failed.restarted).toBe(false) + expect(failed.restartFailure).toEqual({ error: new Error("y") }) + }) +}) + describe("checkpointRestoreFailureMessage", () => { it.each([ ["an Error", new Error("Missing commit"), "Missing commit"], @@ -23,4 +32,12 @@ describe("checkpointRestoreFailureMessage", () => { ) }, ) + + it("says that the task has to be started by the user too when it could not be started again", () => { + const error = new ConversationRewindError(new Error("Failed to save the messages"), { error: "no" }) + + expect(checkpointRestoreFailureMessage(error)).toBe( + "common:errors.checkpoint_rewind_restart_failed: Failed to save the messages", + ) + }) }) diff --git a/src/core/checkpoints/__tests__/checkpoint.test.ts b/src/core/checkpoints/__tests__/checkpoint.test.ts index 01894781c5..5b98747642 100644 --- a/src/core/checkpoints/__tests__/checkpoint.test.ts +++ b/src/core/checkpoints/__tests__/checkpoint.test.ts @@ -334,6 +334,64 @@ describe("Checkpoint functionality", () => { expect(error.cause).toBe(cause) }) + it("awaits the restart after a failed rewind, and tells the caller when the task could not be started again either", async () => { + const failure = new Error("Failed to save the messages") + const restartFailure = new Error("cannot rehydrate") + mockTask.overwriteClineMessages.mockRejectedValueOnce(failure) + mockProvider.cancelTask.mockRejectedValueOnce(restartFailure) + + const restoring = checkpointRestore(mockTask, { + ts: 2, + commitHash: RESTORE_HASH, + mode: "restore", + operation: "edit", + }) + + await expect(restoring).rejects.toMatchObject({ + name: "ConversationRewindError", + cause: failure, // the rewind failure is not hidden by the restart failure + restarted: false, + restartFailure: { error: restartFailure }, // nor the other way round + }) + expect(mockProvider.cancelTask).toHaveBeenCalledTimes(1) + expect(mockProvider.log).toHaveBeenCalledWith( + expect.stringContaining("could not be started again: cannot rehydrate"), + ) + }) + + it("says the task was restarted after a failed rewind when it was", async () => { + mockTask.overwriteClineMessages.mockRejectedValueOnce(new Error("Failed to save the messages")) + + await expect( + checkpointRestore(mockTask, { ts: 2, commitHash: RESTORE_HASH, mode: "restore", operation: "edit" }), + ).rejects.toMatchObject({ restarted: true, restartFailure: undefined }) + }) + + it("does not leave a rejection unhandled when the restart after a rewind that worked fails: it is logged", async () => { + const unhandled = vi.fn() + process.on("unhandledRejection", unhandled) + try { + mockProvider.cancelTask.mockRejectedValueOnce(new Error("cannot rehydrate")) + + await expect( + checkpointRestore(mockTask, { + ts: 2, + commitHash: RESTORE_HASH, + mode: "restore", + operation: "delete", + }), + ).resolves.toBeUndefined() + await new Promise((resolve) => setTimeout(resolve, 20)) // a stray rejection surfaces a turn later + + expect(unhandled).not.toHaveBeenCalled() + expect(mockProvider.log).toHaveBeenCalledWith( + expect.stringContaining("could not be started again: cannot rehydrate"), + ) + } finally { + process.off("unhandledRejection", unhandled) + } + }) + it("does not start the task again when the files could not be restored", async () => { mockCheckpointService.restoreCheckpoint.mockRejectedValueOnce(new Error("no such commit")) diff --git a/src/core/checkpoints/index.ts b/src/core/checkpoints/index.ts index b56f597002..dc58713997 100644 --- a/src/core/checkpoints/index.ts +++ b/src/core/checkpoints/index.ts @@ -268,8 +268,8 @@ export async function checkpointRestore( deletedRequests = await rewindConversation(task, ts, index, operation) } catch (error) { // The workspace is already restored: the task still has to be started again, as after a rewind that worked. - provider?.cancelTask() - throw new ConversationRewindError(error) + // A restart that fails is not hidden by the rewind failure, nor does it hide it: both are told. + throw new ConversationRewindError(error, await startTaskAgain(provider)) } // The rewind is saved. The row that reports the deleted requests is only a note: a task that cannot say it @@ -296,13 +296,28 @@ export async function checkpointRestore( // I'd like to revisit this in the future and try to improve the // task flow and the communication between the webview and the // `Task` instance. - provider?.cancelTask() + void startTaskAgain(provider) } catch (error) { provider?.log("[checkpointRestore] failed to restore checkpoint") throw error } } +/** Starts the task again (the provider cancels it, and it is rehydrated). Resolves to what failed, if the restart did. */ +async function startTaskAgain( + provider: ReturnType, +): Promise<{ error: unknown } | undefined> { + try { + await provider?.cancelTask() + return undefined + } catch (error) { + provider?.log( + `[checkpointRestore] the task could not be started again: ${error instanceof Error ? error.message : String(error)}`, + ) + return { error } + } +} + /** Rewinds the conversation to the checkpoint, and gives the usage of the requests that it deleted. */ async function rewindConversation( task: Task, diff --git a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts index f95660862f..241781fc2c 100644 --- a/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts +++ b/src/core/webview/__tests__/checkpointRestoreHandler.spec.ts @@ -382,6 +382,73 @@ describe("checkpointRestoreHandler", () => { ) }) + describe("when the task could not be started again either", () => { + const notRestarted = () => + new ConversationRewindError(new Error("Failed to save the messages"), { + error: new Error("cannot rehydrate"), + }) + + it("does not leave an edit waiting for a task that will not come: it is cleared and put back in the chat box", async () => { + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(notRestarted()) + + await expect( + handleCheckpointRestoreOperation({ + ...fixture.config, + operation: "edit", + editData: { + editedContent: "Edited prompt", + images: ["image.png"], + apiConversationHistoryIndex: 0, + }, + }), + ).rejects.toMatchObject({ restarted: false }) + + expect(fixture.providerDouble.clearPendingEditOperation).toHaveBeenCalledWith( + `task-${fixture.task.taskId}`, + expect.any(Object), + ) + expect(fixture.store.get(`task-${fixture.task.taskId}`)).toBeUndefined() + expect(fixture.providerDouble.postMessageToWebview).toHaveBeenCalledWith({ + type: "invoke", + invoke: "setChatBoxMessage", + text: "Edited prompt", + images: ["image.png"], + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledExactlyOnceWith( + "common:errors.checkpoint_rewind_restart_failed: Failed to save the messages", + ) + }) + + it("still reports it when the chat box cannot be filled", async () => { + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(notRestarted()) + fixture.providerDouble.postMessageToWebview.mockRejectedValue(new Error("webview gone")) + + await expect( + handleCheckpointRestoreOperation({ + ...fixture.config, + operation: "edit", + editData: { editedContent: "Edited prompt", apiConversationHistoryIndex: 0 }, + }), + ).rejects.toMatchObject({ restarted: false }) + + expect(vscode.window.showErrorMessage).toHaveBeenCalledOnce() + }) + + it("offers to restart a deleted task, since nothing started it", async () => { + fixture.taskDouble.abort = true + fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(notRestarted()) + + await expect(handleCheckpointRestoreOperation(fixture.config)).rejects.toMatchObject({ + restarted: false, + }) + + expect(showWarningMessage).toHaveBeenCalledWith( + "common:checkpoints.restart_after_restore_failure", + "common:checkpoints.restart", + ) + }) + }) + it("for a deleted task that was aborted, tells the user the files changed, and neither saves nor rehydrates it again", async () => { fixture.taskDouble.abort = true // the task was aborted before the restore, as a delete does fixture.taskDouble.checkpointRestore.mockRejectedValueOnce(rewindFailure()) diff --git a/src/core/webview/checkpointRestoreHandler.ts b/src/core/webview/checkpointRestoreHandler.ts index 0ec85ee850..a8fc48f114 100644 --- a/src/core/webview/checkpointRestoreHandler.ts +++ b/src/core/webview/checkpointRestoreHandler.ts @@ -88,9 +88,23 @@ export async function handleCheckpointRestoreOperation(config: CheckpointRestore } catch (error) { // The files were restored and the task is started again (see `ConversationRewindError`): the starting task // takes the pending edit, and hands it back to the user if it cannot apply it, and there is nothing to restart. - const taskRestarted = error instanceof ConversationRewindError + const taskRestarted = error instanceof ConversationRewindError && error.restarted if (operation === "edit" && !checkpointRestored && pendingEditOwner && !taskRestarted) { provider.clearPendingEditOperation(`task-${currentCline.taskId}`, pendingEditOwner) + // The rewind failed and the task could not be started again either: nothing will apply the edit, so it is + // put back in the chat box, as the provider does when it cannot apply it. + if (error instanceof ConversationRewindError && editData) { + try { + await provider.postMessageToWebview({ + type: "invoke", + invoke: "setChatBoxMessage", + text: editData.editedContent, + images: editData.images, + }) + } catch { + // The failure is reported below; the edit cannot be shown if the webview is gone. + } + } } console.error(`Error in checkpoint restore (${operation}):`, error) void vscode.window.showErrorMessage(checkpointRestoreFailureMessage(error)) diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 7b60a52180..e8ef989278 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -23,6 +23,7 @@ "checkpoint_failed": "Ha fallat la restauració del punt de control.", "checkpoint_restore_failed": "Error durant la restauració del punt de control: {{error}}", "checkpoint_rewind_failed": "Els fitxers s'han restaurat al punt de control, però no s'ha pogut rebobinar la conversa: {{error}}. La tasca s'ha reiniciat amb la conversa anterior; torna a restaurar el punt de control per rebobinar-la.", + "checkpoint_rewind_restart_failed": "Els fitxers s'han restaurat al punt de control, però no s'ha pogut rebobinar la conversa: {{error}}. Tampoc no s'ha pogut reiniciar la tasca; reinicia-la o torna a restaurar el punt de control per rebobinar la conversa.", "checkpoint_missing": "No es pot restaurar el punt de control {{commitHash}}: aquest punt de control ja no existeix al repositori de punts de control. No s'ha modificat cap fitxer.", "checkpoint_invalid_hash": "No es pot restaurar ni comparar el punt de control: la referència no correspon a cap punt de control vàlid d'aquesta tasca. No s'ha modificat cap fitxer.", "git_not_installed": "Git és necessari per a la funció de punts de control. Si us plau, instal·la Git per activar els punts de control.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 1ea17f8464..d9c8ed81e0 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Fehler beim Wiederherstellen des Checkpoints.", "checkpoint_restore_failed": "Fehler beim Wiederherstellen des Checkpoints: {{error}}", "checkpoint_rewind_failed": "Die Dateien wurden auf den Checkpoint zurückgesetzt, aber die Unterhaltung konnte nicht zurückgespult werden: {{error}}. Die Aufgabe wurde mit der bisherigen Unterhaltung neu gestartet; stelle den Checkpoint erneut wieder her, um sie zurückzuspulen.", + "checkpoint_rewind_restart_failed": "Die Dateien wurden auf den Checkpoint zurückgesetzt, aber die Unterhaltung konnte nicht zurückgespult werden: {{error}}. Die Aufgabe konnte auch nicht neu gestartet werden; starte sie neu oder stelle den Checkpoint erneut wieder her, um die Unterhaltung zurückzuspulen.", "checkpoint_missing": "Checkpoint {{commitHash}} kann nicht wiederhergestellt werden: Dieser Checkpoint existiert nicht mehr im Checkpoint-Repository. Es wurden keine Dateien geändert.", "checkpoint_invalid_hash": "Checkpoint kann nicht wiederhergestellt oder verglichen werden: Die Referenz ist kein gültiger Checkpoint dieser Aufgabe. Es wurden keine Dateien geändert.", "git_not_installed": "Git ist für die Checkpoint-Funktion erforderlich. Bitte installiere Git, um Checkpoints zu aktivieren.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index a79fe333b1..8e6b1049b4 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Failed to restore checkpoint.", "checkpoint_restore_failed": "Error during checkpoint restore: {{error}}", "checkpoint_rewind_failed": "The files were restored to the checkpoint, but the conversation could not be rewound: {{error}}. The task was restarted with its previous conversation; restore the checkpoint again to rewind it.", + "checkpoint_rewind_restart_failed": "The files were restored to the checkpoint, but the conversation could not be rewound: {{error}}. The task could not be restarted either; restart it, or restore the checkpoint again to rewind the conversation.", "checkpoint_missing": "Cannot restore checkpoint {{commitHash}}: this checkpoint no longer exists in the checkpoint repository. No files were changed.", "checkpoint_invalid_hash": "Cannot restore or compare checkpoint: the checkpoint reference is not a valid checkpoint of this task. No files were changed.", "git_not_installed": "Git is required for the checkpoints feature. Please install Git to enable checkpoints.", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 0e6238fd4c..e10626a6b3 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Error al restaurar el punto de control.", "checkpoint_restore_failed": "Error durante la restauración del punto de control: {{error}}", "checkpoint_rewind_failed": "Los archivos se restauraron al punto de control, pero no se pudo retroceder la conversación: {{error}}. La tarea se reinició con su conversación anterior; restaura el punto de control de nuevo para retroceder la conversación.", + "checkpoint_rewind_restart_failed": "Los archivos se restauraron al punto de control, pero no se pudo retroceder la conversación: {{error}}. Tampoco se pudo reiniciar la tarea; reiníciala o restaura el punto de control de nuevo para retroceder la conversación.", "checkpoint_missing": "No se puede restaurar el punto de control {{commitHash}}: este punto de control ya no existe en el repositorio de puntos de control. No se ha modificado ningún archivo.", "checkpoint_invalid_hash": "No se puede restaurar ni comparar el punto de control: la referencia no es un punto de control válido de esta tarea. No se ha modificado ningún archivo.", "git_not_installed": "Git es necesario para la función de puntos de control. Por favor, instala Git para activar los puntos de control.", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index ae5dde51e3..95ada6a4f0 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Échec du rétablissement du checkpoint.", "checkpoint_restore_failed": "Erreur lors du rétablissement du checkpoint : {{error}}", "checkpoint_rewind_failed": "Les fichiers ont été restaurés au point de contrôle, mais la conversation n'a pas pu être rembobinée : {{error}}. La tâche a été redémarrée avec sa conversation précédente ; restaurez à nouveau le point de contrôle pour la rembobiner.", + "checkpoint_rewind_restart_failed": "Les fichiers ont été restaurés au point de contrôle, mais la conversation n'a pas pu être rembobinée : {{error}}. La tâche n'a pas pu être redémarrée non plus ; redémarrez-la ou restaurez à nouveau le point de contrôle pour rembobiner la conversation.", "checkpoint_missing": "Impossible de rétablir le checkpoint {{commitHash}} : ce checkpoint n'existe plus dans le dépôt de checkpoints. Aucun fichier n'a été modifié.", "checkpoint_invalid_hash": "Impossible de rétablir ou de comparer le checkpoint : la référence ne correspond à aucun checkpoint valide de cette tâche. Aucun fichier n'a été modifié.", "git_not_installed": "Git est requis pour la fonctionnalité des points de contrôle. Veuillez installer Git pour activer les points de contrôle.", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index bbfbe569df..a9248abe8f 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "चेकपॉइंट पुनर्स्थापित करने में विफल।", "checkpoint_restore_failed": "चेकपॉइंट पुनर्स्थापित करते समय त्रुटि: {{error}}", "checkpoint_rewind_failed": "फ़ाइलें चेकपॉइंट पर बहाल हो गईं, लेकिन बातचीत को पीछे नहीं किया जा सका: {{error}}। कार्य को उसकी पिछली बातचीत के साथ पुनः आरंभ किया गया; बातचीत को पीछे करने के लिए चेकपॉइंट को दोबारा बहाल करें।", + "checkpoint_rewind_restart_failed": "फ़ाइलें चेकपॉइंट पर बहाल हो गईं, लेकिन बातचीत को पीछे नहीं किया जा सका: {{error}}। कार्य भी पुनः आरंभ नहीं हो सका; उसे पुनः आरंभ करें, या बातचीत को पीछे करने के लिए चेकपॉइंट को दोबारा बहाल करें।", "checkpoint_missing": "चेकपॉइंट {{commitHash}} पुनर्स्थापित नहीं किया जा सकता: यह चेकपॉइंट अब चेकपॉइंट रिपॉज़िटरी में मौजूद नहीं है। किसी भी फ़ाइल में बदलाव नहीं हुआ।", "checkpoint_invalid_hash": "चेकपॉइंट को पुनर्स्थापित या तुलना नहीं किया जा सकता: यह संदर्भ इस कार्य का मान्य चेकपॉइंट नहीं है। किसी भी फ़ाइल में बदलाव नहीं हुआ।", "git_not_installed": "चेकपॉइंट सुविधा के लिए Git आवश्यक है। कृपया चेकपॉइंट सक्षम करने के लिए Git इंस्टॉल करें।", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index dd2a8da867..118a9d0016 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Gagal memulihkan checkpoint.", "checkpoint_restore_failed": "Terjadi kesalahan saat memulihkan checkpoint: {{error}}", "checkpoint_rewind_failed": "File telah dipulihkan ke checkpoint, tetapi percakapan tidak dapat diputar mundur: {{error}}. Tugas dimulai ulang dengan percakapan sebelumnya; pulihkan checkpoint lagi untuk memutar mundur percakapan.", + "checkpoint_rewind_restart_failed": "File telah dipulihkan ke checkpoint, tetapi percakapan tidak dapat diputar mundur: {{error}}. Tugas juga tidak dapat dimulai ulang; mulai ulang tugas, atau pulihkan checkpoint lagi untuk memutar mundur percakapan.", "checkpoint_missing": "Tidak dapat memulihkan checkpoint {{commitHash}}: checkpoint ini sudah tidak ada di repositori checkpoint. Tidak ada file yang diubah.", "checkpoint_invalid_hash": "Tidak dapat memulihkan atau membandingkan checkpoint: referensi ini bukan checkpoint yang valid untuk tugas ini. Tidak ada file yang diubah.", "git_not_installed": "Git diperlukan untuk fitur checkpoint. Silakan instal Git untuk mengaktifkan checkpoint.", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index b2c4185ac9..fb1e2ea403 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Impossibile ripristinare il checkpoint.", "checkpoint_restore_failed": "Errore durante il ripristino del checkpoint: {{error}}", "checkpoint_rewind_failed": "I file sono stati ripristinati al checkpoint, ma non è stato possibile riavvolgere la conversazione: {{error}}. L'attività è stata riavviata con la conversazione precedente; ripristina di nuovo il checkpoint per riavvolgerla.", + "checkpoint_rewind_restart_failed": "I file sono stati ripristinati al checkpoint, ma non è stato possibile riavvolgere la conversazione: {{error}}. Non è stato possibile nemmeno riavviare l'attività; riavviala o ripristina di nuovo il checkpoint per riavvolgere la conversazione.", "checkpoint_missing": "Impossibile ripristinare il checkpoint {{commitHash}}: questo checkpoint non esiste più nel repository dei checkpoint. Nessun file è stato modificato.", "checkpoint_invalid_hash": "Impossibile ripristinare o confrontare il checkpoint: il riferimento non è un checkpoint valido di questa attività. Nessun file è stato modificato.", "git_not_installed": "Git è richiesto per la funzione di checkpoint. Per favore, installa Git per abilitare i checkpoint.", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 12d4b521ca..6c2257a7da 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "チェックポイントの復元に失敗しました。", "checkpoint_restore_failed": "チェックポイントの復元中にエラーが発生しました: {{error}}", "checkpoint_rewind_failed": "ファイルはチェックポイントに復元されましたが、会話を巻き戻せませんでした: {{error}}。タスクは以前の会話のまま再開されました。会話を巻き戻すには、もう一度チェックポイントを復元してください。", + "checkpoint_rewind_restart_failed": "ファイルはチェックポイントに復元されましたが、会話を巻き戻せませんでした: {{error}}。タスクも再開できませんでした。タスクを再開するか、もう一度チェックポイントを復元して会話を巻き戻してください。", "checkpoint_missing": "チェックポイント {{commitHash}} を復元できません。このチェックポイントはチェックポイントリポジトリに存在しなくなっています。ファイルは変更されていません。", "checkpoint_invalid_hash": "チェックポイントを復元または比較できません。この参照はこのタスクの有効なチェックポイントではありません。ファイルは変更されていません。", "git_not_installed": "チェックポイント機能にはGitが必要です。チェックポイントを有効にするにはGitをインストールしてください。", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 9a80ee964a..45e5eae1a6 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "체크포인트 복원에 실패했습니다.", "checkpoint_restore_failed": "체크포인트 복원 중 오류가 발생했어: {{error}}", "checkpoint_rewind_failed": "파일은 체크포인트로 복원되었지만 대화를 되돌리지 못했습니다: {{error}}. 작업은 이전 대화로 다시 시작되었습니다. 대화를 되돌리려면 체크포인트를 다시 복원하세요.", + "checkpoint_rewind_restart_failed": "파일은 체크포인트로 복원되었지만 대화를 되돌리지 못했습니다: {{error}}. 작업도 다시 시작하지 못했습니다. 작업을 다시 시작하거나 체크포인트를 다시 복원하여 대화를 되돌리세요.", "checkpoint_missing": "체크포인트 {{commitHash}}을(를) 복원할 수 없어. 이 체크포인트는 체크포인트 저장소에 더 이상 존재하지 않아. 변경된 파일은 없어.", "checkpoint_invalid_hash": "체크포인트를 복원하거나 비교할 수 없어. 이 참조는 이 작업의 유효한 체크포인트가 아니야. 변경된 파일은 없어.", "git_not_installed": "체크포인트 기능을 사용하려면 Git이 필요합니다. 체크포인트를 활성화하려면 Git을 설치하세요.", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 0ceaea8516..df5c31b950 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Herstellen van checkpoint mislukt.", "checkpoint_restore_failed": "Fout bij het herstellen van checkpoint: {{error}}", "checkpoint_rewind_failed": "De bestanden zijn hersteld naar het checkpoint, maar het gesprek kon niet worden teruggespoeld: {{error}}. De taak is opnieuw gestart met het vorige gesprek; herstel het checkpoint opnieuw om het terug te spoelen.", + "checkpoint_rewind_restart_failed": "De bestanden zijn hersteld naar het checkpoint, maar het gesprek kon niet worden teruggespoeld: {{error}}. De taak kon ook niet opnieuw worden gestart; start hem opnieuw of herstel het checkpoint opnieuw om het gesprek terug te spoelen.", "checkpoint_missing": "Checkpoint {{commitHash}} kan niet worden hersteld: dit checkpoint bestaat niet meer in de checkpoint-repository. Er zijn geen bestanden gewijzigd.", "checkpoint_invalid_hash": "Checkpoint kan niet worden hersteld of vergeleken: de verwijzing is geen geldig checkpoint van deze taak. Er zijn geen bestanden gewijzigd.", "git_not_installed": "Git is vereist voor de checkpoint-functie. Installeer Git om checkpoints in te schakelen.", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index e71797d8c4..c5a1acbdea 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Nie udało się przywrócić punktu kontrolnego.", "checkpoint_restore_failed": "Błąd podczas przywracania punktu kontrolnego: {{error}}", "checkpoint_rewind_failed": "Pliki zostały przywrócone do punktu kontrolnego, ale nie udało się cofnąć rozmowy: {{error}}. Zadanie uruchomiono ponownie z poprzednią rozmową; przywróć punkt kontrolny jeszcze raz, aby ją cofnąć.", + "checkpoint_rewind_restart_failed": "Pliki zostały przywrócone do punktu kontrolnego, ale nie udało się cofnąć rozmowy: {{error}}. Nie udało się też ponownie uruchomić zadania; uruchom je ponownie albo przywróć punkt kontrolny jeszcze raz, aby cofnąć rozmowę.", "checkpoint_missing": "Nie można przywrócić punktu kontrolnego {{commitHash}}: ten punkt kontrolny nie istnieje już w repozytorium punktów kontrolnych. Żadne pliki nie zostały zmienione.", "checkpoint_invalid_hash": "Nie można przywrócić ani porównać punktu kontrolnego: odwołanie nie jest prawidłowym punktem kontrolnym tego zadania. Żadne pliki nie zostały zmienione.", "git_not_installed": "Funkcja punktów kontrolnych wymaga oprogramowania Git. Zainstaluj Git, aby włączyć punkty kontrolne.", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 2820a06cdf..26c7ded46d 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -23,6 +23,7 @@ "checkpoint_failed": "Falha ao restaurar o ponto de verificação.", "checkpoint_restore_failed": "Erro durante a restauração do ponto de verificação: {{error}}", "checkpoint_rewind_failed": "Os arquivos foram restaurados para o checkpoint, mas não foi possível retroceder a conversa: {{error}}. A tarefa foi reiniciada com a conversa anterior; restaure o checkpoint novamente para retroceder a conversa.", + "checkpoint_rewind_restart_failed": "Os arquivos foram restaurados para o checkpoint, mas não foi possível retroceder a conversa: {{error}}. Também não foi possível reiniciar a tarefa; reinicie-a ou restaure o checkpoint novamente para retroceder a conversa.", "checkpoint_missing": "Não é possível restaurar o ponto de verificação {{commitHash}}: este ponto de verificação não existe mais no repositório de pontos de verificação. Nenhum arquivo foi alterado.", "checkpoint_invalid_hash": "Não é possível restaurar ou comparar o ponto de verificação: a referência não é um ponto de verificação válido desta tarefa. Nenhum arquivo foi alterado.", "git_not_installed": "O Git é necessário para o recurso de checkpoints. Por favor, instale o Git para habilitar os checkpoints.", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index b539ae8fe4..2f90f50770 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Не удалось восстановить контрольную точку.", "checkpoint_restore_failed": "Ошибка при восстановлении контрольной точки: {{error}}", "checkpoint_rewind_failed": "Файлы восстановлены до контрольной точки, но переписку не удалось откатить: {{error}}. Задача перезапущена с прежней перепиской; восстановите контрольную точку ещё раз, чтобы откатить её.", + "checkpoint_rewind_restart_failed": "Файлы восстановлены до контрольной точки, но переписку не удалось откатить: {{error}}. Задачу также не удалось перезапустить; перезапустите её или восстановите контрольную точку ещё раз, чтобы откатить переписку.", "checkpoint_missing": "Невозможно восстановить контрольную точку {{commitHash}}: этой контрольной точки больше нет в репозитории контрольных точек. Ни один файл не был изменён.", "checkpoint_invalid_hash": "Невозможно восстановить или сравнить контрольную точку: ссылка не является действительной контрольной точкой этой задачи. Ни один файл не был изменён.", "git_not_installed": "Для функции контрольных точек требуется Git. Пожалуйста, установите Git, чтобы включить контрольные точки.", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 0a396a20d5..6371b00495 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Kontrol noktası geri yüklenemedi.", "checkpoint_restore_failed": "Kontrol noktası geri yüklenirken hata oluştu: {{error}}", "checkpoint_rewind_failed": "Dosyalar kontrol noktasına geri yüklendi ancak konuşma geri sarılamadı: {{error}}. Görev önceki konuşmasıyla yeniden başlatıldı; konuşmayı geri sarmak için kontrol noktasını yeniden geri yükleyin.", + "checkpoint_rewind_restart_failed": "Dosyalar kontrol noktasına geri yüklendi ancak konuşma geri sarılamadı: {{error}}. Görev de yeniden başlatılamadı; görevi yeniden başlatın veya konuşmayı geri sarmak için kontrol noktasını yeniden geri yükleyin.", "checkpoint_missing": "{{commitHash}} kontrol noktası geri yüklenemiyor: bu kontrol noktası artık kontrol noktası deposunda mevcut değil. Hiçbir dosya değiştirilmedi.", "checkpoint_invalid_hash": "Kontrol noktası geri yüklenemiyor veya karşılaştırılamıyor: bu başvuru, bu görevin geçerli bir kontrol noktası değil. Hiçbir dosya değiştirilmedi.", "git_not_installed": "Kontrol noktaları özelliği için Git gereklidir. Kontrol noktalarını etkinleştirmek için lütfen Git'i yükleyin.", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 51ca6358e7..c0e32f3a94 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "Không thể khôi phục điểm kiểm tra.", "checkpoint_restore_failed": "Lỗi khi khôi phục điểm kiểm tra: {{error}}", "checkpoint_rewind_failed": "Các tệp đã được khôi phục về điểm kiểm tra, nhưng không thể tua lại cuộc trò chuyện: {{error}}. Tác vụ đã được khởi động lại với cuộc trò chuyện trước đó; hãy khôi phục điểm kiểm tra lần nữa để tua lại.", + "checkpoint_rewind_restart_failed": "Các tệp đã được khôi phục về điểm kiểm tra, nhưng không thể tua lại cuộc trò chuyện: {{error}}. Tác vụ cũng không thể khởi động lại; hãy khởi động lại tác vụ, hoặc khôi phục điểm kiểm tra lần nữa để tua lại cuộc trò chuyện.", "checkpoint_missing": "Không thể khôi phục điểm kiểm tra {{commitHash}}: điểm kiểm tra này không còn tồn tại trong kho lưu trữ điểm kiểm tra. Không có tệp nào bị thay đổi.", "checkpoint_invalid_hash": "Không thể khôi phục hoặc so sánh điểm kiểm tra: tham chiếu này không phải là điểm kiểm tra hợp lệ của tác vụ này. Không có tệp nào bị thay đổi.", "git_not_installed": "Yêu cầu Git cho tính năng điểm kiểm tra. Vui lòng cài đặt Git để bật điểm kiểm tra.", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index f27fc63cf4..983894d0cd 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -24,6 +24,7 @@ "checkpoint_failed": "恢复检查点失败。", "checkpoint_restore_failed": "恢复存档点时出错:{{error}}", "checkpoint_rewind_failed": "文件已恢复到检查点,但无法回退对话:{{error}}。任务已使用之前的对话重新启动;请再次恢复检查点以回退对话。", + "checkpoint_rewind_restart_failed": "文件已恢复到检查点,但无法回退对话:{{error}}。任务也无法重新启动;请重新启动任务,或再次恢复检查点以回退对话。", "checkpoint_missing": "无法恢复存档点 {{commitHash}}:此存档点已不存在于存档点仓库中。未更改任何文件。", "checkpoint_invalid_hash": "无法恢复或比较存档点:该引用不是此任务的有效存档点。未更改任何文件。", "git_not_installed": "检查点功能需要 Git。请安装 Git 以启用检查点。", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 16710bc1b5..fc791bda65 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -19,6 +19,7 @@ "checkpoint_failed": "恢復檢查點失敗。", "checkpoint_restore_failed": "還原存檔點時發生錯誤:{{error}}", "checkpoint_rewind_failed": "檔案已還原至檢查點,但無法回溯對話:{{error}}。工作已使用先前的對話重新啟動;請再次還原檢查點以回溯對話。", + "checkpoint_rewind_restart_failed": "檔案已還原至檢查點,但無法回溯對話:{{error}}。工作也無法重新啟動;請重新啟動工作,或再次還原檢查點以回溯對話。", "checkpoint_missing": "無法還原存檔點 {{commitHash}}:此存檔點已不存在於存檔點儲存庫中。未變更任何檔案。", "checkpoint_invalid_hash": "無法還原或比較存檔點:此參照不是此工作的有效存檔點。未變更任何檔案。", "git_not_installed": "存檔點功能需要 Git。請安裝 Git 以啟用存檔點。",

oj6Vwi(TlV@r!u(j#m|Hd%9+OC`Vp>f?C{@ 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 delta 51897 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 Q!&k6w-PBYrRkR5EKjZd)$p8QV diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast.png b/webview-ui/src/components/settings/__tests__/__screenshots__/usage-settings-high-contrast.png index 99a8f61e5174f05eb7abdca967be41f41e981dc1..5ef985f3883f18105c0fc88909942976754a77f6 100644 GIT binary patch delta 59714 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 dHRH%4Ywc&NQZQ&Ac{eEcWy#dLb^dZq~n{M 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