Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3889c56
feat(settings): chat fonts, spacing density, time format, and Usage tab
KarthickMahalingam Oct 27, 2026
4ed35c7
fix(usage): fall back to tokens for zero-cost providers; tighten tab …
KarthickMahalingam Oct 27, 2026
c54718e
feat(settings): provider breakdown in Usage; font presets and live pr…
KarthickMahalingam Oct 27, 2026
b459db1
feat(settings): size steppers in UI tab; live GitHub Copilot credits …
KarthickMahalingam Oct 27, 2026
a58ef4b
feat(usage): provider limits for all connected providers; cleaner lay…
KarthickMahalingam Oct 27, 2026
de76a05
fix(usage): attribute usage to the provider/model that served each re…
KarthickMahalingam Oct 27, 2026
c24f6d2
feat(usage): compact layout from the design mock; broad test coverage…
KarthickMahalingam Oct 27, 2026
01ef145
feat(settings): date & time settings per design mock; handle mixed pr…
KarthickMahalingam Oct 27, 2026
fcd6d89
refactor: tighten quota services and usage view-model after review
KarthickMahalingam Oct 27, 2026
b5fa876
feat(chat): show when a message was sent
KarthickMahalingam Oct 27, 2026
70710f0
chore(settings): use generic sample text in the appearance preview
KarthickMahalingam Oct 27, 2026
b9fce98
chore: add changeset for usage tab and UI settings
KarthickMahalingam Oct 27, 2026
8980045
docs: reword code comments
KarthickMahalingam Oct 27, 2026
7f9ce0d
feat(usage): provider limits for Kimi Code, z.ai, MiniMax, NanoGPT an…
KarthickMahalingam Oct 27, 2026
4581cca
fix(ui): keep code, diff and terminal font sizes unchanged until the …
KarthickMahalingam Oct 27, 2026
e24c9f2
fix(ui): scope editor font size to the diff view only while it is set
KarthickMahalingam Oct 27, 2026
98f1019
test(visual): update UI settings baselines for the new appearance con…
KarthickMahalingam Oct 27, 2026
f257ace
test: cover remaining usage, quota and appearance branches
KarthickMahalingam Oct 27, 2026
ec07ae1
fix: address review feedback on usage limits, CSV export, timestamps …
KarthickMahalingam Oct 27, 2026
96f41c6
test(visual): update UI settings baselines for the preview font fallback
KarthickMahalingam Oct 27, 2026
fa44261
fix(quotas): compare the API key directly instead of hashing it for t…
KarthickMahalingam Oct 28, 2026
e4378be
fix: address second review round (preview isolation, locale-aware pre…
KarthickMahalingam Oct 28, 2026
b1c73f7
test(visual): add appearance preview baselines and refresh UI settings
KarthickMahalingam Oct 28, 2026
d58cfe3
fix(lint): drop the unused DAY_MS constant
KarthickMahalingam Oct 28, 2026
02d82a5
fix: atomic settings save with host acknowledgement, cancellable quot…
KarthickMahalingam Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,37 @@ export const globalSettingsSchema = z.object({
* When unset (or `null`), the webview inherits VS Code's `--vscode-font-size`.
*/
chatFontSize: z.number().int().min(8).max(32).nullish(),
/**
* Scale (percent) applied to Zoo Code's interface text. `null`/unset = 100.
*/
interfaceFontScale: z.number().int().min(50).max(200).nullish(),
/**
* Font size (px) for command/terminal output shown in chat. `null`/unset inherits.
*/
terminalFontSize: z.number().int().min(8).max(32).nullish(),
/**
* Font size (px) for code blocks and diffs shown in chat. `null`/unset inherits.
*/
editorFontSize: z.number().int().min(8).max(32).nullish(),
/**
* Scale (percent) applied to chat row padding. `null`/unset = 100.
*/
spacingScale: z.number().int().min(50).max(200).nullish(),
/**
* Clock format used when rendering timestamps. "auto" follows the display language's locale.
* @default "auto"
*/
timeFormat: z.enum(["auto", "12h", "24h"]).optional(),
/**
* How dates are written. "auto" follows the display language's locale.
* @default "auto"
*/
dateFormat: z.enum(["auto", "mdy", "dmy", "ymd"]).optional(),
/**
* Show relative times ("5 min ago") in task history; when off, show the absolute date/time.
* @default true
*/
showRelativeTimes: z.boolean().optional(),
/**
* Controls the keyboard behavior for sending messages in the chat input.
* - "send": Enter sends message, Shift+Enter creates newline (default)
Expand Down
19 changes: 19 additions & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ export const pendingTaskActionSchema = z.discriminatedUnion("kind", [

export type PendingTaskAction = z.infer<typeof pendingTaskActionSchema>

/**
* Token/cost totals for one provider+model within a task. A task can span
* several (the user may switch profiles mid-task), so history keeps a list.
*/
export const usageByModelEntrySchema = z.object({
apiProvider: z.string().optional(),
modelId: z.string().optional(),
requests: z.number(),
tokensIn: z.number(),
tokensOut: z.number(),
cacheReads: z.number().optional(),
cacheWrites: z.number().optional(),
cost: z.number(),
})

export type UsageByModelEntry = z.infer<typeof usageByModelEntrySchema>

export const historyItemSchema = z.object({
id: z.string(),
rootTaskId: z.string().optional(),
Expand All @@ -39,6 +56,8 @@ export const historyItemSchema = z.object({
cacheWrites: z.number().optional(),
cacheReads: z.number().optional(),
totalCost: z.number(),
/** Per provider+model breakdown, recorded as requests happen. Absent on tasks created before this was tracked. */
usageByModel: z.array(usageByModelEntrySchema).optional(),
size: z.number().optional(),
workspace: z.string().optional(),
mode: z.string().optional(),
Expand Down
58 changes: 58 additions & 0 deletions packages/types/src/providers/vscode-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,66 @@ export const vsCodeLmModelsMessageTypes = [
"githubCopilotManageAccount",
"githubCopilotSignInResult",
"githubCopilotModels",
"requestProviderQuotas",
"cancelProviderQuotas",
"providerQuotas",
] as const

/**
* One limit/balance a provider reports (e.g. Copilot premium credits for the
* month, an OpenRouter key limit, a DeepSeek prepaid balance).
*/
export interface ProviderQuotaWindow {
/** Stable key, e.g. "premium", "key_limit", "balance". */
id: string
/** Pre-formatted primary figure, e.g. "39,389.3 / 140,000 used", "$12.40 left". */
valueLabel: string
/** 0–100 when the provider reports a bounded allowance. */
usedPercent?: number
/** Epoch ms. */
resetAt?: number
}

export type ProviderQuota =
| {
providerId: string
ok: true
plan?: string
windows: ProviderQuotaWindow[]
fetchedAt: number
}
| {
providerId: string
ok: false
reason: "signedOut" | "noCopilot" | "noKey" | "unsupported" | "error"
message?: string
fetchedAt: number
}

/**
* The signed-in account's GitHub Copilot premium-request allowance, as reported by
* `GET /copilot_internal/user` (the same source Copilot Chat's own usage panel reads).
*/
export interface GitHubCopilotQuota {
/** e.g. "business", "individual". */
plan?: string
unlimited: boolean
/** Total credits for the period; undefined when unlimited or not reported. */
entitlement?: number
/** Credits used this period, `entitlement - quota_remaining` (one decimal, like GitHub's panel). */
used?: number
/** 0–100. */
usedPercent?: number
/** Epoch ms when the allowance resets. */
resetAt?: number
/** Epoch ms when this snapshot was fetched. */
fetchedAt: number
}

export type GitHubCopilotQuotaResult =
| { ok: true; quota: GitHubCopilotQuota }
| { ok: false; reason: "signedOut" | "noCopilot" | "error"; message?: string }

export const vsCodeLmModelsMessageTypeSchema = z.enum(vsCodeLmModelsMessageTypes)

export const VsCodeLmModelsMessageType = vsCodeLmModelsMessageTypeSchema.enum
Expand Down
21 changes: 20 additions & 1 deletion packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { RouterModelsMessageType, type ModelInfo, type ModelRecord, type RouterM
import { LmStudioModelsMessageType } from "./providers/lm-studio.js"
import { OllamaModelsMessageType } from "./providers/ollama.js"
import { OpenAiModelsMessageType } from "./providers/openai.js"
import { VsCodeLmModelsMessageType } from "./providers/vscode-llm.js"
import { VsCodeLmModelsMessageType, type ProviderQuota } from "./providers/vscode-llm.js"
import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js"
import type { SkillMetadata } from "./skills.js"
import type { RuleMetadata } from "./rules.js"
Expand Down Expand Up @@ -50,6 +50,8 @@ export interface ExtensionMessage {
| typeof VsCodeLmModelsMessageType.vsCodeLmModels
| typeof VsCodeLmModelsMessageType.githubCopilotModels
| typeof VsCodeLmModelsMessageType.githubCopilotSignInResult
| typeof VsCodeLmModelsMessageType.providerQuotas
| "settingsSaveResult"
| "vsCodeLmApiAvailable"
| "updatePrompt"
| "systemPrompt"
Expand Down Expand Up @@ -158,6 +160,7 @@ export interface ExtensionMessage {
modelInfo?: ModelInfo
}[]
githubCopilotAccount?: string | null
providerQuotas?: ProviderQuota[]
mcpServers?: McpServer[]
commits?: GitCommit[]
listApiConfig?: ProviderSettingsEntry[]
Expand Down Expand Up @@ -339,6 +342,13 @@ export type ExtensionState = Pick<
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "chatFontSize"
| "interfaceFontScale"
| "terminalFontSize"
| "editorFontSize"
| "spacingScale"
| "timeFormat"
| "dateFormat"
| "showRelativeTimes"
| "enterBehavior"
| "includeCurrentTime"
| "includeCurrentCost"
Expand Down Expand Up @@ -516,6 +526,8 @@ export interface WebviewMessage {
| typeof VsCodeLmModelsMessageType.githubCopilotSignIn
| typeof VsCodeLmModelsMessageType.githubCopilotReconnect
| typeof VsCodeLmModelsMessageType.githubCopilotManageAccount
| typeof VsCodeLmModelsMessageType.requestProviderQuotas
| typeof VsCodeLmModelsMessageType.cancelProviderQuotas
| "openImage"
| "saveImage"
| "openFile"
Expand Down Expand Up @@ -951,6 +963,13 @@ export interface ClineApiReqInfo {
cancelReason?: ClineApiReqCancelReason
streamingFailedMessage?: string
apiProtocol?: "anthropic" | "openai"
/**
* The provider and model that actually served this request, recorded at the
* time of the call. Usage reporting reads these instead of the *current*
* profile, so switching providers later doesn't relabel past requests.
*/
apiProvider?: string
modelId?: string
}

export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled"
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, expect, it, vi } from "vitest"

import { providerIdentifiers, type ClineMessage } from "@roo-code/types"

vi.mock("get-folder-size", () => ({ default: { loose: vi.fn(async () => 1234) } }))
vi.mock("../../../utils/storage", () => ({ getTaskDirectoryPath: vi.fn(async () => "/tmp/task") }))
vi.mock("../../../i18n", () => ({ t: (k: string) => k }))

import { taskMetadata } from "../taskMetadata"

const say = (say: ClineMessage["say"], text: string, ts: number): ClineMessage => ({ ts, type: "say", say, text })

const base = {
taskId: "t1",
taskNumber: 1,
globalStoragePath: "/tmp",
workspace: "/ws",
}

describe("taskMetadata – usageByModel", () => {
it("records per provider+model usage on the history item", async () => {
const messages = [
say("text", "do the thing", 1),
say(
"api_req_started",
JSON.stringify({
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
tokensIn: 900,
tokensOut: 90,
cost: 0,
}),
2,
),
say(
"api_req_started",
JSON.stringify({
apiProvider: providerIdentifiers.gemini,
modelId: "gemini-pro",
tokensIn: 100,
tokensOut: 10,
cost: 0.2,
}),
3,
),
]
const { historyItem } = await taskMetadata({ ...base, messages })

expect(historyItem.usageByModel).toEqual([
{
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
requests: 1,
tokensIn: 900,
tokensOut: 90,
cost: 0,
},
{
apiProvider: providerIdentifiers.gemini,
modelId: "gemini-pro",
requests: 1,
tokensIn: 100,
tokensOut: 10,
cost: 0.2,
},
])
// Existing totals are unchanged by the new breakdown.
expect(historyItem.tokensIn).toBe(1000)
expect(historyItem.totalCost).toBeCloseTo(0.2)
})

it("omits usageByModel entirely when nothing was recorded, keeping old items' shape", async () => {
const { historyItem } = await taskMetadata({ ...base, messages: [say("text", "hello", 1)] })
expect(historyItem).not.toHaveProperty("usageByModel")
})

it("omits usageByModel for a task with no messages", async () => {
const { historyItem } = await taskMetadata({ ...base, messages: [] })
expect(historyItem).not.toHaveProperty("usageByModel")
expect(historyItem.tokensIn).toBe(0)
})
})
113 changes: 113 additions & 0 deletions src/core/task-persistence/__tests__/usageByModel.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, it } from "vitest"

import { providerIdentifiers, type ClineMessage } from "@roo-code/types"

import { summarizeUsageByModel } from "../usageByModel"

const req = (data: Record<string, unknown>, ts = 1): ClineMessage => ({
ts,
type: "say",
say: "api_req_started",
text: JSON.stringify(data),
})

describe("summarizeUsageByModel", () => {
it("groups requests by the provider+model recorded on each request", () => {
const result = summarizeUsageByModel([
req({
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
tokensIn: 1000,
tokensOut: 100,
cost: 0,
}),
req({
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
tokensIn: 500,
tokensOut: 50,
cost: 0,
}),
req({
apiProvider: providerIdentifiers.gemini,
modelId: "gemini-3.1-pro-preview",
tokensIn: 200,
tokensOut: 20,
cost: 0.4,
}),
])

expect(result).toEqual([
{
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
requests: 2,
tokensIn: 1500,
tokensOut: 150,
cost: 0,
},
{
apiProvider: providerIdentifiers.gemini,
modelId: "gemini-3.1-pro-preview",
requests: 1,
tokensIn: 200,
tokensOut: 20,
cost: 0.4,
},
])
})

it("keeps a task that switched providers mid-way split by what actually served each request", () => {
const result = summarizeUsageByModel([
req({
apiProvider: providerIdentifiers.githubCopilot,
modelId: "claude-opus-5.5",
tokensIn: 100,
tokensOut: 10,
}),
req({ apiProvider: providerIdentifiers.gemini, modelId: "gemini-pro", tokensIn: 300, tokensOut: 30 }),
])
expect(result.map((r) => r.apiProvider)).toEqual([
providerIdentifiers.githubCopilot,
providerIdentifiers.gemini,
])
})

it("sums cache counters only when reported", () => {
const [entry] = summarizeUsageByModel([
req({
apiProvider: providerIdentifiers.anthropic,
modelId: "m",
tokensIn: 1,
tokensOut: 1,
cacheReads: 5,
cacheWrites: 2,
}),
req({ apiProvider: providerIdentifiers.anthropic, modelId: "m", tokensIn: 1, tokensOut: 1, cacheReads: 3 }),
])
expect(entry.cacheReads).toBe(8)
expect(entry.cacheWrites).toBe(2)

const [bare] = summarizeUsageByModel([req({ apiProvider: "a", modelId: "m", tokensIn: 1, tokensOut: 1 })])
expect(bare).not.toHaveProperty("cacheReads")
})

it("puts requests recorded before this was tracked in an unrecorded entry rather than guessing", () => {
const [entry] = summarizeUsageByModel([req({ apiProtocol: "openai", tokensIn: 10, tokensOut: 5, cost: 0 })])
expect(entry).toMatchObject({ apiProvider: undefined, modelId: undefined, requests: 1, tokensIn: 10 })
})

it("ignores in-flight placeholders and non-request messages", () => {
const result = summarizeUsageByModel([
req({ apiProtocol: "openai", apiProvider: providerIdentifiers.gemini, modelId: "m" }), // no counts yet
{ ts: 2, type: "say", say: "text", text: "hello" },
{ ts: 3, type: "say", say: "api_req_started", text: "not json" },
{ ts: 4, type: "say", say: "api_req_started" },
])
expect(result).toEqual([])
})

it("returns an empty list for no messages", () => {
expect(summarizeUsageByModel([])).toEqual([])
})
})
Loading
Loading