From 611bfbc4b680ae024e40d119d5342ca707a77155 Mon Sep 17 00:00:00 2001 From: Josh Lambert Date: Wed, 12 Aug 2026 14:12:36 -0400 Subject: [PATCH] refactor(provider-usage): simplify usage loading --- packages/core/src/kilocode/provider-usage.ts | 30 +--- .../core/src/kilocode/provider-usage/cloud.ts | 10 +- .../kilocode/provider-usage/minimax/usage.ts | 92 +++++----- .../kilocode-provider-usage-cloud.test.ts | 3 +- .../kilocode-provider-usage-minimax.test.ts | 23 ++- .../core/test/kilocode-provider-usage.test.ts | 92 ++++++---- packages/kilo-gateway/src/provider-usage.ts | 42 +++++ packages/kilo-vscode/src/KiloProvider.ts | 51 +++--- .../src/kilo-provider/handlers/auth.ts | 4 + .../tests/unit/provider-usage.test.ts | 163 +++++++++++------- packages/kilo-vscode/webview-ui/src/App.tsx | 2 - .../src/components/profile/ProfileView.tsx | 11 +- .../components/profile/ProviderUsageCards.tsx | 53 +++--- .../webview-ui/src/context/server.tsx | 42 +---- .../kilo-vscode/webview-ui/src/i18n/ar.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/br.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/bs.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/da.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/de.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/en.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/es.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/fa.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/fr.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/it.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/ja.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/ko.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/nl.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/no.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/pl.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/ru.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/th.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/tr.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/uk.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/zh.ts | 14 ++ .../kilo-vscode/webview-ui/src/i18n/zht.ts | 14 ++ .../src/stories/agent-manager.stories.tsx | 2 - .../src/stories/profile.stories.tsx | 2 +- .../src/types/messages/provider-usage.ts | 9 - .../src/types/messages/webview-messages.ts | 8 +- .../components/dialog-provider-usage.tsx | 4 +- .../server/httpapi/groups/kilocode.ts | 33 ++-- .../server/httpapi/handlers/kilocode.ts | 32 +++- .../middleware/provider-usage-location.ts | 40 ----- .../src/kilocode/server/httpapi/server.ts | 3 +- .../schema/src/kilocode/provider-usage.ts | 8 +- packages/sdk/js/src/v2/gen/types.gen.ts | 7 +- packages/sdk/openapi.json | 22 ++- 47 files changed, 702 insertions(+), 380 deletions(-) delete mode 100644 packages/opencode/src/kilocode/server/httpapi/middleware/provider-usage-location.ts diff --git a/packages/core/src/kilocode/provider-usage.ts b/packages/core/src/kilocode/provider-usage.ts index 304d2bcc98..a27f4a3bd6 100644 --- a/packages/core/src/kilocode/provider-usage.ts +++ b/packages/core/src/kilocode/provider-usage.ts @@ -69,30 +69,9 @@ const minimax: Adapter = { cachePrefixes: ["minimax-direct-"], async run(ctx) { const items = await direct(ctx.candidates, ctx.fetch, ctx.source) - const failed = ctx.failedCandidates.flatMap((providerID) => - ctx.preserve(`minimax-direct-${bindings[providerID].region}`), - ) - const skipped = new Set() - if (ctx.failedCandidates.length) { - const shared = ctx.preserve("minimax-direct-shared") - const match = ctx.candidates.find( - (candidate) => ctx.preserve("minimax-direct-shared", fingerprint(candidate.key)).length, - ) - const group = match ? ctx.candidates.filter((candidate) => candidate.key === match.key) : [] - const id = match - ? group.length > 1 - ? "minimax-direct-shared" - : `minimax-direct-${bindings[match.providerID].region}` - : undefined - const item = items.find((candidate) => candidate.id === id) - if (!item || item.fetchState === "unavailable") { - failed.push(...shared) - if (id) skipped.add(id) - } - } - const merged = [ - ...new Map([...failed, ...items.filter((item) => !skipped.has(item.id))].map((item) => [item.id, item])).values(), - ] + // A candidate is either live or failed, never both, so the id sets are disjoint. + const stale = ctx.failedCandidates.flatMap((id) => ctx.preserve(`minimax-direct-${bindings[id].region}`)) + const merged = [...items, ...stale] ctx.prune( "minimax-direct-", merged.map((item) => item.id), @@ -371,6 +350,9 @@ function makeService( const results = yield* Effect.promise(() => Promise.all( registry.map((adapter) => + // Adapters are expected to be total (they absorb their own failures into + // unavailable/stale snapshots). This catch is the containment boundary so a + // faulty future adapter degrades to stale output instead of failing the endpoint. adapter.run(ctx).catch( (): AdapterResult => ({ items: adapter.cachePrefixes.flatMap((prefix) => diff --git a/packages/core/src/kilocode/provider-usage/cloud.ts b/packages/core/src/kilocode/provider-usage/cloud.ts index 71561337d7..74464ba45e 100644 --- a/packages/core/src/kilocode/provider-usage/cloud.ts +++ b/packages/core/src/kilocode/provider-usage/cloud.ts @@ -70,26 +70,18 @@ function durationMs(period: CodingPlanQuotaWindow["period"]) { return period.value * multipliers[period.unit] } -function periodLabel(period: CodingPlanQuotaWindow["period"]) { - const singular = period.value === 1 - if (period.unit === "hour") return `${period.value}-hour quota` - if (period.unit === "day") return singular ? "Daily quota" : `${period.value}-day quota` - if (period.unit === "week") return singular ? "Weekly quota" : `${period.value}-week quota` - return singular ? "Monthly quota" : `${period.value}-month quota` -} - function window(subscriptionId: string, value: CodingPlanQuotaWindow): ProviderUsage.UsageWindow { const remaining = value.remainingPercent const duration = durationMs(value.period) return { id: `${subscriptionId}:${value.id}`, - label: periodLabel(value.period), resource: "subscription", unit: "percent", orientation: "remaining_percent", used: Math.max(0, 100 - remaining), remaining, limit: 100, + period: value.period, ...(duration !== undefined ? { durationMs: duration } : {}), resetAt: value.resetsAt, state: remaining <= 0 ? "exhausted" : "active", diff --git a/packages/core/src/kilocode/provider-usage/minimax/usage.ts b/packages/core/src/kilocode/provider-usage/minimax/usage.ts index 085c74dbc8..1ce5111cfb 100644 --- a/packages/core/src/kilocode/provider-usage/minimax/usage.ts +++ b/packages/core/src/kilocode/provider-usage/minimax/usage.ts @@ -105,11 +105,17 @@ function duration(start: number | undefined, end: number | undefined) { return end - start } -function label(resource: string, kind: "interval" | "weekly", value: number | undefined) { - const prefix = resource === "general" ? "Shared quota" : resource === "video" ? "Video" : resource - if (value === 300 * 60 * 1000) return `${prefix} 5-hour` - if (value === 10_080 * 60 * 1000) return `${prefix} weekly` - return kind === "weekly" ? `${prefix} weekly` : `${prefix} interval` +const hourMs = 60 * 60 * 1000 +const dayMs = 24 * hourMs +const weekMs = 7 * dayMs + +function cadence(kind: "interval" | "weekly", span: number | undefined): ProviderUsage.UsagePeriod | undefined { + if (kind === "weekly") return { unit: "week", value: 1 } + if (span === undefined) return undefined + if (span % weekMs === 0) return { unit: "week", value: span / weekMs } + if (span % dayMs === 0) return { unit: "day", value: span / dayMs } + if (span % hourMs === 0) return { unit: "hour", value: span / hourMs } + return undefined } function window( @@ -131,8 +137,8 @@ function window( const span = duration(start, end) const base = { id: `${row.model_name}-${kind}`, - label: label(row.model_name, kind, span), resource: row.model_name, + period: cadence(kind, span), durationMs: span, resetAt: reset(end, remains, fetchedAt), } @@ -245,53 +251,33 @@ export async function direct( identity?: string, ) => Promise = (_id, load) => load(), ) { - const groups = new Map() - for (const candidate of candidates) { - if (!candidate.key.startsWith("sk-cp")) continue - const group = groups.get(candidate.key) ?? [] - group.push(candidate) - groups.set(candidate.key, group) - } - + // Each configured provider is an independent plan with a stable per-region + // cache cell, even when providers share a credential. return Promise.all( - [...groups.values()].map(async (group) => { - const shared = group.length > 1 - const first = group[0] - const id = shared ? "minimax-direct-shared" : `minimax-direct-${bindings[first.providerID].region}` - // The key fingerprint scopes the cache cell to the configured credential, so - // swapping keys never reuses the previous account's quota via TTL or stale fallback. - const identity = createHash("sha256").update(first.key).digest("hex") - return cached( - id, - async () => { - const responses = await Promise.allSettled( - group.map((candidate) => query(candidate.providerID, candidate.key, fetcher)), - ) - const index = responses.findIndex((response) => response.status === "fulfilled") - if (index === -1) { - return unavailable( - id, - first.providerID, - shared ? "Direct MiniMax" : first.label, - bindings[first.providerID].manage, - ) - } - - const candidate = group[index] - const response = responses[index] - if (response.status !== "fulfilled") { - return unavailable(id, candidate.providerID, "Direct MiniMax", bindings[candidate.providerID].manage) - } - return normalize(response.value, { - id, - providerID: candidate.providerID, - sourceLabel: candidate.label, - managementUrl: bindings[candidate.providerID].manage, - fetchedAt: new Date().toISOString(), - }) - }, - identity, - ) - }), + candidates + .filter((candidate) => candidate.key.startsWith("sk-cp")) + .map((candidate) => { + const binding = bindings[candidate.providerID] + const id = `minimax-direct-${binding.region}` + // The key fingerprint scopes the cache cell to the configured credential, so + // swapping keys never reuses the previous account's quota via TTL or stale fallback. + const identity = createHash("sha256").update(candidate.key).digest("hex") + return cached( + id, + () => + query(candidate.providerID, candidate.key, fetcher) + .then((native) => + normalize(native, { + id, + providerID: candidate.providerID, + sourceLabel: candidate.label, + managementUrl: binding.manage, + fetchedAt: new Date().toISOString(), + }), + ) + .catch(() => unavailable(id, candidate.providerID, candidate.label, binding.manage)), + identity, + ) + }), ) } diff --git a/packages/core/test/kilocode-provider-usage-cloud.test.ts b/packages/core/test/kilocode-provider-usage-cloud.test.ts index c6002b1ad3..bb1b2b0cb0 100644 --- a/packages/core/test/kilocode-provider-usage-cloud.test.ts +++ b/packages/core/test/kilocode-provider-usage-cloud.test.ts @@ -80,7 +80,8 @@ describe("managed provider usage", () => { windows: [ { id: "byteplus-plan:monthly", - label: "Monthly quota", + resource: "subscription", + period: { unit: "month", value: 1 }, remaining: 75, used: 25, limit: 100, diff --git a/packages/core/test/kilocode-provider-usage-minimax.test.ts b/packages/core/test/kilocode-provider-usage-minimax.test.ts index 184536220d..4f9f48b4f9 100644 --- a/packages/core/test/kilocode-provider-usage-minimax.test.ts +++ b/packages/core/test/kilocode-provider-usage-minimax.test.ts @@ -44,6 +44,7 @@ describe("MiniMax usage normalization", () => { remaining: 80, used: 20, limit: 100, + period: { unit: "hour", value: 5 }, resetAt: "2026-06-19T05:00:00.000Z", }) expect(direct.windows[0]?.remaining).not.toBe(1) @@ -112,6 +113,7 @@ describe("MiniMax usage normalization", () => { orientation: "amount", remaining: 150, limit: 150, + period: { unit: "week", value: 1 }, }) }) @@ -151,7 +153,7 @@ describe("MiniMax usage transport and detection", () => { expect(fn).not.toHaveBeenCalled() }) - test("deduplicates a shared credential while probing fixed regions", async () => { + test("keeps same-key providers as independent regional plans", async () => { const fn = mock((url: string | URL | Request) => Promise.resolve( String(url).includes("api.minimax.io") @@ -168,8 +170,17 @@ describe("MiniMax usage transport and detection", () => { ) expect(fn).toHaveBeenCalledTimes(2) - expect(items).toHaveLength(1) - expect(items[0]).toMatchObject({ id: "minimax-direct-shared", providerID: "minimax-cn-coding-plan" }) + expect(items).toHaveLength(2) + expect(items.find((item) => item.id === "minimax-direct-global")).toMatchObject({ + providerID: "minimax-coding-plan", + sourceLabel: "MiniMax Global", + fetchState: "unavailable", + }) + expect(items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ + providerID: "minimax-cn-coding-plan", + sourceLabel: "MiniMax China", + fetchState: "ready", + }) expect(JSON.stringify(items)).not.toContain("sk-cp-shared") }) @@ -190,15 +201,15 @@ describe("MiniMax usage transport and detection", () => { expect(JSON.stringify(seen)).not.toContain("sk-cp") }) - test("returns one unavailable item when an ambiguous key fails everywhere", async () => { + test("returns per-provider unavailable items when every query fails", async () => { const fn = mock(() => Promise.resolve(response({ message: "raw failure" }, 500))) const items = await direct( [candidate("minimax-coding-plan", "sk-cp-shared"), candidate("minimax-cn-coding-plan", "sk-cp-shared")], fn as unknown as typeof fetch, ) - expect(items).toHaveLength(1) - expect(items[0]).toMatchObject({ id: "minimax-direct-shared", fetchState: "unavailable" }) + expect(items).toHaveLength(2) + expect(items.every((item) => item.fetchState === "unavailable")).toBe(true) expect(JSON.stringify(items)).not.toContain("raw failure") }) }) diff --git a/packages/core/test/kilocode-provider-usage.test.ts b/packages/core/test/kilocode-provider-usage.test.ts index 70a65f7034..78d641af8f 100644 --- a/packages/core/test/kilocode-provider-usage.test.ts +++ b/packages/core/test/kilocode-provider-usage.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, mock } from "bun:test" +import { describe, expect, mock, setSystemTime } from "bun:test" import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect" import { Catalog } from "../src/catalog" import { Credential } from "../src/credential" @@ -406,6 +406,54 @@ describe("ProviderUsage location service", () => { }), ) + it.live("expires the success cache and retries failures on the shorter error TTL", () => + Effect.gen(function* () { + const calls = { direct: 0, cloud: 0 } + const responses = [native(80), new Response("upstream failure", { status: 503 }), native(70)] + const base = Date.parse("2026-08-12T00:00:00.000Z") + setSystemTime(new Date(base)) + const scope = yield* Scope.make() + const usage = Context.get( + yield* Layer.buildWithScope( + configuredLayer({ + calls, + direct: "sk-cp-direct", + accountID: "org", + transport: { + fetch: mock(() => { + calls.direct++ + return Promise.resolve(responses.shift()!) + }) as unknown as typeof fetch, + plans: async () => [], + byok: async () => [], + usage: async () => { + throw new Error("unused") + }, + }, + }), + scope, + ), + ProviderUsage.Service, + ) + + expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 80 }] }) + expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready" }) + expect(calls.direct).toBe(1) + + // Success TTL (60s) elapsed: the next get refetches and degrades to stale on failure. + setSystemTime(new Date(base + 61_000)) + expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "stale", windows: [{ remaining: 80 }] }) + expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "stale" }) + expect(calls.direct).toBe(2) + + // Error TTL (10s) elapsed: the failure is retried and recovers. + setSystemTime(new Date(base + 72_000)) + expect((yield* usage.get()).items[0]).toMatchObject({ fetchState: "ready", windows: [{ remaining: 70 }] }) + expect(calls.direct).toBe(3) + yield* Scope.close(scope, Exit.void) + }).pipe(Effect.ensuring(Effect.sync(() => setSystemTime()))), + ) + it.live("prunes authoritative removal but preserves a transient credential failure", () => Effect.gen(function* () { const removedCalls = { direct: 0, cloud: 0 } @@ -470,7 +518,7 @@ describe("ProviderUsage location service", () => { }), ) - it.live("does not duplicate shared usage when one provider credential fails", () => + it.live("keeps same-key providers independent when one credential fails", () => Effect.gen(function* () { const calls = { direct: 0, cloud: 0 } let failure: "global" | undefined @@ -489,17 +537,19 @@ describe("ProviderUsage location service", () => { ProviderUsage.Service, ) - expect((yield* usage.get()).items).toHaveLength(1) + expect((yield* usage.get()).items).toHaveLength(2) failure = "global" const result = yield* usage.get() - expect(result.items).toHaveLength(1) - expect(result.items[0]).toMatchObject({ providerID: "minimax-cn-coding-plan", fetchState: "ready" }) - expect(calls.direct).toBe(3) + expect(result.items).toHaveLength(2) + expect(result.items.find((item) => item.id === "minimax-direct-global")).toMatchObject({ fetchState: "stale" }) + expect(result.items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ fetchState: "ready" }) + // The surviving sibling serves from cache; the failed one is not refetched. + expect(calls.direct).toBe(2) }), ) - it.live("preserves failed shared usage when the surviving provider rotates credentials", () => + it.live("refreshes a rotated credential while preserving the failed sibling's cached usage", () => Effect.gen(function* () { const calls = { direct: 0, cloud: 0 } let failure: "global" | undefined @@ -519,23 +569,21 @@ describe("ProviderUsage location service", () => { ProviderUsage.Service, ) - expect((yield* usage.get()).items).toHaveLength(1) + expect((yield* usage.get()).items).toHaveLength(2) failure = "global" key = "sk-cp-rotated" const result = yield* usage.get() expect(result.items).toHaveLength(2) - expect(result.items.find((item) => item.id === "minimax-direct-shared")).toMatchObject({ fetchState: "stale" }) + expect(result.items.find((item) => item.id === "minimax-direct-global")).toMatchObject({ fetchState: "stale" }) expect(result.items.find((item) => item.id === "minimax-direct-china")).toMatchObject({ fetchState: "ready" }) expect(calls.direct).toBe(3) }), ) - it.live("preserves shared usage when the surviving same-key query fails", () => + it.live("omits a failed provider with no cached usage", () => Effect.gen(function* () { const calls = { direct: 0, cloud: 0 } - let failure: "global" | undefined - let queryFailure = false const scope = yield* Scope.make() const usage = Context.get( yield* Layer.buildWithScope( @@ -544,32 +592,18 @@ describe("ProviderUsage location service", () => { accountID: "org", config: { china: true }, direct: "sk-cp-shared", - failure: () => failure, - transport: { - fetch: mock(() => { - calls.direct++ - return queryFailure ? Promise.reject(new Error("private query failure")) : Promise.resolve(native(80)) - }) as unknown as typeof fetch, - plans: async () => [], - byok: async () => [], - usage: async () => { - throw new Error("unused") - }, - }, + failure: () => "global" as const, }), scope, ), ProviderUsage.Service, ) - expect((yield* usage.get()).items).toHaveLength(1) - failure = "global" - queryFailure = true const result = yield* usage.get() expect(result.items).toHaveLength(1) - expect(result.items[0]).toMatchObject({ id: "minimax-direct-shared", fetchState: "stale" }) - expect(JSON.stringify(result)).not.toContain("private query failure") + expect(result.items[0]).toMatchObject({ id: "minimax-direct-china", fetchState: "ready" }) + expect(calls.direct).toBe(1) }), ) diff --git a/packages/kilo-gateway/src/provider-usage.ts b/packages/kilo-gateway/src/provider-usage.ts index 7f5fede3ed..00c8e37ac8 100644 --- a/packages/kilo-gateway/src/provider-usage.ts +++ b/packages/kilo-gateway/src/provider-usage.ts @@ -6,10 +6,17 @@ * so the two surfaces can never drift; labels are injectable for i18n. */ +export interface UsagePeriodLike { + unit: "hour" | "day" | "week" | "month" + value: number +} + export interface UsageWindowLike { state: "active" | "exhausted" | "unlimited" | "not_in_plan" | "unknown" orientation: "used_percent" | "remaining_percent" | "amount" | "count" unit: string + resource: string + period?: UsagePeriodLike used?: number remaining?: number limit?: number @@ -24,6 +31,17 @@ export interface UsageLabels { remaining(value: string): string remainingOf(value: string, limit: string): string usedOf(value: string, limit: string): string + quota: string + daily: string + weekly: string + monthly: string + hours(count: number): string + days(count: number): string + weeks(count: number): string + months(count: number): string + /** Display name for MiniMax's pooled "general" resource. */ + shared: string + scoped(resource: string, period: string): string } export const english: UsageLabels = { @@ -35,6 +53,30 @@ export const english: UsageLabels = { remaining: (value) => `${value} remaining`, remainingOf: (value, limit) => `${value} of ${limit} remaining`, usedOf: (value, limit) => `${value} of ${limit} used`, + quota: "Quota", + daily: "Daily quota", + weekly: "Weekly quota", + monthly: "Monthly quota", + hours: (count) => `${count}-hour quota`, + days: (count) => `${count}-day quota`, + weeks: (count) => `${count}-week quota`, + months: (count) => `${count}-month quota`, + shared: "Shared", + scoped: (resource, period) => `${resource} · ${period}`, +} + +const period = (value: UsagePeriodLike, labels: UsageLabels) => { + if (value.unit === "hour") return labels.hours(value.value) + if (value.unit === "day") return value.value === 1 ? labels.daily : labels.days(value.value) + if (value.unit === "week") return value.value === 1 ? labels.weekly : labels.weeks(value.value) + return value.value === 1 ? labels.monthly : labels.months(value.value) +} + +export const windowLabel = (window: UsageWindowLike, labels: UsageLabels = english) => { + const phrase = window.period ? period(window.period, labels) : labels.quota + // Plan-level windows ("subscription") are the whole card; named resources prefix theirs. + if (window.resource === "subscription") return phrase + return labels.scoped(window.resource === "general" ? labels.shared : window.resource, phrase) } const number = (value: number) => value.toLocaleString(undefined, { maximumFractionDigits: 2 }) diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 29124e1aa8..981c05e4ec 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -3,6 +3,7 @@ import { existsSync } from "fs" import * as vscode from "vscode" import type { KiloClient, + ProviderUsage, Session, SessionStatus, Event, @@ -367,8 +368,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** Cached notificationsLoaded payload */ private cachedNotificationsMessage: NotificationsMessage | null = null /** Cached provider usage payload for profile view remounts and temporary disconnects. */ - private cachedProviderUsageMessage: { type: "providerUsageLoaded"; data: unknown } | null = null - private providerUsageRequested = false + private cachedProviderUsageMessage: { type: "providerUsageLoaded"; data: ProviderUsage } | null = null private providerUsageGeneration = 0 private pendingKiloModel: { modelID?: string; agent?: string } | null = null private pendingReviewComments: { comments: unknown[]; autoSend: boolean }[] = [] @@ -575,7 +575,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.postMessage({ type: "workspaceDirectoryChanged", directory: directory ?? "" }) this.postMessage({ type: "configBindingExpired", reason: "project-changed" }) this.requirements.clear() - if (this.providerUsageRequested) void this.fetchAndSendProviderUsage() } public setDiffVirtualProvider(provider: import("./DiffVirtualProvider").DiffVirtualProvider): void { @@ -1560,18 +1559,8 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper await handleRefreshProfile(this.authCtx) return true } - if (message.type === "requestProviderUsage") { - await this.fetchAndSendProviderUsage() - return true - } if (message.type === "refreshProviderUsage") { - await this.fetchAndSendProviderUsage(true) - return true - } - if (message.type === "releaseProviderUsage") { - // Profile view unmounted: stop background refreshes on directory/auth - // changes until the view requests usage again. - this.providerUsageRequested = false + await this.fetchAndSendProviderUsage() return true } return false @@ -2370,32 +2359,28 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper } } - private async fetchAndSendProviderUsage(force = false): Promise { - this.providerUsageRequested = true + private async fetchAndSendProviderUsage(): Promise { const generation = ++this.providerUsageGeneration const client = this.client if (!client) { - if (this.cachedProviderUsageMessage) this.postMessage(this.cachedProviderUsageMessage) + this.postMessage( + this.cachedProviderUsageMessage ?? { + type: "providerUsageLoaded", + error: "Provider usage could not be loaded.", + }, + ) return } const directory = this.getProjectDirectory(this.currentSession?.id) - const result = await ( - force ? client.kilocode.providerUsage.refresh({ directory }) : client.kilocode.providerUsage.get({ directory }) - ).catch((error) => { + const result = await client.kilocode.providerUsage.refresh({ directory }).catch((error) => { console.error("[Kilo New] KiloProvider: Failed to fetch provider usage:", error) return undefined }) if (generation !== this.providerUsageGeneration) return if (!result?.data) { if (this.cachedProviderUsageMessage) { - // Re-serve the cached data, but tell the user when their explicit - // refresh failed instead of silently presenting stale data as fresh. - this.postMessage( - force - ? { ...this.cachedProviderUsageMessage, error: "Provider usage could not be refreshed." } - : this.cachedProviderUsageMessage, - ) + this.postMessage({ ...this.cachedProviderUsageMessage, error: "Provider usage could not be refreshed." }) return } this.postMessage({ type: "providerUsageLoaded", error: "Provider usage could not be loaded." }) @@ -4038,12 +4023,19 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper postMessage: (msg) => this.postMessage(msg), getWorkspaceDirectory: () => this.getWorkspaceDirectory(), disposeGlobal: () => this.disposeGlobal(), + invalidateProviderUsage: () => this.invalidateProviderUsage(), fetchAndSendProviders: () => this.fetchAndSendProviders(), fetchAndSendAgents: () => this.fetchAndSendAgents(), fetchAndSendSpeechToTextModels: () => this.fetchAndSendSpeechToTextModels(), } } + private invalidateProviderUsage(): void { + this.providerUsageGeneration++ + this.cachedProviderUsageMessage = null + this.postMessage({ type: "providerUsageLoaded", reset: true }) + } + private async disposeGlobal(): Promise { if (!this.client) return @@ -4188,9 +4180,7 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper /** Re-fetch all server-side state after an auth change. */ private async reloadAfterAuthChange(): Promise { this.requirements.clear() - this.providerUsageGeneration++ - this.cachedProviderUsageMessage = null - if (this.providerUsageRequested) this.postMessage({ type: "providerUsageLoaded", reset: true }) + this.invalidateProviderUsage() await this.fetchAndSendConfig() await Promise.all([ this.fetchAndSendProviders(), @@ -4199,7 +4189,6 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper this.fetchAndSendCommands(), this.fetchAndSendIndexingStatus(), this.fetchAndSendNotifications(), - this.providerUsageRequested ? this.fetchAndSendProviderUsage() : Promise.resolve(), ]) } diff --git a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts index 07ae712b90..573a1053ec 100644 --- a/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts +++ b/packages/kilo-vscode/src/kilo-provider/handlers/auth.ts @@ -13,6 +13,7 @@ export interface AuthContext { postMessage(msg: unknown): void getWorkspaceDirectory(): string disposeGlobal(): Promise + invalidateProviderUsage(): void fetchAndSendProviders(): Promise fetchAndSendAgents(): Promise fetchAndSendSpeechToTextModels(): Promise @@ -60,6 +61,7 @@ export async function handleLogin(ctx: AuthContext, attempt: number, getAttempt: console.log("[Kilo New] KiloProvider: 🔐 Login successful") + ctx.invalidateProviderUsage() await ctx.disposeGlobal() // Step 3: Fetch profile and push to webview @@ -85,6 +87,7 @@ export async function handleLogout(ctx: AuthContext): Promise { console.log("[Kilo New] KiloProvider: 🚪 Logged out successfully") ctx.postMessage({ type: "profileData", data: null }) + ctx.invalidateProviderUsage() await ctx.disposeGlobal() await ctx.fetchAndSendProviders() @@ -119,6 +122,7 @@ export async function handleSetOrganization(ctx: AuthContext, organizationId: st return } + ctx.invalidateProviderUsage() await ctx.disposeGlobal() // Org switch succeeded — refresh profile and providers independently (best-effort) diff --git a/packages/kilo-vscode/tests/unit/provider-usage.test.ts b/packages/kilo-vscode/tests/unit/provider-usage.test.ts index a0405a1f0d..c59e07f091 100644 --- a/packages/kilo-vscode/tests/unit/provider-usage.test.ts +++ b/packages/kilo-vscode/tests/unit/provider-usage.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "bun:test" import type { ProviderUsage, ProviderUsageWindow } from "@kilocode/sdk/v2/client" -import { formatWindow, windowProgress } from "@kilocode/kilo-gateway/provider-usage" +import { formatWindow, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage" const { KiloProvider } = await import("../../src/KiloProvider") @@ -10,43 +10,46 @@ const data: ProviderUsage = { } type Internals = { - providerUsageRequested: boolean cachedProviderUsageMessage: unknown - fetchAndSendProviderUsage: (force?: boolean) => Promise - handleProfileDataMessage: (message: { type: string }) => Promise + fetchAndSendProviderUsage: () => Promise reloadAfterAuthChange: () => Promise postMessage: (message: unknown) => void - fetchAndSendConfig: () => Promise - fetchAndSendProviders: () => Promise - fetchAndSendAgents: () => Promise - fetchAndSendSkills: () => Promise - fetchAndSendCommands: () => Promise - fetchAndSendIndexingStatus: () => Promise - fetchAndSendNotifications: () => Promise } type UsageClient = { - get: (input: { directory?: string }) => Promise - refresh?: (input: { directory?: string }) => Promise + refresh: (input: { directory?: string }) => Promise } +// Answers any SDK endpoint outside the fake usage client with a benign empty +// response, so tests never have to mirror KiloProvider's internal fetcher list. +const benign = (value: unknown): unknown => + typeof value === "function" + ? value + : new Proxy(() => {}, { + get: (_, prop) => + prop === "then" ? undefined : benign((value as Record | undefined)?.[prop]), + apply: () => Promise.resolve({ data: [] }), + }) + function bridge(usage: UsageClient) { const messages: unknown[] = [] const provider = new KiloProvider( {} as never, - { getClient: () => ({ kilocode: { providerUsage: usage } }) } as never, + { getClient: () => benign({ kilocode: { providerUsage: usage } }) } as never, undefined, { projectDirectory: "/repo" }, ) const internal = provider as unknown as Internals internal.postMessage = (message) => messages.push(message) - return { internal, messages } + return { provider, internal, messages } } +const usageMessages = (messages: unknown[]) => + messages.filter((message) => (message as { type?: string }).type === "providerUsageLoaded") + describe("provider usage presentation", () => { const window = (value: Partial): ProviderUsageWindow => ({ id: "quota", - label: "Quota", resource: "general", unit: "percent", orientation: "remaining_percent", @@ -66,17 +69,20 @@ describe("provider usage presentation", () => { expect(formatWindow(window({ state: "unlimited" }))).toBe("Unlimited") expect(formatWindow(window({ state: "not_in_plan" }))).toBe("Not in plan") }) + + it("composes window labels from structured periods instead of wire strings", () => { + expect(windowLabel(window({ resource: "subscription", period: { unit: "month", value: 1 } }))).toBe("Monthly quota") + expect(windowLabel(window({ resource: "subscription", period: { unit: "day", value: 3 } }))).toBe("3-day quota") + expect(windowLabel(window({ period: { unit: "hour", value: 5 } }))).toBe("Shared · 5-hour quota") + expect(windowLabel(window({ period: { unit: "week", value: 1 } }))).toBe("Shared · Weekly quota") + expect(windowLabel(window({ resource: "image" }))).toBe("image · Quota") + }) }) describe("KiloProvider provider usage bridge", () => { - it("uses cache-aware GET and explicit refresh POST", async () => { - const get: Array<{ directory?: string }> = [] + it("loads usage only through an explicit refresh", async () => { const refresh: Array<{ directory?: string }> = [] const { internal, messages } = bridge({ - get: async (input) => { - get.push(input) - return { data } - }, refresh: async (input) => { refresh.push(input) return { data } @@ -84,36 +90,26 @@ describe("KiloProvider provider usage bridge", () => { }) await internal.fetchAndSendProviderUsage() - await internal.fetchAndSendProviderUsage(true) - expect(get).toEqual([{ directory: "/repo" }]) expect(refresh).toEqual([{ directory: "/repo" }]) - expect(messages).toEqual([ - { type: "providerUsageLoaded", data }, - { type: "providerUsageLoaded", data }, - ]) - expect(internal.providerUsageRequested).toBe(true) + expect(messages).toEqual([{ type: "providerUsageLoaded", data }]) expect(internal.cachedProviderUsageMessage).toEqual({ type: "providerUsageLoaded", data }) }) it("surfaces a failed forced refresh alongside the cached data", async () => { const { internal, messages } = bridge({ - get: async () => ({ data }), refresh: async () => ({ error: { _tag: "ServiceUnavailable" } }), }) + internal.cachedProviderUsageMessage = { type: "providerUsageLoaded", data } await internal.fetchAndSendProviderUsage() - await internal.fetchAndSendProviderUsage(true) - expect(messages).toEqual([ - { type: "providerUsageLoaded", data }, - { type: "providerUsageLoaded", data, error: "Provider usage could not be refreshed." }, - ]) + expect(messages).toEqual([{ type: "providerUsageLoaded", data, error: "Provider usage could not be refreshed." }]) }) it("posts a terminal loading error when the backend has no cached response", async () => { const { internal, messages } = bridge({ - get: async () => ({ error: { _tag: "ServiceUnavailable" } }), + refresh: async () => ({ error: { _tag: "ServiceUnavailable" } }), }) await internal.fetchAndSendProviderUsage() @@ -121,35 +117,76 @@ describe("KiloProvider provider usage bridge", () => { expect(messages).toEqual([{ type: "providerUsageLoaded", error: "Provider usage could not be loaded." }]) }) - it("refreshes usage after auth invalidation only after the profile requested it", async () => { - const provider = new KiloProvider({} as never, {} as never) - const internal = provider as unknown as Internals - let usage = 0 - internal.fetchAndSendConfig = async () => {} - internal.fetchAndSendProviders = async () => {} - internal.fetchAndSendAgents = async () => {} - internal.fetchAndSendSkills = async () => {} - internal.fetchAndSendCommands = async () => {} - internal.fetchAndSendIndexingStatus = async () => {} - internal.fetchAndSendNotifications = async () => {} - internal.fetchAndSendProviderUsage = async () => { - usage++ - } - - await internal.reloadAfterAuthChange() - expect(usage).toBe(0) - internal.providerUsageRequested = true - await internal.reloadAfterAuthChange() - expect(usage).toBe(1) - }) - - it("releases the background refresh latch when the profile view unmounts", async () => { - const { internal } = bridge({ get: async () => ({ data }) }) + it("invalidates cached usage without reloading on auth change", async () => { + const requests: unknown[] = [] + const { internal, messages } = bridge({ + refresh: async (input) => { + requests.push(input) + return { data: { generatedAt: "a", items: [] } } + }, + }) await internal.fetchAndSendProviderUsage() - expect(internal.providerUsageRequested).toBe(true) + await internal.reloadAfterAuthChange() - expect(await internal.handleProfileDataMessage({ type: "releaseProviderUsage" })).toBe(true) - expect(internal.providerUsageRequested).toBe(false) + expect(usageMessages(messages)).toEqual([ + { type: "providerUsageLoaded", data: { generatedAt: "a", items: [] } }, + { type: "providerUsageLoaded", reset: true }, + ]) + expect(requests).toHaveLength(1) + expect(internal.cachedProviderUsageMessage).toBeNull() + }) + + it("resets usage without fetching when auth changes before the profile is opened", async () => { + const requests: unknown[] = [] + const { internal, messages } = bridge({ + refresh: async (input) => { + requests.push(input) + return { data } + }, + }) + + await internal.reloadAfterAuthChange() + + expect(requests).toEqual([]) + expect(usageMessages(messages)).toEqual([{ type: "providerUsageLoaded", reset: true }]) + }) + + it("drops an in-flight usage response from the previous account", async () => { + let release!: (value: { data: ProviderUsage }) => void + const first = new Promise<{ data: ProviderUsage }>((resolve) => (release = resolve)) + const calls: unknown[] = [] + const { internal, messages } = bridge({ + refresh: (input) => { + calls.push(input) + return first + }, + }) + + const hung = internal.fetchAndSendProviderUsage() + await internal.reloadAfterAuthChange() + release({ data: { generatedAt: "a", items: [] } }) + await hung + + expect(usageMessages(messages)).toEqual([{ type: "providerUsageLoaded", reset: true }]) + expect(calls).toHaveLength(1) + expect(internal.cachedProviderUsageMessage).toBeNull() + }) + + it("invalidates cached usage without fetching when the workspace directory changes", async () => { + const requests: unknown[] = [] + const { provider, internal, messages } = bridge({ + refresh: async (input) => { + requests.push(input) + return { data } + }, + }) + + await internal.fetchAndSendProviderUsage() + provider.setProjectDirectory("/other") + + expect(requests).toHaveLength(1) + expect(internal.cachedProviderUsageMessage).toBeNull() + expect(messages).toContainEqual({ type: "workspaceDirectoryChanged", directory: "/other" }) }) }) diff --git a/packages/kilo-vscode/webview-ui/src/App.tsx b/packages/kilo-vscode/webview-ui/src/App.tsx index fedb8b4058..c22470cb5a 100644 --- a/packages/kilo-vscode/webview-ui/src/App.tsx +++ b/packages/kilo-vscode/webview-ui/src/App.tsx @@ -364,9 +364,7 @@ const AppContent: Component = () => { providerUsageError={server.providerUsageError()} deviceAuth={server.deviceAuth()} onLogin={server.startLogin} - onRequestProviderUsage={server.requestProviderUsage} onRefreshProviderUsage={server.refreshProviderUsage} - onReleaseProviderUsage={server.releaseProviderUsage} /> diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx index 9c80af81c3..7c669fd2db 100644 --- a/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/profile/ProfileView.tsx @@ -1,4 +1,4 @@ -import { Component, Show, createSignal, createMemo, createEffect, onCleanup, onMount } from "solid-js" +import { Component, Show, createSignal, createMemo, createEffect, onMount } from "solid-js" import { Button } from "@kilocode/kilo-ui/button" import { Card } from "@kilocode/kilo-ui/card" import { Icon } from "@kilocode/kilo-ui/icon" @@ -19,9 +19,7 @@ export interface ProfileViewProps { providerUsageLoading?: boolean providerUsageError?: string onLogin: () => void - onRequestProviderUsage?: () => void onRefreshProviderUsage?: () => void - onReleaseProviderUsage?: () => void } const formatBalance = (amount: number): string => { @@ -43,15 +41,12 @@ const ProfileView: Component = (props) => { const personal = createMemo(() => props.profileData?.profile.hasPersonalAccount !== false) - // Always fetch fresh profile+balance when navigating to this view + // Load current profile and usage when navigating to this view. onMount(() => { vscode.postMessage({ type: "refreshProfile" }) - props.onRequestProviderUsage?.() + props.onRefreshProviderUsage?.() }) - // Stop background usage refreshes while this view is not visible - onCleanup(() => props.onReleaseProviderUsage?.()) - // Reset pending target whenever profileData changes (success or failure both send a fresh profile) createEffect(() => { props.profileData // track diff --git a/packages/kilo-vscode/webview-ui/src/components/profile/ProviderUsageCards.tsx b/packages/kilo-vscode/webview-ui/src/components/profile/ProviderUsageCards.tsx index 8a9fccb699..2c8451f768 100644 --- a/packages/kilo-vscode/webview-ui/src/components/profile/ProviderUsageCards.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/profile/ProviderUsageCards.tsx @@ -9,7 +9,7 @@ import { Spinner } from "@kilocode/kilo-ui/spinner" import { Tag } from "@kilocode/kilo-ui/tag" import { useLanguage } from "../../context/language" import { localeToBcp47 } from "../../context/language-utils" -import { formatWindow, windowProgress } from "@kilocode/kilo-gateway/provider-usage" +import { formatWindow, windowLabel, windowProgress } from "@kilocode/kilo-gateway/provider-usage" export interface ProviderUsageCardsProps { data: ProviderUsageData | undefined @@ -38,6 +38,16 @@ const labels = (language: Language) => ({ remaining: (value: string) => language.t("profile.usage.window.remaining", { value }), remainingOf: (value: string, limit: string) => language.t("profile.usage.window.remainingOf", { value, limit }), usedOf: (value: string, limit: string) => language.t("profile.usage.window.usedOf", { value, limit }), + quota: language.t("profile.usage.window.quota"), + daily: language.t("profile.usage.window.daily"), + weekly: language.t("profile.usage.window.weekly"), + monthly: language.t("profile.usage.window.monthly"), + hours: (count: number) => language.t("profile.usage.window.hours", { count: String(count) }), + days: (count: number) => language.t("profile.usage.window.days", { count: String(count) }), + weeks: (count: number) => language.t("profile.usage.window.weeks", { count: String(count) }), + months: (count: number) => language.t("profile.usage.window.months", { count: String(count) }), + shared: language.t("profile.usage.window.shared"), + scoped: (resource: string, period: string) => language.t("profile.usage.window.scoped", { resource, period }), }) const variant = (item: ProviderUsageSnapshot) => { @@ -104,6 +114,7 @@ const UsageCard: Component<{ {(window) => { const progress = () => windowProgress(window) const value = () => formatWindow(window, labels(props.language)) + const title = () => windowLabel(window, labels(props.language)) return (
@@ -113,22 +124,24 @@ const UsageCard: Component<{ maxValue={100} showValueLabel getValueLabel={value} - aria-label={`${window.label}: ${value()}`} + aria-label={`${title()}: ${value()}`} aria-valuetext={value()} > - {window.label} + {title()}
- {window.label} + {title()} {value()}
{(reset) => ( - {props.language.t("profile.usage.reset", { date: new Date(reset()).toLocaleString() })} + {props.language.t("profile.usage.reset", { + date: new Date(reset()).toLocaleString(localeToBcp47(props.language.locale())), + })} )} @@ -153,7 +166,7 @@ const UsageCard: Component<{ variant="secondary" size="small" onClick={() => props.onOpen(url())} - aria-label={`Manage ${props.item.planLabel}`} + aria-label={props.language.t("profile.usage.action.managePlan", { plan: props.item.planLabel })} > {props.language.t("profile.usage.action.manage")} @@ -208,11 +221,11 @@ const KiloPassCard: Component<{ used={pass().currentPeriodUsageUsd} paid={pass().currentPeriodBaseCreditsUsd} bonus={pass().currentPeriodBonusCreditsUsd} - label="This month's usage" - paidLabel="Paid" + label={props.language.t("profile.pass.usage")} + paidLabel={props.language.t("profile.pass.paid")} bonusLabel={props.language.t("profile.pass.bonus")} format={money} - aria-label="Kilo Pass monthly usage" + aria-label={props.language.t("profile.pass.meter")} /> {(date) => ( @@ -261,20 +274,20 @@ export const ProviderUsageCards: Component = (props) => +
- } - > - {(error) => ( - - {error()} - - )} -
+
+ + {(error) => ( + + {error()} + + )} + + } > {(data) => ( diff --git a/packages/kilo-vscode/webview-ui/src/context/server.tsx b/packages/kilo-vscode/webview-ui/src/context/server.tsx index e1941152b3..8b28422e9a 100644 --- a/packages/kilo-vscode/webview-ui/src/context/server.tsx +++ b/packages/kilo-vscode/webview-ui/src/context/server.tsx @@ -26,9 +26,7 @@ interface ServerContextValue { providerUsage: Accessor providerUsageLoading: Accessor providerUsageError: Accessor - requestProviderUsage: () => void refreshProviderUsage: () => void - releaseProviderUsage: () => void deviceAuth: Accessor startLogin: () => void goToLogin: () => void @@ -54,7 +52,6 @@ export const ServerProvider: ParentComponent = (props) => { const [providerUsage, setProviderUsage] = createSignal() const [providerUsageLoading, setProviderUsageLoading] = createSignal(false) const [providerUsageError, setProviderUsageError] = createSignal() - let providerUsageRetry: ReturnType | undefined const [deviceAuth, setDeviceAuth] = createSignal(initialDeviceAuth) const [vscodeLanguage, setVscodeLanguage] = createSignal() const [languageOverride, setLanguageOverride] = createSignal() @@ -72,12 +69,10 @@ export const ServerProvider: ParentComponent = (props) => { const usageSub = vscode.onMessage((m: ExtensionMessage) => { if (m.type !== "providerUsageLoaded") return - if (providerUsageRetry) clearTimeout(providerUsageRetry) - providerUsageRetry = undefined if (m.reset) { setProviderUsage(undefined) setProviderUsageError(undefined) - setProviderUsageLoading(true) + setProviderUsageLoading(false) return } if (m.data) setProviderUsage(m.data) @@ -85,18 +80,11 @@ export const ServerProvider: ParentComponent = (props) => { setProviderUsageLoading(false) }) - const usageReadySub = vscode.onMessage((m: ExtensionMessage) => { - if (m.type !== "extensionDataReady" || !providerUsageLoading() || providerUsage()) return - if (providerUsageRetry) clearTimeout(providerUsageRetry) - providerUsageRetry = undefined - vscode.postMessage({ type: "requestProviderUsage" }) - }) - const resetProviderUsageForDirectory = () => { if (providerUsage() === undefined && !providerUsageLoading()) return setProviderUsage(undefined) setProviderUsageError(undefined) - setProviderUsageLoading(true) + setProviderUsageLoading(false) } onMount(() => { @@ -185,8 +173,6 @@ export const ServerProvider: ParentComponent = (props) => { gitSub() fontSub() usageSub() - usageReadySub() - if (providerUsageRetry) clearTimeout(providerUsageRetry) unsubscribe() }) @@ -218,32 +204,10 @@ export const ServerProvider: ParentComponent = (props) => { startLogin() } - const retryProviderUsage = () => { - if (providerUsageRetry) clearTimeout(providerUsageRetry) - providerUsageRetry = setTimeout(() => { - providerUsageRetry = undefined - if (providerUsageLoading() && !providerUsage()) vscode.postMessage({ type: "requestProviderUsage" }) - }, 3000) - } - - const requestProviderUsage = () => { - setProviderUsageLoading(true) - setProviderUsageError(undefined) - vscode.postMessage({ type: "requestProviderUsage" }) - retryProviderUsage() - } - const refreshProviderUsage = () => { setProviderUsageLoading(true) setProviderUsageError(undefined) vscode.postMessage({ type: "refreshProviderUsage" }) - retryProviderUsage() - } - - const releaseProviderUsage = () => { - if (providerUsageRetry) clearTimeout(providerUsageRetry) - providerUsageRetry = undefined - vscode.postMessage({ type: "releaseProviderUsage" }) } const value: ServerContextValue = { @@ -257,9 +221,7 @@ export const ServerProvider: ParentComponent = (props) => { providerUsage, providerUsageLoading, providerUsageError, - requestProviderUsage, refreshProviderUsage, - releaseProviderUsage, deviceAuth, startLogin, goToLogin, diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts index 0e8bd453b7..5693bf740a 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ar.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ar.ts @@ -663,6 +663,7 @@ export const dict = { "profile.usage.plan.canceling": "الخطة: تُلغى في نهاية الفترة", "profile.usage.plan.unknown": "الخطة: الحالة غير معروفة", "profile.usage.action.manage": "إدارة", + "profile.usage.action.managePlan": "إدارة {{plan}}", "profile.usage.routing": "فوترة الخطة مفعّلة. توجيه Kilo Gateway {{state}}.", "profile.usage.routingState.disabled": "معطّل", "profile.usage.routingState.missing": "مفقود", @@ -672,6 +673,16 @@ export const dict = { "profile.usage.window.remaining": "متبقي {{value}}", "profile.usage.window.remainingOf": "متبقي {{value}} من {{limit}}", "profile.usage.window.usedOf": "تم استخدام {{value}} من {{limit}}", + "profile.usage.window.quota": "الحصة", + "profile.usage.window.daily": "الحصة اليومية", + "profile.usage.window.weekly": "الحصة الأسبوعية", + "profile.usage.window.monthly": "الحصة الشهرية", + "profile.usage.window.hours": "حصة كل {{count}} ساعة", + "profile.usage.window.days": "حصة كل {{count}} يوم", + "profile.usage.window.weeks": "حصة كل {{count}} أسبوع", + "profile.usage.window.months": "حصة كل {{count}} شهر", + "profile.usage.window.shared": "مشتركة", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "تتم إعادة الضبط في {{date}}", "profile.usage.status.unknown": "غير معروف", "profile.usage.status.unlimited": "غير محدود", @@ -681,6 +692,9 @@ export const dict = { "profile.action.topUp": "إضافة رصيد", "profile.pass.subscribe": "احصل على Kilo Pass لإضافة رصيد وكسب مكافآت", "profile.pass.bonus": "مكافأة", + "profile.pass.usage": "استخدام هذا الشهر", + "profile.pass.paid": "مدفوع", + "profile.pass.meter": "استخدام Kilo Pass الشهري", "profile.pass.renews": "يتجدد", "profile.action.logout": "تسجيل الخروج", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/br.ts b/packages/kilo-vscode/webview-ui/src/i18n/br.ts index 5a7b3dce90..d6af6457f1 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/br.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/br.ts @@ -679,6 +679,7 @@ export const dict = { "profile.usage.plan.canceling": "Plano: Cancela no fim do período", "profile.usage.plan.unknown": "Plano: Status desconhecido", "profile.usage.action.manage": "Gerenciar", + "profile.usage.action.managePlan": "Gerenciar {{plan}}", "profile.usage.routing": "A cobrança do plano está ativa. O roteamento do Kilo Gateway está {{state}}.", "profile.usage.routingState.disabled": "desativado", "profile.usage.routingState.missing": "ausente", @@ -688,6 +689,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} restante", "profile.usage.window.remainingOf": "{{value}} de {{limit}} restantes", "profile.usage.window.usedOf": "{{value}} de {{limit}} usados", + "profile.usage.window.quota": "Cota", + "profile.usage.window.daily": "Cota diária", + "profile.usage.window.weekly": "Cota semanal", + "profile.usage.window.monthly": "Cota mensal", + "profile.usage.window.hours": "Cota de {{count}} horas", + "profile.usage.window.days": "Cota de {{count}} dias", + "profile.usage.window.weeks": "Cota de {{count}} semanas", + "profile.usage.window.months": "Cota de {{count}} meses", + "profile.usage.window.shared": "Compartilhada", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Redefine em {{date}}", "profile.usage.status.unknown": "Desconhecido", "profile.usage.status.unlimited": "Ilimitado", @@ -697,6 +708,9 @@ export const dict = { "profile.action.topUp": "Adicionar créditos", "profile.pass.subscribe": "Obtenha o Kilo Pass para adicionar créditos e ganhar bônus", "profile.pass.bonus": "Bônus", + "profile.pass.usage": "Uso deste mês", + "profile.pass.paid": "Pago", + "profile.pass.meter": "Uso mensal do Kilo Pass", "profile.pass.renews": "Renova", "profile.action.logout": "Sair", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts index 3fc0422052..b46231ea05 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/bs.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/bs.ts @@ -719,6 +719,7 @@ export const dict = { "profile.usage.plan.canceling": "Plan: Otkazuje se na kraju perioda", "profile.usage.plan.unknown": "Plan: Status nepoznat", "profile.usage.action.manage": "Upravljaj", + "profile.usage.action.managePlan": "Upravljaj planom {{plan}}", "profile.usage.routing": "Naplata plana je aktivna. Kilo Gateway usmjeravanje je {{state}}.", "profile.usage.routingState.disabled": "onemogućeno", "profile.usage.routingState.missing": "odsutno", @@ -728,6 +729,16 @@ export const dict = { "profile.usage.window.remaining": "Preostalo {{value}}", "profile.usage.window.remainingOf": "Preostalo {{value}} od {{limit}}", "profile.usage.window.usedOf": "Iskorišteno {{value}} od {{limit}}", + "profile.usage.window.quota": "Kvota", + "profile.usage.window.daily": "Dnevna kvota", + "profile.usage.window.weekly": "Sedmična kvota", + "profile.usage.window.monthly": "Mjesečna kvota", + "profile.usage.window.hours": "Kvota od {{count}} sati", + "profile.usage.window.days": "Kvota od {{count}} dana", + "profile.usage.window.weeks": "Kvota od {{count}} sedmica", + "profile.usage.window.months": "Kvota od {{count}} mjeseci", + "profile.usage.window.shared": "Dijeljena", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Obnavlja se {{date}}", "profile.usage.status.unknown": "Nepoznato", "profile.usage.status.unlimited": "Neograničeno", @@ -737,6 +748,9 @@ export const dict = { "profile.action.topUp": "Dopuni", "profile.pass.subscribe": "Nabavite Kilo Pass da dodate kredite i zaradite bonuse", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Potrošnja ovog mjeseca", + "profile.pass.paid": "Plaćeno", + "profile.pass.meter": "Mjesečna potrošnja Kilo Passa", "profile.pass.renews": "Obnavlja se", "profile.action.logout": "Odjava", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/da.ts b/packages/kilo-vscode/webview-ui/src/i18n/da.ts index d778b39702..358070097b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/da.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/da.ts @@ -717,6 +717,7 @@ export const dict = { "profile.usage.plan.canceling": "Abonnement: Opsiges ved periodens udgang", "profile.usage.plan.unknown": "Abonnement: Status ukendt", "profile.usage.action.manage": "Administrer", + "profile.usage.action.managePlan": "Administrer {{plan}}", "profile.usage.routing": "Abonnementsfakturering er aktiv. Kilo Gateway-routing er {{state}}.", "profile.usage.routingState.disabled": "deaktiveret", "profile.usage.routingState.missing": "fraværende", @@ -726,6 +727,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} tilbage", "profile.usage.window.remainingOf": "{{value}} af {{limit}} tilbage", "profile.usage.window.usedOf": "{{value}} af {{limit}} brugt", + "profile.usage.window.quota": "Kvote", + "profile.usage.window.daily": "Daglig kvote", + "profile.usage.window.weekly": "Ugentlig kvote", + "profile.usage.window.monthly": "Månedlig kvote", + "profile.usage.window.hours": "{{count}}-timers kvote", + "profile.usage.window.days": "{{count}}-dages kvote", + "profile.usage.window.weeks": "{{count}}-ugers kvote", + "profile.usage.window.months": "{{count}}-måneders kvote", + "profile.usage.window.shared": "Delt", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Nulstilles den {{date}}", "profile.usage.status.unknown": "Ukendt", "profile.usage.status.unlimited": "Ubegrænset", @@ -735,6 +746,9 @@ export const dict = { "profile.action.topUp": "Fyld op", "profile.pass.subscribe": "Få Kilo Pass for at tilføje kredit og optjene bonusser", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Denne måneds forbrug", + "profile.pass.paid": "Betalt", + "profile.pass.meter": "Månedligt Kilo Pass-forbrug", "profile.pass.renews": "Fornyes", "profile.action.logout": "Log ud", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/de.ts b/packages/kilo-vscode/webview-ui/src/i18n/de.ts index 71d29f0736..9570f293b2 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/de.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/de.ts @@ -730,6 +730,7 @@ export const dict = { "profile.usage.plan.canceling": "Tarif: Kündigung zum Ende des Abrechnungszeitraums", "profile.usage.plan.unknown": "Tarif: Status unbekannt", "profile.usage.action.manage": "Verwalten", + "profile.usage.action.managePlan": "{{plan}} verwalten", "profile.usage.routing": "Tarifabrechnung ist aktiv. Kilo-Gateway-Routing ist {{state}}.", "profile.usage.routingState.disabled": "deaktiviert", "profile.usage.routingState.missing": "nicht vorhanden", @@ -739,6 +740,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} verbleibend", "profile.usage.window.remainingOf": "{{value}} von {{limit}} verbleibend", "profile.usage.window.usedOf": "{{value}} von {{limit}} verwendet", + "profile.usage.window.quota": "Kontingent", + "profile.usage.window.daily": "Tägliches Kontingent", + "profile.usage.window.weekly": "Wöchentliches Kontingent", + "profile.usage.window.monthly": "Monatliches Kontingent", + "profile.usage.window.hours": "{{count}}-Stunden-Kontingent", + "profile.usage.window.days": "{{count}}-Tage-Kontingent", + "profile.usage.window.weeks": "{{count}}-Wochen-Kontingent", + "profile.usage.window.months": "{{count}}-Monats-Kontingent", + "profile.usage.window.shared": "Geteilt", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Wird am {{date}} zurückgesetzt", "profile.usage.status.unknown": "Unbekannt", "profile.usage.status.unlimited": "Unbegrenzt", @@ -748,6 +759,9 @@ export const dict = { "profile.action.topUp": "Aufladen", "profile.pass.subscribe": "Hol dir Kilo Pass, um Guthaben hinzuzufügen und Boni zu verdienen", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Verbrauch in diesem Monat", + "profile.pass.paid": "Bezahlt", + "profile.pass.meter": "Monatlicher Verbrauch des Kilo Pass", "profile.pass.renews": "Verlängert sich", "profile.action.logout": "Abmelden", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/en.ts b/packages/kilo-vscode/webview-ui/src/i18n/en.ts index 3c43e2da36..5455898fb8 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/en.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/en.ts @@ -628,6 +628,7 @@ export const dict = { "profile.usage.plan.canceling": "Plan: Cancels at period end", "profile.usage.plan.unknown": "Plan: Status unknown", "profile.usage.action.manage": "Manage", + "profile.usage.action.managePlan": "Manage {{plan}}", "profile.usage.routing": "Plan billing is active. Kilo Gateway routing is {{state}}.", "profile.usage.routingState.disabled": "disabled", "profile.usage.routingState.missing": "missing", @@ -637,6 +638,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} remaining", "profile.usage.window.remainingOf": "{{value}} of {{limit}} remaining", "profile.usage.window.usedOf": "{{value}} of {{limit}} used", + "profile.usage.window.quota": "Quota", + "profile.usage.window.daily": "Daily quota", + "profile.usage.window.weekly": "Weekly quota", + "profile.usage.window.monthly": "Monthly quota", + "profile.usage.window.hours": "{{count}}-hour quota", + "profile.usage.window.days": "{{count}}-day quota", + "profile.usage.window.weeks": "{{count}}-week quota", + "profile.usage.window.months": "{{count}}-month quota", + "profile.usage.window.shared": "Shared", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Resets {{date}}", "profile.usage.status.unknown": "Unknown", "profile.usage.status.unlimited": "Unlimited", @@ -646,6 +657,9 @@ export const dict = { "profile.action.topUp": "Top up", "profile.pass.subscribe": "Get Kilo Pass to add credits and earn bonuses", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "This month's usage", + "profile.pass.paid": "Paid", + "profile.pass.meter": "Kilo Pass monthly usage", "profile.pass.renews": "Renews", "profile.action.logout": "Log Out", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/es.ts b/packages/kilo-vscode/webview-ui/src/i18n/es.ts index ca69629a5f..22b9588251 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/es.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/es.ts @@ -724,6 +724,7 @@ export const dict = { "profile.usage.plan.canceling": "Plan: Se cancela al final del período", "profile.usage.plan.unknown": "Plan: Estado desconocido", "profile.usage.action.manage": "Gestionar", + "profile.usage.action.managePlan": "Gestionar {{plan}}", "profile.usage.routing": "La facturación del plan está activa. El enrutamiento de Kilo Gateway está {{state}}.", "profile.usage.routingState.disabled": "deshabilitado", "profile.usage.routingState.missing": "ausente", @@ -733,6 +734,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} restante", "profile.usage.window.remainingOf": "{{value}} de {{limit}} restantes", "profile.usage.window.usedOf": "{{value}} de {{limit}} usados", + "profile.usage.window.quota": "Cuota", + "profile.usage.window.daily": "Cuota diaria", + "profile.usage.window.weekly": "Cuota semanal", + "profile.usage.window.monthly": "Cuota mensual", + "profile.usage.window.hours": "Cuota de {{count}} horas", + "profile.usage.window.days": "Cuota de {{count}} días", + "profile.usage.window.weeks": "Cuota de {{count}} semanas", + "profile.usage.window.months": "Cuota de {{count}} meses", + "profile.usage.window.shared": "Compartida", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Se restablece el {{date}}", "profile.usage.status.unknown": "Desconocido", "profile.usage.status.unlimited": "Ilimitado", @@ -742,6 +753,9 @@ export const dict = { "profile.action.topUp": "Recargar", "profile.pass.subscribe": "Obtén Kilo Pass para añadir créditos y ganar bonificaciones", "profile.pass.bonus": "Bonificación", + "profile.pass.usage": "Uso de este mes", + "profile.pass.paid": "Pagado", + "profile.pass.meter": "Uso mensual de Kilo Pass", "profile.pass.renews": "Se renueva", "profile.action.logout": "Cerrar sesión", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts index e1f90f20df..56ac554ace 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fa.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fa.ts @@ -631,6 +631,7 @@ export const dict = { "profile.usage.plan.canceling": "طرح: در پایان دوره لغو می‌شود", "profile.usage.plan.unknown": "طرح: وضعیت نامشخص", "profile.usage.action.manage": "مدیریت", + "profile.usage.action.managePlan": "مدیریت {{plan}}", "profile.usage.routing": "صورتحساب طرح فعال است. مسیریابی Kilo Gateway {{state}} است.", "profile.usage.routingState.disabled": "غیرفعال", "profile.usage.routingState.missing": "ناموجود", @@ -640,6 +641,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} باقی‌مانده", "profile.usage.window.remainingOf": "{{value}} از {{limit}} باقی‌مانده", "profile.usage.window.usedOf": "{{value}} از {{limit}} استفاده‌شده", + "profile.usage.window.quota": "سهمیه", + "profile.usage.window.daily": "سهمیه روزانه", + "profile.usage.window.weekly": "سهمیه هفتگی", + "profile.usage.window.monthly": "سهمیه ماهانه", + "profile.usage.window.hours": "سهمیه {{count}} ساعته", + "profile.usage.window.days": "سهمیه {{count}} روزه", + "profile.usage.window.weeks": "سهمیه {{count}} هفته‌ای", + "profile.usage.window.months": "سهمیه {{count}} ماهه", + "profile.usage.window.shared": "مشترک", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "بازنشانی در {{date}}", "profile.usage.status.unknown": "نامشخص", "profile.usage.status.unlimited": "نامحدود", @@ -649,6 +660,9 @@ export const dict = { "profile.action.topUp": "شارژ کردن", "profile.pass.subscribe": "Kilo Pass را دریافت کنید تا اعتبار اضافه کنید و پاداش کسب کنید", "profile.pass.bonus": "پاداش", + "profile.pass.usage": "مصرف این ماه", + "profile.pass.paid": "پرداخت‌شده", + "profile.pass.meter": "مصرف ماهانه Kilo Pass", "profile.pass.renews": "تمدید می‌شود", "profile.action.logout": "خروج", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts index 070c07eb78..6fe5a3f38e 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/fr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/fr.ts @@ -730,6 +730,7 @@ export const dict = { "profile.usage.plan.canceling": "Forfait : résiliation à la fin de la période", "profile.usage.plan.unknown": "Forfait : statut inconnu", "profile.usage.action.manage": "Gérer", + "profile.usage.action.managePlan": "Gérer {{plan}}", "profile.usage.routing": "La facturation du forfait est active. Le routage via Kilo Gateway est {{state}}.", "profile.usage.routingState.disabled": "désactivé", "profile.usage.routingState.missing": "manquant", @@ -739,6 +740,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} restant", "profile.usage.window.remainingOf": "{{value}} sur {{limit}} restants", "profile.usage.window.usedOf": "{{value}} sur {{limit}} utilisés", + "profile.usage.window.quota": "Quota", + "profile.usage.window.daily": "Quota quotidien", + "profile.usage.window.weekly": "Quota hebdomadaire", + "profile.usage.window.monthly": "Quota mensuel", + "profile.usage.window.hours": "Quota de {{count}} heures", + "profile.usage.window.days": "Quota de {{count}} jours", + "profile.usage.window.weeks": "Quota de {{count}} semaines", + "profile.usage.window.months": "Quota de {{count}} mois", + "profile.usage.window.shared": "Partagé", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Réinitialisation le {{date}}", "profile.usage.status.unknown": "Inconnu", "profile.usage.status.unlimited": "Illimité", @@ -748,6 +759,9 @@ export const dict = { "profile.action.topUp": "Recharger", "profile.pass.subscribe": "Obtenez Kilo Pass pour ajouter des crédits et gagner des bonus", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Consommation de ce mois-ci", + "profile.pass.paid": "Payé", + "profile.pass.meter": "Consommation mensuelle du Kilo Pass", "profile.pass.renews": "Renouvelle", "profile.action.logout": "Déconnexion", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/it.ts b/packages/kilo-vscode/webview-ui/src/i18n/it.ts index 49e6d85ce1..9fa156302c 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/it.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/it.ts @@ -534,6 +534,7 @@ export const dict = { "profile.usage.plan.canceling": "Piano: Si annulla al termine del periodo", "profile.usage.plan.unknown": "Piano: Stato sconosciuto", "profile.usage.action.manage": "Gestisci", + "profile.usage.action.managePlan": "Gestisci {{plan}}", "profile.usage.routing": "La fatturazione del piano è attiva. L'instradamento tramite Kilo Gateway è {{state}}.", "profile.usage.routingState.disabled": "disabilitato", "profile.usage.routingState.missing": "mancante", @@ -543,6 +544,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} rimanente", "profile.usage.window.remainingOf": "{{value}} di {{limit}} rimanenti", "profile.usage.window.usedOf": "{{value}} di {{limit}} utilizzati", + "profile.usage.window.quota": "Quota", + "profile.usage.window.daily": "Quota giornaliera", + "profile.usage.window.weekly": "Quota settimanale", + "profile.usage.window.monthly": "Quota mensile", + "profile.usage.window.hours": "Quota di {{count}} ore", + "profile.usage.window.days": "Quota di {{count}} giorni", + "profile.usage.window.weeks": "Quota di {{count}} settimane", + "profile.usage.window.months": "Quota di {{count}} mesi", + "profile.usage.window.shared": "Condivisa", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Si azzera il {{date}}", "profile.usage.status.unknown": "Sconosciuto", "profile.usage.status.unlimited": "Illimitato", @@ -552,6 +563,9 @@ export const dict = { "profile.action.topUp": "Ricarica", "profile.pass.subscribe": "Ottieni Kilo Pass per aggiungere crediti e guadagnare bonus", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Utilizzo di questo mese", + "profile.pass.paid": "Pagato", + "profile.pass.meter": "Utilizzo mensile di Kilo Pass", "profile.pass.renews": "Si rinnova", "profile.action.logout": "Esci", "settings.agentBehaviour.title": "Comportamento agente", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts index 10d960f9d4..7f3578709b 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ja.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ja.ts @@ -711,6 +711,7 @@ export const dict = { "profile.usage.plan.canceling": "プラン:期間終了時に解約", "profile.usage.plan.unknown": "プラン:ステータス不明", "profile.usage.action.manage": "管理", + "profile.usage.action.managePlan": "{{plan}} を管理", "profile.usage.routing": "プランの請求は有効です。Kilo Gatewayのルーティングは{{state}}です。", "profile.usage.routingState.disabled": "無効", "profile.usage.routingState.missing": "欠落", @@ -720,6 +721,16 @@ export const dict = { "profile.usage.window.remaining": "残り {{value}}", "profile.usage.window.remainingOf": "{{limit}} のうち残り {{value}}", "profile.usage.window.usedOf": "{{limit}} のうち {{value}} 使用済み", + "profile.usage.window.quota": "クォータ", + "profile.usage.window.daily": "日次クォータ", + "profile.usage.window.weekly": "週次クォータ", + "profile.usage.window.monthly": "月次クォータ", + "profile.usage.window.hours": "{{count}}時間クォータ", + "profile.usage.window.days": "{{count}}日クォータ", + "profile.usage.window.weeks": "{{count}}週間クォータ", + "profile.usage.window.months": "{{count}}か月クォータ", + "profile.usage.window.shared": "共有", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "{{date}}にリセット", "profile.usage.status.unknown": "不明", "profile.usage.status.unlimited": "無制限", @@ -729,6 +740,9 @@ export const dict = { "profile.action.topUp": "チャージ", "profile.pass.subscribe": "Kilo Passに登録してクレジットを追加し、ボーナスを獲得", "profile.pass.bonus": "ボーナス", + "profile.pass.usage": "今月の使用量", + "profile.pass.paid": "有料分", + "profile.pass.meter": "Kilo Pass の月間使用量", "profile.pass.renews": "更新", "profile.action.logout": "ログアウト", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts index ce401f7f5d..7a152eb377 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ko.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ko.ts @@ -671,6 +671,7 @@ export const dict = { "profile.usage.plan.canceling": "요금제: 기간 종료 시 취소", "profile.usage.plan.unknown": "요금제: 상태 알 수 없음", "profile.usage.action.manage": "관리", + "profile.usage.action.managePlan": "{{plan}} 관리", "profile.usage.routing": "요금제 결제가 활성화되어 있습니다. Kilo Gateway 라우팅은 {{state}}입니다.", "profile.usage.routingState.disabled": "비활성화 상태", "profile.usage.routingState.missing": "누락된 상태", @@ -680,6 +681,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} 남음", "profile.usage.window.remainingOf": "{{limit}} 중 {{value}} 남음", "profile.usage.window.usedOf": "{{limit}} 중 {{value}} 사용됨", + "profile.usage.window.quota": "할당량", + "profile.usage.window.daily": "일일 할당량", + "profile.usage.window.weekly": "주간 할당량", + "profile.usage.window.monthly": "월간 할당량", + "profile.usage.window.hours": "{{count}}시간 할당량", + "profile.usage.window.days": "{{count}}일 할당량", + "profile.usage.window.weeks": "{{count}}주 할당량", + "profile.usage.window.months": "{{count}}개월 할당량", + "profile.usage.window.shared": "공유", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "{{date}}에 초기화", "profile.usage.status.unknown": "알 수 없음", "profile.usage.status.unlimited": "무제한", @@ -689,6 +700,9 @@ export const dict = { "profile.action.topUp": "충전", "profile.pass.subscribe": "Kilo Pass를 구독하여 크레딧을 추가하고 보너스를 받으세요", "profile.pass.bonus": "보너스", + "profile.pass.usage": "이번 달 사용량", + "profile.pass.paid": "유료", + "profile.pass.meter": "Kilo Pass 월간 사용량", "profile.pass.renews": "갱신", "profile.action.logout": "로그아웃", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts index 6d1787dbd5..9d94566a83 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/nl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/nl.ts @@ -672,6 +672,7 @@ export const dict = { "profile.usage.plan.canceling": "Abonnement: Wordt aan het einde van de periode opgezegd", "profile.usage.plan.unknown": "Abonnement: Status onbekend", "profile.usage.action.manage": "Beheren", + "profile.usage.action.managePlan": "{{plan}} beheren", "profile.usage.routing": "De abonnementsfacturering is actief. Kilo Gateway-routering is {{state}}.", "profile.usage.routingState.disabled": "uitgeschakeld", "profile.usage.routingState.missing": "afwezig", @@ -681,6 +682,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} resterend", "profile.usage.window.remainingOf": "{{value}} van {{limit}} resterend", "profile.usage.window.usedOf": "{{value}} van {{limit}} gebruikt", + "profile.usage.window.quota": "Quotum", + "profile.usage.window.daily": "Dagelijks quotum", + "profile.usage.window.weekly": "Wekelijks quotum", + "profile.usage.window.monthly": "Maandelijks quotum", + "profile.usage.window.hours": "Quotum per {{count}} uur", + "profile.usage.window.days": "Quotum per {{count}} dagen", + "profile.usage.window.weeks": "Quotum per {{count}} weken", + "profile.usage.window.months": "Quotum per {{count}} maanden", + "profile.usage.window.shared": "Gedeeld", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Wordt op {{date}} gereset", "profile.usage.status.unknown": "Onbekend", "profile.usage.status.unlimited": "Onbeperkt", @@ -690,6 +701,9 @@ export const dict = { "profile.action.topUp": "Opwaarderen", "profile.pass.subscribe": "Schaf Kilo Pass aan om tegoed toe te voegen en bonussen te verdienen", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Verbruik deze maand", + "profile.pass.paid": "Betaald", + "profile.pass.meter": "Maandelijks Kilo Pass-verbruik", "profile.pass.renews": "Vernieuwt", "profile.action.logout": "Uitloggen", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/no.ts b/packages/kilo-vscode/webview-ui/src/i18n/no.ts index 4b6dab94d2..82795a7475 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/no.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/no.ts @@ -679,6 +679,7 @@ export const dict = { "profile.usage.plan.canceling": "Abonnement: Avsluttes ved periodens slutt", "profile.usage.plan.unknown": "Abonnement: Status ukjent", "profile.usage.action.manage": "Administrer", + "profile.usage.action.managePlan": "Administrer {{plan}}", "profile.usage.routing": "Abonnementsfakturering er aktiv. Kilo Gateway-ruting er {{state}}.", "profile.usage.routingState.disabled": "deaktivert", "profile.usage.routingState.missing": "fraværende", @@ -688,6 +689,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} gjenstår", "profile.usage.window.remainingOf": "{{value}} av {{limit}} gjenstår", "profile.usage.window.usedOf": "{{value}} av {{limit}} brukt", + "profile.usage.window.quota": "Kvote", + "profile.usage.window.daily": "Daglig kvote", + "profile.usage.window.weekly": "Ukentlig kvote", + "profile.usage.window.monthly": "Månedlig kvote", + "profile.usage.window.hours": "{{count}}-timers kvote", + "profile.usage.window.days": "{{count}}-dagers kvote", + "profile.usage.window.weeks": "{{count}}-ukers kvote", + "profile.usage.window.months": "{{count}}-måneders kvote", + "profile.usage.window.shared": "Delt", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Tilbakestilles {{date}}", "profile.usage.status.unknown": "Ukjent", "profile.usage.status.unlimited": "Ubegrenset", @@ -697,6 +708,9 @@ export const dict = { "profile.action.topUp": "Fyll på", "profile.pass.subscribe": "Få Kilo Pass for å legge til kreditt og tjene bonuser", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Denne månedens forbruk", + "profile.pass.paid": "Betalt", + "profile.pass.meter": "Månedlig Kilo Pass-forbruk", "profile.pass.renews": "Fornyes", "profile.action.logout": "Logg ut", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts index 6b2e622758..9026930d62 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/pl.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/pl.ts @@ -675,6 +675,7 @@ export const dict = { "profile.usage.plan.canceling": "Plan: Zostanie anulowany z końcem okresu", "profile.usage.plan.unknown": "Plan: Status nieznany", "profile.usage.action.manage": "Zarządzaj", + "profile.usage.action.managePlan": "Zarządzaj {{plan}}", "profile.usage.routing": "Rozliczanie planu jest aktywne. Routing przez Kilo Gateway jest {{state}}.", "profile.usage.routingState.disabled": "wyłączony", "profile.usage.routingState.missing": "brakujący", @@ -684,6 +685,16 @@ export const dict = { "profile.usage.window.remaining": "Pozostało {{value}}", "profile.usage.window.remainingOf": "Pozostało {{value}} z {{limit}}", "profile.usage.window.usedOf": "Wykorzystano {{value}} z {{limit}}", + "profile.usage.window.quota": "Limit", + "profile.usage.window.daily": "Limit dzienny", + "profile.usage.window.weekly": "Limit tygodniowy", + "profile.usage.window.monthly": "Limit miesięczny", + "profile.usage.window.hours": "Limit {{count}}-godzinny", + "profile.usage.window.days": "Limit {{count}}-dniowy", + "profile.usage.window.weeks": "Limit {{count}}-tygodniowy", + "profile.usage.window.months": "Limit {{count}}-miesięczny", + "profile.usage.window.shared": "Wspólny", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Resetuje się {{date}}", "profile.usage.status.unknown": "Nieznany", "profile.usage.status.unlimited": "Bez limitu", @@ -693,6 +704,9 @@ export const dict = { "profile.action.topUp": "Doładuj", "profile.pass.subscribe": "Zdobądź Kilo Pass, aby dodać środki i zdobywać bonusy", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Zużycie w tym miesiącu", + "profile.pass.paid": "Opłacone", + "profile.pass.meter": "Miesięczne zużycie Kilo Pass", "profile.pass.renews": "Odnawia się", "profile.action.logout": "Wyloguj się", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts index 11da7b2795..df8b84ca95 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/ru.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/ru.ts @@ -716,6 +716,7 @@ export const dict = { "profile.usage.plan.canceling": "Тариф: Отмена в конце периода", "profile.usage.plan.unknown": "Тариф: Статус неизвестен", "profile.usage.action.manage": "Управлять", + "profile.usage.action.managePlan": "Управление {{plan}}", "profile.usage.routing": "Оплата тарифа активна. Маршрутизация через Kilo Gateway {{state}}.", "profile.usage.routingState.disabled": "отключена", "profile.usage.routingState.missing": "отсутствует", @@ -725,6 +726,16 @@ export const dict = { "profile.usage.window.remaining": "Осталось {{value}}", "profile.usage.window.remainingOf": "Осталось {{value}} из {{limit}}", "profile.usage.window.usedOf": "Использовано {{value}} из {{limit}}", + "profile.usage.window.quota": "Квота", + "profile.usage.window.daily": "Дневная квота", + "profile.usage.window.weekly": "Недельная квота", + "profile.usage.window.monthly": "Месячная квота", + "profile.usage.window.hours": "{{count}}-часовая квота", + "profile.usage.window.days": "{{count}}-дневная квота", + "profile.usage.window.weeks": "{{count}}-недельная квота", + "profile.usage.window.months": "{{count}}-месячная квота", + "profile.usage.window.shared": "Общая", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Сбрасывается {{date}}", "profile.usage.status.unknown": "Неизвестно", "profile.usage.status.unlimited": "Без ограничений", @@ -734,6 +745,9 @@ export const dict = { "profile.action.topUp": "Пополнить", "profile.pass.subscribe": "Оформите Kilo Pass, чтобы добавить кредиты и получать бонусы", "profile.pass.bonus": "Бонус", + "profile.pass.usage": "Использование за этот месяц", + "profile.pass.paid": "Оплачено", + "profile.pass.meter": "Ежемесячное использование Kilo Pass", "profile.pass.renews": "Продлевается", "profile.action.logout": "Выйти", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/th.ts b/packages/kilo-vscode/webview-ui/src/i18n/th.ts index 6fe159442d..f0999a1c66 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/th.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/th.ts @@ -709,6 +709,7 @@ export const dict = { "profile.usage.plan.canceling": "แผน: ยกเลิกเมื่อสิ้นสุดรอบ", "profile.usage.plan.unknown": "แผน: ไม่ทราบสถานะ", "profile.usage.action.manage": "จัดการ", + "profile.usage.action.managePlan": "จัดการ {{plan}}", "profile.usage.routing": "การเรียกเก็บเงินตามแผนเปิดใช้งานอยู่ การกำหนดเส้นทาง Kilo Gateway {{state}}", "profile.usage.routingState.disabled": "ปิดใช้งาน", "profile.usage.routingState.missing": "ขาดหาย", @@ -718,6 +719,16 @@ export const dict = { "profile.usage.window.remaining": "เหลือ {{value}}", "profile.usage.window.remainingOf": "เหลือ {{value}} จาก {{limit}}", "profile.usage.window.usedOf": "ใช้ไป {{value}} จาก {{limit}}", + "profile.usage.window.quota": "โควตา", + "profile.usage.window.daily": "โควตารายวัน", + "profile.usage.window.weekly": "โควตารายสัปดาห์", + "profile.usage.window.monthly": "โควตารายเดือน", + "profile.usage.window.hours": "โควตา {{count}} ชั่วโมง", + "profile.usage.window.days": "โควตา {{count}} วัน", + "profile.usage.window.weeks": "โควตา {{count}} สัปดาห์", + "profile.usage.window.months": "โควตา {{count}} เดือน", + "profile.usage.window.shared": "ใช้ร่วมกัน", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "รีเซ็ตในวันที่ {{date}}", "profile.usage.status.unknown": "ไม่ทราบ", "profile.usage.status.unlimited": "ไม่จำกัด", @@ -727,6 +738,9 @@ export const dict = { "profile.action.topUp": "เติมเงิน", "profile.pass.subscribe": "รับ Kilo Pass เพื่อเพิ่มเครดิตและรับโบนัส", "profile.pass.bonus": "โบนัส", + "profile.pass.usage": "การใช้งานเดือนนี้", + "profile.pass.paid": "ชำระแล้ว", + "profile.pass.meter": "การใช้งาน Kilo Pass รายเดือน", "profile.pass.renews": "ต่ออายุ", "profile.action.logout": "ออกจากระบบ", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts index acc1fbfee9..8f45bb90c0 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/tr.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/tr.ts @@ -667,6 +667,7 @@ export const dict = { "profile.usage.plan.canceling": "Plan: Dönem sonunda iptal edilecek", "profile.usage.plan.unknown": "Plan: Durum bilinmiyor", "profile.usage.action.manage": "Yönet", + "profile.usage.action.managePlan": "{{plan}} planını yönet", "profile.usage.routing": "Plan faturalandırması etkin. Kilo Gateway yönlendirmesi {{state}}.", "profile.usage.routingState.disabled": "devre dışı", "profile.usage.routingState.missing": "eksik", @@ -676,6 +677,16 @@ export const dict = { "profile.usage.window.remaining": "{{value}} kaldı", "profile.usage.window.remainingOf": "{{limit}} içinden {{value}} kaldı", "profile.usage.window.usedOf": "{{limit}} içinden {{value}} kullanıldı", + "profile.usage.window.quota": "Kota", + "profile.usage.window.daily": "Günlük kota", + "profile.usage.window.weekly": "Haftalık kota", + "profile.usage.window.monthly": "Aylık kota", + "profile.usage.window.hours": "{{count}} saatlik kota", + "profile.usage.window.days": "{{count}} günlük kota", + "profile.usage.window.weeks": "{{count}} haftalık kota", + "profile.usage.window.months": "{{count}} aylık kota", + "profile.usage.window.shared": "Paylaşımlı", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "{{date}} tarihinde sıfırlanır", "profile.usage.status.unknown": "Bilinmiyor", "profile.usage.status.unlimited": "Sınırsız", @@ -685,6 +696,9 @@ export const dict = { "profile.action.topUp": "Bakiye yükle", "profile.pass.subscribe": "Kredi eklemek ve bonus kazanmak için Kilo Pass edinin", "profile.pass.bonus": "Bonus", + "profile.pass.usage": "Bu ayki kullanım", + "profile.pass.paid": "Ücretli", + "profile.pass.meter": "Aylık Kilo Pass kullanımı", "profile.pass.renews": "Yenilenir", "profile.action.logout": "Çıkış Yap", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts index 974d6e90ca..9a28bed353 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/uk.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/uk.ts @@ -669,6 +669,7 @@ export const dict = { "profile.usage.plan.canceling": "План: Скасування наприкінці періоду", "profile.usage.plan.unknown": "План: Статус невідомий", "profile.usage.action.manage": "Керувати", + "profile.usage.action.managePlan": "Керування {{plan}}", "profile.usage.routing": "Оплата плану активна. Маршрутизація через Kilo Gateway {{state}}.", "profile.usage.routingState.disabled": "вимкнена", "profile.usage.routingState.missing": "відсутня", @@ -678,6 +679,16 @@ export const dict = { "profile.usage.window.remaining": "Залишилося {{value}}", "profile.usage.window.remainingOf": "Залишилося {{value}} з {{limit}}", "profile.usage.window.usedOf": "Використано {{value}} з {{limit}}", + "profile.usage.window.quota": "Квота", + "profile.usage.window.daily": "Денна квота", + "profile.usage.window.weekly": "Тижнева квота", + "profile.usage.window.monthly": "Місячна квота", + "profile.usage.window.hours": "{{count}}-годинна квота", + "profile.usage.window.days": "{{count}}-денна квота", + "profile.usage.window.weeks": "{{count}}-тижнева квота", + "profile.usage.window.months": "{{count}}-місячна квота", + "profile.usage.window.shared": "Спільна", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "Скидається {{date}}", "profile.usage.status.unknown": "Невідомо", "profile.usage.status.unlimited": "Без обмежень", @@ -687,6 +698,9 @@ export const dict = { "profile.action.topUp": "Поповнити", "profile.pass.subscribe": "Отримайте Kilo Pass, щоб додати кредити та заробляти бонуси", "profile.pass.bonus": "Бонус", + "profile.pass.usage": "Використання за цей місяць", + "profile.pass.paid": "Оплачено", + "profile.pass.meter": "Щомісячне використання Kilo Pass", "profile.pass.renews": "Поновлюється", "profile.action.logout": "Вийти", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts index 02fc3839b6..809b932489 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zh.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zh.ts @@ -692,6 +692,7 @@ export const dict = { "profile.usage.plan.canceling": "套餐:将在周期结束时取消", "profile.usage.plan.unknown": "套餐:状态未知", "profile.usage.action.manage": "管理", + "profile.usage.action.managePlan": "管理 {{plan}}", "profile.usage.routing": "套餐账单处于有效状态。Kilo Gateway 路由状态为 {{state}}。", "profile.usage.routingState.disabled": "已禁用", "profile.usage.routingState.missing": "缺失", @@ -701,6 +702,16 @@ export const dict = { "profile.usage.window.remaining": "剩余 {{value}}", "profile.usage.window.remainingOf": "共 {{limit}},剩余 {{value}}", "profile.usage.window.usedOf": "共 {{limit}},已使用 {{value}}", + "profile.usage.window.quota": "配额", + "profile.usage.window.daily": "每日配额", + "profile.usage.window.weekly": "每周配额", + "profile.usage.window.monthly": "每月配额", + "profile.usage.window.hours": "{{count}}小时配额", + "profile.usage.window.days": "{{count}}天配额", + "profile.usage.window.weeks": "{{count}}周配额", + "profile.usage.window.months": "{{count}}个月配额", + "profile.usage.window.shared": "共享", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "{{date}} 重置", "profile.usage.status.unknown": "未知", "profile.usage.status.unlimited": "无限制", @@ -710,6 +721,9 @@ export const dict = { "profile.action.topUp": "充值", "profile.pass.subscribe": "订阅 Kilo Pass 以添加额度并赚取奖励", "profile.pass.bonus": "奖励", + "profile.pass.usage": "本月用量", + "profile.pass.paid": "付费", + "profile.pass.meter": "Kilo Pass 每月用量", "profile.pass.renews": "续订", "profile.action.logout": "退出登录", diff --git a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts index 92329bf0df..fc9133667f 100644 --- a/packages/kilo-vscode/webview-ui/src/i18n/zht.ts +++ b/packages/kilo-vscode/webview-ui/src/i18n/zht.ts @@ -652,6 +652,7 @@ export const dict = { "profile.usage.plan.canceling": "方案:將於週期結束時取消", "profile.usage.plan.unknown": "方案:狀態未知", "profile.usage.action.manage": "管理", + "profile.usage.action.managePlan": "管理 {{plan}}", "profile.usage.routing": "方案帳單目前有效。Kilo Gateway 路由狀態為 {{state}}。", "profile.usage.routingState.disabled": "已停用", "profile.usage.routingState.missing": "缺失", @@ -661,6 +662,16 @@ export const dict = { "profile.usage.window.remaining": "剩餘 {{value}}", "profile.usage.window.remainingOf": "共 {{limit}},剩餘 {{value}}", "profile.usage.window.usedOf": "共 {{limit}},已使用 {{value}}", + "profile.usage.window.quota": "配額", + "profile.usage.window.daily": "每日配額", + "profile.usage.window.weekly": "每週配額", + "profile.usage.window.monthly": "每月配額", + "profile.usage.window.hours": "{{count}}小時配額", + "profile.usage.window.days": "{{count}}天配額", + "profile.usage.window.weeks": "{{count}}週配額", + "profile.usage.window.months": "{{count}}個月配額", + "profile.usage.window.shared": "共享", + "profile.usage.window.scoped": "{{resource}} · {{period}}", "profile.usage.reset": "{{date}} 重設", "profile.usage.status.unknown": "未知", "profile.usage.status.unlimited": "無限制", @@ -670,6 +681,9 @@ export const dict = { "profile.action.topUp": "儲值", "profile.pass.subscribe": "訂閱 Kilo Pass 以新增額度並賺取獎勵", "profile.pass.bonus": "獎勵", + "profile.pass.usage": "本月用量", + "profile.pass.paid": "付費", + "profile.pass.meter": "Kilo Pass 每月用量", "profile.pass.renews": "續訂", "profile.action.logout": "登出", diff --git a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx index 2062155257..270ce49e33 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/agent-manager.stories.tsx @@ -230,9 +230,7 @@ const chatServer = { providerUsage: () => undefined, providerUsageLoading: () => false, providerUsageError: () => undefined, - requestProviderUsage: () => undefined, refreshProviderUsage: () => undefined, - releaseProviderUsage: () => undefined, deviceAuth: () => ({ status: "idle" as const }), startLogin: () => undefined, goToLogin: () => undefined, diff --git a/packages/kilo-vscode/webview-ui/src/stories/profile.stories.tsx b/packages/kilo-vscode/webview-ui/src/stories/profile.stories.tsx index 93a932053c..688823c4bd 100644 --- a/packages/kilo-vscode/webview-ui/src/stories/profile.stories.tsx +++ b/packages/kilo-vscode/webview-ui/src/stories/profile.stories.tsx @@ -64,8 +64,8 @@ const usage: ProviderUsageData = { windows: [ { id: "general-interval", - label: "Shared quota 5-hour", resource: "general", + period: { unit: "hour", value: 5 }, unit: "percent", orientation: "remaining_percent", used: 24, diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/provider-usage.ts b/packages/kilo-vscode/webview-ui/src/types/messages/provider-usage.ts index 7fcf885b06..8e643737f5 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/provider-usage.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/provider-usage.ts @@ -9,15 +9,6 @@ export interface ProviderUsageLoadedMessage { reset?: boolean } -export interface RequestProviderUsageMessage { - type: "requestProviderUsage" -} - export interface RefreshProviderUsageMessage { type: "refreshProviderUsage" } - -/** Sent when the Profile view unmounts so the extension stops background usage refreshes. */ -export interface ReleaseProviderUsageMessage { - type: "releaseProviderUsage" -} diff --git a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts index 55edea36f2..d81aefec6f 100644 --- a/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts +++ b/packages/kilo-vscode/webview-ui/src/types/messages/webview-messages.ts @@ -7,11 +7,7 @@ import type { Config } from "./config" import type { ModelAllocation, ReviewComment, TerminalDestination, TerminalPlacement } from "./agent-manager" import type { ReviewMessageData } from "../../../../src/shared/review-comments" import type { WorkStyle, WorkStyleState } from "../../../../src/shared/work-style-presets" -import type { - RefreshProviderUsageMessage, - ReleaseProviderUsageMessage, - RequestProviderUsageMessage, -} from "./provider-usage" +import type { RefreshProviderUsageMessage } from "./provider-usage" import type { AnacondaDesktopWebviewMessage } from "../../../../src/shared/anaconda-desktop-messages" import type { ClearLegacyDataMessage, @@ -1401,9 +1397,7 @@ export type WebviewMessage = | LoginRequest | LogoutRequest | RefreshProfileRequest - | RequestProviderUsageMessage | RefreshProviderUsageMessage - | ReleaseProviderUsageMessage | OpenExternalRequest | OpenSettingsPanelRequest | OpenProfilePanelRequest diff --git a/packages/opencode/src/kilocode/components/dialog-provider-usage.tsx b/packages/opencode/src/kilocode/components/dialog-provider-usage.tsx index e4b5ec9cf4..08c9d1bc19 100644 --- a/packages/opencode/src/kilocode/components/dialog-provider-usage.tsx +++ b/packages/opencode/src/kilocode/components/dialog-provider-usage.tsx @@ -1,7 +1,7 @@ import { TextAttributes } from "@opentui/core" import { useKeyboard } from "@opentui/solid" import type { ProviderUsage, ProviderUsageSnapshot } from "@kilocode/sdk/v2" -import { formatWindow } from "@kilocode/kilo-gateway/provider-usage" +import { formatWindow, windowLabel } from "@kilocode/kilo-gateway/provider-usage" import { useTheme } from "@tui/context/theme" import { useSDK } from "@tui/context/sdk" import { useDialog } from "@tui/ui/dialog" @@ -26,7 +26,7 @@ function Item(props: { item: ProviderUsageSnapshot }) { {(window) => ( - {window.label}: {formatWindow(window)} + {windowLabel(window)}: {formatWindow(window)} {(reset) => Resets {new Date(reset()).toLocaleString()}} diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/kilocode.ts b/packages/opencode/src/kilocode/server/httpapi/groups/kilocode.ts index cc34d61010..780cf43588 100644 --- a/packages/opencode/src/kilocode/server/httpapi/groups/kilocode.ts +++ b/packages/opencode/src/kilocode/server/httpapi/groups/kilocode.ts @@ -10,7 +10,6 @@ import { import { described } from "@/server/routes/instance/httpapi/groups/metadata" import { ProviderUsage } from "@opencode-ai/schema/kilocode/provider-usage" import { AnacondaDesktopApi } from "./anaconda-desktop" -import { ProviderUsageLocationMiddleware } from "../middleware/provider-usage-location" import { Result as AgentRequirementResult } from "@/kilocode/agent-requirements" import { Failure as AgentManagerFailure, @@ -145,28 +144,24 @@ export const KilocodeApi = HttpApi.make("kilocode") query: WorkspaceRoutingQuery, success: described(ProviderUsage.Info, "Current provider usage"), error: HttpApiError.ServiceUnavailable, - }) - .annotateMerge( - OpenApi.annotations({ - identifier: "kilocode.providerUsage.get", - summary: "Get provider usage", - description: "Get cache-aware, secret-free provider plan usage and personal billing status.", - }), - ) - .middleware(ProviderUsageLocationMiddleware), + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilocode.providerUsage.get", + summary: "Get provider usage", + description: "Get cache-aware, secret-free provider plan usage and personal billing status.", + }), + ), HttpApiEndpoint.post("providerUsageRefresh", KilocodePaths.providerUsageRefresh, { query: WorkspaceRoutingQuery, success: described(ProviderUsage.Info, "Refreshed provider usage"), error: HttpApiError.ServiceUnavailable, - }) - .annotateMerge( - OpenApi.annotations({ - identifier: "kilocode.providerUsage.refresh", - summary: "Refresh provider usage", - description: "Refresh provider plan usage while coalescing concurrent source requests.", - }), - ) - .middleware(ProviderUsageLocationMiddleware), + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilocode.providerUsage.refresh", + summary: "Refresh provider usage", + description: "Refresh provider plan usage while coalescing concurrent source requests.", + }), + ), HttpApiEndpoint.get("notebookList", KilocodePaths.notebookList, { query: WorkspaceRoutingQuery, success: described(Schema.Array(NotebookRequest), "Pending notebook host requests"), diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilocode.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilocode.ts index e03cb2567e..9b69258fdf 100644 --- a/packages/opencode/src/kilocode/server/httpapi/handlers/kilocode.ts +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilocode.ts @@ -6,6 +6,7 @@ import * as KiloSkill from "@/kilocode/skill-remove" import { Agent } from "@/agent/agent" import { Command } from "@/command" import { Config } from "@/config/config" +import { WorkspaceRef } from "@/effect/instance-ref" import { InstanceState } from "@/effect/instance-state" import { HeapSnapshot } from "@/kilocode/cli/heap-snapshot" import type { RequestID as AgentManagerRequestID } from "@/kilocode/agent-manager/protocol" @@ -14,6 +15,9 @@ import type { RequestID as NotebookRequestID } from "@/kilocode/notebook/protoco import { Notebook } from "@/kilocode/notebook/service" import { ModelUsage } from "@/kilocode/session/model-usage" import { ProviderUsage } from "@opencode-ai/core/kilocode/provider-usage" +import { Location } from "@opencode-ai/core/location" +import { LocationServiceMap } from "@opencode-ai/core/location-services" +import { AbsolutePath } from "@opencode-ai/core/schema" import { InstanceStore } from "@/project/instance-store" import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api" import { Skill } from "@/skill" @@ -37,6 +41,22 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode" const store = yield* InstanceStore.Service const manager = yield* AgentManager.Service const notebook = yield* Notebook.Service + const locations = yield* LocationServiceMap.Service + + // Location-scoped services, keyed by the request's directory and workspace. + const located = Effect.fnUntraced(function* (effect: Effect.Effect) { + return yield* effect.pipe( + Effect.provide( + locations.get( + Location.Ref.make({ + directory: AbsolutePath.make((yield* InstanceState.context).directory), + workspaceID: yield* WorkspaceRef, + }), + ), + ), + ) + }) + const heapSnapshot = Effect.fn("KilocodeHttpApi.heapSnapshot")(function* () { return yield* Effect.sync(() => HeapSnapshot.write()) }) @@ -108,15 +128,15 @@ export const kilocodeHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilocode" }) const providerUsage = Effect.fn("KilocodeHttpApi.providerUsage")(function* () { - return yield* (yield* ProviderUsage.Service) - .get() - .pipe(Effect.mapError(() => new HttpApiError.ServiceUnavailable({}))) + return yield* located(ProviderUsage.Service.use((usage) => usage.get())).pipe( + Effect.mapError(() => new HttpApiError.ServiceUnavailable({})), + ) }) const providerUsageRefresh = Effect.fn("KilocodeHttpApi.providerUsageRefresh")(function* () { - return yield* (yield* ProviderUsage.Service) - .refresh() - .pipe(Effect.mapError(() => new HttpApiError.ServiceUnavailable({}))) + return yield* located(ProviderUsage.Service.use((usage) => usage.refresh())).pipe( + Effect.mapError(() => new HttpApiError.ServiceUnavailable({})), + ) }) const notebookList = Effect.fn("KilocodeHttpApi.notebookList")(function* () { diff --git a/packages/opencode/src/kilocode/server/httpapi/middleware/provider-usage-location.ts b/packages/opencode/src/kilocode/server/httpapi/middleware/provider-usage-location.ts deleted file mode 100644 index 342f392e22..0000000000 --- a/packages/opencode/src/kilocode/server/httpapi/middleware/provider-usage-location.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Location } from "@opencode-ai/core/location" -import { LocationServiceMap } from "@opencode-ai/core/location-services" -import { AbsolutePath } from "@opencode-ai/core/schema" -import { Effect, Layer } from "effect" -import { HttpApiMiddleware } from "effect/unstable/httpapi" -import { WorkspaceRouteContext } from "@/server/routes/instance/httpapi/middleware/workspace-routing" - -export type ProviderUsageLocationServices = Layer.Success> - -export class ProviderUsageLocationMiddleware extends HttpApiMiddleware.Service< - ProviderUsageLocationMiddleware, - { - provides: ProviderUsageLocationServices - requires: WorkspaceRouteContext - } ->()("@kilocode/HttpApiProviderUsageLocation") {} - -export const layer = Layer.effect( - ProviderUsageLocationMiddleware, - Effect.gen(function* () { - const locations = yield* LocationServiceMap.Service - return ProviderUsageLocationMiddleware.of((effect) => - Effect.gen(function* () { - const route = yield* WorkspaceRouteContext - const directory = (() => { - try { - return decodeURIComponent(route.directory) - } catch { - return route.directory - } - })() - const ref = Location.Ref.make({ - directory: AbsolutePath.make(directory), - workspaceID: route.workspaceID, - }) - return yield* effect.pipe(Effect.provide(locations.get(ref))) - }), - ) - }), -) diff --git a/packages/opencode/src/kilocode/server/httpapi/server.ts b/packages/opencode/src/kilocode/server/httpapi/server.ts index 17ae6a31df..57a9f0d274 100644 --- a/packages/opencode/src/kilocode/server/httpapi/server.ts +++ b/packages/opencode/src/kilocode/server/httpapi/server.ts @@ -29,7 +29,6 @@ import { sandboxHandlers } from "./handlers/sandbox" import { sessionImportHandlers } from "./handlers/session-import" import { suggestionHandlers } from "./handlers/suggestion" import { telemetryHandlers } from "./handlers/telemetry" -import { layer as providerUsageLocationLayer } from "./middleware/provider-usage-location" export const provide = Layer.provide([ agentBuilderHandlers, @@ -43,7 +42,7 @@ export const provide = Layer.provide([ instanceReloadHandlers, interactiveTerminalHandlers, kiloGatewayHandlers, - kilocodeHandlers.pipe(Layer.provide(providerUsageLocationLayer)), + kilocodeHandlers, memoryHandlers, networkHandlers, remoteHandlers, diff --git a/packages/schema/src/kilocode/provider-usage.ts b/packages/schema/src/kilocode/provider-usage.ts index e7237ad906..90b0f5b0e6 100644 --- a/packages/schema/src/kilocode/provider-usage.ts +++ b/packages/schema/src/kilocode/provider-usage.ts @@ -10,16 +10,22 @@ export const UsageError = Schema.Struct({ retryable: Schema.Boolean, }).annotate({ identifier: "ProviderUsageError" }) +export interface UsagePeriod extends Schema.Schema.Type {} +export const UsagePeriod = Schema.Struct({ + unit: Schema.Literals(["hour", "day", "week", "month"]), + value: Schema.Int, +}).annotate({ identifier: "ProviderUsagePeriod" }) + export interface UsageWindow extends Schema.Schema.Type {} export const UsageWindow = Schema.Struct({ id: Schema.String, - label: Schema.String, resource: Schema.String, unit: Schema.String, orientation: Schema.Literals(["used_percent", "remaining_percent", "amount", "count"]), used: optional(Schema.Finite), remaining: optional(Schema.Finite), limit: optional(Schema.Finite), + period: optional(UsagePeriod), durationMs: optional(Schema.Finite), resetAt: optional(Schema.String), state: Schema.Literals(["active", "exhausted", "unlimited", "not_in_plan", "unknown"]), diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 647e8204a4..f6ead487f8 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -4186,15 +4186,20 @@ export type CommandFile = { hints: Array } +export type ProviderUsagePeriod = { + unit: "hour" | "day" | "week" | "month" + value: number +} + export type ProviderUsageWindow = { id: string - label: string resource: string unit: string orientation: "used_percent" | "remaining_percent" | "amount" | "count" used?: number remaining?: number limit?: number + period?: ProviderUsagePeriod durationMs?: number resetAt?: string state: "active" | "exhausted" | "unlimited" | "not_in_plan" | "unknown" diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index 2cc5d82a0d..6a3fdfef3b 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -38758,15 +38758,26 @@ "required": ["name", "builtin", "location", "editable", "hints"], "additionalProperties": false }, + "ProviderUsagePeriod": { + "type": "object", + "properties": { + "unit": { + "type": "string", + "enum": ["hour", "day", "week", "month"] + }, + "value": { + "type": "integer" + } + }, + "required": ["unit", "value"], + "additionalProperties": false + }, "ProviderUsageWindow": { "type": "object", "properties": { "id": { "type": "string" }, - "label": { - "type": "string" - }, "resource": { "type": "string" }, @@ -38786,6 +38797,9 @@ "limit": { "type": "number" }, + "period": { + "$ref": "#/components/schemas/ProviderUsagePeriod" + }, "durationMs": { "type": "number" }, @@ -38797,7 +38811,7 @@ "enum": ["active", "exhausted", "unlimited", "not_in_plan", "unknown"] } }, - "required": ["id", "label", "resource", "unit", "orientation", "state"], + "required": ["id", "resource", "unit", "orientation", "state"], "additionalProperties": false }, "ProviderUsageError": {