From 3889c56b303e495fbf0b8243f05eb5d3fbfcf0ef Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 11:46:31 -0700 Subject: [PATCH 01/25] feat(settings): chat fonts, spacing density, time format, and Usage tab UI tab - Interface font / code font (free-text font-family, sanitized; empty = inherit VS Code) - Spacing density (compact / comfortable / spacious) via data-zoo-density + CSS tokens; comfortable matches existing 10px/15px padding so defaults are unchanged Language tab - Time format (auto / 12h / 24h) with live preview; applied to history timestamps Usage tab (new) - Read-only rollup of local task history: cost, tokens, cache-hit rate, tasks - Range (today / 7d / 30d / all) and workspace scope filters - Daily cost chart, breakdown by API profile and mode, top tasks (click to open) - CSV export Plumbing - New nullable/optional global settings: interfaceFontFamily, codeFontFamily, spacingDensity, timeFormat (types, ClineProvider state, webview context, save payload) - Font resets send null so they survive JSON and clear the stored value i18n: keys added to all locales (non-English values are English placeholders) Tests: appearance, usage aggregation, time formatting; history spec mocks updated --- packages/types/src/global-settings.ts | 18 ++ packages/types/src/vscode-extension-host.ts | 4 + src/core/webview/ClineProvider.ts | 12 + webview-ui/src/components/chat/ChatRow.tsx | 2 +- .../src/components/chat/QueuedMessages.tsx | 2 +- .../src/components/common/MarkdownBlock.tsx | 4 +- .../src/components/history/TaskItemFooter.tsx | 9 +- .../history/__tests__/TaskGroupItem.spec.tsx | 1 + .../history/__tests__/TaskItem.spec.tsx | 1 + .../history/__tests__/TaskItemFooter.spec.tsx | 1 + .../components/settings/LanguageSettings.tsx | 55 ++++- .../src/components/settings/SettingsView.tsx | 25 +- .../src/components/settings/UISettings.tsx | 153 ++++++++++++- .../src/components/settings/UsageSettings.tsx | 213 ++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 25 ++ webview-ui/src/i18n/locales/ca/settings.json | 64 +++++- webview-ui/src/i18n/locales/de/settings.json | 64 +++++- webview-ui/src/i18n/locales/en/settings.json | 64 +++++- webview-ui/src/i18n/locales/es/settings.json | 64 +++++- webview-ui/src/i18n/locales/fr/settings.json | 64 +++++- webview-ui/src/i18n/locales/hi/settings.json | 64 +++++- webview-ui/src/i18n/locales/id/settings.json | 64 +++++- webview-ui/src/i18n/locales/it/settings.json | 64 +++++- webview-ui/src/i18n/locales/ja/settings.json | 64 +++++- webview-ui/src/i18n/locales/ko/settings.json | 64 +++++- webview-ui/src/i18n/locales/nl/settings.json | 64 +++++- webview-ui/src/i18n/locales/pl/settings.json | 64 +++++- .../src/i18n/locales/pt-BR/settings.json | 64 +++++- webview-ui/src/i18n/locales/ru/settings.json | 64 +++++- webview-ui/src/i18n/locales/tr/settings.json | 64 +++++- webview-ui/src/i18n/locales/vi/settings.json | 64 +++++- .../src/i18n/locales/zh-CN/settings.json | 64 +++++- .../src/i18n/locales/zh-TW/settings.json | 64 +++++- webview-ui/src/index.css | 18 ++ .../src/utils/__tests__/appearance.spec.ts | 56 +++++ webview-ui/src/utils/__tests__/format.spec.ts | 35 ++- webview-ui/src/utils/__tests__/usage.spec.ts | 122 ++++++++++ webview-ui/src/utils/appearance.ts | 49 ++++ webview-ui/src/utils/format.ts | 32 ++- webview-ui/src/utils/usage.ts | 190 ++++++++++++++++ 40 files changed, 2145 insertions(+), 34 deletions(-) create mode 100644 webview-ui/src/components/settings/UsageSettings.tsx create mode 100644 webview-ui/src/utils/__tests__/appearance.spec.ts create mode 100644 webview-ui/src/utils/__tests__/usage.spec.ts create mode 100644 webview-ui/src/utils/appearance.ts create mode 100644 webview-ui/src/utils/usage.ts diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index eb51c1d67d..6c26024d0e 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -280,6 +280,24 @@ 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(), + /** + * CSS font-family for Zoo Code's chat prose. `null`/unset inherits `--vscode-font-family`. + */ + interfaceFontFamily: z.string().max(256).nullish(), + /** + * CSS font-family for inline/fenced code in chat. `null`/unset inherits `--vscode-editor-font-family`. + */ + codeFontFamily: z.string().max(256).nullish(), + /** + * Vertical/horizontal spacing scale for chat rows. + * @default "comfortable" + */ + spacingDensity: z.enum(["compact", "comfortable", "spacious"]).optional(), + /** + * Clock format used when rendering timestamps. "auto" follows the display language's locale. + * @default "auto" + */ + timeFormat: z.enum(["auto", "12h", "24h"]).optional(), /** * Controls the keyboard behavior for sending messages in the chat input. * - "send": Enter sends message, Shift+Enter creates newline (default) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index dc407325d9..70047fc63a 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -339,6 +339,10 @@ export type ExtensionState = Pick< | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" | "chatFontSize" + | "interfaceFontFamily" + | "codeFontFamily" + | "spacingDensity" + | "timeFormat" | "enterBehavior" | "includeCurrentTime" | "includeCurrentCost" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 28a42913f3..05b1b41ae7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2810,6 +2810,10 @@ export class ClineProvider historyPreviewCollapsed, reasoningBlockCollapsed, chatFontSize, + interfaceFontFamily, + codeFontFamily, + spacingDensity, + timeFormat, enterBehavior, cloudUserInfo, cloudIsAuthenticated, @@ -2991,6 +2995,10 @@ export class ClineProvider historyPreviewCollapsed: historyPreviewCollapsed ?? false, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize, + interfaceFontFamily, + codeFontFamily, + spacingDensity: spacingDensity ?? "comfortable", + timeFormat: timeFormat ?? "auto", enterBehavior: enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated: cloudIsAuthenticated ?? false, @@ -3224,6 +3232,10 @@ export class ClineProvider historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, chatFontSize: stateValues.chatFontSize, + interfaceFontFamily: stateValues.interfaceFontFamily, + codeFontFamily: stateValues.codeFontFamily, + spacingDensity: stateValues.spacingDensity ?? "comfortable", + timeFormat: stateValues.timeFormat ?? "auto", enterBehavior: stateValues.enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated, diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 77ae4529c0..df127f5a13 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -142,7 +142,7 @@ const ChatRow = memo( const prevHeightRef = useRef(0) const [chatrow, { height }] = useSize( -
+
, ) diff --git a/webview-ui/src/components/chat/QueuedMessages.tsx b/webview-ui/src/components/chat/QueuedMessages.tsx index 5bfb6fdb48..ea8f1a5234 100644 --- a/webview-ui/src/components/chat/QueuedMessages.tsx +++ b/webview-ui/src/components/chat/QueuedMessages.tsx @@ -40,7 +40,7 @@ export const QueuedMessages = ({ queue, onRemove, onUpdate }: QueuedMessagesProp } return ( -
+
{t("queuedMessages.title")}
{queue.map((message, index) => { diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index 02f696553f..b69f9af3ac 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -44,7 +44,7 @@ const StyledMarkdown = styled.div` } code:not(pre > code) { - font-family: var(--vscode-editor-font-family, monospace); + font-family: var(--zoo-code-font-family, var(--vscode-editor-font-family, monospace)); font-size: 0.85em; filter: saturation(110%) brightness(95%); color: var(--vscode-textPreformat-foreground) !important; @@ -89,7 +89,7 @@ const StyledMarkdown = styled.div` } font-family: - var(--vscode-font-family), + var(--zoo-chat-font-family, var(--vscode-font-family)), system-ui, -apple-system, BlinkMacSystemFont, diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index 5d75822f4c..c43ce27ba2 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -1,6 +1,7 @@ -import React from "react" +import React, { useContext } from "react" import type { HistoryItem } from "@roo-code/types" -import { formatTimeAgo } from "@/utils/format" +import { formatDateTime, formatTimeAgo } from "@/utils/format" +import { ExtensionStateContext } from "@/context/ExtensionStateContext" import { CopyButton } from "./CopyButton" import { ExportButton } from "./ExportButton" import { DeleteButton } from "./DeleteButton" @@ -28,6 +29,8 @@ const TaskItemFooter: React.FC = ({ onRename, }) => { const { t } = useAppTranslation() + // Optional read: this leaf renders standalone (tests, previews) without a provider. + const timeFormat = useContext(ExtensionStateContext)?.timeFormat return (
@@ -48,7 +51,7 @@ const TaskItemFooter: React.FC = ({ )} {/* Datetime with time-ago format */} - + {formatTimeAgo(item.ts)} diff --git a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx index b04fac6b54..ea1e698f63 100644 --- a/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskGroupItem.spec.tsx @@ -19,6 +19,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ vi.mock("@/utils/format", () => ({ formatTimeAgo: vi.fn(() => "2 hours ago"), formatDate: vi.fn(() => "January 15 at 2:30 PM"), + formatDateTime: vi.fn(() => "Jan 15, 2024, 2:30 PM"), formatLargeNumber: vi.fn((num: number) => num.toString()), })) diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 4fb4a82d72..59f8e7cef7 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -14,6 +14,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ vi.mock("@/utils/format", () => ({ formatTimeAgo: vi.fn(() => "2 hours ago"), formatDate: vi.fn(() => "January 15 at 2:30 PM"), + formatDateTime: vi.fn(() => "Jan 15, 2024, 2:30 PM"), formatLargeNumber: vi.fn((num: number) => num.toString()), })) diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index 83047dc0b4..5e5f428479 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -11,6 +11,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({ vi.mock("@/utils/format", () => ({ formatTimeAgo: vi.fn(() => "2 hours ago"), formatDate: vi.fn(() => "January 15 at 2:30 PM"), + formatDateTime: vi.fn(() => "Jan 15, 2024, 2:30 PM"), formatLargeNumber: vi.fn((num: number) => num.toString()), })) diff --git a/webview-ui/src/components/settings/LanguageSettings.tsx b/webview-ui/src/components/settings/LanguageSettings.tsx index 26a807a498..c96995ba5e 100644 --- a/webview-ui/src/components/settings/LanguageSettings.tsx +++ b/webview-ui/src/components/settings/LanguageSettings.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes } from "react" +import { HTMLAttributes, useMemo } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import type { Language } from "@roo-code/types" @@ -6,6 +6,7 @@ import type { Language } from "@roo-code/types" import { LANGUAGES } from "@roo/language" import { cn } from "@src/lib/utils" +import { formatDateTime, type TimeFormat } from "@src/utils/format" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { SetCachedStateField } from "./types" @@ -13,14 +14,33 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" +const TIME_FORMATS: TimeFormat[] = ["auto", "12h", "24h"] + +// Fixed sample (14:41 local) so the preview clearly shows 12h vs 24h. +const PREVIEW_TS = (() => { + const d = new Date() + d.setHours(14, 41, 0, 0) + return d.getTime() +})() + type LanguageSettingsProps = HTMLAttributes & { language: string - setCachedStateField: SetCachedStateField<"language"> + timeFormat?: TimeFormat + setCachedStateField: SetCachedStateField<"language" | "timeFormat"> } -export const LanguageSettings = ({ language, setCachedStateField, className, ...props }: LanguageSettingsProps) => { +export const LanguageSettings = ({ + language, + timeFormat = "auto", + setCachedStateField, + className, + ...props +}: LanguageSettingsProps) => { const { t } = useAppTranslation() + // Recompute when language changes too, since "auto" is locale-driven. + const preview = useMemo(() => formatDateTime(PREVIEW_TS, timeFormat), [timeFormat, language]) // eslint-disable-line react-hooks/exhaustive-deps + return (
{t("settings:sections.language")} @@ -48,6 +68,35 @@ export const LanguageSettings = ({ language, setCachedStateField, className, ... + + +
+ + +
+ {t("settings:language.timeFormat.description")} +
+
+ {t("settings:language.timeFormat.preview", { example: preview })} +
+
+
) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6100a373e1..8ab8f0d3f2 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -29,6 +29,7 @@ import { GitCommitVertical, GraduationCap, ScrollText, + ChartColumn, } from "lucide-react" import { @@ -82,6 +83,7 @@ import { SlashCommandsSettings } from "./SlashCommandsSettings" import { SkillsSettings } from "./SkillsSettings" import { RulesSettings } from "./RulesSettings" import { UISettings } from "./UISettings" +import { UsageSettings } from "./UsageSettings" import ModesView from "../modes/ModesView" import McpView from "../mcp/McpView" import { WorktreesView } from "../worktrees/WorktreesView" @@ -114,6 +116,7 @@ export const sectionNames = [ "worktrees", "prompts", "ui", + "usage", "experimental", "language", "about", @@ -214,6 +217,10 @@ const SettingsView = forwardRef(({ onDone, t openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, chatFontSize, + interfaceFontFamily, + codeFontFamily, + spacingDensity, + timeFormat, enterBehavior, includeCurrentTime, includeCurrentCost, @@ -442,6 +449,11 @@ const SettingsView = forwardRef(({ onDone, t includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize: chatFontSize ?? null, + // null (not undefined) so a reset survives JSON and clears the stored value + interfaceFontFamily: interfaceFontFamily ?? null, + codeFontFamily: codeFontFamily ?? null, + spacingDensity: spacingDensity ?? "comfortable", + timeFormat: timeFormat ?? "auto", enterBehavior: enterBehavior ?? "send", includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, @@ -556,6 +568,7 @@ const SettingsView = forwardRef(({ onDone, t { id: "prompts", icon: MessageSquare }, { id: "worktrees", icon: GitBranch }, { id: "ui", icon: Glasses }, + { id: "usage", icon: ChartColumn }, { id: "experimental", icon: FlaskConical }, { id: "language", icon: Globe }, { id: "about", icon: Info }, @@ -944,6 +957,9 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed={reasoningBlockCollapsed ?? true} enterBehavior={enterBehavior ?? "send"} chatFontSize={chatFontSize ?? undefined} + interfaceFontFamily={interfaceFontFamily ?? undefined} + codeFontFamily={codeFontFamily ?? undefined} + spacingDensity={spacingDensity} autoCloseZooOpenedFiles={autoCloseZooOpenedFiles} autoCloseZooOpenedFilesAfterUserEdited={autoCloseZooOpenedFilesAfterUserEdited} autoCloseZooOpenedNewFiles={autoCloseZooOpenedNewFiles} @@ -951,6 +967,9 @@ const SettingsView = forwardRef(({ onDone, t /> )} + {/* Usage Section */} + {renderTab === "usage" && } + {/* Experimental Section */} {renderTab === "experimental" && ( (({ onDone, t {/* Language Section */} {renderTab === "language" && ( - + )} {/* About Section */} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 46353c26ad..8f340aa823 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, useMemo } from "react" +import { HTMLAttributes, useEffect, useMemo, useState } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { telemetryClient } from "@/utils/TelemetryClient" @@ -12,8 +12,9 @@ import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" -import { Slider, Button } from "../ui" +import { Slider, Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" import { ExtensionStateContextType } from "@/context/ExtensionStateContext" +import { DEFAULT_SPACING_DENSITY, SPACING_DENSITIES, type SpacingDensity, sanitizeFontFamily } from "@/utils/appearance" export const CHAT_FONT_SIZE_MIN = 8 export const CHAT_FONT_SIZE_MAX = 32 @@ -23,6 +24,9 @@ interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" chatFontSize?: number + interfaceFontFamily?: string + codeFontFamily?: string + spacingDensity?: SpacingDensity autoCloseZooOpenedFiles?: boolean autoCloseZooOpenedFilesAfterUserEdited?: boolean autoCloseZooOpenedNewFiles?: boolean @@ -33,6 +37,9 @@ export const UISettings = ({ reasoningBlockCollapsed, enterBehavior, chatFontSize, + interfaceFontFamily, + codeFontFamily, + spacingDensity, autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, autoCloseZooOpenedNewFiles, @@ -160,6 +167,67 @@ export const UISettings = ({
+ {/* Interface (chat prose) font */} + + setCachedStateField("interfaceFontFamily", value)} + testId="interface-font" + /> + + + {/* Code font */} + + setCachedStateField("codeFontFamily", value)} + testId="code-font" + /> + + + {/* Spacing density */} + +
+ + +
+ {t("settings:ui.spacingDensity.description")} +
+
+
+ {/* Auto-close Zoo opened files */} ) } + +interface FontFamilyFieldProps { + label: string + description: string + placeholder: string + invalidMessage: string + resetLabel: string + value?: string + onChange: (value: string | undefined) => void + testId: string +} + +/** + * Free-text font-family input. Keeps a local draft so the user can type + * freely, and only commits a sanitized value (or `undefined` = inherit) to + * cached settings. Invalid input is flagged but never persisted. + */ +const FontFamilyField = ({ + label, + description, + placeholder, + invalidMessage, + resetLabel, + value, + onChange, + testId, +}: FontFamilyFieldProps) => { + const [draft, setDraft] = useState(value ?? "") + + // Re-sync when the persisted value changes underneath us (reset, import). + useEffect(() => setDraft(value ?? ""), [value]) + + const trimmed = draft.trim() + const isInvalid = trimmed.length > 0 && sanitizeFontFamily(trimmed) === undefined + + const handleChange = (next: string) => { + setDraft(next) + const nextTrimmed = next.trim() + + if (!nextTrimmed) { + onChange(undefined) + } else { + const safe = sanitizeFontFamily(nextTrimmed) + if (safe) onChange(safe) + } + } + + return ( +
+ +
+ handleChange(e.target.value)} + data-testid={`${testId}-input`} + /> + +
+ {isInvalid ? ( +
+ {invalidMessage} +
+ ) : ( +
{description}
+ )} +
+ ) +} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx new file mode 100644 index 0000000000..96414aa828 --- /dev/null +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -0,0 +1,213 @@ +import { HTMLAttributes, useMemo, useState } from "react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { useExtensionState } from "@/context/ExtensionStateContext" +import { cn } from "@src/lib/utils" +import { vscode } from "@src/utils/vscode" +import { formatLargeNumber, formatTimeAgo } from "@src/utils/format" +import { USAGE_RANGES, type UsageRange, summarizeUsage, usageToCsv } from "@src/utils/usage" +import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" + +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" +import { SearchableSetting } from "./SearchableSetting" + +const formatCost = (cost: number) => `$${cost.toFixed(2)}` + +/** + * Usage tab: read-only rollup of locally persisted task history. No network + * calls and no new persisted state, so it is safe to ship independently of + * any provider-quota integration. + */ +export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { + const { t } = useAppTranslation() + const { taskHistory, cwd } = useExtensionState() + + const [range, setRange] = useState("7d") + const [scope, setScope] = useState<"workspace" | "all">("workspace") + + const workspace = scope === "workspace" ? cwd : undefined + const summary = useMemo( + () => summarizeUsage(taskHistory ?? [], { range, workspace }), + [taskHistory, range, workspace], + ) + + const maxDaily = Math.max(0, ...summary.daily.map((d) => d.cost)) + const isEmpty = summary.taskCount === 0 + + const handleExport = () => { + const csv = usageToCsv(taskHistory ?? [], { range, workspace }) + const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" })) + const a = document.createElement("a") + a.href = url + a.download = `zoo-usage-${range}.csv` + a.click() + URL.revokeObjectURL(url) + } + + return ( +
+ {t("settings:sections.usage")} + +
+ +
+ {USAGE_RANGES.map((r) => ( + + ))} +
+
+ + {t("settings:usage.scope.label")} + + +
+
+ + {isEmpty ? ( +
+ {t("settings:usage.empty")} +
+ ) : ( + <> + {/* Stat cards */} +
+ + + +
+ + {/* Daily cost chart */} + {summary.daily.length > 1 && ( +
+

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

+
+ {summary.daily.map((d) => ( +
0 ? `${(d.cost / maxDaily) * 100}%` : "2px" }} + title={`${new Date(d.day).toLocaleDateString()} · ${formatCost(d.cost)}`} + /> + ))} +
+
+ )} + + + + + {/* Top tasks */} +
+

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

+ {summary.topTasks.map((task) => ( + + ))} +
+ +
+ + {t("settings:usage.disclaimer")} + + +
+ + )} +
+
+ ) +} + +const StatCard = ({ value, label, hint }: { value: string; label: string; hint?: string }) => ( +
+
{value}
+
{label}
+ {hint &&
{hint}
} +
+) + +const Breakdown = ({ + title, + buckets, + total, + testId, +}: { + title: string + buckets: { key: string; cost: number }[] + total: number + testId: string +}) => ( +
+

+ {title} +

+ {buckets.slice(0, 6).map((b) => { + const pct = total > 0 ? Math.round((b.cost / total) * 100) : 0 + return ( +
+ {b.key} + {formatCost(b.cost)} + + + + {pct}% +
+ ) + })} +
+) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c4fa00f479..7590752d2f 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -34,6 +34,7 @@ import { CustomSupportPrompts } from "@roo/support-prompt" import { experimentDefault } from "@roo/experiments" import { vscode } from "@src/utils/vscode" +import { applyFontVariable } from "@src/utils/appearance" import { convertTextMateToHljs } from "@src/utils/textMateToHljs" export interface ExtensionStateContextType extends ExtensionState { @@ -132,6 +133,10 @@ export interface ExtensionStateContextType extends ExtensionState { setReasoningBlockCollapsed: (value: boolean) => void chatFontSize?: number setChatFontSize: (value: number | undefined) => void + interfaceFontFamily?: string + codeFontFamily?: string + spacingDensity?: "compact" | "comfortable" | "spacious" + timeFormat?: "auto" | "12h" | "24h" enterBehavior?: "send" | "newline" setEnterBehavior: (value: "send" | "newline") => void autoCondenseContext: boolean @@ -533,11 +538,31 @@ export const ExtensionStateContextProvider: React.FC<{ } }, [state.chatFontSize]) + // Chat prose / code font overrides. Removing the inline property lets the + // stylesheet defaults (VS Code's own font variables) take over again. + useEffect(() => { + applyFontVariable("--zoo-chat-font-family", state.interfaceFontFamily) + }, [state.interfaceFontFamily]) + + useEffect(() => { + applyFontVariable("--zoo-code-font-family", state.codeFontFamily) + }, [state.codeFontFamily]) + + // Spacing density is expressed as a data attribute so all spacing tokens + // live in CSS (index.css) rather than being scattered across components. + useEffect(() => { + document.documentElement.dataset.zooDensity = state.spacingDensity ?? "comfortable" + }, [state.spacingDensity]) + const contextValue: ExtensionStateContextType = { ...state, // `chatFontSize` is persisted as nullish (null on reset); normalize null to // undefined so it matches the context type and means "use VS Code default". chatFontSize: state.chatFontSize ?? undefined, + interfaceFontFamily: state.interfaceFontFamily ?? undefined, + codeFontFamily: state.codeFontFamily ?? undefined, + spacingDensity: state.spacingDensity ?? "comfortable", + timeFormat: state.timeFormat ?? "auto", reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true, didHydrateState, showWelcome, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index d39d8edd6a..6ba338eed1 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Sobre Zoo Code", "skills": "Skills", - "rules": "Regles" + "rules": "Regles", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Tanca automaticament els fitxers nous creats", "description": "Quan esta activat, les pestanyes dels fitxers que Zoo ha creat (en lloc de modificar) tambe es tanquen automaticament despres d'acceptar el canvi." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "La descripció ha de tenir com a màxim 1024 caràcters" }, "footer": "Crea les teves pròpies skills amb el mode Skill Writer, disponible al Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 983db1df5d..666bbfc6ea 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -42,7 +42,8 @@ "language": "Sprache", "about": "Über Zoo Code", "skills": "Skills", - "rules": "Regeln" + "rules": "Regeln", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Neu erstellte Dateien automatisch schließen", "description": "Wenn aktiviert, werden Tabs für Dateien, die Zoo neu erstellt hat (anstatt sie zu bearbeiten), nach dem Akzeptieren der Änderung ebenfalls automatisch geschlossen." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Beschreibung muss 1024 Zeichen oder weniger sein" }, "footer": "Erstelle deine eigenen Skills mit dem Skill Writer Modus, verfügbar im Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index ea924ce79e..ff34c0d87b 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -42,7 +42,8 @@ "ui": "UI", "experimental": "Experimental", "language": "Language", - "about": "About Zoo Code" + "about": "About Zoo Code", + "usage": "Usage" }, "about": { "bugReport": { @@ -242,6 +243,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Auto-close newly created files", "description": "When enabled, tabs for files that Zoo created (rather than modified) will also be closed automatically after the edit is accepted." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "prompts": { @@ -1233,5 +1255,45 @@ "output": "Output", "cacheReads": "Cache reads" } + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 57612bf3e5..a4fe519b48 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Acerca de Zoo Code", "skills": "Skills", - "rules": "Reglas" + "rules": "Reglas", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Cerrar automaticamente los archivos nuevos creados", "description": "Cuando esta opcion esta activada, las pestanas de archivos que Zoo ha creado (en lugar de modificar) tambien se cierran automaticamente despues de aceptar la modificacion." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "La descripción debe tener como máximo 1024 caracteres" }, "footer": "Crea tus propias skills con el modo Skill Writer, disponible en el Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index a91a5137d3..0ca07fbfb4 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -42,7 +42,8 @@ "language": "Langue", "about": "À propos de Zoo Code", "skills": "Skills", - "rules": "Règles" + "rules": "Règles", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Fermer automatiquement les nouveaux fichiers créés", "description": "Quand cette option est activée, les onglets des fichiers que Zoo a créés (plutôt que modifiés) sont également fermés automatiquement après l'acceptation de la modification." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "La description doit contenir au maximum 1024 caractères" }, "footer": "Créez vos propres skills avec le mode Skill Writer, disponible dans le Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index bc6e973623..dce6b212d4 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -42,7 +42,8 @@ "language": "भाषा", "about": "परिचय", "skills": "Skills", - "rules": "नियम" + "rules": "नियम", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Nai banayi gayi files ko svachalit band karo", "description": "Saksham hone par, Zoo dvara banaayi gayi (sanshodhit nahin) files ki tabs ko bhi parivartan svikaar hone ke baad svachalit roop se band kar diya jaata hai." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "विवरण 1024 वर्णों से अधिक नहीं होना चाहिए" }, "footer": "Skill Writer मोड के साथ अपने स्वयं के Skills बनाएं, Modes Marketplace में उपलब्ध।" + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 3ed57f373e..27e4a67a0c 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -42,7 +42,8 @@ "language": "Bahasa", "about": "Tentang Zoo Code", "skills": "Skills", - "rules": "Aturan" + "rules": "Aturan", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Tutup otomatis file baru yang dibuat", "description": "Saat diaktifkan, tab untuk file yang dibuat Zoo (bukan dimodifikasi) juga ditutup secara otomatis setelah perubahan diterima." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Deskripsi harus 1024 karakter atau kurang" }, "footer": "Buat skills Anda sendiri dengan mode Skill Writer, tersedia di Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index b708304e63..d8c2c721ad 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -42,7 +42,8 @@ "language": "Lingua", "about": "Informazioni su Zoo Code", "skills": "Skills", - "rules": "Regole" + "rules": "Regole", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Chiudi automaticamente i nuovi file creati", "description": "Quando abilitata, le schede dei file che Zoo ha creato (invece di modificare) vengono chiuse automaticamente dopo l'accettazione della modifica." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "La descrizione deve essere di massimo 1024 caratteri" }, "footer": "Crea le tue skill con la modalità Skill Writer, disponibile in Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index f6959b50dc..5dce2a599d 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -42,7 +42,8 @@ "language": "言語", "about": "Zoo Codeについて", "skills": "Skills", - "rules": "ルール" + "rules": "ルール", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "新しく作成されたファイルを自動的に閉じる", "description": "有効にすると、Zooが作成した(変更ではなく)ファイルのタブも、変更を承認した後に自動的に閉じられます。" + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "説明は1024文字以内である必要があります" }, "footer": "スキルライターモードで独自のスキルを作成します。モードマーケットプレイスで入手可能です。" + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 6cadbd8a3b..9e205bb806 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -42,7 +42,8 @@ "language": "언어", "about": "Zoo Code 정보", "skills": "Skills", - "rules": "규칙" + "rules": "규칙", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "새로 만든 파일 자동 닫기", "description": "활성화 시, Zoo가 수정한 것이 아니라 새로 만든 파일의 탭도 변경 사항을 승인한 후 자동으로 닫힙니다." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "설명은 1024자 이하여야 합니다" }, "footer": "스킬 작성자 모드로 자신만의 스킬을 만들 수 있습니다. 모드 마켓플레이스에서 사용 가능합니다." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 992e9c53e0..f22f7ba82d 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -42,7 +42,8 @@ "language": "Taal", "about": "Over Zoo Code", "skills": "Skills", - "rules": "Regels" + "rules": "Regels", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Automatisch sluiten van nieuw gemaakte bestanden", "description": "Wanneer ingeschakeld, worden tabbladen van bestanden die Zoo heeft aangemaakt (in plaats van gewijzigd) ook automatisch gesloten nadat de wijziging is geaccepteerd." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Beschrijving moet maximaal 1024 tekens zijn" }, "footer": "Maak je eigen skills met de Skill Writer modus, beschikbaar in de Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 459675b0e1..53754d4855 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -42,7 +42,8 @@ "language": "Język", "about": "O Zoo Code", "skills": "Skills", - "rules": "Reguły" + "rules": "Reguły", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Automatycznie zamykaj nowo utworzone pliki", "description": "Gdy wlaczone, karty plikow, ktore Zoo utworzylo (zamiast zmodyfikowalo), sa rowniez automatycznie zamykane po zaakceptowaniu zmiany." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Opis musi mieć maksymalnie 1024 znaki" }, "footer": "Twórz własne umiejętności za pomocą trybu Skill Writer, dostępnego w Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 500babaaa8..8c55bcdffb 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -42,7 +42,8 @@ "language": "Idioma", "about": "Sobre", "skills": "Skills", - "rules": "Regras" + "rules": "Regras", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Fechar automaticamente novos arquivos criados", "description": "Quando ativada, as abas de arquivos que o Zoo criou (em vez de modificar) tambem sao fechadas automaticamente apos aceitar a modificacao." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "A descrição deve ter no máximo 1024 caracteres" }, "footer": "Crie suas próprias skills com o modo Skill Writer, disponível em Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 053ff0684d..221c36761a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -42,7 +42,8 @@ "language": "Язык", "about": "О Zoo Code", "skills": "Skills", - "rules": "Правила" + "rules": "Правила", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Автоматически закрывать новые созданные файлы", "description": "Если включено, вкладки файлов, которые Zoo создал (а не изменил), также автоматически закрываются после принятия изменения." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Описание должно быть не более 1024 символов" }, "footer": "Создавайте собственные навыки с режимом Skill Writer, доступным в Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index b37f28d44d..2a735256a7 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -42,7 +42,8 @@ "language": "Dil", "about": "Zoo Code Hakkında", "skills": "Skills", - "rules": "Kurallar" + "rules": "Kurallar", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Yeni olusturulan dosyalari otomatik kapat", "description": "Etkinlestirildiginde, Zoo'nun olusturdugu (degistirmedigi) dosyalarin sekmeleri de degisiklik kabul edildikten sonra otomatik olarak kapatilir." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Açıklama en fazla 1024 karakter olmalıdır" }, "footer": "Skill Writer modu ile kendi becerilerinizi oluşturun. Modes Marketplace'de mevcut." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 94843217ae..0f11017af7 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -42,7 +42,8 @@ "language": "Ngôn ngữ", "about": "Giới thiệu", "skills": "Skills", - "rules": "Quy tắc" + "rules": "Quy tắc", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "Tự động đóng các tệp mới tạo", "description": "Khi bật, các tab của các tệp mà Zoo đã tạo (thay vì chỉnh sửa) cũng sẽ tự động đóng sau khi chấp nhận thay đổi." + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "Mô tả phải có tối đa 1024 ký tự" }, "footer": "Tạo các skill của riêng bạn với chế độ Skill Writer, có sẵn tại Modes Marketplace." + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 8be0fe1532..eb59a4f704 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -42,7 +42,8 @@ "language": "语言", "about": "关于 Zoo Code", "skills": "Skills", - "rules": "规则" + "rules": "规则", + "usage": "Usage" }, "about": { "bugReport": { @@ -1179,6 +1180,27 @@ "autoCloseZooOpenedNewFiles": { "label": "自动关闭新建的文件", "description": "启用后,Zoo 新建(而非修改)的文件标签页在接受更改后也会自动关闭。" + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "skills": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "描述不得超过1024个字符" }, "footer": "使用技能编写器模式创建您自己的技能,可在 模式市集 中获得。" + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 65eb5c67a5..4ee26ea013 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -42,7 +42,8 @@ "language": "語言", "about": "關於 Zoo Code", "skills": "Skills", - "rules": "規則" + "rules": "規則", + "usage": "Usage" }, "about": { "bugReport": { @@ -189,6 +190,27 @@ "autoCloseZooOpenedNewFiles": { "label": "自動關閉新建立的檔案", "description": "啟用後,Zoo 新建(而非修改)的檔案分頁在接受變更後也會自動關閉。" + }, + "interfaceFont": { + "label": "Interface font", + "description": "Font family for chat messages. Leave empty to use VS Code's font.", + "placeholder": "VS Code default" + }, + "codeFont": { + "label": "Code font", + "description": "Font family for code in chat. Leave empty to use the editor font.", + "placeholder": "Editor font" + }, + "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", + "fontReset": "Use default", + "spacingDensity": { + "label": "Spacing density", + "description": "How much padding chat messages use. Compact fits more on screen.", + "options": { + "compact": "Compact", + "comfortable": "Comfortable", + "spacious": "Spacious" + } } }, "prompts": { @@ -1233,5 +1255,45 @@ "descriptionTooLong": "說明不得超過1024個字元" }, "footer": "使用技能編寫器模式建立您自己的技能,可在 模式市集 中取得。" + }, + "language": { + "timeFormat": { + "label": "Time format", + "description": "How times are shown in Zoo Code. Auto follows the display language.", + "preview": "Example: {{example}}", + "options": { + "auto": "Auto", + "12h": "12-hour", + "24h": "24-hour" + } + } + }, + "usage": { + "description": "Tokens and cost from your Zoo Code task history on this machine.", + "range": { + "label": "Time range", + "today": "Today", + "7d": "7 days", + "30d": "30 days", + "all": "All time" + }, + "scope": { + "label": "Show", + "workspace": "This workspace", + "all": "All workspaces" + }, + "empty": "No tasks in this time range.", + "stats": { + "cost": "cost", + "tokens": "tokens", + "tasks": "tasks", + "cacheHit": "{{percent}}% from cache" + }, + "dailyCost": "Daily cost", + "byProfile": "By API profile", + "byMode": "By mode", + "topTasks": "Most expensive tasks", + "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", + "exportCsv": "Export CSV" } } diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index bccbdf7031..900f20f6a7 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -266,6 +266,24 @@ html { * runtime. Scoped to chat rendering so it does not resize settings/other UI. */ --zoo-chat-font-size: var(--vscode-font-size); + + /* + * Spacing density tokens for chat rows. `data-zoo-density` is set on + * from the "Spacing density" UI setting; "comfortable" matches the + * historical hardcoded padding (10px / 15px) so defaults are unchanged. + */ + --zoo-row-py: 10px; + --zoo-row-px: 15px; +} + +html[data-zoo-density="compact"] { + --zoo-row-py: 5px; + --zoo-row-px: 10px; +} + +html[data-zoo-density="spacious"] { + --zoo-row-py: 16px; + --zoo-row-px: 18px; } body { diff --git a/webview-ui/src/utils/__tests__/appearance.spec.ts b/webview-ui/src/utils/__tests__/appearance.spec.ts new file mode 100644 index 0000000000..2574adc7e5 --- /dev/null +++ b/webview-ui/src/utils/__tests__/appearance.spec.ts @@ -0,0 +1,56 @@ +import { applyFontVariable, sanitizeFontFamily } from "../appearance" + +describe("sanitizeFontFamily", () => { + it.each([ + ["Inter", "Inter"], + [" Inter, sans-serif ", "Inter, sans-serif"], + ['"JetBrains Mono", monospace', '"JetBrains Mono", monospace'], + ["'Fira Code', Menlo", "'Fira Code', Menlo"], + ["Noto Sans CJK JP", "Noto Sans CJK JP"], + ["ヒラギノ角ゴ", "ヒラギノ角ゴ"], + ])("accepts %j", (input, expected) => { + expect(sanitizeFontFamily(input)).toBe(expected) + }) + + it.each([ + ["", "empty"], + [" ", "whitespace"], + ["Inter; color: red", "semicolon"], + ["Inter}body{display:none", "braces"], + ["url(http://evil)", "url()"], + ["Inter", "angle brackets"], + ["a".repeat(257), "too long"], + ])("rejects %j (%s)", (input) => { + expect(sanitizeFontFamily(input)).toBeUndefined() + }) + + it("handles null/undefined", () => { + expect(sanitizeFontFamily(null)).toBeUndefined() + expect(sanitizeFontFamily(undefined)).toBeUndefined() + }) +}) + +describe("applyFontVariable", () => { + let root: HTMLElement + + beforeEach(() => { + root = document.createElement("div") + }) + + it("sets the variable for a valid font", () => { + applyFontVariable("--zoo-chat-font-family", "Inter, sans-serif", root) + expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("Inter, sans-serif") + }) + + it("removes the variable when cleared", () => { + applyFontVariable("--zoo-chat-font-family", "Inter", root) + applyFontVariable("--zoo-chat-font-family", undefined, root) + expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("") + }) + + it("removes the variable for an invalid value instead of applying it", () => { + applyFontVariable("--zoo-chat-font-family", "Inter", root) + applyFontVariable("--zoo-chat-font-family", "Inter; x: y", root) + expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("") + }) +}) diff --git a/webview-ui/src/utils/__tests__/format.spec.ts b/webview-ui/src/utils/__tests__/format.spec.ts index 2f633d364d..27970a513a 100644 --- a/webview-ui/src/utils/__tests__/format.spec.ts +++ b/webview-ui/src/utils/__tests__/format.spec.ts @@ -1,4 +1,4 @@ -import { formatLargeNumber, formatDate, formatTimeAgo } from "../format" +import { formatLargeNumber, formatDate, formatDateTime, formatTimeAgo, hourCycleOptions } from "../format" // Mock i18next vi.mock("i18next", () => ({ @@ -65,6 +65,39 @@ describe("formatDate", () => { expect(result).toMatch(/january|jan/i) expect(result).toMatch(/15/) }) + + it("honours an explicit 24-hour time format", () => { + const timestamp = new Date("2024-01-15T14:30:00").getTime() + expect(formatDate(timestamp, "24h")).toMatch(/14:30/) + expect(formatDate(timestamp, "24h")).not.toMatch(/PM/i) + }) + + it("honours an explicit 12-hour time format", () => { + const timestamp = new Date("2024-01-15T14:30:00").getTime() + expect(formatDate(timestamp, "12h")).toMatch(/2:30\s?PM/i) + }) +}) + +describe("hourCycleOptions", () => { + it("leaves auto to the locale", () => { + expect(hourCycleOptions("auto")).toEqual({}) + expect(hourCycleOptions()).toEqual({}) + }) + + it("maps 12h / 24h", () => { + expect(hourCycleOptions("12h")).toEqual({ hour12: true }) + // h23 (not hour12:false) avoids "24:05" rendering in some engines + expect(hourCycleOptions("24h")).toEqual({ hourCycle: "h23" }) + }) +}) + +describe("formatDateTime", () => { + it("includes date and time honouring the format", () => { + const timestamp = new Date("2024-01-15T00:05:00").getTime() + const result = formatDateTime(timestamp, "24h") + expect(result).toMatch(/2024/) + expect(result).toMatch(/00:05/) + }) }) describe("formatTimeAgo", () => { diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts new file mode 100644 index 0000000000..d780e14bdb --- /dev/null +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -0,0 +1,122 @@ +import type { HistoryItem } from "@roo-code/types" + +import { rangeStart, summarizeUsage, usageToCsv } from "../usage" + +// Fixed "now": Wed 2026-10-14 15:00 local time. +const NOW = new Date(2026, 9, 14, 15, 0, 0).getTime() +const DAY = 24 * 60 * 60 * 1000 + +let counter = 0 +const item = (overrides: Partial): HistoryItem => ({ + id: `t${++counter}`, + number: counter, + ts: NOW, + task: `task ${counter}`, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: "/ws/a", + ...overrides, +}) + +describe("rangeStart", () => { + it("returns local midnight for today", () => { + expect(rangeStart("today", NOW)).toBe(new Date(2026, 9, 14).getTime()) + }) + + it("includes today in 7d / 30d windows", () => { + expect(rangeStart("7d", NOW)).toBe(new Date(2026, 9, 8).getTime()) + expect(rangeStart("30d", NOW)).toBe(new Date(2026, 8, 15).getTime()) + }) + + it("is unbounded for all", () => { + expect(rangeStart("all", NOW)).toBeUndefined() + }) +}) + +describe("summarizeUsage", () => { + const history = [ + item({ totalCost: 1.5, tokensIn: 1000, tokensOut: 200, cacheReads: 3000, apiConfigName: "opus", mode: "code" }), + item({ totalCost: 0.5, tokensIn: 500, tokensOut: 100, apiConfigName: "gpt", mode: "architect", ts: NOW - DAY }), + item({ totalCost: 2, tokensIn: 10, tokensOut: 10, apiConfigName: "opus", mode: "code", workspace: "/ws/b" }), + // Outside 7d window + item({ totalCost: 99, apiConfigName: "opus", mode: "code", ts: NOW - 10 * DAY }), + ] + + it("totals only items inside the range", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.taskCount).toBe(3) + expect(s.totalCost).toBeCloseTo(4) + expect(s.totalTokens).toBe(1000 + 200 + 500 + 100 + 20) + }) + + it("filters by workspace", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW, workspace: "/ws/a" }) + expect(s.taskCount).toBe(2) + expect(s.totalCost).toBeCloseTo(2) + }) + + it("groups by profile and mode sorted by cost desc", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.byProfile.map((b) => [b.key, b.tasks])).toEqual([ + ["opus", 2], + ["gpt", 1], + ]) + expect(s.byProfile[0].cost).toBeCloseTo(3.5) + expect(s.byMode.map((b) => b.key)).toEqual(["code", "architect"]) + }) + + it("buckets missing profile/mode under fallbacks", () => { + const s = summarizeUsage([item({ totalCost: 1 })], { range: "all", now: NOW }) + expect(s.byProfile[0].key).toBe("default") + expect(s.byMode[0].key).toBe("unknown") + }) + + it("computes cache hit rate over input-side tokens", () => { + const s = summarizeUsage(history, { range: "today", now: NOW }) + // today: tokensIn 1000 + 10, cacheReads 3000 + expect(s.cacheHitRate).toBeCloseTo(3000 / (1010 + 3000)) + }) + + it("produces a dense daily series for bounded ranges", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.daily).toHaveLength(7) + expect(s.daily.at(-1)!.cost).toBeCloseTo(3.5) + expect(s.daily.at(-2)!.cost).toBeCloseTo(0.5) + expect(s.daily[0].cost).toBe(0) + }) + + it("returns top tasks by cost, limited", () => { + const s = summarizeUsage(history, { range: "all", now: NOW, topTaskLimit: 2 }) + expect(s.topTasks.map((t) => t.totalCost)).toEqual([99, 2]) + }) + + it("is safe on empty history", () => { + const s = summarizeUsage([], { range: "7d", now: NOW }) + expect(s.taskCount).toBe(0) + expect(s.totalCost).toBe(0) + expect(s.cacheHitRate).toBe(0) + }) + + it("ignores non-finite costs", () => { + const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) + expect(s.totalCost).toBe(0) + }) +}) + +describe("usageToCsv", () => { + it("escapes quotes and newlines and respects the range", () => { + const csv = usageToCsv( + [ + item({ task: 'say "hi"\nthen go', totalCost: 0.12345, mode: "code" }), + item({ task: "old", ts: NOW - 40 * DAY }), + ], + { range: "30d", now: NOW }, + ) + const lines = csv.split("\n") + expect(lines[0]).toBe("date,task,mode,profile,tokensIn,tokensOut,cacheReads,cacheWrites,cost") + expect(lines).toHaveLength(2) + expect(lines[1]).toContain('"say ""hi"" then go"') + expect(lines[1].endsWith(",0.1235")).toBe(true) + }) +}) diff --git a/webview-ui/src/utils/appearance.ts b/webview-ui/src/utils/appearance.ts new file mode 100644 index 0000000000..496df8eac9 --- /dev/null +++ b/webview-ui/src/utils/appearance.ts @@ -0,0 +1,49 @@ +/** + * Helpers for user-configurable appearance settings (fonts, density). + * + * Font family values are free text typed by the user and are written into a + * CSS custom property. `style.setProperty` can't break out of the declaration, + * but we still reject characters that have no business in a font-family list + * (braces, semicolons, url(), etc.) so a bad value degrades to "inherit" + * instead of producing a confusing half-applied style. + */ + +const FONT_FAMILY_ALLOWED = /^[\p{L}\p{N}\s,'"\-_.]+$/u +const FONT_FAMILY_MAX_LENGTH = 256 + +/** Returns a trimmed, safe font-family list, or `undefined` if empty/invalid. */ +export function sanitizeFontFamily(value: string | null | undefined): string | undefined { + if (typeof value !== "string") { + return undefined + } + + const trimmed = value.trim() + + if (!trimmed || trimmed.length > FONT_FAMILY_MAX_LENGTH || !FONT_FAMILY_ALLOWED.test(trimmed)) { + return undefined + } + + return trimmed +} + +/** + * Sets (or clears) a font-family CSS variable on the document root. Clearing + * lets the stylesheet fallback (VS Code's own font variable) apply. + */ +export function applyFontVariable( + name: string, + value: string | null | undefined, + root: HTMLElement = document.documentElement, +): void { + const safe = sanitizeFontFamily(value) + + if (safe) { + root.style.setProperty(name, safe) + } else { + root.style.removeProperty(name) + } +} + +export const SPACING_DENSITIES = ["compact", "comfortable", "spacious"] as const +export type SpacingDensity = (typeof SPACING_DENSITIES)[number] +export const DEFAULT_SPACING_DENSITY: SpacingDensity = "comfortable" diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts index cd3521aed2..8b162fce74 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -1,5 +1,7 @@ import i18next from "i18next" +export type TimeFormat = "auto" | "12h" | "24h" + export function formatLargeNumber(num: number): string { if (num >= 1e9) { return (num / 1e9).toFixed(1) + i18next.t("common:number_format.billion_suffix") @@ -13,10 +15,25 @@ export function formatLargeNumber(num: number): string { return num.toString() } +/** + * Maps the user's time format preference to `Intl` hour-cycle options. + * "auto" returns `{}` so the locale decides (e.g. 24h for de, 12h for en-US). + */ +export function hourCycleOptions(timeFormat: TimeFormat = "auto"): Intl.DateTimeFormatOptions { + switch (timeFormat) { + case "12h": + return { hour12: true } + case "24h": + return { hourCycle: "h23" } + default: + return {} + } +} + /** * @internal */ -export const formatDate = (timestamp: number) => { +export const formatDate = (timestamp: number, timeFormat: TimeFormat = "auto") => { const date = new Date(timestamp) const locale = i18next.language || "en" @@ -25,7 +42,18 @@ export const formatDate = (timestamp: number) => { day: "numeric", hour: "numeric", minute: "2-digit", - hour12: true, + ...hourCycleOptions(timeFormat), + }) +} + +/** Full date + time, used for tooltips on relative timestamps. */ +export const formatDateTime = (timestamp: number, timeFormat: TimeFormat = "auto") => { + const locale = i18next.language || "en" + + return new Date(timestamp).toLocaleString(locale, { + dateStyle: "medium", + timeStyle: "short", + ...hourCycleOptions(timeFormat), }) } diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts new file mode 100644 index 0000000000..9fbccdf25a --- /dev/null +++ b/webview-ui/src/utils/usage.ts @@ -0,0 +1,190 @@ +import type { HistoryItem } from "@roo-code/types" + +/** + * Pure aggregation over locally persisted task history for the Usage tab. + * + * Notes on correctness: + * - `HistoryItem.totalCost` is a task's *own* spend; subtasks are separate + * history items. Summing every item therefore counts each dollar once. + * - History stores the provider profile (`apiConfigName`), not the model id, + * so the breakdown is by profile. + */ + +export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const +export type UsageRange = (typeof USAGE_RANGES)[number] + +const DAY_MS = 24 * 60 * 60 * 1000 + +export interface UsageBucket { + key: string + cost: number + tokens: number + tasks: number +} + +export interface DailyUsage { + /** Local-midnight timestamp of the day. */ + day: number + cost: number +} + +export interface UsageSummary { + totalCost: number + tokensIn: number + tokensOut: number + cacheReads: number + cacheWrites: number + totalTokens: number + /** Share of input-side tokens served from cache, 0–1. */ + cacheHitRate: number + taskCount: number + byProfile: UsageBucket[] + byMode: UsageBucket[] + daily: DailyUsage[] + topTasks: HistoryItem[] +} + +export interface UsageOptions { + range: UsageRange + /** When set, only tasks from this workspace are included. */ + workspace?: string + now?: number + topTaskLimit?: number +} + +function startOfLocalDay(ts: number): number { + const d = new Date(ts) + d.setHours(0, 0, 0, 0) + return d.getTime() +} + +/** Inclusive lower bound for a range, or `undefined` for "all". */ +export function rangeStart(range: UsageRange, now: number): number | undefined { + const today = startOfLocalDay(now) + + switch (range) { + case "today": + return today + case "7d": + return today - 6 * DAY_MS + case "30d": + return today - 29 * DAY_MS + default: + return undefined + } +} + +function addTo(map: Map, key: string, cost: number, tokens: number) { + const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0 } + bucket.cost += cost + bucket.tokens += tokens + bucket.tasks += 1 + map.set(key, bucket) +} + +const byCostDesc = (a: UsageBucket, b: UsageBucket) => b.cost - a.cost || b.tokens - a.tokens + +export function summarizeUsage(history: HistoryItem[], options: UsageOptions): UsageSummary { + const now = options.now ?? Date.now() + const start = rangeStart(options.range, now) + const topTaskLimit = options.topTaskLimit ?? 5 + + const items = history.filter( + (item) => + typeof item.ts === "number" && + (start === undefined || item.ts >= start) && + (!options.workspace || item.workspace === options.workspace), + ) + + const profiles = new Map() + const modes = new Map() + const daily = new Map() + + let totalCost = 0 + let tokensIn = 0 + let tokensOut = 0 + let cacheReads = 0 + let cacheWrites = 0 + + for (const item of items) { + const cost = Number.isFinite(item.totalCost) ? item.totalCost : 0 + const tIn = item.tokensIn || 0 + const tOut = item.tokensOut || 0 + const tokens = tIn + tOut + + totalCost += cost + tokensIn += tIn + tokensOut += tOut + cacheReads += item.cacheReads || 0 + cacheWrites += item.cacheWrites || 0 + + addTo(profiles, item.apiConfigName || "default", cost, tokens) + addTo(modes, item.mode || "unknown", cost, tokens) + + const day = startOfLocalDay(item.ts) + daily.set(day, (daily.get(day) ?? 0) + cost) + } + + // Densify the daily series for bounded ranges so the chart has a bar per day + // (including zero-spend days). "all" stays sparse to avoid huge arrays. + const dailySeries: DailyUsage[] = [] + if (start !== undefined) { + for (let day = start; day <= startOfLocalDay(now); day += DAY_MS) { + // Re-normalise each step to survive DST transitions. + const normalised = startOfLocalDay(day + DAY_MS / 2) + dailySeries.push({ day: normalised, cost: daily.get(normalised) ?? 0 }) + } + } else { + for (const [day, cost] of [...daily.entries()].sort(([a], [b]) => a - b)) { + dailySeries.push({ day, cost }) + } + } + + const inputSide = tokensIn + cacheReads + + return { + totalCost, + tokensIn, + tokensOut, + cacheReads, + cacheWrites, + totalTokens: tokensIn + tokensOut, + cacheHitRate: inputSide > 0 ? cacheReads / inputSide : 0, + taskCount: items.length, + byProfile: [...profiles.values()].sort(byCostDesc), + byMode: [...modes.values()].sort(byCostDesc), + daily: dailySeries, + topTasks: [...items].sort((a, b) => (b.totalCost || 0) - (a.totalCost || 0)).slice(0, topTaskLimit), + } +} + +/** Serialises the filtered usage rows as CSV (one row per task). */ +export function usageToCsv(history: HistoryItem[], options: UsageOptions): string { + const start = rangeStart(options.range, options.now ?? Date.now()) + const header = ["date", "task", "mode", "profile", "tokensIn", "tokensOut", "cacheReads", "cacheWrites", "cost"] + + const escape = (value: string) => `"${value.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` + + const rows = history + .filter( + (item) => + (start === undefined || item.ts >= start) && + (!options.workspace || item.workspace === options.workspace), + ) + .sort((a, b) => a.ts - b.ts) + .map((item) => + [ + new Date(item.ts).toISOString(), + escape(item.title || item.task || ""), + escape(item.mode || ""), + escape(item.apiConfigName || ""), + item.tokensIn || 0, + item.tokensOut || 0, + item.cacheReads || 0, + item.cacheWrites || 0, + (item.totalCost || 0).toFixed(4), + ].join(","), + ) + + return [header.join(","), ...rows].join("\n") +} From 4ed35c72b2705137801cbe400462fd017d366fbd Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 11:57:18 -0700 Subject: [PATCH 02/25] fix(usage): fall back to tokens for zero-cost providers; tighten tab layout GitHub Copilot / VS Code LM tasks persist totalCost=0 (subscription billing), so the Usage tab showed $0.00 everywhere with empty bars and 0% shares. - pickMetric(): when a range has tokens but no cost, charts, breakdowns and top tasks switch to tokens, with an inline note explaining why - daily series now tracks tokens alongside cost; top tasks tie-break on tokens - token card shows in/out split when there are no cache reads - layout closer to the mock: segmented range control, weekday labels under the chart, colored dots + bars in breakdowns, section dividers, export button above the disclaimer - add missing charts-blue/purple/orange Tailwind color tokens --- .../src/components/settings/UsageSettings.tsx | 221 +++++++++++++----- webview-ui/src/i18n/locales/ca/settings.json | 9 +- webview-ui/src/i18n/locales/de/settings.json | 9 +- webview-ui/src/i18n/locales/en/settings.json | 9 +- webview-ui/src/i18n/locales/es/settings.json | 9 +- webview-ui/src/i18n/locales/fr/settings.json | 9 +- webview-ui/src/i18n/locales/hi/settings.json | 9 +- webview-ui/src/i18n/locales/id/settings.json | 9 +- webview-ui/src/i18n/locales/it/settings.json | 9 +- webview-ui/src/i18n/locales/ja/settings.json | 9 +- webview-ui/src/i18n/locales/ko/settings.json | 9 +- webview-ui/src/i18n/locales/nl/settings.json | 9 +- webview-ui/src/i18n/locales/pl/settings.json | 9 +- .../src/i18n/locales/pt-BR/settings.json | 9 +- webview-ui/src/i18n/locales/ru/settings.json | 9 +- webview-ui/src/i18n/locales/tr/settings.json | 9 +- webview-ui/src/i18n/locales/vi/settings.json | 9 +- .../src/i18n/locales/zh-CN/settings.json | 9 +- .../src/i18n/locales/zh-TW/settings.json | 9 +- webview-ui/src/index.css | 3 + webview-ui/src/utils/__tests__/usage.spec.ts | 29 ++- webview-ui/src/utils/usage.ts | 35 ++- 22 files changed, 346 insertions(+), 104 deletions(-) diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 96414aa828..dfae89df8f 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -5,7 +5,14 @@ import { useExtensionState } from "@/context/ExtensionStateContext" import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { formatLargeNumber, formatTimeAgo } from "@src/utils/format" -import { USAGE_RANGES, type UsageRange, summarizeUsage, usageToCsv } from "@src/utils/usage" +import { + USAGE_RANGES, + type UsageMetric, + type UsageRange, + pickMetric, + summarizeUsage, + usageToCsv, +} from "@src/utils/usage" import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { SectionHeader } from "./SectionHeader" @@ -14,10 +21,25 @@ import { SearchableSetting } from "./SearchableSetting" const formatCost = (cost: number) => `$${cost.toFixed(2)}` +const PALETTE = [ + "var(--vscode-charts-blue)", + "var(--vscode-charts-purple)", + "var(--vscode-charts-green)", + "var(--vscode-charts-orange)", + "var(--vscode-charts-yellow)", + "var(--vscode-charts-red)", +] + +const WEEKDAY_FMT = new Intl.DateTimeFormat(undefined, { weekday: "narrow" }) +const DAY_FMT = new Intl.DateTimeFormat(undefined, { day: "numeric" }) + /** * Usage tab: read-only rollup of locally persisted task history. No network - * calls and no new persisted state, so it is safe to ship independently of - * any provider-quota integration. + * calls and no new persisted state. + * + * Subscription providers (GitHub Copilot / VS Code LM) report $0 per task, so + * when the selected range has tokens but no cost the tab switches its charts + * and breakdowns to tokens and explains why. */ export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { const { t } = useAppTranslation() @@ -32,7 +54,13 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes d.cost)) + const metric: UsageMetric = pickMetric(summary) + const value = (cost: number, tokens: number) => (metric === "cost" ? cost : tokens) + const fmt = (cost: number, tokens: number) => (metric === "cost" ? formatCost(cost) : formatLargeNumber(tokens)) + const total = value(summary.totalCost, summary.totalTokens) + + const maxDaily = Math.max(0, ...summary.daily.map((d) => value(d.cost, d.tokens))) + const showDayLabels = summary.daily.length <= 14 const isEmpty = summary.taskCount === 0 const handleExport = () => { @@ -51,21 +79,27 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes + {/* Segmented range control */}
{USAGE_RANGES.map((r) => ( - + ))}
@@ -92,79 +126,136 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes {/* Stat cards */}
- + 0 + ? t("settings:usage.stats.cacheHit", { + percent: Math.round(summary.cacheHitRate * 100), + }) + : t("settings:usage.stats.inOut", { + input: formatLargeNumber(summary.tokensIn), + output: formatLargeNumber(summary.tokensOut), + }) + } />
- {/* Daily cost chart */} + {metric === "tokens" && ( +
+ {t("settings:usage.noPricingNote")} +
+ )} + + {/* Daily chart */} {summary.daily.length > 1 && ( -
-

- {t("settings:usage.dailyCost")} -

-
- {summary.daily.map((d) => ( -
0 ? `${(d.cost / maxDaily) * 100}%` : "2px" }} - title={`${new Date(d.day).toLocaleDateString()} · ${formatCost(d.cost)}`} - /> - ))} +
+ + {metric === "cost" + ? t("settings:usage.dailyCost") + : t("settings:usage.dailyTokens")} + +
+ {summary.daily.map((d) => { + const v = value(d.cost, d.tokens) + return ( +
0 ? "bg-vscode-charts-blue" : "bg-vscode-input-background", + )} + style={{ + height: + maxDaily > 0 && v > 0 + ? `${Math.max(4, (v / maxDaily) * 100)}%` + : "3px", + }} + title={`${new Date(d.day).toLocaleDateString()} · ${fmt(d.cost, d.tokens)}`} + /> + ) + })}
+ {showDayLabels && ( +
+ {summary.daily.map((d) => ( + + {summary.daily.length <= 7 + ? WEEKDAY_FMT.format(d.day) + : DAY_FMT.format(d.day)} + + ))} +
+ )}
)} ({ + key: b.key, + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} testId="usage-by-profile" /> ({ + key: b.key, + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} testId="usage-by-mode" /> {/* Top tasks */} -
-

- {t("settings:usage.topTasks")} -

+
+ + {metric === "cost" ? t("settings:usage.topTasks") : t("settings:usage.topTasksTokens")} + {summary.topTasks.map((task) => ( ))}
-
+
+ {t("settings:usage.disclaimer")} -
)} @@ -173,39 +264,47 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ( +

+ {children} +

+) + const StatCard = ({ value, label, hint }: { value: string; label: string; hint?: string }) => ( -
-
{value}
+
+
{value}
{label}
- {hint &&
{hint}
} + {hint &&
{hint}
}
) const Breakdown = ({ title, - buckets, + rows, total, testId, }: { title: string - buckets: { key: string; cost: number }[] + rows: { key: string; value: number; label: string }[] total: number testId: string }) => ( -
-

- {title} -

- {buckets.slice(0, 6).map((b) => { - const pct = total > 0 ? Math.round((b.cost / total) * 100) : 0 +
+ {title} + {rows.slice(0, 6).map((row, i) => { + const pct = total > 0 ? Math.round((row.value / total) * 100) : 0 + const color = PALETTE[i % PALETTE.length] return ( -
- {b.key} - {formatCost(b.cost)} +
+ + {row.key} + {row.label} - + - {pct}% + {pct}%
) })} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 6ba338eed1..f5e7eae8dd 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 666bbfc6ea..0024c994c3 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index ff34c0d87b..834888c6d5 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index a4fe519b48..54eca131cc 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 0ca07fbfb4..8c0816825d 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index dce6b212d4..3bed1c4a16 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 27e4a67a0c..5ea54ee897 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index d8c2c721ad..467215cd1b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 5dce2a599d..224e8d58ad 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 9e205bb806..e87e141a8b 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index f22f7ba82d..e96625f172 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 53754d4855..a4a5ef46e1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 8c55bcdffb..6278c62c60 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 221c36761a..23d8bb731b 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 2a735256a7..2453c0cd3f 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 0f11017af7..8230784fb8 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index eb59a4f704..66f03771fa 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 4ee26ea013..dc0d38b484 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1287,13 +1287,18 @@ "cost": "cost", "tokens": "tokens", "tasks": "tasks", - "cacheHit": "{{percent}}% from cache" + "cacheHit": "{{percent}}% from cache", + "noPricing": "no per-token price", + "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV" + "exportCsv": "Export CSV", + "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "dailyTokens": "Daily tokens", + "topTasksTokens": "Largest tasks" } } diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 900f20f6a7..3d6e82e115 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -129,6 +129,9 @@ --color-vscode-titleBar-activeForeground: var(--vscode-titleBar-activeForeground); --color-vscode-titleBar-inactiveForeground: var(--vscode-titleBar-inactiveForeground); + --color-vscode-charts-blue: var(--vscode-charts-blue); + --color-vscode-charts-purple: var(--vscode-charts-purple); + --color-vscode-charts-orange: var(--vscode-charts-orange); --color-vscode-charts-green: var(--vscode-charts-green); --color-vscode-charts-red: var(--vscode-charts-red); --color-vscode-charts-yellow: var(--vscode-charts-yellow); diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index d780e14bdb..cf809afb58 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -1,6 +1,6 @@ import type { HistoryItem } from "@roo-code/types" -import { rangeStart, summarizeUsage, usageToCsv } from "../usage" +import { pickMetric, rangeStart, summarizeUsage, usageToCsv } from "../usage" // Fixed "now": Wed 2026-10-14 15:00 local time. const NOW = new Date(2026, 9, 14, 15, 0, 0).getTime() @@ -98,12 +98,39 @@ describe("summarizeUsage", () => { expect(s.cacheHitRate).toBe(0) }) + it("tracks tokens per day alongside cost", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.daily.at(-1)!.tokens).toBe(1000 + 200 + 20) + }) + + it("ranks zero-cost tasks by tokens (subscription providers)", () => { + const s = summarizeUsage([item({ id: "small", tokensIn: 10 }), item({ id: "big", tokensIn: 5_000_000 })], { + range: "all", + now: NOW, + }) + expect(s.topTasks.map((t) => t.id)).toEqual(["big", "small"]) + }) + it("ignores non-finite costs", () => { const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) expect(s.totalCost).toBe(0) }) }) +describe("pickMetric", () => { + it("uses cost when any cost was recorded", () => { + expect(pickMetric({ totalCost: 0.01, totalTokens: 10 })).toBe("cost") + }) + + it("falls back to tokens when tokens exist but cost is zero (e.g. GitHub Copilot)", () => { + expect(pickMetric({ totalCost: 0, totalTokens: 6_200_000 })).toBe("tokens") + }) + + it("stays on cost when there is no usage at all", () => { + expect(pickMetric({ totalCost: 0, totalTokens: 0 })).toBe("cost") + }) +}) + describe("usageToCsv", () => { it("escapes quotes and newlines and respects the range", () => { const csv = usageToCsv( diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 9fbccdf25a..94a9dd001e 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -26,6 +26,18 @@ export interface DailyUsage { /** Local-midnight timestamp of the day. */ day: number cost: number + tokens: number +} + +/** + * Which metric the Usage tab should chart. Subscription-backed providers + * (e.g. GitHub Copilot via the VS Code LM API) report no per-token price, so + * their tasks persist `totalCost: 0`. Charting cost would show flat zeros. + */ +export type UsageMetric = "cost" | "tokens" + +export function pickMetric(summary: Pick): UsageMetric { + return summary.totalCost > 0 || summary.totalTokens === 0 ? "cost" : "tokens" } export interface UsageSummary { @@ -98,7 +110,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U const profiles = new Map() const modes = new Map() - const daily = new Map() + const daily = new Map() let totalCost = 0 let tokensIn = 0 @@ -122,7 +134,10 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U addTo(modes, item.mode || "unknown", cost, tokens) const day = startOfLocalDay(item.ts) - daily.set(day, (daily.get(day) ?? 0) + cost) + const entry = daily.get(day) ?? { cost: 0, tokens: 0 } + entry.cost += cost + entry.tokens += tokens + daily.set(day, entry) } // Densify the daily series for bounded ranges so the chart has a bar per day @@ -132,11 +147,12 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U for (let day = start; day <= startOfLocalDay(now); day += DAY_MS) { // Re-normalise each step to survive DST transitions. const normalised = startOfLocalDay(day + DAY_MS / 2) - dailySeries.push({ day: normalised, cost: daily.get(normalised) ?? 0 }) + const entry = daily.get(normalised) + dailySeries.push({ day: normalised, cost: entry?.cost ?? 0, tokens: entry?.tokens ?? 0 }) } } else { - for (const [day, cost] of [...daily.entries()].sort(([a], [b]) => a - b)) { - dailySeries.push({ day, cost }) + for (const [day, entry] of [...daily.entries()].sort(([a], [b]) => a - b)) { + dailySeries.push({ day, ...entry }) } } @@ -154,7 +170,14 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U byProfile: [...profiles.values()].sort(byCostDesc), byMode: [...modes.values()].sort(byCostDesc), daily: dailySeries, - topTasks: [...items].sort((a, b) => (b.totalCost || 0) - (a.totalCost || 0)).slice(0, topTaskLimit), + // Cost first, tokens as tie-breaker so zero-cost (subscription) usage still ranks meaningfully. + topTasks: [...items] + .sort( + (a, b) => + (b.totalCost || 0) - (a.totalCost || 0) || + (b.tokensIn || 0) + (b.tokensOut || 0) - ((a.tokensIn || 0) + (a.tokensOut || 0)), + ) + .slice(0, topTaskLimit), } } From c54718ebef28271dd4f2bf72cba34d6e1ea965b1 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 12:04:08 -0700 Subject: [PATCH 03/25] feat(settings): provider breakdown in Usage; font presets and live preview in UI tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Usage - New 'By provider' breakdown; history only stores the profile name, so the provider is resolved from the current profile list (unknown if deleted) - Profile rows show 'Provider · model' under the profile name UI tab - Replace free-text font boxes with preset pickers (Match VS Code / curated fonts rendered in their own face / Custom…); text input only for Custom… - Live preview card shows interface font, code font, chat size and density using unsaved values - Tests: every preset stack passes sanitization; preset round-trip --- .scout/scout.config.yaml | 16 ++ .../settings/AppearanceControls.tsx | 219 ++++++++++++++++++ .../src/components/settings/UISettings.tsx | 112 ++------- .../src/components/settings/UsageSettings.tsx | 51 +++- .../__tests__/AppearanceControls.spec.tsx | 29 +++ webview-ui/src/i18n/locales/ca/settings.json | 24 +- webview-ui/src/i18n/locales/de/settings.json | 24 +- webview-ui/src/i18n/locales/en/settings.json | 24 +- webview-ui/src/i18n/locales/es/settings.json | 24 +- webview-ui/src/i18n/locales/fr/settings.json | 24 +- webview-ui/src/i18n/locales/hi/settings.json | 24 +- webview-ui/src/i18n/locales/id/settings.json | 24 +- webview-ui/src/i18n/locales/it/settings.json | 24 +- webview-ui/src/i18n/locales/ja/settings.json | 24 +- webview-ui/src/i18n/locales/ko/settings.json | 24 +- webview-ui/src/i18n/locales/nl/settings.json | 24 +- webview-ui/src/i18n/locales/pl/settings.json | 24 +- .../src/i18n/locales/pt-BR/settings.json | 24 +- webview-ui/src/i18n/locales/ru/settings.json | 24 +- webview-ui/src/i18n/locales/tr/settings.json | 24 +- webview-ui/src/i18n/locales/vi/settings.json | 24 +- .../src/i18n/locales/zh-CN/settings.json | 24 +- .../src/i18n/locales/zh-TW/settings.json | 24 +- webview-ui/src/utils/__tests__/usage.spec.ts | 17 ++ webview-ui/src/utils/usage.ts | 13 +- 25 files changed, 685 insertions(+), 204 deletions(-) create mode 100644 .scout/scout.config.yaml create mode 100644 webview-ui/src/components/settings/AppearanceControls.tsx create mode 100644 webview-ui/src/components/settings/__tests__/AppearanceControls.spec.tsx diff --git a/.scout/scout.config.yaml b/.scout/scout.config.yaml new file mode 100644 index 0000000000..cdb25bb166 --- /dev/null +++ b/.scout/scout.config.yaml @@ -0,0 +1,16 @@ +# Scout per-repo config template. +# +# Seeded into `/.scout/scout.config.yaml` by `scout attach` when the +# repo has no config yet. Templates here only carry the repo-specific +# ignore / dampen / boost / tune overrides; defaults for +# repository/markdown/ranking/index/projects are filled in at load time. +ignore: + - '**/CHANGELOG.md' + - '**/CHANGELOG*' + - '**/CHANGES.md' + - '**/HISTORY.md' + +dampen: + - factor: 0.3 + patterns: + - '**/package.json' diff --git a/webview-ui/src/components/settings/AppearanceControls.tsx b/webview-ui/src/components/settings/AppearanceControls.tsx new file mode 100644 index 0000000000..c22c1a29fa --- /dev/null +++ b/webview-ui/src/components/settings/AppearanceControls.tsx @@ -0,0 +1,219 @@ +import { useEffect, useState } from "react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@/lib/utils" +import { sanitizeFontFamily, type SpacingDensity } from "@/utils/appearance" + +import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" + +/** + * Curated presets. Each stack ends in a generic family so an uninstalled font + * degrades gracefully. `DEFAULT` = no override (inherit from VS Code). + */ +const DEFAULT = "__default__" +const CUSTOM = "__custom__" + +export const INTERFACE_FONT_PRESETS = [ + { id: "system", label: "System UI", stack: "system-ui, -apple-system, 'Segoe UI', sans-serif" }, + { id: "inter", label: "Inter", stack: "Inter, system-ui, sans-serif" }, + { id: "sf", label: "SF Pro", stack: "'SF Pro Text', -apple-system, sans-serif" }, + { id: "segoe", label: "Segoe UI", stack: "'Segoe UI', system-ui, sans-serif" }, + { id: "roboto", label: "Roboto", stack: "Roboto, system-ui, sans-serif" }, + { id: "helvetica", label: "Helvetica Neue", stack: "'Helvetica Neue', Helvetica, Arial, sans-serif" }, + { id: "georgia", label: "Georgia (serif)", stack: "Georgia, 'Times New Roman', serif" }, +] as const + +export const CODE_FONT_PRESETS = [ + { id: "sfmono", label: "SF Mono", stack: "'SF Mono', SFMono-Regular, Menlo, monospace" }, + { id: "menlo", label: "Menlo", stack: "Menlo, Monaco, monospace" }, + { id: "jetbrains", label: "JetBrains Mono", stack: "'JetBrains Mono', Menlo, monospace" }, + { id: "fira", label: "Fira Code", stack: "'Fira Code', Menlo, monospace" }, + { id: "cascadia", label: "Cascadia Code", stack: "'Cascadia Code', Consolas, monospace" }, + { id: "consolas", label: "Consolas", stack: "Consolas, 'Courier New', monospace" }, +] as const + +type Preset = { id: string; label: string; stack: string } + +/** Maps a stored stack back to its preset id, "custom", or the default. */ +export function presetIdFor(value: string | undefined, presets: readonly Preset[]): string { + if (!value) return DEFAULT + return presets.find((p) => p.stack === value)?.id ?? CUSTOM +} + +interface FontPresetSelectProps { + label: string + description: string + /** Label for the "inherit" option, e.g. "Match VS Code". */ + defaultLabel: string + customLabel: string + customPlaceholder: string + invalidMessage: string + presets: readonly Preset[] + value?: string + onChange: (value: string | undefined) => void + testId: string +} + +/** + * Font picker: inherit / curated presets / custom. Each option renders in its + * own face so users see the font before choosing. The free-text input only + * appears for "Custom…", and only sanitized values are committed. + */ +export const FontPresetSelect = ({ + label, + description, + defaultLabel, + customLabel, + customPlaceholder, + invalidMessage, + presets, + value, + onChange, + testId, +}: FontPresetSelectProps) => { + const [selected, setSelected] = useState(() => presetIdFor(value, presets)) + const [draft, setDraft] = useState(selected === CUSTOM ? (value ?? "") : "") + + // Re-sync when the persisted value changes underneath us (reset/import), + // but don't kick the user out of "Custom…" while they're typing. + useEffect(() => { + const next = presetIdFor(value, presets) + if (next !== CUSTOM || selected !== CUSTOM) setSelected(next) + if (next === CUSTOM) setDraft(value ?? "") + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [value, presets]) + + const handleSelect = (id: string) => { + setSelected(id) + if (id === DEFAULT) onChange(undefined) + else if (id !== CUSTOM) onChange(presets.find((p) => p.id === id)?.stack) + // Custom: wait for valid input before committing. + } + + const trimmed = draft.trim() + const isInvalid = selected === CUSTOM && trimmed.length > 0 && sanitizeFontFamily(trimmed) === undefined + + const handleDraft = (next: string) => { + setDraft(next) + const safe = sanitizeFontFamily(next) + if (!next.trim()) onChange(undefined) + else if (safe) onChange(safe) + } + + return ( +
+ + + + {selected === CUSTOM && ( + handleDraft(e.target.value)} + data-testid={`${testId}-custom-input`} + /> + )} + +
+ {isInvalid ? invalidMessage : description} +
+
+ ) +} + +const DENSITY_PADDING: Record = { + compact: { py: 5, px: 10 }, + comfortable: { py: 10, px: 15 }, + spacious: { py: 16, px: 18 }, +} + +interface AppearancePreviewProps { + interfaceFontFamily?: string + codeFontFamily?: string + chatFontSize?: number + spacingDensity: SpacingDensity +} + +/** + * Live preview of chat appearance using the *unsaved* settings, applied + * inline so it updates instantly without touching the real chat. + */ +export const AppearancePreview = ({ + interfaceFontFamily, + codeFontFamily, + chatFontSize, + spacingDensity, +}: AppearancePreviewProps) => { + const { t } = useAppTranslation() + const { py, px } = DENSITY_PADDING[spacingDensity] + + const rowStyle = { padding: `${py}px ${px}px` } + const textStyle = { + fontFamily: sanitizeFontFamily(interfaceFontFamily) ?? "var(--vscode-font-family)", + fontSize: chatFontSize ? `${chatFontSize}px` : "var(--vscode-font-size)", + } + const codeStyle = { + fontFamily: sanitizeFontFamily(codeFontFamily) ?? "var(--vscode-editor-font-family, monospace)", + fontSize: "0.85em", + } + + return ( +
+
+ {t("settings:ui.preview.label")} +
+
+
+ {t("settings:ui.preview.you")} +
+
{t("settings:ui.preview.userMessage")}
+
+
+
+ {t("settings:ui.preview.zoo")} +
+
+ {t("settings:ui.preview.assistantBefore")}{" "} + + Edge.tsx + {" "} + {t("settings:ui.preview.assistantAfter")} +
+
+					{"const stroke = selected ? 2 : 1"}
+				
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 8f340aa823..1470e6f81d 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, useEffect, useMemo, useState } from "react" +import { HTMLAttributes, useMemo } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { telemetryClient } from "@/utils/TelemetryClient" @@ -12,9 +12,10 @@ import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" -import { Slider, Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" +import { Slider, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" import { ExtensionStateContextType } from "@/context/ExtensionStateContext" -import { DEFAULT_SPACING_DENSITY, SPACING_DENSITIES, type SpacingDensity, sanitizeFontFamily } from "@/utils/appearance" +import { DEFAULT_SPACING_DENSITY, SPACING_DENSITIES, type SpacingDensity } from "@/utils/appearance" +import { AppearancePreview, CODE_FONT_PRESETS, FontPresetSelect, INTERFACE_FONT_PRESETS } from "./AppearanceControls" export const CHAT_FONT_SIZE_MIN = 8 export const CHAT_FONT_SIZE_MAX = 32 @@ -172,12 +173,14 @@ export const UISettings = ({ settingId="ui-interface-font" section="ui" label={t("settings:ui.interfaceFont.label")}> - setCachedStateField("interfaceFontFamily", value)} testId="interface-font" @@ -186,12 +189,14 @@ export const UISettings = ({ {/* Code font */} - setCachedStateField("codeFontFamily", value)} testId="code-font" @@ -228,6 +233,14 @@ export const UISettings = ({
+ {/* Live preview of the appearance settings above (unsaved values) */} + + {/* Auto-close Zoo opened files */} ) } - -interface FontFamilyFieldProps { - label: string - description: string - placeholder: string - invalidMessage: string - resetLabel: string - value?: string - onChange: (value: string | undefined) => void - testId: string -} - -/** - * Free-text font-family input. Keeps a local draft so the user can type - * freely, and only commits a sanitized value (or `undefined` = inherit) to - * cached settings. Invalid input is flagged but never persisted. - */ -const FontFamilyField = ({ - label, - description, - placeholder, - invalidMessage, - resetLabel, - value, - onChange, - testId, -}: FontFamilyFieldProps) => { - const [draft, setDraft] = useState(value ?? "") - - // Re-sync when the persisted value changes underneath us (reset, import). - useEffect(() => setDraft(value ?? ""), [value]) - - const trimmed = draft.trim() - const isInvalid = trimmed.length > 0 && sanitizeFontFamily(trimmed) === undefined - - const handleChange = (next: string) => { - setDraft(next) - const nextTrimmed = next.trim() - - if (!nextTrimmed) { - onChange(undefined) - } else { - const safe = sanitizeFontFamily(nextTrimmed) - if (safe) onChange(safe) - } - } - - return ( -
- -
- handleChange(e.target.value)} - data-testid={`${testId}-input`} - /> - -
- {isInvalid ? ( -
- {invalidMessage} -
- ) : ( -
{description}
- )} -
- ) -} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index dfae89df8f..5cf4c9bd03 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -18,6 +18,10 @@ import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" +import { PROVIDERS } from "./constants" + +const PROVIDER_LABELS = new Map(PROVIDERS.map((p) => [p.value, p.label])) +const providerLabel = (id: string) => PROVIDER_LABELS.get(id) ?? id const formatCost = (cost: number) => `$${cost.toFixed(2)}` @@ -43,17 +47,33 @@ const DAY_FMT = new Intl.DateTimeFormat(undefined, { day: "numeric" }) */ export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { const { t } = useAppTranslation() - const { taskHistory, cwd } = useExtensionState() + const { taskHistory, cwd, listApiConfigMeta } = useExtensionState() const [range, setRange] = useState("7d") const [scope, setScope] = useState<"workspace" | "all">("workspace") const workspace = scope === "workspace" ? cwd : undefined + + // History stores the profile name only; resolve provider/model from the + // current profile list. Renamed or deleted profiles fall back to "unknown". + const profileMeta = useMemo(() => new Map((listApiConfigMeta ?? []).map((p) => [p.name, p])), [listApiConfigMeta]) + const summary = useMemo( - () => summarizeUsage(taskHistory ?? [], { range, workspace }), - [taskHistory, range, workspace], + () => + summarizeUsage(taskHistory ?? [], { + range, + workspace, + providerForProfile: (name) => profileMeta.get(name)?.apiProvider, + }), + [taskHistory, range, workspace, profileMeta], ) + const profileSubtitle = (name: string) => { + const meta = profileMeta.get(name) + if (!meta?.apiProvider) return t("settings:usage.unknownProvider") + return [providerLabel(meta.apiProvider), meta.modelId].filter(Boolean).join(" · ") + } + const metric: UsageMetric = pickMetric(summary) const value = (cost: number, tokens: number) => (metric === "cost" ? cost : tokens) const fmt = (cost: number, tokens: number) => (metric === "cost" ? formatCost(cost) : formatLargeNumber(tokens)) @@ -199,10 +219,23 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes )} + ({ + key: b.key, + name: b.key === "unknown" ? t("settings:usage.unknownProvider") : providerLabel(b.key), + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} + testId="usage-by-provider" + /> ({ key: b.key, + name: b.key, + subtitle: profileSubtitle(b.key), value: value(b.cost, b.tokens), label: fmt(b.cost, b.tokens), }))} @@ -213,6 +246,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ({ key: b.key, + name: b.key, value: value(b.cost, b.tokens), label: fmt(b.cost, b.tokens), }))} @@ -285,7 +319,7 @@ const Breakdown = ({ testId, }: { title: string - rows: { key: string; value: number; label: string }[] + rows: { key: string; name: string; subtitle?: string; value: number; label: string }[] total: number testId: string }) => ( @@ -299,7 +333,14 @@ const Breakdown = ({ key={row.key} className="grid grid-cols-[10px_minmax(0,1fr)_auto_minmax(48px,72px)_32px] items-center gap-2 mb-1.5 text-sm"> - {row.key} + + {row.name} + {row.subtitle && ( + + {row.subtitle} + + )} + {row.label} diff --git a/webview-ui/src/components/settings/__tests__/AppearanceControls.spec.tsx b/webview-ui/src/components/settings/__tests__/AppearanceControls.spec.tsx new file mode 100644 index 0000000000..bdfdb4909a --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/AppearanceControls.spec.tsx @@ -0,0 +1,29 @@ +import { CODE_FONT_PRESETS, INTERFACE_FONT_PRESETS, presetIdFor } from "../AppearanceControls" +import { sanitizeFontFamily } from "@/utils/appearance" + +describe("font presets", () => { + it.each([...INTERFACE_FONT_PRESETS, ...CODE_FONT_PRESETS])("$label stack passes sanitization", (preset) => { + // A preset that the sanitizer rejects would silently fall back to inherit. + expect(sanitizeFontFamily(preset.stack)).toBe(preset.stack) + }) + + it("has unique ids", () => { + const ids = [...INTERFACE_FONT_PRESETS, ...CODE_FONT_PRESETS].map((p) => p.id) + expect(new Set(ids).size).toBe(ids.length) + }) +}) + +describe("presetIdFor", () => { + it("maps empty to the default option", () => { + expect(presetIdFor(undefined, INTERFACE_FONT_PRESETS)).toBe("__default__") + expect(presetIdFor("", INTERFACE_FONT_PRESETS)).toBe("__default__") + }) + + it("maps a known stack back to its preset", () => { + expect(presetIdFor(CODE_FONT_PRESETS[2].stack, CODE_FONT_PRESETS)).toBe(CODE_FONT_PRESETS[2].id) + }) + + it("treats anything else as custom", () => { + expect(presetIdFor("Comic Sans MS", INTERFACE_FONT_PRESETS)).toBe("__custom__") + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index f5e7eae8dd..492a46b00d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 0024c994c3..eb0c4589b4 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 834888c6d5..84df8eb7ea 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -246,16 +246,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -264,6 +265,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "prompts": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 54eca131cc..d2b3789326 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 8c0816825d..05cc7879ba 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 3bed1c4a16..2bb59132f0 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 5ea54ee897..4c0ced4f85 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 467215cd1b..4d936a7d3b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 224e8d58ad..ba074856a2 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index e87e141a8b..dd88b1546c 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index e96625f172..4e19b86309 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index a4a5ef46e1..7ffaac45d2 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 6278c62c60..ea67ecca39 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 23d8bb731b..d9d6715ccd 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 2453c0cd3f..554c97baba 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 8230784fb8..07c4b42dde 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 66f03771fa..e37f9a2c08 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1183,16 +1183,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -1201,6 +1202,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index dc0d38b484..56d5f713b1 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -193,16 +193,17 @@ }, "interfaceFont": { "label": "Interface font", - "description": "Font family for chat messages. Leave empty to use VS Code's font.", - "placeholder": "VS Code default" + "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. Inter, sans-serif", + "default": "Match VS Code" }, "codeFont": { "label": "Code font", - "description": "Font family for code in chat. Leave empty to use the editor font.", - "placeholder": "Editor font" + "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", + "placeholder": "e.g. 'JetBrains Mono', monospace", + "default": "Match editor font" }, "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "fontReset": "Use default", "spacingDensity": { "label": "Spacing density", "description": "How much padding chat messages use. Compact fits more on screen.", @@ -211,6 +212,15 @@ "comfortable": "Comfortable", "spacious": "Spacious" } + }, + "fontCustom": "Custom…", + "preview": { + "label": "Preview", + "you": "You", + "zoo": "Zoo", + "userMessage": "Make the selected edge thicker", + "assistantBefore": "Updated", + "assistantAfter": "to use a 2px stroke when selected." } }, "prompts": { @@ -1299,6 +1309,8 @@ "exportCsv": "Export CSV", "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks" + "topTasksTokens": "Largest tasks", + "byProvider": "By provider", + "unknownProvider": "Unknown provider" } } diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index cf809afb58..5ae66b7a6f 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -111,6 +111,23 @@ describe("summarizeUsage", () => { expect(s.topTasks.map((t) => t.id)).toEqual(["big", "small"]) }) + it("groups by provider using the profile resolver, bucketing unknown profiles", () => { + const providers: Record = { opus: "anthropic", gpt: "github-copilot" } + const s = summarizeUsage([...history, item({ apiConfigName: "deleted-profile", tokensIn: 1 })], { + range: "7d", + now: NOW, + providerForProfile: (p) => providers[p], + }) + expect(s.byProvider.map((b) => b.key)).toEqual(["anthropic", "github-copilot", "unknown"]) + expect(s.byProvider[0].tasks).toBe(2) + }) + + it("buckets everything as unknown provider when no resolver is given", () => { + const s = summarizeUsage(history, { range: "7d", now: NOW }) + expect(s.byProvider).toHaveLength(1) + expect(s.byProvider[0].key).toBe("unknown") + }) + it("ignores non-finite costs", () => { const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) expect(s.totalCost).toBe(0) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 94a9dd001e..638bd490bd 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -50,6 +50,7 @@ export interface UsageSummary { /** Share of input-side tokens served from cache, 0–1. */ cacheHitRate: number taskCount: number + byProvider: UsageBucket[] byProfile: UsageBucket[] byMode: UsageBucket[] daily: DailyUsage[] @@ -62,6 +63,12 @@ export interface UsageOptions { workspace?: string now?: number topTaskLimit?: number + /** + * Resolves a profile name (`HistoryItem.apiConfigName`) to its provider id. + * History doesn't store the provider, so the caller supplies this from the + * current profile list. Unknown/deleted profiles bucket as "unknown". + */ + providerForProfile?: (profile: string) => string | undefined } function startOfLocalDay(ts: number): number { @@ -108,6 +115,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U (!options.workspace || item.workspace === options.workspace), ) + const providers = new Map() const profiles = new Map() const modes = new Map() const daily = new Map() @@ -130,7 +138,9 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U cacheReads += item.cacheReads || 0 cacheWrites += item.cacheWrites || 0 - addTo(profiles, item.apiConfigName || "default", cost, tokens) + const profile = item.apiConfigName || "default" + addTo(providers, options.providerForProfile?.(profile) || "unknown", cost, tokens) + addTo(profiles, profile, cost, tokens) addTo(modes, item.mode || "unknown", cost, tokens) const day = startOfLocalDay(item.ts) @@ -167,6 +177,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U totalTokens: tokensIn + tokensOut, cacheHitRate: inputSide > 0 ? cacheReads / inputSide : 0, taskCount: items.length, + byProvider: [...providers.values()].sort(byCostDesc), byProfile: [...profiles.values()].sort(byCostDesc), byMode: [...modes.values()].sort(byCostDesc), daily: dailySeries, From b459db18ea5c438086c6d866cc93ef236531a99d Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 12:42:09 -0700 Subject: [PATCH 04/25] feat(settings): size steppers in UI tab; live GitHub Copilot credits in Usage UI tab - Replace font pickers/preview with four steppers: Interface font size (%), Terminal font size (px), Editor font size (px), Spacing density (%). -/+ buttons, typed input (commit on blur/Enter), arrow keys, clamping and a reset icon; the default clears the override so VS Code changes still flow through - CSS vars --zoo-ui-scale / --zoo-terminal-font-size / --zoo-editor-font-size / --zoo-spacing-scale drive body + Tailwind text tokens, TerminalOutput, CodeBlock, diff view and chat row padding; defaults reproduce today's look - Settings: interfaceFontScale, terminalFontSize, editorFontSize, spacingScale (replaces unshipped interfaceFontFamily/codeFontFamily/spacingDensity) Usage tab - GitHub Copilot card: premium credits used/total, % bar, reset date, plan, last updated, refresh. Shown only when a Copilot profile exists - Host reads GET api.github.com/copilot_internal/user (same source as Copilot Chat's usage panel) with VS Code's existing GitHub session, silent (never prompts); 60s cache, coalesced requests, 15s timeout; 401 -> signed out, 403/404 -> no Copilot - Parser unit-tested against the recorded response shape --- packages/types/src/global-settings.ts | 17 +- packages/types/src/providers/vscode-llm.ts | 26 +++ packages/types/src/vscode-extension-host.ts | 12 +- src/core/webview/ClineProvider.ts | 21 +- src/core/webview/webviewMessageHandler.ts | 9 + .../__tests__/copilotQuota.spec.ts | 81 +++++++ src/services/language-models/copilotQuota.ts | 145 ++++++++++++ .../src/components/chat/TerminalOutput.tsx | 2 +- .../src/components/common/CodeBlock.tsx | 2 +- .../src/components/common/MarkdownBlock.tsx | 6 +- .../settings/AppearanceControls.tsx | 219 ------------------ .../components/settings/CopilotQuotaCard.tsx | 138 +++++++++++ .../src/components/settings/NumberStepper.tsx | 115 +++++++++ .../src/components/settings/SettingsView.tsx | 21 +- .../src/components/settings/UISettings.tsx | 125 ++++------ .../src/components/settings/UsageSettings.tsx | 9 + .../__tests__/AppearanceControls.spec.tsx | 29 --- .../src/context/ExtensionStateContext.tsx | 39 ++-- webview-ui/src/i18n/locales/ca/settings.json | 62 ++--- webview-ui/src/i18n/locales/de/settings.json | 62 ++--- webview-ui/src/i18n/locales/en/settings.json | 62 ++--- webview-ui/src/i18n/locales/es/settings.json | 62 ++--- webview-ui/src/i18n/locales/fr/settings.json | 62 ++--- webview-ui/src/i18n/locales/hi/settings.json | 62 ++--- webview-ui/src/i18n/locales/id/settings.json | 62 ++--- webview-ui/src/i18n/locales/it/settings.json | 62 ++--- webview-ui/src/i18n/locales/ja/settings.json | 62 ++--- webview-ui/src/i18n/locales/ko/settings.json | 62 ++--- webview-ui/src/i18n/locales/nl/settings.json | 62 ++--- webview-ui/src/i18n/locales/pl/settings.json | 62 ++--- .../src/i18n/locales/pt-BR/settings.json | 62 ++--- webview-ui/src/i18n/locales/ru/settings.json | 62 ++--- webview-ui/src/i18n/locales/tr/settings.json | 62 ++--- webview-ui/src/i18n/locales/vi/settings.json | 62 ++--- .../src/i18n/locales/zh-CN/settings.json | 62 ++--- .../src/i18n/locales/zh-TW/settings.json | 62 ++--- webview-ui/src/index.css | 38 +-- .../src/utils/__tests__/appearance.spec.ts | 82 +++---- webview-ui/src/utils/appearance.ts | 108 ++++++--- 39 files changed, 1340 insertions(+), 1020 deletions(-) create mode 100644 src/services/language-models/__tests__/copilotQuota.spec.ts create mode 100644 src/services/language-models/copilotQuota.ts delete mode 100644 webview-ui/src/components/settings/AppearanceControls.tsx create mode 100644 webview-ui/src/components/settings/CopilotQuotaCard.tsx create mode 100644 webview-ui/src/components/settings/NumberStepper.tsx delete mode 100644 webview-ui/src/components/settings/__tests__/AppearanceControls.spec.tsx diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 6c26024d0e..0b555f3927 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -281,18 +281,21 @@ export const globalSettingsSchema = z.object({ */ chatFontSize: z.number().int().min(8).max(32).nullish(), /** - * CSS font-family for Zoo Code's chat prose. `null`/unset inherits `--vscode-font-family`. + * Scale (percent) applied to Zoo Code's interface text. `null`/unset = 100. */ - interfaceFontFamily: z.string().max(256).nullish(), + interfaceFontScale: z.number().int().min(50).max(200).nullish(), /** - * CSS font-family for inline/fenced code in chat. `null`/unset inherits `--vscode-editor-font-family`. + * Font size (px) for command/terminal output shown in chat. `null`/unset inherits. */ - codeFontFamily: z.string().max(256).nullish(), + terminalFontSize: z.number().int().min(8).max(32).nullish(), /** - * Vertical/horizontal spacing scale for chat rows. - * @default "comfortable" + * Font size (px) for code blocks and diffs shown in chat. `null`/unset inherits. */ - spacingDensity: z.enum(["compact", "comfortable", "spacious"]).optional(), + 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" diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index b6ba122d53..a97905245b 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -11,8 +11,34 @@ export const vsCodeLmModelsMessageTypes = [ "githubCopilotManageAccount", "githubCopilotSignInResult", "githubCopilotModels", + "requestGithubCopilotQuota", + "githubCopilotQuota", ] as const +/** + * The signed-in account's GitHub Copilot premium-request allowance, as reported by + * `GET /copilot_internal/user` (the same source Copilot Chat's own usage panel reads). + */ +export interface GitHubCopilotQuota { + /** e.g. "business", "individual". */ + plan?: string + unlimited: boolean + /** Total credits for the period; undefined when unlimited or not reported. */ + entitlement?: number + /** Credits used this period, `entitlement - quota_remaining` (one decimal, like GitHub's panel). */ + used?: number + /** 0–100. */ + usedPercent?: number + /** Epoch ms when the allowance resets. */ + resetAt?: number + /** Epoch ms when this snapshot was fetched. */ + fetchedAt: number +} + +export type GitHubCopilotQuotaResult = + | { ok: true; quota: GitHubCopilotQuota } + | { ok: false; reason: "signedOut" | "noCopilot" | "error"; message?: string } + export const vsCodeLmModelsMessageTypeSchema = z.enum(vsCodeLmModelsMessageTypes) export const VsCodeLmModelsMessageType = vsCodeLmModelsMessageTypeSchema.enum diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 70047fc63a..c3ed3773a2 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -16,7 +16,7 @@ import { RouterModelsMessageType, type ModelInfo, type ModelRecord, type RouterM import { LmStudioModelsMessageType } from "./providers/lm-studio.js" import { OllamaModelsMessageType } from "./providers/ollama.js" import { OpenAiModelsMessageType } from "./providers/openai.js" -import { VsCodeLmModelsMessageType } from "./providers/vscode-llm.js" +import { VsCodeLmModelsMessageType, type GitHubCopilotQuotaResult } from "./providers/vscode-llm.js" import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js" import type { SkillMetadata } from "./skills.js" import type { RuleMetadata } from "./rules.js" @@ -50,6 +50,7 @@ export interface ExtensionMessage { | typeof VsCodeLmModelsMessageType.vsCodeLmModels | typeof VsCodeLmModelsMessageType.githubCopilotModels | typeof VsCodeLmModelsMessageType.githubCopilotSignInResult + | typeof VsCodeLmModelsMessageType.githubCopilotQuota | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" @@ -158,6 +159,7 @@ export interface ExtensionMessage { modelInfo?: ModelInfo }[] githubCopilotAccount?: string | null + githubCopilotQuota?: GitHubCopilotQuotaResult mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] @@ -339,9 +341,10 @@ export type ExtensionState = Pick< | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" | "chatFontSize" - | "interfaceFontFamily" - | "codeFontFamily" - | "spacingDensity" + | "interfaceFontScale" + | "terminalFontSize" + | "editorFontSize" + | "spacingScale" | "timeFormat" | "enterBehavior" | "includeCurrentTime" @@ -520,6 +523,7 @@ export interface WebviewMessage { | typeof VsCodeLmModelsMessageType.githubCopilotSignIn | typeof VsCodeLmModelsMessageType.githubCopilotReconnect | typeof VsCodeLmModelsMessageType.githubCopilotManageAccount + | typeof VsCodeLmModelsMessageType.requestGithubCopilotQuota | "openImage" | "saveImage" | "openFile" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05b1b41ae7..32c40583f0 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2810,9 +2810,10 @@ export class ClineProvider historyPreviewCollapsed, reasoningBlockCollapsed, chatFontSize, - interfaceFontFamily, - codeFontFamily, - spacingDensity, + interfaceFontScale, + terminalFontSize, + editorFontSize, + spacingScale, timeFormat, enterBehavior, cloudUserInfo, @@ -2995,9 +2996,10 @@ export class ClineProvider historyPreviewCollapsed: historyPreviewCollapsed ?? false, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize, - interfaceFontFamily, - codeFontFamily, - spacingDensity: spacingDensity ?? "comfortable", + interfaceFontScale, + terminalFontSize, + editorFontSize, + spacingScale, timeFormat: timeFormat ?? "auto", enterBehavior: enterBehavior ?? "send", cloudUserInfo, @@ -3232,9 +3234,10 @@ export class ClineProvider historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, chatFontSize: stateValues.chatFontSize, - interfaceFontFamily: stateValues.interfaceFontFamily, - codeFontFamily: stateValues.codeFontFamily, - spacingDensity: stateValues.spacingDensity ?? "comfortable", + interfaceFontScale: stateValues.interfaceFontScale, + terminalFontSize: stateValues.terminalFontSize, + editorFontSize: stateValues.editorFontSize, + spacingScale: stateValues.spacingScale, timeFormat: stateValues.timeFormat ?? "auto", enterBehavior: stateValues.enterBehavior ?? "send", cloudUserInfo, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index bd7c589958..4eeea8d434 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1570,6 +1570,15 @@ export const webviewMessageHandler = async ( } break } + case VsCodeLmModelsMessageType.requestGithubCopilotQuota: { + const { getGitHubCopilotQuota } = await import("../../services/language-models/copilotQuota") + const githubCopilotQuota = await getGitHubCopilotQuota({ force: message.bool === true }) + await provider.postMessageToWebview({ + type: VsCodeLmModelsMessageType.githubCopilotQuota, + githubCopilotQuota, + }) + break + } case VsCodeLmModelsMessageType.githubCopilotManageAccount: try { await openGitHubAccountManagement() diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts new file mode 100644 index 0000000000..ea37c04f02 --- /dev/null +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from "vitest" + +vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) + +import { parseCopilotQuota } from "../copilotQuota" + +const NOW = 1_800_000_000_000 + +// Shape recorded from GET api.github.com/copilot_internal/user (business plan). +const businessPayload = { + copilot_plan: "business", + quota_reset_date: "2026-11-01", + quota_reset_date_utc: "2026-11-01T00:00:00.000Z", + quota_snapshots: { + chat: { unlimited: true, entitlement: 0, remaining: 0 }, + completions: { unlimited: true, entitlement: 0, remaining: 0 }, + premium_interactions: { + entitlement: 140000, + remaining: 100610, + quota_remaining: 100610.7, + unlimited: false, + percent_remaining: 71.8, + }, + }, +} + +describe("parseCopilotQuota", () => { + it("derives used credits from the precise quota_remaining, like GitHub's panel", () => { + const q = parseCopilotQuota(businessPayload, NOW)! + expect(q.plan).toBe("business") + expect(q.unlimited).toBe(false) + expect(q.entitlement).toBe(140000) + expect(q.used).toBe(39389.3) + expect(q.usedPercent).toBeCloseTo(28.135, 2) + expect(q.resetAt).toBe(Date.parse("2026-11-01T00:00:00.000Z")) + expect(q.fetchedAt).toBe(NOW) + }) + + it("falls back to the integer remaining twin", () => { + const payload = structuredClone(businessPayload) + delete (payload.quota_snapshots.premium_interactions as Record).quota_remaining + expect(parseCopilotQuota(payload, NOW)!.used).toBe(39390) + }) + + it("falls back to percent_remaining when counts are missing", () => { + const q = parseCopilotQuota( + { quota_snapshots: { premium_interactions: { unlimited: false, percent_remaining: 40 } } }, + NOW, + )! + expect(q.used).toBeUndefined() + expect(q.entitlement).toBeUndefined() + expect(q.usedPercent).toBe(60) + }) + + it("reports unlimited plans without numbers", () => { + const q = parseCopilotQuota( + { copilot_plan: "enterprise", quota_snapshots: { premium_interactions: { unlimited: true } } }, + NOW, + )! + expect(q).toMatchObject({ plan: "enterprise", unlimited: true }) + expect(q.usedPercent).toBeUndefined() + }) + + it("clamps over-consumption to 100%", () => { + const q = parseCopilotQuota( + { quota_snapshots: { premium_interactions: { entitlement: 300, quota_remaining: -12 } } }, + NOW, + )! + expect(q.used).toBe(300) + expect(q.usedPercent).toBe(100) + }) + + it("uses the date-only reset field when the UTC variant is absent", () => { + const payload = { ...businessPayload, quota_reset_date_utc: undefined } + expect(parseCopilotQuota(payload, NOW)!.resetAt).toBe(Date.parse("2026-11-01")) + }) + + it.each([null, undefined, "nope", [], {}, { quota_snapshots: {} }])("returns undefined for %j", (payload) => { + expect(parseCopilotQuota(payload, NOW)).toBeUndefined() + }) +}) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts new file mode 100644 index 0000000000..845aed6e62 --- /dev/null +++ b/src/services/language-models/copilotQuota.ts @@ -0,0 +1,145 @@ +import * as vscode from "vscode" + +import { githubCopilotLanguageModel, type GitHubCopilotQuota, type GitHubCopilotQuotaResult } from "@roo-code/types" + +/** + * GitHub Copilot premium-request allowance for the Usage tab. + * + * Source: `GET https://api.github.com/copilot_internal/user`, the endpoint + * Copilot Chat's own usage panel reads. It returns + * `quota_snapshots.premium_interactions` plus `quota_reset_date`. + * + * Auth reuses the GitHub session VS Code already holds for Copilot (silent: + * never prompts). The token is only ever sent to api.github.com. + */ + +const COPILOT_USER_URL = "https://api.github.com/copilot_internal/user" +const REQUEST_TIMEOUT_MS = 15_000 +/** Avoid hammering the endpoint when the Usage tab is reopened repeatedly. */ +const CACHE_TTL_MS = 60_000 + +const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + return undefined +} + +const asRecord = (value: unknown): Record | undefined => + value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined + +/** One decimal, matching GitHub's panel ("38,903.9 / 140,000"). */ +const roundCredits = (value: number) => Math.round(value * 10) / 10 + +/** + * Parses the endpoint payload. Pure, so it is unit-tested against recorded + * shapes. Returns undefined when the payload has no premium snapshot at all. + * + * Semantics mirror Copilot Chat (CopilotUserQuotaInfo): + * - `quota_remaining` is the precise (fractional) twin of `remaining`; prefer it. + * - used = entitlement - remaining, so the figure matches GitHub's own panel. + * - `percent_remaining` is a server-computed fallback when counts are missing. + */ +export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCopilotQuota | undefined { + const root = asRecord(payload) + const snapshot = asRecord(asRecord(root?.quota_snapshots)?.premium_interactions) + if (!root || !snapshot) return undefined + + const plan = typeof root.copilot_plan === "string" ? root.copilot_plan : undefined + const resetRaw = root.quota_reset_date_utc ?? root.quota_reset_date + const resetAt = + typeof resetRaw === "string" && !Number.isNaN(Date.parse(resetRaw)) ? Date.parse(resetRaw) : undefined + + if (snapshot.unlimited === true) { + return { plan, unlimited: true, resetAt, fetchedAt: now } + } + + const entitlement = toNumber(snapshot.entitlement) + const remaining = toNumber(snapshot.quota_remaining) ?? toNumber(snapshot.remaining) + + let used: number | undefined + let usedPercent: number | undefined + + if (entitlement !== undefined && entitlement > 0 && remaining !== undefined) { + used = roundCredits(Math.min(entitlement, Math.max(0, entitlement - remaining))) + usedPercent = Math.min(100, Math.max(0, (used / entitlement) * 100)) + } else { + const percentRemaining = toNumber(snapshot.percent_remaining) + if (percentRemaining !== undefined) usedPercent = Math.min(100, Math.max(0, 100 - percentRemaining)) + } + + return { + plan, + unlimited: false, + entitlement: entitlement !== undefined && entitlement > 0 ? entitlement : undefined, + used, + usedPercent, + resetAt, + fetchedAt: now, + } +} + +async function getGitHubToken(): Promise { + const { authProviderId, authScopeSets } = githubCopilotLanguageModel + for (const scopes of authScopeSets) { + const session = await vscode.authentication.getSession(authProviderId, scopes, { silent: true }) + if (session) return session.accessToken + } + return undefined +} + +let cache: { at: number; result: GitHubCopilotQuotaResult } | undefined +let inFlight: Promise | undefined + +/** + * Fetches the quota. Concurrent callers share one request; successful results + * are cached briefly unless `force` is set (the Refresh button). + */ +export function getGitHubCopilotQuota( + { force = false }: { force?: boolean } = {}, + fetchImpl: typeof fetch = fetch, +): Promise { + if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) return Promise.resolve(cache.result) + if (inFlight) return inFlight + + inFlight = (async (): Promise => { + try { + const token = await getGitHubToken() + if (!token) return { ok: false, reason: "signedOut" } + + const response = await fetchImpl(COPILOT_USER_URL, { + method: "GET", + headers: { + Authorization: `token ${token}`, + Accept: "application/json", + "Editor-Version": `vscode/${vscode.version}`, + "X-Github-Api-Version": "2025-04-01", + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + + if (response.status === 401) return { ok: false, reason: "signedOut" } + if (response.status === 403 || response.status === 404) return { ok: false, reason: "noCopilot" } + if (!response.ok) return { ok: false, reason: "error", message: `GitHub API error ${response.status}` } + + const quota = parseCopilotQuota(await response.json()) + const result: GitHubCopilotQuotaResult = quota ? { ok: true, quota } : { ok: false, reason: "noCopilot" } + if (result.ok) cache = { at: Date.now(), result } + return result + } catch (error) { + return { ok: false, reason: "error", message: error instanceof Error ? error.message : String(error) } + } finally { + inFlight = undefined + } + })() + + return inFlight +} + +/** @internal test hook */ +export function resetGitHubCopilotQuotaCache() { + cache = undefined + inFlight = undefined +} diff --git a/webview-ui/src/components/chat/TerminalOutput.tsx b/webview-ui/src/components/chat/TerminalOutput.tsx index 78684e3753..80a4870a5f 100644 --- a/webview-ui/src/components/chat/TerminalOutput.tsx +++ b/webview-ui/src/components/chat/TerminalOutput.tsx @@ -59,7 +59,7 @@ export const TerminalOutput: React.FC = ({ content, classNa style={{ fontFamily: "var(--vscode-editor-font-family, 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Menlo', 'Monaco', 'Courier New', monospace)", - fontSize: "var(--vscode-editor-font-size, 13px)", + fontSize: "var(--zoo-terminal-font-size, var(--vscode-editor-font-size, 13px))", lineHeight: "var(--vscode-editor-line-height, 1.4)", whiteSpace: "pre-wrap", wordBreak: "break-word", diff --git a/webview-ui/src/components/common/CodeBlock.tsx b/webview-ui/src/components/common/CodeBlock.tsx index c9c5d85aa0..c1d07e6bc2 100644 --- a/webview-ui/src/components/common/CodeBlock.tsx +++ b/webview-ui/src/components/common/CodeBlock.tsx @@ -150,7 +150,7 @@ export const StyledPre = styled.div<{ white-space: ${({ wordwrap }) => (wordwrap === "false" ? "pre" : "pre-wrap")}; word-break: ${({ wordwrap }) => (wordwrap === "false" ? "normal" : "normal")}; overflow-wrap: ${({ wordwrap }) => (wordwrap === "false" ? "normal" : "break-word")}; - font-size: 0.95em; + font-size: var(--zoo-editor-font-size, 0.95em); font-family: var(--vscode-editor-font-family); } diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index b69f9af3ac..dfc51a7737 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -44,7 +44,7 @@ const StyledMarkdown = styled.div` } code:not(pre > code) { - font-family: var(--zoo-code-font-family, var(--vscode-editor-font-family, monospace)); + font-family: var(--vscode-editor-font-family, monospace); font-size: 0.85em; filter: saturation(110%) brightness(95%); color: var(--vscode-textPreformat-foreground) !important; @@ -89,7 +89,7 @@ const StyledMarkdown = styled.div` } font-family: - var(--zoo-chat-font-family, var(--vscode-font-family)), + var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, @@ -102,7 +102,7 @@ const StyledMarkdown = styled.div` "Helvetica Neue", sans-serif; - font-size: var(--zoo-chat-font-size, var(--vscode-font-size, 13px)); + font-size: calc(var(--zoo-chat-font-size, var(--vscode-font-size, 13px)) * var(--zoo-ui-scale, 1)); p, li, diff --git a/webview-ui/src/components/settings/AppearanceControls.tsx b/webview-ui/src/components/settings/AppearanceControls.tsx deleted file mode 100644 index c22c1a29fa..0000000000 --- a/webview-ui/src/components/settings/AppearanceControls.tsx +++ /dev/null @@ -1,219 +0,0 @@ -import { useEffect, useState } from "react" - -import { useAppTranslation } from "@/i18n/TranslationContext" -import { cn } from "@/lib/utils" -import { sanitizeFontFamily, type SpacingDensity } from "@/utils/appearance" - -import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" - -/** - * Curated presets. Each stack ends in a generic family so an uninstalled font - * degrades gracefully. `DEFAULT` = no override (inherit from VS Code). - */ -const DEFAULT = "__default__" -const CUSTOM = "__custom__" - -export const INTERFACE_FONT_PRESETS = [ - { id: "system", label: "System UI", stack: "system-ui, -apple-system, 'Segoe UI', sans-serif" }, - { id: "inter", label: "Inter", stack: "Inter, system-ui, sans-serif" }, - { id: "sf", label: "SF Pro", stack: "'SF Pro Text', -apple-system, sans-serif" }, - { id: "segoe", label: "Segoe UI", stack: "'Segoe UI', system-ui, sans-serif" }, - { id: "roboto", label: "Roboto", stack: "Roboto, system-ui, sans-serif" }, - { id: "helvetica", label: "Helvetica Neue", stack: "'Helvetica Neue', Helvetica, Arial, sans-serif" }, - { id: "georgia", label: "Georgia (serif)", stack: "Georgia, 'Times New Roman', serif" }, -] as const - -export const CODE_FONT_PRESETS = [ - { id: "sfmono", label: "SF Mono", stack: "'SF Mono', SFMono-Regular, Menlo, monospace" }, - { id: "menlo", label: "Menlo", stack: "Menlo, Monaco, monospace" }, - { id: "jetbrains", label: "JetBrains Mono", stack: "'JetBrains Mono', Menlo, monospace" }, - { id: "fira", label: "Fira Code", stack: "'Fira Code', Menlo, monospace" }, - { id: "cascadia", label: "Cascadia Code", stack: "'Cascadia Code', Consolas, monospace" }, - { id: "consolas", label: "Consolas", stack: "Consolas, 'Courier New', monospace" }, -] as const - -type Preset = { id: string; label: string; stack: string } - -/** Maps a stored stack back to its preset id, "custom", or the default. */ -export function presetIdFor(value: string | undefined, presets: readonly Preset[]): string { - if (!value) return DEFAULT - return presets.find((p) => p.stack === value)?.id ?? CUSTOM -} - -interface FontPresetSelectProps { - label: string - description: string - /** Label for the "inherit" option, e.g. "Match VS Code". */ - defaultLabel: string - customLabel: string - customPlaceholder: string - invalidMessage: string - presets: readonly Preset[] - value?: string - onChange: (value: string | undefined) => void - testId: string -} - -/** - * Font picker: inherit / curated presets / custom. Each option renders in its - * own face so users see the font before choosing. The free-text input only - * appears for "Custom…", and only sanitized values are committed. - */ -export const FontPresetSelect = ({ - label, - description, - defaultLabel, - customLabel, - customPlaceholder, - invalidMessage, - presets, - value, - onChange, - testId, -}: FontPresetSelectProps) => { - const [selected, setSelected] = useState(() => presetIdFor(value, presets)) - const [draft, setDraft] = useState(selected === CUSTOM ? (value ?? "") : "") - - // Re-sync when the persisted value changes underneath us (reset/import), - // but don't kick the user out of "Custom…" while they're typing. - useEffect(() => { - const next = presetIdFor(value, presets) - if (next !== CUSTOM || selected !== CUSTOM) setSelected(next) - if (next === CUSTOM) setDraft(value ?? "") - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, presets]) - - const handleSelect = (id: string) => { - setSelected(id) - if (id === DEFAULT) onChange(undefined) - else if (id !== CUSTOM) onChange(presets.find((p) => p.id === id)?.stack) - // Custom: wait for valid input before committing. - } - - const trimmed = draft.trim() - const isInvalid = selected === CUSTOM && trimmed.length > 0 && sanitizeFontFamily(trimmed) === undefined - - const handleDraft = (next: string) => { - setDraft(next) - const safe = sanitizeFontFamily(next) - if (!next.trim()) onChange(undefined) - else if (safe) onChange(safe) - } - - return ( -
- - - - {selected === CUSTOM && ( - handleDraft(e.target.value)} - data-testid={`${testId}-custom-input`} - /> - )} - -
- {isInvalid ? invalidMessage : description} -
-
- ) -} - -const DENSITY_PADDING: Record = { - compact: { py: 5, px: 10 }, - comfortable: { py: 10, px: 15 }, - spacious: { py: 16, px: 18 }, -} - -interface AppearancePreviewProps { - interfaceFontFamily?: string - codeFontFamily?: string - chatFontSize?: number - spacingDensity: SpacingDensity -} - -/** - * Live preview of chat appearance using the *unsaved* settings, applied - * inline so it updates instantly without touching the real chat. - */ -export const AppearancePreview = ({ - interfaceFontFamily, - codeFontFamily, - chatFontSize, - spacingDensity, -}: AppearancePreviewProps) => { - const { t } = useAppTranslation() - const { py, px } = DENSITY_PADDING[spacingDensity] - - const rowStyle = { padding: `${py}px ${px}px` } - const textStyle = { - fontFamily: sanitizeFontFamily(interfaceFontFamily) ?? "var(--vscode-font-family)", - fontSize: chatFontSize ? `${chatFontSize}px` : "var(--vscode-font-size)", - } - const codeStyle = { - fontFamily: sanitizeFontFamily(codeFontFamily) ?? "var(--vscode-editor-font-family, monospace)", - fontSize: "0.85em", - } - - return ( -
-
- {t("settings:ui.preview.label")} -
-
-
- {t("settings:ui.preview.you")} -
-
{t("settings:ui.preview.userMessage")}
-
-
-
- {t("settings:ui.preview.zoo")} -
-
- {t("settings:ui.preview.assistantBefore")}{" "} - - Edge.tsx - {" "} - {t("settings:ui.preview.assistantAfter")} -
-
-					{"const stroke = selected ? 2 : 1"}
-				
-
-
- ) -} diff --git a/webview-ui/src/components/settings/CopilotQuotaCard.tsx b/webview-ui/src/components/settings/CopilotQuotaCard.tsx new file mode 100644 index 0000000000..9860940a12 --- /dev/null +++ b/webview-ui/src/components/settings/CopilotQuotaCard.tsx @@ -0,0 +1,138 @@ +import { useCallback, useEffect, useState } from "react" +import { RefreshCw } from "lucide-react" + +import { VsCodeLmModelsMessageType, type GitHubCopilotQuotaResult } from "@roo-code/types" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@src/lib/utils" +import { vscode } from "@src/utils/vscode" + +const formatCredits = (value: number) => + value.toLocaleString(undefined, { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) + +const formatReset = (ts: number) => + new Date(ts).toLocaleString(undefined, { + weekday: "short", + month: "short", + day: "numeric", + hour: "numeric", + minute: "2-digit", + }) + +/** + * GitHub Copilot premium-request credits (used / total, % bar, reset date), + * fetched by the extension host from the same endpoint Copilot's own usage + * panel reads. Only rendered when the user has a Copilot profile. + */ +export const CopilotQuotaCard = () => { + const { t } = useAppTranslation() + const [result, setResult] = useState() + const [loading, setLoading] = useState(true) + + const request = useCallback((force: boolean) => { + setLoading(true) + vscode.postMessage({ type: VsCodeLmModelsMessageType.requestGithubCopilotQuota, bool: force }) + }, []) + + useEffect(() => { + const onMessage = (event: MessageEvent) => { + const message = event.data + if (message?.type === VsCodeLmModelsMessageType.githubCopilotQuota) { + setResult(message.githubCopilotQuota) + setLoading(false) + } + } + window.addEventListener("message", onMessage) + request(false) + return () => window.removeEventListener("message", onMessage) + }, [request]) + + const quota = result?.ok ? result.quota : undefined + const pct = quota?.usedPercent ?? 0 + const barColor = + pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" + + return ( +
+
+
+ + {t("settings:usage.copilot.title")} + {quota?.plan && ( + · {quota.plan} + )} +
+ +
+ + {loading && !result && ( +
{t("settings:usage.copilot.loading")}
+ )} + + {result && !result.ok && ( +
+ {t(`settings:usage.copilot.${result.reason}`)} + {result.reason === "error" && result.message ? ` (${result.message})` : ""} +
+ )} + + {quota && quota.unlimited &&
{t("settings:usage.copilot.unlimited")}
} + + {quota && !quota.unlimited && ( + <> +
+ {t("settings:usage.copilot.credits")} + + {quota.used !== undefined && quota.entitlement !== undefined + ? t("settings:usage.copilot.usedOf", { + used: formatCredits(quota.used), + total: formatCredits(quota.entitlement), + }) + : `${Math.round(pct)}%`} + +
+
+
+
+
+ + {quota.resetAt + ? t("settings:usage.copilot.resets", { date: formatReset(quota.resetAt) }) + : ""} + + + {t("settings:usage.copilot.percentUsed", { percent: Math.round(pct) })} + +
+ + )} +
+ ) +} diff --git a/webview-ui/src/components/settings/NumberStepper.tsx b/webview-ui/src/components/settings/NumberStepper.tsx new file mode 100644 index 0000000000..ac2b7a922f --- /dev/null +++ b/webview-ui/src/components/settings/NumberStepper.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react" +import { Minus, Plus, RotateCcw } from "lucide-react" + +import { cn } from "@/lib/utils" +import { type AppearanceSetting } from "@/utils/appearance" + +interface NumberStepperProps { + label: string + description: string + resetLabel: string + setting: AppearanceSetting + /** Undefined = unset (shows `setting.fallback`, reset disabled). */ + value?: number + onChange: (value: number | undefined) => void + testId: string +} + +/** + * `[ − ] [ value ] [ + ] unit ↺` + * Typing is allowed; the value commits (clamped) on blur/Enter so partial + * input like "1" on the way to "120" is never saved or applied. + */ +export const NumberStepper = ({ + label, + description, + resetLabel, + setting, + value, + onChange, + testId, +}: NumberStepperProps) => { + const { min, max, step, fallback, unit } = setting + const current = value ?? fallback + const [draft, setDraft] = useState(String(current)) + + useEffect(() => setDraft(String(current)), [current]) + + const commit = (next: number) => { + if (!Number.isFinite(next)) { + setDraft(String(current)) + return + } + const clamped = Math.min(max, Math.max(min, Math.round(next))) + setDraft(String(clamped)) + // Committing the default clears the override so VS Code changes still flow through. + onChange(clamped === fallback ? undefined : clamped) + } + + const buttonClass = + "flex items-center justify-center w-8 self-stretch bg-vscode-input-background text-vscode-foreground hover:bg-vscode-list-hoverBackground disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer" + + return ( +
+ +
+
+ + setDraft(e.target.value.replace(/[^\d]/g, ""))} + onBlur={() => commit(Number(draft))} + onKeyDown={(e) => { + if (e.key === "Enter") commit(Number(draft)) + if (e.key === "ArrowUp") { + e.preventDefault() + commit(current + step) + } + if (e.key === "ArrowDown") { + e.preventDefault() + commit(current - step) + } + }} + data-testid={`${testId}-input`} + /> + +
+ {unit} + +
+
{description}
+
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 8ab8f0d3f2..129bde5cc6 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -217,9 +217,10 @@ const SettingsView = forwardRef(({ onDone, t openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, chatFontSize, - interfaceFontFamily, - codeFontFamily, - spacingDensity, + interfaceFontScale, + terminalFontSize, + editorFontSize, + spacingScale, timeFormat, enterBehavior, includeCurrentTime, @@ -450,9 +451,10 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize: chatFontSize ?? null, // null (not undefined) so a reset survives JSON and clears the stored value - interfaceFontFamily: interfaceFontFamily ?? null, - codeFontFamily: codeFontFamily ?? null, - spacingDensity: spacingDensity ?? "comfortable", + interfaceFontScale: interfaceFontScale ?? null, + terminalFontSize: terminalFontSize ?? null, + editorFontSize: editorFontSize ?? null, + spacingScale: spacingScale ?? null, timeFormat: timeFormat ?? "auto", enterBehavior: enterBehavior ?? "send", includeCurrentTime: includeCurrentTime ?? true, @@ -957,9 +959,10 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed={reasoningBlockCollapsed ?? true} enterBehavior={enterBehavior ?? "send"} chatFontSize={chatFontSize ?? undefined} - interfaceFontFamily={interfaceFontFamily ?? undefined} - codeFontFamily={codeFontFamily ?? undefined} - spacingDensity={spacingDensity} + interfaceFontScale={interfaceFontScale ?? undefined} + terminalFontSize={terminalFontSize ?? undefined} + editorFontSize={editorFontSize ?? undefined} + spacingScale={spacingScale ?? undefined} autoCloseZooOpenedFiles={autoCloseZooOpenedFiles} autoCloseZooOpenedFilesAfterUserEdited={autoCloseZooOpenedFilesAfterUserEdited} autoCloseZooOpenedNewFiles={autoCloseZooOpenedNewFiles} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 1470e6f81d..ce57b12e98 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -12,10 +12,12 @@ import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" -import { Slider, Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui" +import { Slider, Button } from "../ui" import { ExtensionStateContextType } from "@/context/ExtensionStateContext" -import { DEFAULT_SPACING_DENSITY, SPACING_DENSITIES, type SpacingDensity } from "@/utils/appearance" -import { AppearancePreview, CODE_FONT_PRESETS, FontPresetSelect, INTERFACE_FONT_PRESETS } from "./AppearanceControls" +import { APPEARANCE_SETTINGS, type AppearanceKey } from "@/utils/appearance" +import { NumberStepper } from "./NumberStepper" + +const APPEARANCE_KEYS = Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[] export const CHAT_FONT_SIZE_MIN = 8 export const CHAT_FONT_SIZE_MAX = 32 @@ -25,9 +27,10 @@ interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" chatFontSize?: number - interfaceFontFamily?: string - codeFontFamily?: string - spacingDensity?: SpacingDensity + interfaceFontScale?: number + terminalFontSize?: number + editorFontSize?: number + spacingScale?: number autoCloseZooOpenedFiles?: boolean autoCloseZooOpenedFilesAfterUserEdited?: boolean autoCloseZooOpenedNewFiles?: boolean @@ -38,9 +41,10 @@ export const UISettings = ({ reasoningBlockCollapsed, enterBehavior, chatFontSize, - interfaceFontFamily, - codeFontFamily, - spacingDensity, + interfaceFontScale, + terminalFontSize, + editorFontSize, + spacingScale, autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, autoCloseZooOpenedNewFiles, @@ -49,6 +53,13 @@ export const UISettings = ({ }: UISettingsProps) => { const { t } = useAppTranslation() + const appearance: Record = { + interfaceFontScale, + terminalFontSize, + editorFontSize, + spacingScale, + } + // Detect platform for dynamic modifier key display const primaryMod = useMemo(() => { const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 @@ -168,78 +179,30 @@ export const UISettings = ({
- {/* Interface (chat prose) font */} - - setCachedStateField("interfaceFontFamily", value)} - testId="interface-font" - /> - - - {/* Code font */} - - setCachedStateField("codeFontFamily", value)} - testId="code-font" - /> - - - {/* Spacing density */} - -
- - -
- {t("settings:ui.spacingDensity.description")} -
-
-
- - {/* Live preview of the appearance settings above (unsaved values) */} - + {/* Appearance sizes: interface %, terminal px, editor px, spacing % */} + {APPEARANCE_KEYS.map((key) => ( + + { + setCachedStateField(key, value) + telemetryClient.capture("ui_settings_appearance_changed", { + key, + value: value ?? null, + }) + }} + testId={`appearance-${key}`} + /> + + ))} {/* Auto-close Zoo opened files */} [p.value, p.label])) const providerLabel = (id: string) => PROVIDER_LABELS.get(id) ?? id @@ -68,6 +71,10 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes p.apiProvider === providerIdentifiers.githubCopilot) + const profileSubtitle = (name: string) => { const meta = profileMeta.get(name) if (!meta?.apiProvider) return t("settings:usage.unknownProvider") @@ -98,6 +105,8 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes{t("settings:sections.usage")}
+ {hasCopilotProfile && } + {/* Segmented range control */}
{ - it.each([...INTERFACE_FONT_PRESETS, ...CODE_FONT_PRESETS])("$label stack passes sanitization", (preset) => { - // A preset that the sanitizer rejects would silently fall back to inherit. - expect(sanitizeFontFamily(preset.stack)).toBe(preset.stack) - }) - - it("has unique ids", () => { - const ids = [...INTERFACE_FONT_PRESETS, ...CODE_FONT_PRESETS].map((p) => p.id) - expect(new Set(ids).size).toBe(ids.length) - }) -}) - -describe("presetIdFor", () => { - it("maps empty to the default option", () => { - expect(presetIdFor(undefined, INTERFACE_FONT_PRESETS)).toBe("__default__") - expect(presetIdFor("", INTERFACE_FONT_PRESETS)).toBe("__default__") - }) - - it("maps a known stack back to its preset", () => { - expect(presetIdFor(CODE_FONT_PRESETS[2].stack, CODE_FONT_PRESETS)).toBe(CODE_FONT_PRESETS[2].id) - }) - - it("treats anything else as custom", () => { - expect(presetIdFor("Comic Sans MS", INTERFACE_FONT_PRESETS)).toBe("__custom__") - }) -}) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 7590752d2f..4211f7ba8e 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -34,7 +34,7 @@ import { CustomSupportPrompts } from "@roo/support-prompt" import { experimentDefault } from "@roo/experiments" import { vscode } from "@src/utils/vscode" -import { applyFontVariable } from "@src/utils/appearance" +import { applyAppearanceVariables } from "@src/utils/appearance" import { convertTextMateToHljs } from "@src/utils/textMateToHljs" export interface ExtensionStateContextType extends ExtensionState { @@ -133,9 +133,10 @@ export interface ExtensionStateContextType extends ExtensionState { setReasoningBlockCollapsed: (value: boolean) => void chatFontSize?: number setChatFontSize: (value: number | undefined) => void - interfaceFontFamily?: string - codeFontFamily?: string - spacingDensity?: "compact" | "comfortable" | "spacious" + interfaceFontScale?: number + terminalFontSize?: number + editorFontSize?: number + spacingScale?: number timeFormat?: "auto" | "12h" | "24h" enterBehavior?: "send" | "newline" setEnterBehavior: (value: "send" | "newline") => void @@ -538,30 +539,26 @@ export const ExtensionStateContextProvider: React.FC<{ } }, [state.chatFontSize]) - // Chat prose / code font overrides. Removing the inline property lets the - // stylesheet defaults (VS Code's own font variables) take over again. + // Appearance scales/sizes. Unset values remove the inline override so the + // stylesheet defaults in index.css (100% / VS Code's sizes) apply. useEffect(() => { - applyFontVariable("--zoo-chat-font-family", state.interfaceFontFamily) - }, [state.interfaceFontFamily]) - - useEffect(() => { - applyFontVariable("--zoo-code-font-family", state.codeFontFamily) - }, [state.codeFontFamily]) - - // Spacing density is expressed as a data attribute so all spacing tokens - // live in CSS (index.css) rather than being scattered across components. - useEffect(() => { - document.documentElement.dataset.zooDensity = state.spacingDensity ?? "comfortable" - }, [state.spacingDensity]) + applyAppearanceVariables(document.documentElement, { + interfaceFontScale: state.interfaceFontScale, + terminalFontSize: state.terminalFontSize, + editorFontSize: state.editorFontSize, + spacingScale: state.spacingScale, + }) + }, [state.interfaceFontScale, state.terminalFontSize, state.editorFontSize, state.spacingScale]) const contextValue: ExtensionStateContextType = { ...state, // `chatFontSize` is persisted as nullish (null on reset); normalize null to // undefined so it matches the context type and means "use VS Code default". chatFontSize: state.chatFontSize ?? undefined, - interfaceFontFamily: state.interfaceFontFamily ?? undefined, - codeFontFamily: state.codeFontFamily ?? undefined, - spacingDensity: state.spacingDensity ?? "comfortable", + interfaceFontScale: state.interfaceFontScale ?? undefined, + terminalFontSize: state.terminalFontSize ?? undefined, + editorFontSize: state.editorFontSize ?? undefined, + spacingScale: state.spacingScale ?? undefined, timeFormat: state.timeFormat ?? "auto", reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true, didHydrateState, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 492a46b00d..6aeeb7745d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1181,36 +1181,24 @@ "label": "Tanca automaticament els fitxers nous creats", "description": "Quan esta activat, les pestanyes dels fitxers que Zoo ha creat (en lloc de modificar) tambe es tanquen automaticament despres d'acceptar el canvi." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index eb0c4589b4..905075216a 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1181,36 +1181,24 @@ "label": "Neu erstellte Dateien automatisch schließen", "description": "Wenn aktiviert, werden Tabs für Dateien, die Zoo neu erstellt hat (anstatt sie zu bearbeiten), nach dem Akzeptieren der Änderung ebenfalls automatisch geschlossen." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 84df8eb7ea..e3808dcca4 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -244,36 +244,24 @@ "label": "Auto-close newly created files", "description": "When enabled, tabs for files that Zoo created (rather than modified) will also be closed automatically after the edit is accepted." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "prompts": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index d2b3789326..858ed46c7b 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1181,36 +1181,24 @@ "label": "Cerrar automaticamente los archivos nuevos creados", "description": "Cuando esta opcion esta activada, las pestanas de archivos que Zoo ha creado (en lugar de modificar) tambien se cierran automaticamente despues de aceptar la modificacion." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 05cc7879ba..9ea4121d9a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1181,36 +1181,24 @@ "label": "Fermer automatiquement les nouveaux fichiers créés", "description": "Quand cette option est activée, les onglets des fichiers que Zoo a créés (plutôt que modifiés) sont également fermés automatiquement après l'acceptation de la modification." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 2bb59132f0..d5574a4677 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1181,36 +1181,24 @@ "label": "Nai banayi gayi files ko svachalit band karo", "description": "Saksham hone par, Zoo dvara banaayi gayi (sanshodhit nahin) files ki tabs ko bhi parivartan svikaar hone ke baad svachalit roop se band kar diya jaata hai." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 4c0ced4f85..6c064905bf 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1181,36 +1181,24 @@ "label": "Tutup otomatis file baru yang dibuat", "description": "Saat diaktifkan, tab untuk file yang dibuat Zoo (bukan dimodifikasi) juga ditutup secara otomatis setelah perubahan diterima." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4d936a7d3b..8f1fae17ff 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1181,36 +1181,24 @@ "label": "Chiudi automaticamente i nuovi file creati", "description": "Quando abilitata, le schede dei file che Zoo ha creato (invece di modificare) vengono chiuse automaticamente dopo l'accettazione della modifica." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index ba074856a2..af7e4b2f17 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1181,36 +1181,24 @@ "label": "新しく作成されたファイルを自動的に閉じる", "description": "有効にすると、Zooが作成した(変更ではなく)ファイルのタブも、変更を承認した後に自動的に閉じられます。" }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index dd88b1546c..b146cf6b7a 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1181,36 +1181,24 @@ "label": "새로 만든 파일 자동 닫기", "description": "활성화 시, Zoo가 수정한 것이 아니라 새로 만든 파일의 탭도 변경 사항을 승인한 후 자동으로 닫힙니다." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 4e19b86309..e02cff1538 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1181,36 +1181,24 @@ "label": "Automatisch sluiten van nieuw gemaakte bestanden", "description": "Wanneer ingeschakeld, worden tabbladen van bestanden die Zoo heeft aangemaakt (in plaats van gewijzigd) ook automatisch gesloten nadat de wijziging is geaccepteerd." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 7ffaac45d2..318d239ea3 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1181,36 +1181,24 @@ "label": "Automatycznie zamykaj nowo utworzone pliki", "description": "Gdy wlaczone, karty plikow, ktore Zoo utworzylo (zamiast zmodyfikowalo), sa rowniez automatycznie zamykane po zaakceptowaniu zmiany." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index ea67ecca39..99ed836c31 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1181,36 +1181,24 @@ "label": "Fechar automaticamente novos arquivos criados", "description": "Quando ativada, as abas de arquivos que o Zoo criou (em vez de modificar) tambem sao fechadas automaticamente apos aceitar a modificacao." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index d9d6715ccd..092e06ba87 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1181,36 +1181,24 @@ "label": "Автоматически закрывать новые созданные файлы", "description": "Если включено, вкладки файлов, которые Zoo создал (а не изменил), также автоматически закрываются после принятия изменения." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 554c97baba..e01b594736 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1181,36 +1181,24 @@ "label": "Yeni olusturulan dosyalari otomatik kapat", "description": "Etkinlestirildiginde, Zoo'nun olusturdugu (degistirmedigi) dosyalarin sekmeleri de degisiklik kabul edildikten sonra otomatik olarak kapatilir." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 07c4b42dde..fffa278487 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1181,36 +1181,24 @@ "label": "Tự động đóng các tệp mới tạo", "description": "Khi bật, các tab của các tệp mà Zoo đã tạo (thay vì chỉnh sửa) cũng sẽ tự động đóng sau khi chấp nhận thay đổi." }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index e37f9a2c08..5913d175ed 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1181,36 +1181,24 @@ "label": "自动关闭新建的文件", "description": "启用后,Zoo 新建(而非修改)的文件标签页在接受更改后也会自动关闭。" }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "skills": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 56d5f713b1..54d06eee43 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -191,36 +191,24 @@ "label": "自動關閉新建立的檔案", "description": "啟用後,Zoo 新建(而非修改)的檔案分頁在接受變更後也會自動關閉。" }, - "interfaceFont": { - "label": "Interface font", - "description": "Font for chat messages. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. Inter, sans-serif", - "default": "Match VS Code" - }, - "codeFont": { - "label": "Code font", - "description": "Font for code in chat. Pick a preset or choose Custom… to enter any installed font.", - "placeholder": "e.g. 'JetBrains Mono', monospace", - "default": "Match editor font" - }, - "fontInvalid": "Enter a font family list, e.g. Inter, sans-serif. This value won't be saved.", - "spacingDensity": { - "label": "Spacing density", - "description": "How much padding chat messages use. Compact fits more on screen.", - "options": { - "compact": "Compact", - "comfortable": "Comfortable", - "spacious": "Spacious" + "appearance": { + "reset": "Reset to default", + "interfaceFontScale": { + "label": "Interface font size", + "description": "Scales all Zoo Code text. 100% matches VS Code." + }, + "terminalFontSize": { + "label": "Terminal font size", + "description": "Font size for command output shown in chat." + }, + "editorFontSize": { + "label": "Editor font size", + "description": "Font size for code blocks and diffs shown in chat." + }, + "spacingScale": { + "label": "Spacing density", + "description": "Padding between chat messages. Lower is more compact." } - }, - "fontCustom": "Custom…", - "preview": { - "label": "Preview", - "you": "You", - "zoo": "Zoo", - "userMessage": "Make the selected edge thicker", - "assistantBefore": "Updated", - "assistantAfter": "to use a 2px stroke when selected." } }, "prompts": { @@ -1311,6 +1299,20 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider" + "unknownProvider": "Unknown provider", + "copilot": { + "title": "GitHub Copilot", + "credits": "Premium credits", + "usedOf": "{{used}} / {{total}} used", + "percentUsed": "{{percent}}% used", + "resets": "Resets {{date}}", + "updated": "Updated {{time}}", + "refresh": "Refresh", + "loading": "Loading Copilot usage…", + "unlimited": "Unlimited premium requests on this plan.", + "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", + "noCopilot": "This GitHub account has no Copilot quota to show.", + "error": "Couldn't load Copilot usage." + } } } diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index 3d6e82e115..df133323d0 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -27,10 +27,10 @@ @theme { --font-display: var(--vscode-font-family); - --text-xs: calc(var(--vscode-font-size) * 0.85); - --text-sm: calc(var(--vscode-font-size) * 0.9); - --text-base: var(--vscode-font-size); - --text-lg: calc(var(--vscode-font-size) * 1.1); + --text-xs: calc(var(--vscode-font-size) * var(--zoo-ui-scale, 1) * 0.85); + --text-sm: calc(var(--vscode-font-size) * var(--zoo-ui-scale, 1) * 0.9); + --text-base: calc(var(--vscode-font-size) * var(--zoo-ui-scale, 1)); + --text-lg: calc(var(--vscode-font-size) * var(--zoo-ui-scale, 1) * 1.1); --color-background: var(--background); --color-foreground: var(--foreground); @@ -271,22 +271,25 @@ html { --zoo-chat-font-size: var(--vscode-font-size); /* - * Spacing density tokens for chat rows. `data-zoo-density` is set on - * from the "Spacing density" UI setting; "comfortable" matches the - * historical hardcoded padding (10px / 15px) so defaults are unchanged. + * Appearance settings (UI tab). Each is overridden inline on by + * ExtensionStateContext only when the user sets it, so the defaults below + * reproduce the historical look exactly (scale 1, VS Code sizes, 10/15px). */ - --zoo-row-py: 10px; - --zoo-row-px: 15px; + --zoo-ui-scale: 1; + --zoo-spacing-scale: 1; + --zoo-terminal-font-size: var(--vscode-editor-font-size, 13px); + --zoo-editor-font-size: var(--vscode-editor-font-size, 13px); + --zoo-row-py: calc(10px * var(--zoo-spacing-scale)); + --zoo-row-px: calc(15px * var(--zoo-spacing-scale)); } -html[data-zoo-density="compact"] { - --zoo-row-py: 5px; - --zoo-row-px: 10px; -} - -html[data-zoo-density="spacious"] { - --zoo-row-py: 16px; - --zoo-row-px: 18px; +/* + * Interface font size scales the whole webview's base text. Tailwind's text-* + * tokens and most components derive from body font-size, so one rule covers + * chat, settings and history without touching each component. + */ +body { + font-size: calc(var(--vscode-font-size) * var(--zoo-ui-scale)); } body { @@ -562,6 +565,7 @@ input[cmdk-input]:focus { .diff-view pre, .diff-view code, .diff-view .hljs { + font-size: var(--zoo-editor-font-size); font-family: var(--vscode-editor-font-family), ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; diff --git a/webview-ui/src/utils/__tests__/appearance.spec.ts b/webview-ui/src/utils/__tests__/appearance.spec.ts index 2574adc7e5..3f55c19da1 100644 --- a/webview-ui/src/utils/__tests__/appearance.spec.ts +++ b/webview-ui/src/utils/__tests__/appearance.spec.ts @@ -1,56 +1,56 @@ -import { applyFontVariable, sanitizeFontFamily } from "../appearance" - -describe("sanitizeFontFamily", () => { - it.each([ - ["Inter", "Inter"], - [" Inter, sans-serif ", "Inter, sans-serif"], - ['"JetBrains Mono", monospace', '"JetBrains Mono", monospace'], - ["'Fira Code', Menlo", "'Fira Code', Menlo"], - ["Noto Sans CJK JP", "Noto Sans CJK JP"], - ["ヒラギノ角ゴ", "ヒラギノ角ゴ"], - ])("accepts %j", (input, expected) => { - expect(sanitizeFontFamily(input)).toBe(expected) - }) - - it.each([ - ["", "empty"], - [" ", "whitespace"], - ["Inter; color: red", "semicolon"], - ["Inter}body{display:none", "braces"], - ["url(http://evil)", "url()"], - ["Inter", "angle brackets"], - ["a".repeat(257), "too long"], - ])("rejects %j (%s)", (input) => { - expect(sanitizeFontFamily(input)).toBeUndefined() - }) - - it("handles null/undefined", () => { - expect(sanitizeFontFamily(null)).toBeUndefined() - expect(sanitizeFontFamily(undefined)).toBeUndefined() +import { APPEARANCE_SETTINGS, applyAppearanceVariables, clampAppearance } from "../appearance" + +describe("clampAppearance", () => { + it("passes through in-range integers", () => { + expect(clampAppearance("interfaceFontScale", 110)).toBe(110) + expect(clampAppearance("terminalFontSize", 14)).toBe(14) + }) + + it("clamps to the setting's range", () => { + expect(clampAppearance("interfaceFontScale", 10)).toBe(APPEARANCE_SETTINGS.interfaceFontScale.min) + expect(clampAppearance("editorFontSize", 999)).toBe(APPEARANCE_SETTINGS.editorFontSize.max) + }) + + it("rounds fractional input", () => { + expect(clampAppearance("spacingScale", 87.6)).toBe(88) + }) + + it("treats null/undefined/NaN as unset", () => { + expect(clampAppearance("spacingScale", undefined)).toBeUndefined() + expect(clampAppearance("spacingScale", null)).toBeUndefined() + expect(clampAppearance("spacingScale", NaN)).toBeUndefined() }) }) -describe("applyFontVariable", () => { +describe("applyAppearanceVariables", () => { let root: HTMLElement beforeEach(() => { root = document.createElement("div") }) - it("sets the variable for a valid font", () => { - applyFontVariable("--zoo-chat-font-family", "Inter, sans-serif", root) - expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("Inter, sans-serif") + it("writes percent settings as unitless multipliers and px settings as px", () => { + applyAppearanceVariables(root, { + interfaceFontScale: 120, + terminalFontSize: 14, + editorFontSize: 13, + spacingScale: 80, + }) + expect(root.style.getPropertyValue("--zoo-ui-scale")).toBe("1.2") + expect(root.style.getPropertyValue("--zoo-terminal-font-size")).toBe("14px") + expect(root.style.getPropertyValue("--zoo-editor-font-size")).toBe("13px") + expect(root.style.getPropertyValue("--zoo-spacing-scale")).toBe("0.8") }) - it("removes the variable when cleared", () => { - applyFontVariable("--zoo-chat-font-family", "Inter", root) - applyFontVariable("--zoo-chat-font-family", undefined, root) - expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("") + it("removes overrides for unset values so stylesheet defaults apply", () => { + applyAppearanceVariables(root, { interfaceFontScale: 120, terminalFontSize: 14 }) + applyAppearanceVariables(root, { interfaceFontScale: undefined, terminalFontSize: null }) + expect(root.style.getPropertyValue("--zoo-ui-scale")).toBe("") + expect(root.style.getPropertyValue("--zoo-terminal-font-size")).toBe("") }) - it("removes the variable for an invalid value instead of applying it", () => { - applyFontVariable("--zoo-chat-font-family", "Inter", root) - applyFontVariable("--zoo-chat-font-family", "Inter; x: y", root) - expect(root.style.getPropertyValue("--zoo-chat-font-family")).toBe("") + it("clamps out-of-range values before writing", () => { + applyAppearanceVariables(root, { spacingScale: 1000 }) + expect(root.style.getPropertyValue("--zoo-spacing-scale")).toBe("2") }) }) diff --git a/webview-ui/src/utils/appearance.ts b/webview-ui/src/utils/appearance.ts index 496df8eac9..c5870b30d1 100644 --- a/webview-ui/src/utils/appearance.ts +++ b/webview-ui/src/utils/appearance.ts @@ -1,49 +1,81 @@ /** - * Helpers for user-configurable appearance settings (fonts, density). + * User-configurable appearance settings: interface text scale, terminal and + * editor (code) font sizes, and spacing scale. * - * Font family values are free text typed by the user and are written into a - * CSS custom property. `style.setProperty` can't break out of the declaration, - * but we still reject characters that have no business in a font-family list - * (braces, semicolons, url(), etc.) so a bad value degrades to "inherit" - * instead of producing a confusing half-applied style. + * Each value maps to one CSS custom property on ; index.css owns the + * defaults and every consumer, so components never read these settings. + * Unset (undefined/null) removes the override and restores the default. */ -const FONT_FAMILY_ALLOWED = /^[\p{L}\p{N}\s,'"\-_.]+$/u -const FONT_FAMILY_MAX_LENGTH = 256 - -/** Returns a trimmed, safe font-family list, or `undefined` if empty/invalid. */ -export function sanitizeFontFamily(value: string | null | undefined): string | undefined { - if (typeof value !== "string") { - return undefined - } +export interface AppearanceSetting { + min: number + max: number + step: number + /** Value shown in the stepper when the setting is unset. */ + fallback: number + unit: "%" | "px" + cssVar: string + toCss: (value: number) => string +} - const trimmed = value.trim() +export const APPEARANCE_SETTINGS = { + interfaceFontScale: { + min: 50, + max: 200, + step: 5, + fallback: 100, + unit: "%", + cssVar: "--zoo-ui-scale", + toCss: (v) => String(v / 100), + }, + terminalFontSize: { + min: 8, + max: 32, + step: 1, + fallback: 12, + unit: "px", + cssVar: "--zoo-terminal-font-size", + toCss: (v) => `${v}px`, + }, + editorFontSize: { + min: 8, + max: 32, + step: 1, + fallback: 13, + unit: "px", + cssVar: "--zoo-editor-font-size", + toCss: (v) => `${v}px`, + }, + spacingScale: { + min: 50, + max: 200, + step: 5, + fallback: 100, + unit: "%", + cssVar: "--zoo-spacing-scale", + toCss: (v) => String(v / 100), + }, +} satisfies Record - if (!trimmed || trimmed.length > FONT_FAMILY_MAX_LENGTH || !FONT_FAMILY_ALLOWED.test(trimmed)) { - return undefined - } +export type AppearanceKey = keyof typeof APPEARANCE_SETTINGS +export type AppearanceValues = Partial> - return trimmed +/** Clamps to the setting's range and snaps to an integer; non-finite → undefined. */ +export function clampAppearance(key: AppearanceKey, value: number | null | undefined): number | undefined { + if (typeof value !== "number" || !Number.isFinite(value)) return undefined + const { min, max } = APPEARANCE_SETTINGS[key] + return Math.min(max, Math.max(min, Math.round(value))) } -/** - * Sets (or clears) a font-family CSS variable on the document root. Clearing - * lets the stylesheet fallback (VS Code's own font variable) apply. - */ -export function applyFontVariable( - name: string, - value: string | null | undefined, - root: HTMLElement = document.documentElement, -): void { - const safe = sanitizeFontFamily(value) - - if (safe) { - root.style.setProperty(name, safe) - } else { - root.style.removeProperty(name) +/** Writes (or clears) every appearance CSS variable on `root`. */ +export function applyAppearanceVariables(root: HTMLElement, values: AppearanceValues): void { + for (const key of Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[]) { + const setting = APPEARANCE_SETTINGS[key] + const value = clampAppearance(key, values[key]) + if (value === undefined) { + root.style.removeProperty(setting.cssVar) + } else { + root.style.setProperty(setting.cssVar, setting.toCss(value)) + } } } - -export const SPACING_DENSITIES = ["compact", "comfortable", "spacious"] as const -export type SpacingDensity = (typeof SPACING_DENSITIES)[number] -export const DEFAULT_SPACING_DENSITY: SpacingDensity = "comfortable" From a58ef4b12abae283b5803f09b8637b52311860a7 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 12:59:12 -0700 Subject: [PATCH 05/25] feat(usage): provider limits for all connected providers; cleaner layout; live appearance preview Usage tab - 'Provider limits' lists live credits/balances for every connected provider with a usage API: GitHub Copilot (premium credits, reset date, plan), OpenRouter (key limit or monthly spend), DeepSeek (balance), Moonshot (balance). Endpoints are each provider own usage API. Keys stay in the extension host, are read from saved profiles, and are only sent to the provider's own host; profiles with a custom base URL are skipped. Parallel, 15s timeout, 60s cache - No more '$0.00': the cost stat only appears when a provider reported a price - Layout: one stat strip, compact range/scope row, quieter section titles, stacked breakdown rows with thin bars, bordered top-task list, export moved to an icon link in the header - Replaces the Copilot-only card/route with the generic provider list UI tab - Preview card under the size steppers renders a sample chat, code block and terminal output with the unsaved values (scoped CSS vars), so changes are visible before Save Tests: OpenRouter/DeepSeek/Moonshot parsers, connected-only querying, no key sent to custom hosts, 401 handling, caching --- packages/types/src/providers/vscode-llm.ts | 35 +- packages/types/src/vscode-extension-host.ts | 8 +- src/core/webview/webviewMessageHandler.ts | 21 +- .../__tests__/providerQuotas.spec.ts | 156 ++++++ .../language-models/providerQuotas.ts | 246 +++++++++ .../components/settings/AppearancePreview.tsx | 91 ++++ .../components/settings/CopilotQuotaCard.tsx | 138 ----- .../components/settings/ProviderLimits.tsx | 145 +++++ .../src/components/settings/UISettings.tsx | 4 + .../src/components/settings/UsageSettings.tsx | 504 ++++++++++-------- webview-ui/src/i18n/locales/ca/settings.json | 33 +- webview-ui/src/i18n/locales/de/settings.json | 33 +- webview-ui/src/i18n/locales/en/settings.json | 33 +- webview-ui/src/i18n/locales/es/settings.json | 33 +- webview-ui/src/i18n/locales/fr/settings.json | 33 +- webview-ui/src/i18n/locales/hi/settings.json | 33 +- webview-ui/src/i18n/locales/id/settings.json | 33 +- webview-ui/src/i18n/locales/it/settings.json | 33 +- webview-ui/src/i18n/locales/ja/settings.json | 33 +- webview-ui/src/i18n/locales/ko/settings.json | 33 +- webview-ui/src/i18n/locales/nl/settings.json | 33 +- webview-ui/src/i18n/locales/pl/settings.json | 33 +- .../src/i18n/locales/pt-BR/settings.json | 33 +- webview-ui/src/i18n/locales/ru/settings.json | 33 +- webview-ui/src/i18n/locales/tr/settings.json | 33 +- webview-ui/src/i18n/locales/vi/settings.json | 33 +- .../src/i18n/locales/zh-CN/settings.json | 33 +- .../src/i18n/locales/zh-TW/settings.json | 33 +- 28 files changed, 1304 insertions(+), 638 deletions(-) create mode 100644 src/services/language-models/__tests__/providerQuotas.spec.ts create mode 100644 src/services/language-models/providerQuotas.ts create mode 100644 webview-ui/src/components/settings/AppearancePreview.tsx delete mode 100644 webview-ui/src/components/settings/CopilotQuotaCard.tsx create mode 100644 webview-ui/src/components/settings/ProviderLimits.tsx diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index a97905245b..078ad0b999 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -11,10 +11,41 @@ export const vsCodeLmModelsMessageTypes = [ "githubCopilotManageAccount", "githubCopilotSignInResult", "githubCopilotModels", - "requestGithubCopilotQuota", - "githubCopilotQuota", + "requestProviderQuotas", + "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). diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index c3ed3773a2..9aae20ac7c 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -16,7 +16,7 @@ import { RouterModelsMessageType, type ModelInfo, type ModelRecord, type RouterM import { LmStudioModelsMessageType } from "./providers/lm-studio.js" import { OllamaModelsMessageType } from "./providers/ollama.js" import { OpenAiModelsMessageType } from "./providers/openai.js" -import { VsCodeLmModelsMessageType, type GitHubCopilotQuotaResult } from "./providers/vscode-llm.js" +import { VsCodeLmModelsMessageType, type ProviderQuota } from "./providers/vscode-llm.js" import type { OpenAiCodexRateLimitInfo } from "./providers/openai-codex-rate-limits.js" import type { SkillMetadata } from "./skills.js" import type { RuleMetadata } from "./rules.js" @@ -50,7 +50,7 @@ export interface ExtensionMessage { | typeof VsCodeLmModelsMessageType.vsCodeLmModels | typeof VsCodeLmModelsMessageType.githubCopilotModels | typeof VsCodeLmModelsMessageType.githubCopilotSignInResult - | typeof VsCodeLmModelsMessageType.githubCopilotQuota + | typeof VsCodeLmModelsMessageType.providerQuotas | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" @@ -159,7 +159,7 @@ export interface ExtensionMessage { modelInfo?: ModelInfo }[] githubCopilotAccount?: string | null - githubCopilotQuota?: GitHubCopilotQuotaResult + providerQuotas?: ProviderQuota[] mcpServers?: McpServer[] commits?: GitCommit[] listApiConfig?: ProviderSettingsEntry[] @@ -523,7 +523,7 @@ export interface WebviewMessage { | typeof VsCodeLmModelsMessageType.githubCopilotSignIn | typeof VsCodeLmModelsMessageType.githubCopilotReconnect | typeof VsCodeLmModelsMessageType.githubCopilotManageAccount - | typeof VsCodeLmModelsMessageType.requestGithubCopilotQuota + | typeof VsCodeLmModelsMessageType.requestProviderQuotas | "openImage" | "saveImage" | "openFile" diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4eeea8d434..e0164b956e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1570,13 +1570,20 @@ export const webviewMessageHandler = async ( } break } - case VsCodeLmModelsMessageType.requestGithubCopilotQuota: { - const { getGitHubCopilotQuota } = await import("../../services/language-models/copilotQuota") - const githubCopilotQuota = await getGitHubCopilotQuota({ force: message.bool === true }) - await provider.postMessageToWebview({ - type: VsCodeLmModelsMessageType.githubCopilotQuota, - githubCopilotQuota, - }) + case VsCodeLmModelsMessageType.requestProviderQuotas: { + const { getProviderQuotas } = await import("../../services/language-models/providerQuotas") + // Load full settings (incl. API keys) host-side; keys never leave the extension host. + const entries = await provider.providerSettingsManager.listConfig() + const profiles = await Promise.all( + entries.map((entry) => + provider.providerSettingsManager.getProfile({ id: entry.id }).catch(() => undefined), + ), + ) + const providerQuotas = await getProviderQuotas( + profiles.filter((p): p is NonNullable => !!p), + { force: message.bool === true }, + ) + await provider.postMessageToWebview({ type: VsCodeLmModelsMessageType.providerQuotas, providerQuotas }) break } case VsCodeLmModelsMessageType.githubCopilotManageAccount: diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts new file mode 100644 index 0000000000..9e6d0f4b7f --- /dev/null +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -0,0 +1,156 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers } from "@roo-code/types" + +vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) +vi.mock("../copilotQuota", () => ({ + getGitHubCopilotQuota: vi.fn(async () => ({ + ok: true, + quota: { + plan: "business", + unlimited: false, + entitlement: 140000, + used: 39389.3, + usedPercent: 28.1, + resetAt: 1, + fetchedAt: 2, + }, + })), +})) + +import { + getProviderQuotas, + parseDeepSeekBalance, + parseMoonshotBalance, + parseOpenRouterKey, + resetProviderQuotaCache, +} from "../providerQuotas" + +describe("parseOpenRouterKey", () => { + it("reports a bounded key limit as used / limit with a percent", () => { + expect(parseOpenRouterKey({ data: { limit: 50, limit_remaining: 37.6 } })).toEqual([ + { id: "key_limit", valueLabel: "$12.40 / $50.00 used", usedPercent: expect.closeTo(24.8, 5) }, + ]) + }) + + it("falls back to monthly spend for unlimited keys", () => { + expect(parseOpenRouterKey({ data: { limit: null, usage_monthly: 3.2 } })).toEqual([ + { id: "spend", valueLabel: "$3.20 spent this month" }, + ]) + }) + + it("returns undefined without data", () => { + expect(parseOpenRouterKey({})).toBeUndefined() + expect(parseOpenRouterKey({ data: {} })).toBeUndefined() + }) +}) + +describe("parseDeepSeekBalance", () => { + it("prefers a positive USD balance", () => { + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "CNY", total_balance: "10.00" }, + { currency: "USD", total_balance: "4.25" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "$4.25 left" }]) + }) + + it("uses ¥ for a CNY-only balance", () => { + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "CNY", total_balance: "8" }] })).toEqual([ + { id: "balance", valueLabel: "¥8.00 left" }, + ]) + }) + + it("returns undefined for empty or malformed payloads", () => { + expect(parseDeepSeekBalance({ balance_infos: [] })).toBeUndefined() + expect(parseDeepSeekBalance(null)).toBeUndefined() + }) +}) + +describe("parseMoonshotBalance", () => { + it("reads available_balance", () => { + expect(parseMoonshotBalance({ data: { available_balance: 19.5 } })).toEqual([ + { id: "balance", valueLabel: "$19.50 left" }, + ]) + }) +}) + +describe("getProviderQuotas", () => { + beforeEach(() => resetProviderQuotaCache()) + + const okFetch = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + + it("only queries providers that are connected", async () => { + const result = await getProviderQuotas([{ apiProvider: providerIdentifiers.anthropic } as never], {}, okFetch) + expect(result).toEqual([]) + expect(okFetch).not.toHaveBeenCalled() + }) + + it("returns Copilot credits formatted for display", async () => { + const [copilot] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) + expect(copilot).toMatchObject({ + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + windows: [{ id: "premium", valueLabel: "39,389.3 / 140,000 used", usedPercent: 28.1, resetAt: 1 }], + }) + }) + + it("reports noKey when the profile has no API key", async () => { + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.openrouter } as never], {}, okFetch) + expect(q).toMatchObject({ providerId: providerIdentifiers.openrouter, ok: false, reason: "noKey" }) + expect(okFetch).not.toHaveBeenCalled() + }) + + it("never sends a key to a custom base URL", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "https://evil.example/api/v1", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("calls the provider's own host with the saved key", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-test" } as never], + {}, + fetchSpy, + ) + expect(fetchSpy).toHaveBeenCalledWith( + "https://openrouter.ai/api/v1/key", + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer sk-test" }) }), + ) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "$5.00 / $10.00 used" }] }) + }) + + it("maps 401 to noKey", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "bad" } as never], + {}, + vi.fn(async () => new Response("", { status: 401 })), + ) + expect(q).toMatchObject({ providerId: providerIdentifiers.deepseek, ok: false, reason: "noKey" }) + }) + + it("caches results for the same set of providers unless forced", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + await getProviderQuotas(profiles, {}, fetchSpy) + await getProviderQuotas(profiles, {}, fetchSpy) + expect(fetchSpy).toHaveBeenCalledTimes(1) + await getProviderQuotas(profiles, { force: true }, fetchSpy) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts new file mode 100644 index 0000000000..181b148a70 --- /dev/null +++ b/src/services/language-models/providerQuotas.ts @@ -0,0 +1,246 @@ +import { + providerIdentifiers, + type ProviderQuota, + type ProviderQuotaWindow, + type ProviderSettings, +} from "@roo-code/types" + +import { getGitHubCopilotQuota } from "./copilotQuota" + +/** + * Live limits/balances for every connected provider that exposes a usage API, + * for the Usage tab's "Provider limits" section. + * + * Endpoints are each provider own usage API. Credentials come from the + * user's saved Zoo profiles and are sent only to that provider's own host. + * Providers without a usage API are simply not listed. + */ + +const REQUEST_TIMEOUT_MS = 15_000 +const CACHE_TTL_MS = 60_000 + +const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string" && value.trim() !== "") { + const n = Number(value) + return Number.isFinite(n) ? n : undefined + } + return undefined +} + +const asRecord = (value: unknown): Record | undefined => + value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined + +const money = (value: number, symbol = "$") => `${value < 0 ? "-" : ""}${symbol}${Math.abs(value).toFixed(2)}` + +const credits = (value: number) => + value.toLocaleString("en-US", { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) + +const clampPct = (value: number) => Math.min(100, Math.max(0, value)) + +// Parsers (pure; unit-tested) ---------------------------------------------------------------------- + +/** OpenRouter `GET /api/v1/key` → key limit, or monthly spend when unlimited. */ +export function parseOpenRouterKey(payload: unknown): ProviderQuotaWindow[] | undefined { + const data = asRecord(asRecord(payload)?.data) + if (!data) return undefined + + const limit = toNumber(data.limit) + const remaining = toNumber(data.limit_remaining) + + if (limit !== undefined && limit > 0 && remaining !== undefined) { + const used = Math.max(0, limit - remaining) + return [ + { + id: "key_limit", + valueLabel: `${money(used)} / ${money(limit)} used`, + usedPercent: clampPct((used / limit) * 100), + }, + ] + } + + const usage = toNumber(data.usage_monthly) ?? toNumber(data.usage) + return usage === undefined ? undefined : [{ id: "spend", valueLabel: `${money(usage)} spent this month` }] +} + +/** DeepSeek `GET /user/balance` → prepaid balance (prefers a positive USD balance). */ +export function parseDeepSeekBalance(payload: unknown): ProviderQuotaWindow[] | undefined { + const infos = asRecord(payload)?.balance_infos + if (!Array.isArray(infos) || infos.length === 0) return undefined + + const rows = infos.map(asRecord).filter((r): r is Record => !!r) + const pick = + rows.find((r) => r.currency === "USD" && (toNumber(r.total_balance) ?? 0) > 0) ?? + rows.find((r) => r.currency === "CNY" && (toNumber(r.total_balance) ?? 0) > 0) ?? + rows.find((r) => r.currency === "USD") ?? + rows[0] + + const balance = toNumber(pick?.total_balance) + if (balance === undefined) return undefined + return [{ id: "balance", valueLabel: `${money(balance, pick?.currency === "CNY" ? "¥" : "$")} left` }] +} + +/** Moonshot `GET /v1/users/me/balance` → available balance. */ +export function parseMoonshotBalance(payload: unknown): ProviderQuotaWindow[] | undefined { + const balance = toNumber(asRecord(asRecord(payload)?.data)?.available_balance) + return balance === undefined ? undefined : [{ id: "balance", valueLabel: `${money(balance)} left` }] +} + +// Fetchers ---------------------------------------------------------------------------------------- + +type Fetch = typeof fetch + +async function getJson(fetchImpl: Fetch, url: string, apiKey: string): Promise { + const response = await fetchImpl(url, { + method: "GET", + headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + if (response.status === 401 || response.status === 403) throw new AuthError() + if (!response.ok) throw new Error(`HTTP ${response.status}`) + return response.json() +} + +class AuthError extends Error { + constructor() { + super("Invalid or expired API key") + } +} + +interface KeyedProvider { + providerId: string + /** Returns the API key and optional base URL for this provider from a saved profile. */ + credentials: (s: ProviderSettings) => { apiKey?: string; baseUrl?: string } + /** Only the provider's own default host is queried; custom gateways are skipped. */ + defaultBase: string + path: string + parse: (payload: unknown) => ProviderQuotaWindow[] | undefined +} + +const KEYED_PROVIDERS: KeyedProvider[] = [ + { + providerId: providerIdentifiers.openrouter, + credentials: (s) => ({ apiKey: s.openRouterApiKey, baseUrl: s.openRouterBaseUrl }), + defaultBase: "https://openrouter.ai/api/v1", + path: "/key", + parse: parseOpenRouterKey, + }, + { + providerId: providerIdentifiers.deepseek, + credentials: (s) => ({ apiKey: s.deepSeekApiKey, baseUrl: s.deepSeekBaseUrl }), + defaultBase: "https://api.deepseek.com", + path: "/user/balance", + parse: parseDeepSeekBalance, + }, + { + providerId: providerIdentifiers.moonshot, + credentials: (s) => ({ apiKey: s.moonshotApiKey, baseUrl: s.moonshotBaseUrl }), + defaultBase: "https://api.moonshot.ai/v1", + path: "/users/me/balance", + parse: parseMoonshotBalance, + }, +] + +/** Providers this service can report on (used by the webview to decide what to show). */ +export const QUOTA_PROVIDER_IDS = [ + providerIdentifiers.githubCopilot, + ...KEYED_PROVIDERS.map((p) => p.providerId), +] as string[] + +const sameOrigin = (a: string, b: string) => { + try { + return new URL(a).origin === new URL(b).origin + } catch { + return false + } +} + +async function fetchKeyed( + provider: KeyedProvider, + profiles: ProviderSettings[], + fetchImpl: Fetch, +): Promise { + const now = Date.now() + const creds = profiles + .filter((p) => p.apiProvider === provider.providerId) + .map(provider.credentials) + .find((c) => !!c.apiKey) + + if (!creds?.apiKey) return { providerId: provider.providerId, ok: false, reason: "noKey", fetchedAt: now } + + // SAFETY: never send a key to a host other than the provider's own API. A + // profile pointed at a custom gateway is reported as unsupported instead. + if (creds.baseUrl && !sameOrigin(creds.baseUrl, provider.defaultBase)) { + return { providerId: provider.providerId, ok: false, reason: "unsupported", fetchedAt: now } + } + + try { + const windows = provider.parse(await getJson(fetchImpl, provider.defaultBase + provider.path, creds.apiKey)) + return windows?.length + ? { providerId: provider.providerId, ok: true, windows, fetchedAt: now } + : { providerId: provider.providerId, ok: false, reason: "error", message: "No usage data", fetchedAt: now } + } catch (error) { + return { + providerId: provider.providerId, + ok: false, + reason: error instanceof AuthError ? "noKey" : "error", + message: error instanceof Error ? error.message : String(error), + fetchedAt: now, + } + } +} + +async function fetchCopilot(force: boolean): Promise { + const result = await getGitHubCopilotQuota({ force }) + const providerId = providerIdentifiers.githubCopilot + if (!result.ok) + return { providerId, ok: false, reason: result.reason, message: result.message, fetchedAt: Date.now() } + + const q = result.quota + const window: ProviderQuotaWindow = q.unlimited + ? { id: "premium", valueLabel: "Unlimited", resetAt: q.resetAt } + : { + id: "premium", + valueLabel: + q.used !== undefined && q.entitlement !== undefined + ? `${credits(q.used)} / ${credits(q.entitlement)} used` + : `${Math.round(q.usedPercent ?? 0)}% used`, + usedPercent: q.usedPercent, + resetAt: q.resetAt, + } + return { providerId, ok: true, plan: q.plan, windows: [window], fetchedAt: q.fetchedAt } +} + +// Entry point ------------------------------------------------------------------------------------- + +let cache: { at: number; key: string; result: ProviderQuota[] } | undefined + +/** + * Quotas for every *connected* provider with a usage API. `profiles` are the + * user's saved provider settings; only providers present there are queried. + */ +export async function getProviderQuotas( + profiles: ProviderSettings[], + { force = false }: { force?: boolean } = {}, + fetchImpl: Fetch = fetch, +): Promise { + const connected = new Set(profiles.map((p) => p.apiProvider).filter(Boolean) as string[]) + const key = [...connected].sort().join(",") + + if (!force && cache && cache.key === key && Date.now() - cache.at < CACHE_TTL_MS) return cache.result + + const tasks: Promise[] = [] + if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force)) + for (const provider of KEYED_PROVIDERS) { + if (connected.has(provider.providerId)) tasks.push(fetchKeyed(provider, profiles, fetchImpl)) + } + + const result = await Promise.all(tasks) + cache = { at: Date.now(), key, result } + return result +} + +/** @internal test hook */ +export function resetProviderQuotaCache() { + cache = undefined +} diff --git a/webview-ui/src/components/settings/AppearancePreview.tsx b/webview-ui/src/components/settings/AppearancePreview.tsx new file mode 100644 index 0000000000..7de089e3bf --- /dev/null +++ b/webview-ui/src/components/settings/AppearancePreview.tsx @@ -0,0 +1,91 @@ +import type { CSSProperties } from "react" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { APPEARANCE_SETTINGS, clampAppearance, type AppearanceKey } from "@/utils/appearance" + +interface AppearancePreviewProps { + values: Partial> + chatFontSize?: number +} + +/** + * Live preview of the appearance settings using the *unsaved* values. + * + * It sets the same CSS custom properties the real UI reads, but only on this + * element, so the sample updates on every stepper click without touching the + * rest of the webview until the user presses Save. + */ +export const AppearancePreview = ({ values, chatFontSize }: AppearancePreviewProps) => { + const { t } = useAppTranslation() + + const vars: Record = {} + for (const key of Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[]) { + const setting = APPEARANCE_SETTINGS[key] + const v = clampAppearance(key, values[key]) ?? setting.fallback + vars[setting.cssVar] = setting.toCss(v) + } + if (chatFontSize) vars["--zoo-chat-font-size"] = `${chatFontSize}px` + + const uiText: CSSProperties = { + fontSize: "calc(var(--zoo-chat-font-size, var(--vscode-font-size)) * var(--zoo-ui-scale))", + } + const row: CSSProperties = { + padding: "calc(10px * var(--zoo-spacing-scale)) calc(15px * var(--zoo-spacing-scale))", + } + const mono: CSSProperties = { + fontFamily: "var(--vscode-editor-font-family, monospace)", + margin: 0, + padding: "6px 10px", + borderRadius: 4, + overflowX: "auto", + whiteSpace: "pre", + } + + return ( +
+
+ {t("settings:ui.appearance.preview")} +
+ +
+
+ {t("settings:ui.appearance.sample.you")} +
+
{t("settings:ui.appearance.sample.user")}
+
+ +
+
+ {t("settings:ui.appearance.sample.zoo")} +
+
{t("settings:ui.appearance.sample.assistant")}
+ +
+ {t("settings:ui.appearance.editorFontSize.label")} +
+
+					{"const stroke = selected ? 2 : 1"}
+				
+ +
+ {t("settings:ui.appearance.terminalFontSize.label")} +
+
+					{"$ pnpm test\n✓ 42 passed"}
+				
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/CopilotQuotaCard.tsx b/webview-ui/src/components/settings/CopilotQuotaCard.tsx deleted file mode 100644 index 9860940a12..0000000000 --- a/webview-ui/src/components/settings/CopilotQuotaCard.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useCallback, useEffect, useState } from "react" -import { RefreshCw } from "lucide-react" - -import { VsCodeLmModelsMessageType, type GitHubCopilotQuotaResult } from "@roo-code/types" - -import { useAppTranslation } from "@/i18n/TranslationContext" -import { cn } from "@src/lib/utils" -import { vscode } from "@src/utils/vscode" - -const formatCredits = (value: number) => - value.toLocaleString(undefined, { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) - -const formatReset = (ts: number) => - new Date(ts).toLocaleString(undefined, { - weekday: "short", - month: "short", - day: "numeric", - hour: "numeric", - minute: "2-digit", - }) - -/** - * GitHub Copilot premium-request credits (used / total, % bar, reset date), - * fetched by the extension host from the same endpoint Copilot's own usage - * panel reads. Only rendered when the user has a Copilot profile. - */ -export const CopilotQuotaCard = () => { - const { t } = useAppTranslation() - const [result, setResult] = useState() - const [loading, setLoading] = useState(true) - - const request = useCallback((force: boolean) => { - setLoading(true) - vscode.postMessage({ type: VsCodeLmModelsMessageType.requestGithubCopilotQuota, bool: force }) - }, []) - - useEffect(() => { - const onMessage = (event: MessageEvent) => { - const message = event.data - if (message?.type === VsCodeLmModelsMessageType.githubCopilotQuota) { - setResult(message.githubCopilotQuota) - setLoading(false) - } - } - window.addEventListener("message", onMessage) - request(false) - return () => window.removeEventListener("message", onMessage) - }, [request]) - - const quota = result?.ok ? result.quota : undefined - const pct = quota?.usedPercent ?? 0 - const barColor = - pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" - - return ( -
-
-
- - {t("settings:usage.copilot.title")} - {quota?.plan && ( - · {quota.plan} - )} -
- -
- - {loading && !result && ( -
{t("settings:usage.copilot.loading")}
- )} - - {result && !result.ok && ( -
- {t(`settings:usage.copilot.${result.reason}`)} - {result.reason === "error" && result.message ? ` (${result.message})` : ""} -
- )} - - {quota && quota.unlimited &&
{t("settings:usage.copilot.unlimited")}
} - - {quota && !quota.unlimited && ( - <> -
- {t("settings:usage.copilot.credits")} - - {quota.used !== undefined && quota.entitlement !== undefined - ? t("settings:usage.copilot.usedOf", { - used: formatCredits(quota.used), - total: formatCredits(quota.entitlement), - }) - : `${Math.round(pct)}%`} - -
-
-
-
-
- - {quota.resetAt - ? t("settings:usage.copilot.resets", { date: formatReset(quota.resetAt) }) - : ""} - - - {t("settings:usage.copilot.percentUsed", { percent: Math.round(pct) })} - -
- - )} -
- ) -} diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx new file mode 100644 index 0000000000..f90a0dce80 --- /dev/null +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -0,0 +1,145 @@ +import { useCallback, useEffect, useState } from "react" +import { RefreshCw } from "lucide-react" + +import { VsCodeLmModelsMessageType, type ProviderQuota } from "@roo-code/types" + +import { useAppTranslation } from "@/i18n/TranslationContext" +import { cn } from "@src/lib/utils" +import { vscode } from "@src/utils/vscode" + +const formatReset = (ts: number) => + new Date(ts).toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric" }) + +const barColor = (pct: number) => + pct >= 90 ? "bg-vscode-charts-red" : pct >= 75 ? "bg-vscode-charts-yellow" : "bg-vscode-charts-green" + +/** + * "Provider limits": live credits/balances for every connected provider that + * exposes a usage API (GitHub Copilot, OpenRouter, DeepSeek, Moonshot), one + * compact row each. Fetched by the extension host; keys never reach the webview. + */ +export const ProviderLimits = ({ providerLabel }: { providerLabel: (id: string) => string }) => { + const { t } = useAppTranslation() + const [quotas, setQuotas] = useState() + const [loading, setLoading] = useState(true) + + const request = useCallback((force: boolean) => { + setLoading(true) + vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force }) + }, []) + + useEffect(() => { + const onMessage = (event: MessageEvent) => { + if (event.data?.type === VsCodeLmModelsMessageType.providerQuotas) { + setQuotas(event.data.providerQuotas ?? []) + setLoading(false) + } + } + window.addEventListener("message", onMessage) + request(false) + return () => window.removeEventListener("message", onMessage) + }, [request]) + + // Nothing connected that has a usage API: hide the section entirely. + if (quotas && quotas.length === 0) return null + + const fetchedAt = quotas?.reduce((max, q) => Math.max(max, q.fetchedAt), 0) + + return ( +
+
+

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

+ +
+ +
+ {!quotas && ( +
+ {t("settings:usage.limits.loading")} +
+ )} + {quotas?.map((q) => ( + + ))} +
+
+ ) +} + +const QuotaRow = ({ quota, name }: { quota: ProviderQuota; name: string }) => { + const { t } = useAppTranslation() + + return ( +
+
+ + {name} + {quota.ok && quota.plan && ( + {quota.plan} + )} + {!quota.ok && ( + + {t(`settings:usage.limits.${quota.reason}`)} + + )} +
+ + {quota.ok && + quota.windows.map((w) => ( +
+
+ {w.valueLabel} + {w.usedPercent !== undefined && ( + + {Math.round(w.usedPercent)}% + + )} +
+ {w.usedPercent !== undefined && ( +
+
+
+ )} + {w.resetAt && ( +
+ {t("settings:usage.limits.resets", { date: formatReset(w.resetAt) })} +
+ )} +
+ ))} +
+ ) +} diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index ce57b12e98..40807c832e 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -16,6 +16,7 @@ import { Slider, Button } from "../ui" import { ExtensionStateContextType } from "@/context/ExtensionStateContext" import { APPEARANCE_SETTINGS, type AppearanceKey } from "@/utils/appearance" import { NumberStepper } from "./NumberStepper" +import { AppearancePreview } from "./AppearancePreview" const APPEARANCE_KEYS = Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[] @@ -204,6 +205,9 @@ export const UISettings = ({ ))} + {/* Live preview of the (unsaved) size settings above */} + + {/* Auto-close Zoo opened files */} [p.value, p.label])) const providerLabel = (id: string) => PROVIDER_LABELS.get(id) ?? id @@ -41,12 +40,15 @@ const WEEKDAY_FMT = new Intl.DateTimeFormat(undefined, { weekday: "narrow" }) const DAY_FMT = new Intl.DateTimeFormat(undefined, { day: "numeric" }) /** - * Usage tab: read-only rollup of locally persisted task history. No network - * calls and no new persisted state. + * Usage tab. + * + * 1. Provider limits: live credits/balances from each connected provider. + * 2. Usage history: rollup of local task history (tokens, cost when the + * provider reports a price, breakdowns, top tasks). * - * Subscription providers (GitHub Copilot / VS Code LM) report $0 per task, so - * when the selected range has tokens but no cost the tab switches its charts - * and breakdowns to tokens and explains why. + * Subscription providers (e.g. GitHub Copilot) record no per-task price, so + * when a range has tokens but no cost the history switches to tokens and the + * cost stat is hidden rather than shown as a misleading "$0.00". */ export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { const { t } = useAppTranslation() @@ -71,10 +73,6 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes p.apiProvider === providerIdentifiers.githubCopilot) - const profileSubtitle = (name: string) => { const meta = profileMeta.get(name) if (!meta?.apiProvider) return t("settings:usage.unknownProvider") @@ -101,223 +99,256 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes +
{t("settings:sections.usage")}
- {hasCopilotProfile && } - - - {/* Segmented range control */} -
- {USAGE_RANGES.map((r) => ( - - ))} -
-
- - {t("settings:usage.scope.label")} - - -
-
+
+ - {isEmpty ? ( -
- {t("settings:usage.empty")} -
- ) : ( - <> - {/* Stat cards */} -
- - 0 - ? t("settings:usage.stats.cacheHit", { - percent: Math.round(summary.cacheHitRate * 100), - }) - : t("settings:usage.stats.inOut", { - input: formatLargeNumber(summary.tokensIn), - output: formatLargeNumber(summary.tokensOut), - }) - } - /> - +
+
+ {t("settings:usage.history")} + {!isEmpty && ( + + )}
- {metric === "tokens" && ( -
- {t("settings:usage.noPricingNote")} + {/* Filters */} + +
+
+ {USAGE_RANGES.map((r) => ( + + ))} +
+
- )} +
- {/* Daily chart */} - {summary.daily.length > 1 && ( -
- - {metric === "cost" - ? t("settings:usage.dailyCost") - : t("settings:usage.dailyTokens")} - -
- {summary.daily.map((d) => { - const v = value(d.cost, d.tokens) - return ( -
0 ? "bg-vscode-charts-blue" : "bg-vscode-input-background", - )} - style={{ - height: - maxDaily > 0 && v > 0 - ? `${Math.max(4, (v / maxDaily) * 100)}%` - : "3px", - }} - title={`${new Date(d.day).toLocaleDateString()} · ${fmt(d.cost, d.tokens)}`} - /> - ) - })} + {isEmpty ? ( +
+ {t("settings:usage.empty")} +
+ ) : ( + <> + {/* Stat strip — cost only when a provider actually reported a price */} +
+ {metric === "cost" && ( + + )} + 0 + ? t("settings:usage.stats.cacheHit", { + percent: Math.round(summary.cacheHitRate * 100), + }) + : t("settings:usage.stats.inOut", { + input: formatLargeNumber(summary.tokensIn), + output: formatLargeNumber(summary.tokensOut), + }) + } + /> +
- {showDayLabels && ( -
- {summary.daily.map((d) => ( - - {summary.daily.length <= 7 - ? WEEKDAY_FMT.format(d.day) - : DAY_FMT.format(d.day)} - - ))} -
+ + {metric === "tokens" && ( +

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

)} -
- )} - ({ - key: b.key, - name: b.key === "unknown" ? t("settings:usage.unknownProvider") : providerLabel(b.key), - value: value(b.cost, b.tokens), - label: fmt(b.cost, b.tokens), - }))} - total={total} - testId="usage-by-provider" - /> - ({ - key: b.key, - name: b.key, - subtitle: profileSubtitle(b.key), - value: value(b.cost, b.tokens), - label: fmt(b.cost, b.tokens), - }))} - total={total} - testId="usage-by-profile" - /> - ({ - key: b.key, - name: b.key, - value: value(b.cost, b.tokens), - label: fmt(b.cost, b.tokens), - }))} - total={total} - testId="usage-by-mode" - /> + {summary.daily.length > 1 && ( + +
+ {summary.daily.map((d) => { + const v = value(d.cost, d.tokens) + return ( +
0 + ? "bg-vscode-charts-blue" + : "bg-vscode-input-background", + )} + style={{ + height: + maxDaily > 0 && v > 0 + ? `${Math.max(6, (v / maxDaily) * 100)}%` + : "2px", + }} + title={`${new Date(d.day).toLocaleDateString()} · ${fmt(d.cost, d.tokens)}`} + /> + ) + })} +
+ {showDayLabels && ( +
+ {summary.daily.map((d) => ( + + {summary.daily.length <= 7 + ? WEEKDAY_FMT.format(d.day) + : DAY_FMT.format(d.day)} + + ))} +
+ )} + + )} - {/* Top tasks */} -
- - {metric === "cost" ? t("settings:usage.topTasks") : t("settings:usage.topTasksTokens")} - - {summary.topTasks.map((task) => ( - - ))} -
+ ({ + key: b.key, + name: + b.key === "unknown" + ? t("settings:usage.unknownProvider") + : providerLabel(b.key), + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} + testId="usage-by-provider" + /> + ({ + key: b.key, + name: b.key, + subtitle: profileSubtitle(b.key), + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} + testId="usage-by-profile" + /> + ({ + key: b.key, + name: b.key, + value: value(b.cost, b.tokens), + label: fmt(b.cost, b.tokens), + }))} + total={total} + testId="usage-by-mode" + /> -
- - - {t("settings:usage.disclaimer")} - -
- - )} + +
+ {summary.topTasks.map((task) => ( + + ))} +
+
+ +

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

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

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

{children}

+) + +const Block = ({ title, children, testId }: { title: string; children: React.ReactNode; testId?: string }) => ( +
+
{title}
{children} - +
) -const StatCard = ({ value, label, hint }: { value: string; label: string; hint?: string }) => ( -
-
{value}
-
{label}
- {hint &&
{hint}
} +const Stat = ({ value, label, hint }: { value: string; label: string; hint?: string }) => ( +
+
{value}
+
{hint ?? label}
+ {hint &&
{label}
}
) @@ -332,31 +363,34 @@ const Breakdown = ({ total: number testId: string }) => ( -
- {title} - {rows.slice(0, 6).map((row, i) => { - const pct = total > 0 ? Math.round((row.value / total) * 100) : 0 - const color = PALETTE[i % PALETTE.length] - return ( -
- - - {row.name} - {row.subtitle && ( - - {row.subtitle} + +
+ {rows.slice(0, 6).map((row, i) => { + const pct = total > 0 ? Math.round((row.value / total) * 100) : 0 + const color = PALETTE[i % PALETTE.length] + return ( +
+
+ + + {row.name} + {row.subtitle && ( + + {row.subtitle} + + )} - )} - - {row.label} - - - - {pct}% -
- ) - })} -
+ + {row.label} + {pct}% + +
+
+
+
+
+ ) + })} +
+ ) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 6aeeb7745d..423928a627 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 905075216a..b53c129ea9 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index e3808dcca4..77bfc7ce50 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -261,6 +261,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 858ed46c7b..54e7817ae9 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 9ea4121d9a..8b18463ffd 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index d5574a4677..afbccebbeb 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 6c064905bf..684f92d8b3 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 8f1fae17ff..57d2396d3f 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index af7e4b2f17..3f1f0bbc93 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index b146cf6b7a..6fd695dab2 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index e02cff1538..4e537e4ebe 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 318d239ea3..762bd5a364 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 99ed836c31..bc30a368cd 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 092e06ba87..f2935e1870 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index e01b594736..0f2143ec06 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index fffa278487..4830f017f5 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 5913d175ed..374deb165e 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1198,6 +1198,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 54d06eee43..75381a5a99 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -208,6 +208,13 @@ "spacingScale": { "label": "Spacing density", "description": "Padding between chat messages. Lower is more compact." + }, + "preview": "Preview", + "sample": { + "you": "You", + "zoo": "Zoo", + "user": "Make the selected edge thicker", + "assistant": "Updated Edge.tsx to use a 2px stroke when selected." } } }, @@ -1286,7 +1293,6 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "noPricing": "no per-token price", "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", @@ -1295,24 +1301,23 @@ "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", "exportCsv": "Export CSV", - "noPricingNote": "This provider doesn't report a per-token price (for example, GitHub Copilot is billed by subscription), so charts below show tokens instead of cost.", + "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", "unknownProvider": "Unknown provider", - "copilot": { - "title": "GitHub Copilot", - "credits": "Premium credits", - "usedOf": "{{used}} / {{total}} used", - "percentUsed": "{{percent}}% used", - "resets": "Resets {{date}}", - "updated": "Updated {{time}}", + "history": "Usage history", + "limits": { + "title": "Provider limits", "refresh": "Refresh", - "loading": "Loading Copilot usage…", - "unlimited": "Unlimited premium requests on this plan.", - "signedOut": "Sign in to GitHub Copilot in the Providers tab to see credits.", - "noCopilot": "This GitHub account has no Copilot quota to show.", - "error": "Couldn't load Copilot usage." + "updated": "Updated {{time}}", + "loading": "Checking connected providers…", + "resets": "Resets {{date}}", + "signedOut": "Not signed in", + "noCopilot": "No Copilot quota", + "noKey": "API key missing or invalid", + "unsupported": "Custom base URL — not checked", + "error": "Couldn't load" } } } From de76a054d5bf3a674d6d16dcfe1b4a1ee5a12634 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 13:34:19 -0700 Subject: [PATCH 06/25] fix(usage): attribute usage to the provider/model that served each request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: the Usage tab resolved a task's provider/model from the *currently selected* profile, so switching providers relabelled all past usage (Copilot work showed up as Gemini 'gemini-3.1-pro-preview'). History stores only the profile name, so there was nothing correct to read. Now the provider and model are recorded per request: - Task writes apiProvider + modelId into each api_req_started message when the request is made (placeholder and final update) - taskMetadata derives HistoryItem.usageByModel (per provider+model: requests, tokens, cache, cost) from those messages; a task that switches providers mid-way is split by what actually served each request - Usage tab aggregates from usageByModel: 'By provider' and new 'By model'; the live profile lookup and 'By API profile' are removed. Top-task rows show provider · model. CSV gains provider/model columns Tasks created before this change carry no record, so they are shown as 'Not recorded' rather than guessed. Tests: per-request aggregation, mid-task provider switch, unrecorded tasks, and a regression test proving a profile change cannot relabel past usage. --- packages/types/src/history.ts | 19 +++ packages/types/src/vscode-extension-host.ts | 7 + .../__tests__/usageByModel.spec.ts | 113 +++++++++++++ src/core/task-persistence/taskMetadata.ts | 5 + src/core/task-persistence/usageByModel.ts | 51 ++++++ src/core/task/Task.ts | 5 + .../src/components/settings/UsageSettings.tsx | 57 ++++--- webview-ui/src/i18n/locales/ca/settings.json | 6 +- webview-ui/src/i18n/locales/de/settings.json | 6 +- webview-ui/src/i18n/locales/en/settings.json | 6 +- webview-ui/src/i18n/locales/es/settings.json | 6 +- webview-ui/src/i18n/locales/fr/settings.json | 6 +- webview-ui/src/i18n/locales/hi/settings.json | 6 +- webview-ui/src/i18n/locales/id/settings.json | 6 +- webview-ui/src/i18n/locales/it/settings.json | 6 +- webview-ui/src/i18n/locales/ja/settings.json | 6 +- webview-ui/src/i18n/locales/ko/settings.json | 6 +- webview-ui/src/i18n/locales/nl/settings.json | 6 +- webview-ui/src/i18n/locales/pl/settings.json | 6 +- .../src/i18n/locales/pt-BR/settings.json | 6 +- webview-ui/src/i18n/locales/ru/settings.json | 6 +- webview-ui/src/i18n/locales/tr/settings.json | 6 +- webview-ui/src/i18n/locales/vi/settings.json | 6 +- .../src/i18n/locales/zh-CN/settings.json | 6 +- .../src/i18n/locales/zh-TW/settings.json | 6 +- webview-ui/src/utils/__tests__/usage.spec.ts | 155 ++++++++++++++---- webview-ui/src/utils/usage.ts | 101 +++++++++--- 27 files changed, 489 insertions(+), 132 deletions(-) create mode 100644 src/core/task-persistence/__tests__/usageByModel.spec.ts create mode 100644 src/core/task-persistence/usageByModel.ts diff --git a/packages/types/src/history.ts b/packages/types/src/history.ts index 131194212e..21cef89e47 100644 --- a/packages/types/src/history.ts +++ b/packages/types/src/history.ts @@ -26,6 +26,23 @@ export const pendingTaskActionSchema = z.discriminatedUnion("kind", [ export type PendingTaskAction = z.infer +/** + * Token/cost totals for one provider+model within a task. A task can span + * several (the user may switch profiles mid-task), so history keeps a list. + */ +export const usageByModelEntrySchema = z.object({ + apiProvider: z.string().optional(), + modelId: z.string().optional(), + requests: z.number(), + tokensIn: z.number(), + tokensOut: z.number(), + cacheReads: z.number().optional(), + cacheWrites: z.number().optional(), + cost: z.number(), +}) + +export type UsageByModelEntry = z.infer + export const historyItemSchema = z.object({ id: z.string(), rootTaskId: z.string().optional(), @@ -39,6 +56,8 @@ export const historyItemSchema = z.object({ cacheWrites: z.number().optional(), cacheReads: z.number().optional(), totalCost: z.number(), + /** Per provider+model breakdown, recorded as requests happen. Absent on tasks created before this was tracked. */ + usageByModel: z.array(usageByModelEntrySchema).optional(), size: z.number().optional(), workspace: z.string().optional(), mode: z.string().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 9aae20ac7c..328eff8a1a 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -959,6 +959,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" diff --git a/src/core/task-persistence/__tests__/usageByModel.spec.ts b/src/core/task-persistence/__tests__/usageByModel.spec.ts new file mode 100644 index 0000000000..092272ff6d --- /dev/null +++ b/src/core/task-persistence/__tests__/usageByModel.spec.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest" + +import { providerIdentifiers, type ClineMessage } from "@roo-code/types" + +import { summarizeUsageByModel } from "../usageByModel" + +const req = (data: Record, ts = 1): ClineMessage => ({ + ts, + type: "say", + say: "api_req_started", + text: JSON.stringify(data), +}) + +describe("summarizeUsageByModel", () => { + it("groups requests by the provider+model recorded on each request", () => { + const result = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 1000, + tokensOut: 100, + cost: 0, + }), + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 500, + tokensOut: 50, + cost: 0, + }), + req({ + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-3.1-pro-preview", + tokensIn: 200, + tokensOut: 20, + cost: 0.4, + }), + ]) + + expect(result).toEqual([ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 2, + tokensIn: 1500, + tokensOut: 150, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-3.1-pro-preview", + requests: 1, + tokensIn: 200, + tokensOut: 20, + cost: 0.4, + }, + ]) + }) + + it("keeps a task that switched providers mid-way split by what actually served each request", () => { + const result = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 100, + tokensOut: 10, + }), + req({ apiProvider: providerIdentifiers.gemini, modelId: "gemini-pro", tokensIn: 300, tokensOut: 30 }), + ]) + expect(result.map((r) => r.apiProvider)).toEqual([ + providerIdentifiers.githubCopilot, + providerIdentifiers.gemini, + ]) + }) + + it("sums cache counters only when reported", () => { + const [entry] = summarizeUsageByModel([ + req({ + apiProvider: providerIdentifiers.anthropic, + modelId: "m", + tokensIn: 1, + tokensOut: 1, + cacheReads: 5, + cacheWrites: 2, + }), + req({ apiProvider: providerIdentifiers.anthropic, modelId: "m", tokensIn: 1, tokensOut: 1, cacheReads: 3 }), + ]) + expect(entry.cacheReads).toBe(8) + expect(entry.cacheWrites).toBe(2) + + const [bare] = summarizeUsageByModel([req({ apiProvider: "a", modelId: "m", tokensIn: 1, tokensOut: 1 })]) + expect(bare).not.toHaveProperty("cacheReads") + }) + + it("puts requests recorded before this was tracked in an unrecorded entry rather than guessing", () => { + const [entry] = summarizeUsageByModel([req({ apiProtocol: "openai", tokensIn: 10, tokensOut: 5, cost: 0 })]) + expect(entry).toMatchObject({ apiProvider: undefined, modelId: undefined, requests: 1, tokensIn: 10 }) + }) + + it("ignores in-flight placeholders and non-request messages", () => { + const result = summarizeUsageByModel([ + req({ apiProtocol: "openai", apiProvider: providerIdentifiers.gemini, modelId: "m" }), // no counts yet + { ts: 2, type: "say", say: "text", text: "hello" }, + { ts: 3, type: "say", say: "api_req_started", text: "not json" }, + { ts: 4, type: "say", say: "api_req_started" }, + ]) + expect(result).toEqual([]) + }) + + it("returns an empty list for no messages", () => { + expect(summarizeUsageByModel([])).toEqual([]) + }) +}) diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts index c52b0bdc33..827095f67a 100644 --- a/src/core/task-persistence/taskMetadata.ts +++ b/src/core/task-persistence/taskMetadata.ts @@ -5,6 +5,7 @@ import type { ClineMessage, HistoryItem } from "@roo-code/types" import { combineApiRequests } from "../../shared/combineApiRequests" import { combineCommandSequences } from "../../shared/combineCommandSequences" +import { summarizeUsageByModel } from "./usageByModel" import { getApiMetrics } from "../../shared/getApiMetrics" import { findLastIndex } from "../../shared/array" import { getTaskDirectoryPath } from "../../utils/storage" @@ -89,6 +90,8 @@ export async function taskMetadata({ } } + const usageByModel = hasMessages ? summarizeUsageByModel(messages) : [] + // Create historyItem once with pre-calculated values. // initialStatus is included when provided (e.g., "active" for child tasks) // to ensure the status is set from the very first save, avoiding race conditions @@ -107,6 +110,8 @@ export async function taskMetadata({ cacheWrites: tokenUsage.totalCacheWrites, cacheReads: tokenUsage.totalCacheReads, totalCost: tokenUsage.totalCost, + // Only emitted when something was recorded, so older tasks and empty tasks keep their exact shape. + ...(usageByModel.length > 0 ? { usageByModel } : {}), size: taskDirSize, workspace, mode, diff --git a/src/core/task-persistence/usageByModel.ts b/src/core/task-persistence/usageByModel.ts new file mode 100644 index 0000000000..16027c281e --- /dev/null +++ b/src/core/task-persistence/usageByModel.ts @@ -0,0 +1,51 @@ +import type { ClineMessage, UsageByModelEntry } from "@roo-code/types" + +/** + * Totals per provider+model from a task's `api_req_started` messages. + * + * Provider and model are read from the message itself (recorded when the + * request was made), never from the *current* profile, so a task that ran on + * Copilot keeps saying so after the user switches providers. A task can span + * several models, hence a list. + * + * Messages written before this was recorded carry no provider/model; their + * tokens are grouped under an entry with both fields undefined ("not + * recorded") rather than guessed. + */ +export function summarizeUsageByModel(messages: ClineMessage[]): UsageByModelEntry[] { + const byKey = new Map() + + for (const message of messages) { + if (message.type !== "say" || message.say !== "api_req_started" || !message.text) continue + + let parsed: Record + try { + parsed = JSON.parse(message.text) + } catch { + continue + } + + // A placeholder (request not finished) has no token counts yet. + if (typeof parsed.tokensIn !== "number" && typeof parsed.tokensOut !== "number") continue + + const apiProvider = typeof parsed.apiProvider === "string" ? parsed.apiProvider : undefined + const modelId = typeof parsed.modelId === "string" ? parsed.modelId : undefined + const key = `${apiProvider ?? ""}\u0000${modelId ?? ""}` + + let entry = byKey.get(key) + if (!entry) { + entry = { apiProvider, modelId, requests: 0, tokensIn: 0, tokensOut: 0, cost: 0 } + byKey.set(key, entry) + } + + const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : 0) + entry.requests += 1 + entry.tokensIn += num(parsed.tokensIn) + entry.tokensOut += num(parsed.tokensOut) + entry.cost += num(parsed.cost) + if (typeof parsed.cacheReads === "number") entry.cacheReads = (entry.cacheReads ?? 0) + parsed.cacheReads + if (typeof parsed.cacheWrites === "number") entry.cacheWrites = (entry.cacheWrites ?? 0) + parsed.cacheWrites + } + + return [...byKey.values()] +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e573a7f107..d5e8cc7b79 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -3936,6 +3936,9 @@ export class Task extends EventEmitter implements TaskLike { "api_req_started", JSON.stringify({ apiProtocol, + // Recorded now so Usage reflects what served this request, not the profile selected later. + apiProvider, + modelId, }), ) @@ -4013,6 +4016,8 @@ export class Task extends EventEmitter implements TaskLike { this.clineMessages[lastApiReqIndex].text = JSON.stringify({ apiProtocol, + apiProvider, + modelId, } satisfies ClineApiReqInfo) await this.saveClineMessages() diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 1c7f16a29f..4a74593021 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -7,10 +7,13 @@ import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { formatLargeNumber, formatTimeAgo } from "@src/utils/format" import { + UNRECORDED, USAGE_RANGES, type UsageMetric, type UsageRange, + modelIdFromKey, pickMetric, + primaryUsage, summarizeUsage, usageToCsv, } from "@src/utils/usage" @@ -52,32 +55,21 @@ const DAY_FMT = new Intl.DateTimeFormat(undefined, { day: "numeric" }) */ export const UsageSettings = ({ className, ...props }: HTMLAttributes) => { const { t } = useAppTranslation() - const { taskHistory, cwd, listApiConfigMeta } = useExtensionState() + const { taskHistory, cwd } = useExtensionState() const [range, setRange] = useState("7d") const [scope, setScope] = useState<"workspace" | "all">("workspace") const workspace = scope === "workspace" ? cwd : undefined - // History stores the profile name only; resolve provider/model from the - // current profile list. Renamed or deleted profiles fall back to "unknown". - const profileMeta = useMemo(() => new Map((listApiConfigMeta ?? []).map((p) => [p.name, p])), [listApiConfigMeta]) - + // Provider and model come from what each request recorded, never from the + // currently selected profile, so switching providers doesn't relabel history. const summary = useMemo( - () => - summarizeUsage(taskHistory ?? [], { - range, - workspace, - providerForProfile: (name) => profileMeta.get(name)?.apiProvider, - }), - [taskHistory, range, workspace, profileMeta], + () => summarizeUsage(taskHistory ?? [], { range, workspace }), + [taskHistory, range, workspace], ) - const profileSubtitle = (name: string) => { - const meta = profileMeta.get(name) - if (!meta?.apiProvider) return t("settings:usage.unknownProvider") - return [providerLabel(meta.apiProvider), meta.modelId].filter(Boolean).join(" · ") - } + const providerName = (key: string) => (key === UNRECORDED ? t("settings:usage.notRecorded") : providerLabel(key)) const metric: UsageMetric = pickMetric(summary) const value = (cost: number, tokens: number) => (metric === "cost" ? cost : tokens) @@ -254,10 +246,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ({ key: b.key, - name: - b.key === "unknown" - ? t("settings:usage.unknownProvider") - : providerLabel(b.key), + name: providerName(b.key), value: value(b.cost, b.tokens), label: fmt(b.cost, b.tokens), }))} @@ -265,16 +254,16 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ({ + title={t("settings:usage.byModel")} + rows={summary.byModel.map((b) => ({ key: b.key, - name: b.key, - subtitle: profileSubtitle(b.key), + name: modelIdFromKey(b.key) ?? t("settings:usage.notRecorded"), + subtitle: b.apiProvider ? providerLabel(b.apiProvider) : undefined, value: value(b.cost, b.tokens), label: fmt(b.cost, b.tokens), }))} total={total} - testId="usage-by-profile" + testId="usage-by-model" /> vscode.postMessage({ type: "showTaskWithId", text: task.id }) }> - {task.task} + + {task.task} + {primaryUsage(task) && ( + + {[ + primaryUsage(task)?.apiProvider + ? providerLabel(primaryUsage(task)!.apiProvider!) + : undefined, + primaryUsage(task)?.modelId, + ] + .filter(Boolean) + .join(" · ")} + + )} + {fmt( diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 423928a627..3f9f2ac7c5 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index b53c129ea9..adebc1b033 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 77bfc7ce50..45bdf39d32 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 54e7817ae9..0ddd8ec224 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 8b18463ffd..7aa7cdc4dc 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index afbccebbeb..b643ef4139 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 684f92d8b3..f0bfd5aa49 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 57d2396d3f..9dcd24a556 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 3f1f0bbc93..ac67d19a10 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 6fd695dab2..0e03aa9c91 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 4e537e4ebe..b846992ecd 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 762bd5a364..0492aec7cb 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index bc30a368cd..0e34d1c32e 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index f2935e1870..47e4babb24 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 0f2143ec06..be04174123 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 4830f017f5..9f8bb2167d 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 374deb165e..4f79a40c7d 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 75381a5a99..b67347098e 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1296,7 +1296,6 @@ "inOut": "{{input}} in · {{output}} out" }, "dailyCost": "Daily cost", - "byProfile": "By API profile", "byMode": "By mode", "topTasks": "Most expensive tasks", "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", @@ -1305,7 +1304,6 @@ "dailyTokens": "Daily tokens", "topTasksTokens": "Largest tasks", "byProvider": "By provider", - "unknownProvider": "Unknown provider", "history": "Usage history", "limits": { "title": "Provider limits", @@ -1318,6 +1316,8 @@ "noKey": "API key missing or invalid", "unsupported": "Custom base URL — not checked", "error": "Couldn't load" - } + }, + "byModel": "By model", + "notRecorded": "Not recorded" } } diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 5ae66b7a6f..10317f5184 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -1,6 +1,6 @@ -import type { HistoryItem } from "@roo-code/types" +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" -import { pickMetric, rangeStart, summarizeUsage, usageToCsv } from "../usage" +import { UNRECORDED, modelIdFromKey, pickMetric, primaryUsage, rangeStart, summarizeUsage, usageToCsv } from "../usage" // Fixed "now": Wed 2026-10-14 15:00 local time. const NOW = new Date(2026, 9, 14, 15, 0, 0).getTime() @@ -35,12 +35,41 @@ describe("rangeStart", () => { }) describe("summarizeUsage", () => { + const used = (apiProvider: string, modelId: string, tokensIn: number, tokensOut: number, cost: number) => [ + { apiProvider, modelId, requests: 1, tokensIn, tokensOut, cost }, + ] const history = [ - item({ totalCost: 1.5, tokensIn: 1000, tokensOut: 200, cacheReads: 3000, apiConfigName: "opus", mode: "code" }), - item({ totalCost: 0.5, tokensIn: 500, tokensOut: 100, apiConfigName: "gpt", mode: "architect", ts: NOW - DAY }), - item({ totalCost: 2, tokensIn: 10, tokensOut: 10, apiConfigName: "opus", mode: "code", workspace: "/ws/b" }), + item({ + totalCost: 1.5, + tokensIn: 1000, + tokensOut: 200, + cacheReads: 3000, + mode: "code", + usageByModel: used(providerIdentifiers.anthropic, "opus", 1000, 200, 1.5), + }), + item({ + totalCost: 0.5, + tokensIn: 500, + tokensOut: 100, + mode: "architect", + ts: NOW - DAY, + usageByModel: used(providerIdentifiers.githubCopilot, "gpt-5", 500, 100, 0.5), + }), + item({ + totalCost: 2, + tokensIn: 10, + tokensOut: 10, + mode: "code", + workspace: "/ws/b", + usageByModel: used(providerIdentifiers.anthropic, "opus", 10, 10, 2), + }), // Outside 7d window - item({ totalCost: 99, apiConfigName: "opus", mode: "code", ts: NOW - 10 * DAY }), + item({ + totalCost: 99, + mode: "code", + ts: NOW - 10 * DAY, + usageByModel: used(providerIdentifiers.anthropic, "opus", 0, 0, 99), + }), ] it("totals only items inside the range", () => { @@ -56,19 +85,81 @@ describe("summarizeUsage", () => { expect(s.totalCost).toBeCloseTo(2) }) - it("groups by profile and mode sorted by cost desc", () => { + it("groups by recorded provider and by model, sorted by cost desc", () => { const s = summarizeUsage(history, { range: "7d", now: NOW }) - expect(s.byProfile.map((b) => [b.key, b.tasks])).toEqual([ - ["opus", 2], - ["gpt", 1], + expect(s.byProvider.map((b) => [b.key, b.tasks])).toEqual([ + [providerIdentifiers.anthropic, 2], + [providerIdentifiers.githubCopilot, 1], + ]) + expect(s.byProvider[0].cost).toBeCloseTo(3.5) + expect(s.byModel.map((b) => [modelIdFromKey(b.key), b.apiProvider])).toEqual([ + ["opus", providerIdentifiers.anthropic], + ["gpt-5", providerIdentifiers.githubCopilot], ]) - expect(s.byProfile[0].cost).toBeCloseTo(3.5) expect(s.byMode.map((b) => b.key)).toEqual(["code", "architect"]) }) - it("buckets missing profile/mode under fallbacks", () => { - const s = summarizeUsage([item({ totalCost: 1 })], { range: "all", now: NOW }) - expect(s.byProfile[0].key).toBe("default") + it("REGRESSION: provider/model come from what was recorded, not from any current profile", () => { + // The bug: usage was attributed via the *currently selected* profile, so + // switching providers relabelled past Copilot work as the new provider. + // Aggregation now takes no profile information at all. + const copilotTask = item({ + tokensIn: 16_700_000, + tokensOut: 83_400, + usageByModel: used(providerIdentifiers.githubCopilot, "claude-opus-5.5", 16_700_000, 83_400, 0), + apiConfigName: "default", // the profile name is irrelevant to attribution + }) + const before = summarizeUsage([copilotTask], { range: "all", now: NOW }) + + // "Switch provider": same history, but the user's profile now points at Gemini. + const after = summarizeUsage([{ ...copilotTask, apiConfigName: "gemini-profile" }], { range: "all", now: NOW }) + + for (const s of [before, after]) { + expect(s.byProvider.map((b) => b.key)).toEqual([providerIdentifiers.githubCopilot]) + expect(modelIdFromKey(s.byModel[0].key)).toBe("claude-opus-5.5") + } + }) + + it("attributes a task that switched providers mid-way to each provider by its own tokens", () => { + const s = summarizeUsage( + [ + item({ + tokensIn: 400, + tokensOut: 0, + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 100, + tokensOut: 0, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + requests: 1, + tokensIn: 300, + tokensOut: 0, + cost: 0, + }, + ], + }), + ], + { range: "all", now: NOW }, + ) + expect(s.byProvider.map((b) => [b.key, b.tokens])).toEqual([ + [providerIdentifiers.gemini, 300], + [providerIdentifiers.githubCopilot, 100], + ]) + expect(s.byProvider.every((b) => b.tasks === 1)).toBe(true) + }) + + it("buckets tasks with no recorded provider/model as unrecorded instead of guessing", () => { + const s = summarizeUsage([item({ totalCost: 1, tokensIn: 10, tokensOut: 5 })], { range: "all", now: NOW }) + expect(s.byProvider.map((b) => b.key)).toEqual([UNRECORDED]) + expect(s.byModel.map((b) => b.key)).toEqual([UNRECORDED]) + expect(modelIdFromKey(UNRECORDED)).toBeUndefined() expect(s.byMode[0].key).toBe("unknown") }) @@ -111,26 +202,28 @@ describe("summarizeUsage", () => { expect(s.topTasks.map((t) => t.id)).toEqual(["big", "small"]) }) - it("groups by provider using the profile resolver, bucketing unknown profiles", () => { - const providers: Record = { opus: "anthropic", gpt: "github-copilot" } - const s = summarizeUsage([...history, item({ apiConfigName: "deleted-profile", tokensIn: 1 })], { - range: "7d", - now: NOW, - providerForProfile: (p) => providers[p], - }) - expect(s.byProvider.map((b) => b.key)).toEqual(["anthropic", "github-copilot", "unknown"]) - expect(s.byProvider[0].tasks).toBe(2) + it("ignores non-finite costs", () => { + const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) + expect(s.totalCost).toBe(0) }) +}) - it("buckets everything as unknown provider when no resolver is given", () => { - const s = summarizeUsage(history, { range: "7d", now: NOW }) - expect(s.byProvider).toHaveLength(1) - expect(s.byProvider[0].key).toBe("unknown") +describe("primaryUsage", () => { + it("returns the provider+model that used the most tokens", () => { + const task = item({ + usageByModel: [ + { apiProvider: "a", modelId: "small", requests: 1, tokensIn: 10, tokensOut: 0, cost: 0 }, + { apiProvider: "b", modelId: "big", requests: 1, tokensIn: 900, tokensOut: 0, cost: 0 }, + ], + }) + expect(primaryUsage(task)).toEqual({ apiProvider: "b", modelId: "big" }) }) - it("ignores non-finite costs", () => { - const s = summarizeUsage([item({ totalCost: NaN })], { range: "all", now: NOW }) - expect(s.totalCost).toBe(0) + it("is undefined when nothing was recorded", () => { + expect(primaryUsage(item({}))).toBeUndefined() + expect( + primaryUsage(item({ usageByModel: [{ requests: 1, tokensIn: 1, tokensOut: 1, cost: 0 }] })), + ).toBeUndefined() }) }) @@ -158,7 +251,7 @@ describe("usageToCsv", () => { { range: "30d", now: NOW }, ) const lines = csv.split("\n") - expect(lines[0]).toBe("date,task,mode,profile,tokensIn,tokensOut,cacheReads,cacheWrites,cost") + expect(lines[0]).toBe("date,task,mode,provider,model,tokensIn,tokensOut,cacheReads,cacheWrites,cost") expect(lines).toHaveLength(2) expect(lines[1]).toContain('"say ""hi"" then go"') expect(lines[1].endsWith(",0.1235")).toBe(true) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 638bd490bd..c7d1d420d1 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -6,10 +6,15 @@ import type { HistoryItem } from "@roo-code/types" * Notes on correctness: * - `HistoryItem.totalCost` is a task's *own* spend; subtasks are separate * history items. Summing every item therefore counts each dollar once. - * - History stores the provider profile (`apiConfigName`), not the model id, - * so the breakdown is by profile. + * - Provider and model come from `HistoryItem.usageByModel`, recorded when each + * request was made. They are never derived from the *current* profile, so + * switching providers does not relabel past usage. Tasks created before this + * was recorded have no such data and are bucketed as `UNRECORDED`. */ +/** Bucket key for usage whose provider/model was not recorded (older tasks). */ +export const UNRECORDED = "__unrecorded__" + export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const export type UsageRange = (typeof USAGE_RANGES)[number] @@ -19,7 +24,10 @@ export interface UsageBucket { key: string cost: number tokens: number + /** Tasks that contributed to this bucket (a task using two models counts once in each). */ tasks: number + /** For model buckets: the provider that served it (undefined when unrecorded). */ + apiProvider?: string } export interface DailyUsage { @@ -51,7 +59,7 @@ export interface UsageSummary { cacheHitRate: number taskCount: number byProvider: UsageBucket[] - byProfile: UsageBucket[] + byModel: UsageBucket[] byMode: UsageBucket[] daily: DailyUsage[] topTasks: HistoryItem[] @@ -63,12 +71,17 @@ export interface UsageOptions { workspace?: string now?: number topTaskLimit?: number - /** - * Resolves a profile name (`HistoryItem.apiConfigName`) to its provider id. - * History doesn't store the provider, so the caller supplies this from the - * current profile list. Unknown/deleted profiles bucket as "unknown". - */ - providerForProfile?: (profile: string) => string | undefined +} + +/** + * The provider+model that did most of a task's work (by tokens), for labelling + * a task row. Undefined when nothing was recorded. + */ +export function primaryUsage(item: HistoryItem): { apiProvider?: string; modelId?: string } | undefined { + const entries = item.usageByModel?.filter((e) => e.apiProvider || e.modelId) + if (!entries?.length) return undefined + const top = entries.reduce((a, b) => (b.tokensIn + b.tokensOut > a.tokensIn + a.tokensOut ? b : a)) + return { apiProvider: top.apiProvider, modelId: top.modelId } } function startOfLocalDay(ts: number): number { @@ -93,11 +106,22 @@ export function rangeStart(range: UsageRange, now: number): number | undefined { } } -function addTo(map: Map, key: string, cost: number, tokens: number) { - const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0 } +function addTo( + map: Map, + key: string, + cost: number, + tokens: number, + countedForTask: Set, + extra?: Partial, +) { + const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0, ...extra } bucket.cost += cost bucket.tokens += tokens - bucket.tasks += 1 + // Count the task once per bucket even if several of its requests land here. + if (!countedForTask.has(key)) { + countedForTask.add(key) + bucket.tasks += 1 + } map.set(key, bucket) } @@ -116,7 +140,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U ) const providers = new Map() - const profiles = new Map() + const models = new Map() const modes = new Map() const daily = new Map() @@ -138,10 +162,28 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U cacheReads += item.cacheReads || 0 cacheWrites += item.cacheWrites || 0 - const profile = item.apiConfigName || "default" - addTo(providers, options.providerForProfile?.(profile) || "unknown", cost, tokens) - addTo(profiles, profile, cost, tokens) - addTo(modes, item.mode || "unknown", cost, tokens) + // Provider/model from what was recorded per request. A task with no record + // (created before this was tracked) goes to a single "unrecorded" bucket. + const seenProviders = new Set() + const seenModels = new Set() + const entries = item.usageByModel?.length + ? item.usageByModel + : [ + { requests: 0, tokensIn: tIn, tokensOut: tOut, cost } as NonNullable< + HistoryItem["usageByModel"] + >[number], + ] + + for (const e of entries) { + const entryTokens = e.tokensIn + e.tokensOut + const providerKey = e.apiProvider || UNRECORDED + const modelKey = e.modelId ? `${e.apiProvider ?? ""}\u0000${e.modelId}` : UNRECORDED + addTo(providers, providerKey, e.cost, entryTokens, seenProviders) + addTo(models, modelKey, e.cost, entryTokens, seenModels, { apiProvider: e.apiProvider }) + } + + const modeSeen = new Set() + addTo(modes, item.mode || "unknown", cost, tokens, modeSeen) const day = startOfLocalDay(item.ts) const entry = daily.get(day) ?? { cost: 0, tokens: 0 } @@ -178,7 +220,7 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U cacheHitRate: inputSide > 0 ? cacheReads / inputSide : 0, taskCount: items.length, byProvider: [...providers.values()].sort(byCostDesc), - byProfile: [...profiles.values()].sort(byCostDesc), + byModel: [...models.values()].sort(byCostDesc), byMode: [...modes.values()].sort(byCostDesc), daily: dailySeries, // Cost first, tokens as tie-breaker so zero-cost (subscription) usage still ranks meaningfully. @@ -192,10 +234,28 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U } } +/** The model id from a `byModel` bucket key (or undefined for the unrecorded bucket). */ +export function modelIdFromKey(key: string): string | undefined { + if (key === UNRECORDED) return undefined + const i = key.indexOf("\u0000") + return i === -1 ? key : key.slice(i + 1) +} + /** Serialises the filtered usage rows as CSV (one row per task). */ export function usageToCsv(history: HistoryItem[], options: UsageOptions): string { const start = rangeStart(options.range, options.now ?? Date.now()) - const header = ["date", "task", "mode", "profile", "tokensIn", "tokensOut", "cacheReads", "cacheWrites", "cost"] + const header = [ + "date", + "task", + "mode", + "provider", + "model", + "tokensIn", + "tokensOut", + "cacheReads", + "cacheWrites", + "cost", + ] const escape = (value: string) => `"${value.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` @@ -211,7 +271,8 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin new Date(item.ts).toISOString(), escape(item.title || item.task || ""), escape(item.mode || ""), - escape(item.apiConfigName || ""), + escape(primaryUsage(item)?.apiProvider ?? ""), + escape(primaryUsage(item)?.modelId ?? ""), item.tokensIn || 0, item.tokensOut || 0, item.cacheReads || 0, From c24f6d2d04b203a9a773c5f5e2cd5e27504f15ee Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 13:51:18 -0700 Subject: [PATCH 07/25] feat(usage): compact layout from the design mock; broad test coverage for new code Usage tab layout (follows the mock) - three compact stat cards (cost|tokens, tokens|models, tasks) - breakdown rows on one line: dot, name (+provider), value, bar, % - 'By mode' as a single summary line - tighter vertical rhythm between sections Fixes found while testing - NumberStepper: an emptied input reverted to the minimum instead of the current value Tests - NumberStepper, ProviderLimits, UsageSettings (cost vs tokens, empty, unrecorded, filters, export, interactions), UISettings steppers + live preview, Language time format - copilotQuota fetch/auth/cache/error paths, taskMetadata usageByModel - coverage of new/changed code: webview 97% stmts, host 96% stmts --- .../taskMetadata.usageByModel.spec.ts | 82 +++++++ .../__tests__/copilotQuota.spec.ts | 104 ++++++++- .../src/components/settings/NumberStepper.tsx | 5 + .../src/components/settings/UsageSettings.tsx | 122 +++++----- .../LanguageSettings.timeFormat.spec.tsx | 40 ++++ .../settings/__tests__/NumberStepper.spec.tsx | 132 +++++++++++ .../__tests__/ProviderLimits.spec.tsx | 113 ++++++++++ .../settings/__tests__/UISettings.spec.tsx | 63 ++++++ .../settings/__tests__/UsageSettings.spec.tsx | 210 ++++++++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 3 +- webview-ui/src/i18n/locales/de/settings.json | 3 +- webview-ui/src/i18n/locales/en/settings.json | 3 +- webview-ui/src/i18n/locales/es/settings.json | 3 +- webview-ui/src/i18n/locales/fr/settings.json | 3 +- webview-ui/src/i18n/locales/hi/settings.json | 3 +- webview-ui/src/i18n/locales/id/settings.json | 3 +- webview-ui/src/i18n/locales/it/settings.json | 3 +- webview-ui/src/i18n/locales/ja/settings.json | 3 +- webview-ui/src/i18n/locales/ko/settings.json | 3 +- webview-ui/src/i18n/locales/nl/settings.json | 3 +- webview-ui/src/i18n/locales/pl/settings.json | 3 +- .../src/i18n/locales/pt-BR/settings.json | 3 +- webview-ui/src/i18n/locales/ru/settings.json | 3 +- webview-ui/src/i18n/locales/tr/settings.json | 3 +- webview-ui/src/i18n/locales/vi/settings.json | 3 +- .../src/i18n/locales/zh-CN/settings.json | 3 +- .../src/i18n/locales/zh-TW/settings.json | 3 +- 27 files changed, 849 insertions(+), 76 deletions(-) create mode 100644 src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts create mode 100644 webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/NumberStepper.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx diff --git a/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts new file mode 100644 index 0000000000..a16e27259d --- /dev/null +++ b/src/core/task-persistence/__tests__/taskMetadata.usageByModel.spec.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type ClineMessage } from "@roo-code/types" + +vi.mock("get-folder-size", () => ({ default: { loose: vi.fn(async () => 1234) } })) +vi.mock("../../../utils/storage", () => ({ getTaskDirectoryPath: vi.fn(async () => "/tmp/task") })) +vi.mock("../../../i18n", () => ({ t: (k: string) => k })) + +import { taskMetadata } from "../taskMetadata" + +const say = (say: ClineMessage["say"], text: string, ts: number): ClineMessage => ({ ts, type: "say", say, text }) + +const base = { + taskId: "t1", + taskNumber: 1, + globalStoragePath: "/tmp", + workspace: "/ws", +} + +describe("taskMetadata – usageByModel", () => { + it("records per provider+model usage on the history item", async () => { + const messages = [ + say("text", "do the thing", 1), + say( + "api_req_started", + JSON.stringify({ + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + tokensIn: 900, + tokensOut: 90, + cost: 0, + }), + 2, + ), + say( + "api_req_started", + JSON.stringify({ + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + tokensIn: 100, + tokensOut: 10, + cost: 0.2, + }), + 3, + ), + ] + const { historyItem } = await taskMetadata({ ...base, messages }) + + expect(historyItem.usageByModel).toEqual([ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 900, + tokensOut: 90, + cost: 0, + }, + { + apiProvider: providerIdentifiers.gemini, + modelId: "gemini-pro", + requests: 1, + tokensIn: 100, + tokensOut: 10, + cost: 0.2, + }, + ]) + // Existing totals are unchanged by the new breakdown. + expect(historyItem.tokensIn).toBe(1000) + expect(historyItem.totalCost).toBeCloseTo(0.2) + }) + + it("omits usageByModel entirely when nothing was recorded, keeping old items' shape", async () => { + const { historyItem } = await taskMetadata({ ...base, messages: [say("text", "hello", 1)] }) + expect(historyItem).not.toHaveProperty("usageByModel") + }) + + it("omits usageByModel for a task with no messages", async () => { + const { historyItem } = await taskMetadata({ ...base, messages: [] }) + expect(historyItem).not.toHaveProperty("usageByModel") + expect(historyItem.tokensIn).toBe(0) + }) +}) diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index ea37c04f02..08a9b4a688 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest" +import { beforeEach, describe, expect, it, vi } from "vitest" vi.mock("vscode", () => ({ version: "1.0.0", authentication: { getSession: vi.fn() } })) @@ -79,3 +79,105 @@ describe("parseCopilotQuota", () => { expect(parseCopilotQuota(payload, NOW)).toBeUndefined() }) }) + +// --- getGitHubCopilotQuota (network + auth paths) ---------------------------------------------- + +import * as vscodeApi from "vscode" +import { getGitHubCopilotQuota, resetGitHubCopilotQuotaCache } from "../copilotQuota" + +const getSession = vscodeApi.authentication.getSession as unknown as ReturnType + +const json = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status }) + +describe("getGitHubCopilotQuota", () => { + beforeEach(() => { + resetGitHubCopilotQuotaCache() + getSession.mockReset() + getSession.mockResolvedValue({ accessToken: "gho_token" }) + }) + + it("returns the parsed quota using the existing VS Code GitHub session, silently", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + const result = await getGitHubCopilotQuota({}, fetchSpy as never) + + expect(result).toMatchObject({ ok: true, quota: { plan: "business", entitlement: 140000 } }) + // Silent: must never raise a sign-in prompt from a background usage refresh. + expect(getSession).toHaveBeenCalledWith("github", expect.any(Array), { silent: true }) + expect(fetchSpy).toHaveBeenCalledWith( + "https://api.github.com/copilot_internal/user", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "token gho_token" }), + }), + ) + }) + + it("falls back to the second scope set when the first has no session", async () => { + getSession.mockResolvedValueOnce(undefined).mockResolvedValueOnce({ accessToken: "second" }) + const fetchSpy = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, fetchSpy as never) + expect(getSession).toHaveBeenCalledTimes(2) + expect(fetchSpy).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ headers: expect.objectContaining({ Authorization: "token second" }) }), + ) + }) + + it("reports signedOut without calling GitHub when there is no session", async () => { + getSession.mockResolvedValue(undefined) + const fetchSpy = vi.fn() + expect(await getGitHubCopilotQuota({}, fetchSpy as never)).toEqual({ ok: false, reason: "signedOut" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it.each([ + [401, "signedOut"], + [403, "noCopilot"], + [404, "noCopilot"], + ] as const)("maps HTTP %s to %s", async (status, reason) => { + const result = await getGitHubCopilotQuota({}, (async () => json({}, status)) as never) + expect(result).toMatchObject({ ok: false, reason }) + }) + + it("reports other HTTP failures as an error with the status", async () => { + const result = await getGitHubCopilotQuota({}, (async () => json({}, 500)) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "GitHub API error 500" }) + }) + + it("reports noCopilot when the payload has no premium snapshot", async () => { + const result = await getGitHubCopilotQuota({}, (async () => json({ quota_snapshots: {} })) as never) + expect(result).toMatchObject({ ok: false, reason: "noCopilot" }) + }) + + it("turns network errors into an error result instead of throwing", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw new Error("offline") + }) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "offline" }) + }) + + it("caches successful results and bypasses the cache when forced", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + await getGitHubCopilotQuota({}, fetchSpy as never) + await getGitHubCopilotQuota({}, fetchSpy as never) + expect(fetchSpy).toHaveBeenCalledTimes(1) + await getGitHubCopilotQuota({ force: true }, fetchSpy as never) + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + + it("does not cache failures", async () => { + const fail = vi.fn(async () => json({}, 500)) + await getGitHubCopilotQuota({}, fail as never) + await getGitHubCopilotQuota({}, fail as never) + expect(fail).toHaveBeenCalledTimes(2) + }) + + it("shares one in-flight request between concurrent callers", async () => { + const fetchSpy = vi.fn(async () => json(businessPayload)) + const [a, b] = await Promise.all([ + getGitHubCopilotQuota({}, fetchSpy as never), + getGitHubCopilotQuota({}, fetchSpy as never), + ]) + expect(fetchSpy).toHaveBeenCalledTimes(1) + expect(a).toEqual(b) + }) +}) diff --git a/webview-ui/src/components/settings/NumberStepper.tsx b/webview-ui/src/components/settings/NumberStepper.tsx index ac2b7a922f..aa4063b692 100644 --- a/webview-ui/src/components/settings/NumberStepper.tsx +++ b/webview-ui/src/components/settings/NumberStepper.tsx @@ -40,6 +40,11 @@ export const NumberStepper = ({ setDraft(String(current)) return } + // An emptied box means "no entry", not 0: revert instead of clamping to the minimum. + if (draft.trim() === "" && next === 0) { + setDraft(String(current)) + return + } const clamped = Math.min(max, Math.max(min, Math.round(next))) setDraft(String(clamped)) // Committing the default clears the override so VS Code changes still flow through. diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 4a74593021..1ebf5ecfa0 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -95,10 +95,10 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes{t("settings:sections.usage")}
-
+
-
+
{t("settings:usage.history")} {!isEmpty && ( @@ -163,30 +163,43 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ) : ( <> - {/* Stat strip — cost only when a provider actually reported a price */} -
- {metric === "cost" && ( + {/* Three compact stat cards, as in the design mock */} +
+ {metric === "cost" ? ( + ) : ( + + )} + {metric === "cost" ? ( + 0 + ? t("settings:usage.stats.cacheHit", { + percent: Math.round(summary.cacheHitRate * 100), + }) + : undefined + } + /> + ) : ( + m.key !== UNRECORDED).length || "–", + )} + label={t("settings:usage.stats.models")} + /> )} - 0 - ? t("settings:usage.stats.cacheHit", { - percent: Math.round(summary.cacheHitRate * 100), - }) - : t("settings:usage.stats.inOut", { - input: formatLargeNumber(summary.tokensIn), - output: formatLargeNumber(summary.tokensOut), - }) - } - />
@@ -265,17 +278,12 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes - ({ - key: b.key, - name: b.key, - value: value(b.cost, b.tokens), - label: fmt(b.cost, b.tokens), - }))} - total={total} - testId="usage-by-mode" - /> +

+ {t("settings:usage.byMode")}:{" "} + {summary.byMode.map((m) => `${m.key} ${fmt(m.cost, m.tokens)}`).join(" · ")} +

( -
-
{value}
-
{hint ?? label}
- {hint &&
{label}
} +
+
{value}
+
{label}
+ {hint &&
{hint}
}
) @@ -367,30 +375,30 @@ const Breakdown = ({ testId: string }) => ( -
- {rows.slice(0, 6).map((row, i) => { +
+ {rows.slice(0, 5).map((row, i) => { const pct = total > 0 ? Math.round((row.value / total) * 100) : 0 const color = PALETTE[i % PALETTE.length] return ( -
-
- - - {row.name} - {row.subtitle && ( - - {row.subtitle} - - )} - - - {row.label} - {pct}% - -
-
-
-
+
+ + + {row.name} + {row.subtitle && ( + {row.subtitle} + )} + + {row.label} + + + + {pct}%
) })} diff --git a/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx new file mode 100644 index 0000000000..beb82b36e3 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx @@ -0,0 +1,40 @@ +import { fireEvent, render, screen } from "@testing-library/react" +import { describe, expect, it, vi } from "vitest" + +import { LanguageSettings } from "../LanguageSettings" + +// The shared translation mock drops interpolation values; interpolate like i18next does +// so the preview text (the thing under test) is observable. +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, options?: Record) => + options ? Object.entries(options).reduce((acc, [k, v]) => `${acc} ${k}=${v}`, key) : key, + }), +})) + +describe("LanguageSettings – time format", () => { + it("defaults to auto and shows a live example", () => { + render() + expect(screen.getByTestId("time-format-select")).toBeTruthy() + expect(screen.getByTestId("time-format-preview").textContent).toMatch(/\d{1,2}:41/) + }) + + it("previews 24-hour time without an AM/PM marker", () => { + render() + const text = screen.getByTestId("time-format-preview").textContent! + expect(text).toContain("14:41") + // Look only at the formatted example (after "example="), not the translation key text. + expect(text.split("example=")[1]).not.toMatch(/\b(AM|PM)\b/i) + }) + + it("previews 12-hour time with an AM/PM marker", () => { + render() + expect(screen.getByTestId("time-format-preview").textContent).toMatch(/2:41\s?PM/i) + }) + + it("keeps the language selector", () => { + const { container } = render() + expect(container.querySelectorAll("[role=combobox]").length).toBeGreaterThanOrEqual(2) + fireEvent.click(screen.getByTestId("time-format-select")) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/NumberStepper.spec.tsx b/webview-ui/src/components/settings/__tests__/NumberStepper.spec.tsx new file mode 100644 index 0000000000..7408996c08 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/NumberStepper.spec.tsx @@ -0,0 +1,132 @@ +import { fireEvent, render, screen } from "@testing-library/react" +import { describe, expect, it, vi } from "vitest" + +import { APPEARANCE_SETTINGS } from "@/utils/appearance" + +import { NumberStepper } from "../NumberStepper" + +const setting = APPEARANCE_SETTINGS.terminalFontSize // 8–32 px, step 1, fallback 12 + +const setup = (props: Partial> = {}) => { + const onChange = vi.fn() + render( + , + ) + return { onChange, input: screen.getByTestId("t-input") as HTMLInputElement } +} + +describe("NumberStepper", () => { + it("shows the fallback when unset and hides reset", () => { + const { input } = setup() + expect(input.value).toBe("12") + expect(screen.getByTestId("t-reset").className).toContain("invisible") + }) + + it("shows the stored value and an active reset", () => { + const { input } = setup({ value: 16 }) + expect(input.value).toBe("16") + expect(screen.getByTestId("t-reset").className).not.toContain("invisible") + }) + + it("increments and decrements by the step", () => { + const { onChange } = setup({ value: 16 }) + fireEvent.click(screen.getByTestId("t-increment")) + expect(onChange).toHaveBeenLastCalledWith(17) + fireEvent.click(screen.getByTestId("t-decrement")) + expect(onChange).toHaveBeenLastCalledWith(15) + }) + + it("clears the override when stepping back to the default", () => { + const { onChange } = setup({ value: 13 }) + fireEvent.click(screen.getByTestId("t-decrement")) // 12 == fallback + expect(onChange).toHaveBeenLastCalledWith(undefined) + }) + + it("disables the buttons at the limits", () => { + setup({ value: setting.max }) + expect((screen.getByTestId("t-increment") as HTMLButtonElement).disabled).toBe(true) + expect((screen.getByTestId("t-decrement") as HTMLButtonElement).disabled).toBe(false) + }) + + it("disables decrement at the minimum", () => { + setup({ value: setting.min }) + expect((screen.getByTestId("t-decrement") as HTMLButtonElement).disabled).toBe(true) + }) + + it("does not commit while typing, then commits clamped on blur", () => { + const { onChange, input } = setup({ value: 16 }) + fireEvent.change(input, { target: { value: "999" } }) + expect(onChange).not.toHaveBeenCalled() + fireEvent.blur(input) + expect(onChange).toHaveBeenCalledWith(setting.max) + expect(input.value).toBe(String(setting.max)) + }) + + it("commits on Enter and strips non-digits", () => { + const { onChange, input } = setup({ value: 16 }) + fireEvent.change(input, { target: { value: "2a0" } }) + expect(input.value).toBe("20") + fireEvent.keyDown(input, { key: "Enter" }) + expect(onChange).toHaveBeenCalledWith(20) + }) + + it("supports arrow keys", () => { + const { onChange, input } = setup({ value: 16 }) + fireEvent.keyDown(input, { key: "ArrowUp" }) + expect(onChange).toHaveBeenLastCalledWith(17) + fireEvent.keyDown(input, { key: "ArrowDown" }) + expect(onChange).toHaveBeenLastCalledWith(15) + }) + + it("reverts an empty entry to the current value", () => { + const { onChange, input } = setup({ value: 16 }) + fireEvent.change(input, { target: { value: "" } }) + fireEvent.blur(input) + expect(input.value).toBe("16") + expect(onChange).not.toHaveBeenCalledWith(NaN) + }) + + it("reset clears the override", () => { + const { onChange } = setup({ value: 20 }) + fireEvent.click(screen.getByTestId("t-reset")) + expect(onChange).toHaveBeenCalledWith(undefined) + }) + + it("re-syncs the draft when the value changes from outside", () => { + const { rerender } = (() => { + const r = render( + , + ) + return r + })() + expect((screen.getByTestId("x-input") as HTMLInputElement).value).toBe("14") + rerender( + , + ) + expect((screen.getByTestId("x-input") as HTMLInputElement).value).toBe("20") + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx new file mode 100644 index 0000000000..9caea0810d --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -0,0 +1,113 @@ +import { act, fireEvent, render, screen } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type ProviderQuota } from "@roo-code/types" + +import { ProviderLimits } from "../ProviderLimits" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const respond = (providerQuotas: ProviderQuota[]) => + act(() => { + window.dispatchEvent(new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas } })) + }) + +const label = (id: string) => `Label:${id}` +const COPILOT = providerIdentifiers.githubCopilot + +describe("ProviderLimits", () => { + beforeEach(() => post.mockClear()) + + it("asks the extension for quotas on mount without forcing", () => { + render() + expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: false }) + }) + + it("shows a loading row until the first response", () => { + render() + expect(screen.getByTestId("provider-limits")).toBeTruthy() + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`)).toBeNull() + }) + + it("renders credits with a progress bar and reset date", () => { + render() + respond([ + { + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + fetchedAt: Date.now(), + windows: [ + { + id: "premium", + valueLabel: "39,389.3 / 140,000 used", + usedPercent: 28.1, + resetAt: Date.parse("2026-11-01T12:00:00Z"), + }, + ], + }, + ]) + + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`) + expect(row.textContent).toContain(`Label:${COPILOT}`) + expect(row.textContent).toContain("business") + expect(row.textContent).toContain("39,389.3 / 140,000 used") + expect(row.textContent).toContain("28%") + const bar = row.querySelector("[role=progressbar]")! + expect(bar.getAttribute("aria-valuenow")).toBe("28") + expect(row.textContent).toContain("resets") + }) + + it("omits the bar for balance-only providers", () => { + render() + respond([ + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: 1, + windows: [{ id: "balance", valueLabel: "$4.25 left" }], + }, + ]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.deepseek}`) + expect(row.textContent).toContain("$4.25 left") + expect(row.querySelector("[role=progressbar]")).toBeNull() + }) + + it.each(["noKey", "signedOut", "unsupported", "error"] as const)("shows a quiet status for %s", (reason) => { + render() + respond([{ providerId: providerIdentifiers.openrouter, ok: false, reason, fetchedAt: 1 }]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + expect(row.textContent).toContain(`settings:usage.limits.${reason}`.split(".").pop()!) + expect(row.querySelector("[role=progressbar]")).toBeNull() + }) + + it("hides the whole section when nothing connected has a usage API", () => { + render() + respond([]) + expect(screen.queryByTestId("provider-limits")).toBeNull() + }) + + it("refresh forces a new request", () => { + render() + respond([ + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: 1, + windows: [{ id: "b", valueLabel: "$1.00 left" }], + }, + ]) + post.mockClear() + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: true }) + }) + + it("ignores unrelated messages", () => { + render() + act(() => { + window.dispatchEvent(new MessageEvent("message", { data: { type: "somethingElse" } })) + }) + expect(screen.getByTestId("provider-limits")).toBeTruthy() + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 4e61f0fcff..245d50a05a 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -176,4 +176,67 @@ describe("UISettings", () => { }) }) }) + + describe("appearance steppers", () => { + const keys = ["interfaceFontScale", "terminalFontSize", "editorFontSize", "spacingScale"] as const + + it("renders all four size controls, in the design order, with defaults", () => { + const { getByTestId } = render() + const inputs = keys.map((k) => (getByTestId(`appearance-${k}-input`) as HTMLInputElement).value) + expect(inputs).toEqual(["100", "12", "13", "100"]) + }) + + it("shows the saved values", () => { + const { getByTestId } = render( + , + ) + expect((getByTestId("appearance-interfaceFontScale-input") as HTMLInputElement).value).toBe("120") + expect((getByTestId("appearance-terminalFontSize-input") as HTMLInputElement).value).toBe("14") + expect((getByTestId("appearance-editorFontSize-input") as HTMLInputElement).value).toBe("15") + expect((getByTestId("appearance-spacingScale-input") as HTMLInputElement).value).toBe("80") + }) + + it.each([ + ["interfaceFontScale", 105], + ["terminalFontSize", 13], + ["editorFontSize", 14], + ["spacingScale", 105], + ] as const)("stores %s as %s when incremented from the default", (key, expected) => { + const setCachedStateField = vi.fn() + const { getByTestId } = render() + fireEvent.click(getByTestId(`appearance-${key}-increment`)) + expect(setCachedStateField).toHaveBeenCalledWith(key, expected) + }) + + it("reset clears the stored value (undefined → null on save)", () => { + const setCachedStateField = vi.fn() + const { getByTestId } = render( + , + ) + fireEvent.click(getByTestId("appearance-terminalFontSize-reset")) + expect(setCachedStateField).toHaveBeenCalledWith("terminalFontSize", undefined) + }) + + it("records telemetry for changes", () => { + const { getByTestId } = render() + fireEvent.click(getByTestId("appearance-editorFontSize-increment")) + expect(telemetryClient.capture).toHaveBeenCalledWith("ui_settings_appearance_changed", { + key: "editorFontSize", + value: 14, + }) + }) + + it("previews the unsaved values live", () => { + const { getByTestId } = render() + const preview = getByTestId("appearance-preview") + expect(preview.style.getPropertyValue("--zoo-ui-scale")).toBe("1.5") + expect(preview.style.getPropertyValue("--zoo-spacing-scale")).toBe("0.5") + }) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx new file mode 100644 index 0000000000..3522aaf9cd --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -0,0 +1,210 @@ +import { fireEvent, render, screen, within } from "@testing-library/react" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) + +// The provider-limits section talks to the extension host; it has its own spec. +vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
})) + +const NOW = Date.now() + +let n = 0 +const task = (overrides: Partial = {}): HistoryItem => ({ + id: `t${++n}`, + number: n, + ts: NOW - 60_000, + task: `Task ${n}`, + tokensIn: 1000, + tokensOut: 100, + totalCost: 0, + workspace: "/ws/a", + mode: "code", + ...overrides, +}) + +const priced = (cost: number, overrides: Partial = {}) => + task({ + totalCost: cost, + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost, + }, + ], + ...overrides, + }) + +const copilot = (overrides: Partial = {}) => + task({ + usageByModel: [ + { + apiProvider: providerIdentifiers.githubCopilot, + modelId: "claude-opus-5.5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost: 0, + }, + ], + ...overrides, + }) + +describe("UsageSettings", () => { + beforeEach(() => { + post.mockClear() + state.taskHistory = [] + state.cwd = "/ws/a" + }) + + it("always renders the provider limits section", () => { + render() + expect(screen.getByTestId("provider-limits-stub")).toBeTruthy() + }) + + it("shows an empty state with no tasks", () => { + render() + expect(screen.getByTestId("usage-empty")).toBeTruthy() + expect(screen.queryByTestId("usage-stats")).toBeNull() + expect(screen.queryByTestId("usage-export-csv")).toBeNull() + }) + + describe("when a provider reports a price", () => { + beforeEach(() => { + state.taskHistory = [priced(1.25), priced(0.5)] + }) + + it("shows cost first and tokens second", () => { + render() + const stats = screen.getByTestId("usage-stats") + expect(stats.textContent).toContain("$1.75") + expect(stats.textContent).toContain("2.2") // 2 tasks x 1,100 tokens (suffix is a translation key in tests) + expect(screen.queryByTestId("usage-no-pricing-note")).toBeNull() + }) + + it("charts and ranks by cost", () => { + render() + const top = screen.getByTestId("usage-top-tasks") + expect(top.textContent).toContain("$1.25") + expect(within(screen.getByTestId("usage-by-provider")).getByText("settings:usage.byProvider")).toBeTruthy() + }) + }) + + describe("when no provider reports a price (e.g. GitHub Copilot)", () => { + beforeEach(() => { + state.taskHistory = [copilot()] + }) + + it("never shows a dollar figure and explains why", () => { + render() + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$") + expect(screen.getByTestId("usage-no-pricing-note")).toBeTruthy() + }) + + it("leads with tokens and the in/out split", () => { + render() + const stats = screen.getByTestId("usage-stats").textContent! + expect(stats).toContain("1.1") + expect(stats).toContain("settings:usage.stats.inOut".split(".").pop()!) + }) + }) + + describe("attribution", () => { + it("lists the provider and model that were recorded", () => { + state.taskHistory = [copilot()] + render() + expect(screen.getByTestId("usage-by-provider").textContent).toContain("GitHub Copilot") + expect(screen.getByTestId("usage-by-model").textContent).toContain("claude-opus-5.5") + }) + + it("labels tasks with no recorded provider as not recorded rather than guessing", () => { + state.taskHistory = [task()] + render() + expect(screen.getByTestId("usage-by-provider").textContent).toContain("notRecorded") + expect(screen.getByTestId("usage-by-model").textContent).toContain("notRecorded") + }) + + it("shows provider · model under each top task", () => { + state.taskHistory = [copilot()] + render() + expect(screen.getByTestId("usage-top-tasks").textContent).toContain("GitHub Copilot · claude-opus-5.5") + }) + + it("summarises modes on a single line", () => { + state.taskHistory = [priced(1, { mode: "code" }), priced(0.5, { mode: "architect" })] + render() + const line = screen.getByTestId("usage-by-mode").textContent! + expect(line).toContain("code $1.00") + expect(line).toContain("architect $0.50") + }) + }) + + describe("filters", () => { + it("defaults to this workspace and can widen to all", () => { + state.taskHistory = [priced(1), priced(2, { workspace: "/ws/other" })] + render() + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + expect(screen.getByTestId("usage-stats").textContent).not.toContain("$3.00") + }) + + it("changes the range", () => { + state.taskHistory = [priced(1), priced(5, { ts: NOW - 20 * 24 * 60 * 60 * 1000 })] + render() + // default 7 days → only the recent task + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + fireEvent.click(screen.getByTestId("usage-range-30d")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$6.00") + fireEvent.click(screen.getByTestId("usage-range-today")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") + }) + + it("marks the active range for assistive tech", () => { + render() + expect(screen.getByTestId("usage-range-7d").getAttribute("aria-checked")).toBe("true") + expect(screen.getByTestId("usage-range-30d").getAttribute("aria-checked")).toBe("false") + }) + }) + + describe("interactions", () => { + it("opens a task when its row is clicked", () => { + const t = priced(1) + state.taskHistory = [t] + render() + fireEvent.click(within(screen.getByTestId("usage-top-tasks")).getByRole("button")) + expect(post).toHaveBeenCalledWith({ type: "showTaskWithId", text: t.id }) + }) + + it("exports a CSV of the filtered range", () => { + state.taskHistory = [priced(1)] + const createObjectURL = vi.fn(() => "blob:x") + const revokeObjectURL = vi.fn() + Object.assign(URL, { createObjectURL, revokeObjectURL }) + const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) + + render() + fireEvent.click(screen.getByTestId("usage-export-csv")) + + expect(createObjectURL).toHaveBeenCalledTimes(1) + expect(click).toHaveBeenCalled() + expect(revokeObjectURL).toHaveBeenCalledWith("blob:x") + click.mockRestore() + }) + }) + + it("draws one chart bar per day for a bounded range", () => { + state.taskHistory = [priced(1)] + render() + expect(screen.getByTestId("usage-daily-chart").children).toHaveLength(7) + }) +}) diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 3f9f2ac7c5..c8346cedd4 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index adebc1b033..a97a9a9c0f 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 45bdf39d32..f77803e174 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 0ddd8ec224..0a18f2f566 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 7aa7cdc4dc..b66324e758 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index b643ef4139..5dc845b57c 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index f0bfd5aa49..6bc4bede57 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 9dcd24a556..0d456d14eb 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index ac67d19a10..4ea8d7685b 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 0e03aa9c91..09e09bbdf8 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index b846992ecd..aa49991887 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 0492aec7cb..bcd4d5605b 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 0e34d1c32e..55b138e9b9 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 47e4babb24..a9c4a01e67 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index be04174123..6af6edfde6 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 9f8bb2167d..932cc8ba0c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 4f79a40c7d..c384767be6 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index b67347098e..92f9750288 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1293,7 +1293,8 @@ "tokens": "tokens", "tasks": "tasks", "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out" + "inOut": "{{input}} in · {{output}} out", + "models": "models" }, "dailyCost": "Daily cost", "byMode": "By mode", From 01ef1457672bdf3e41c7ccb95efaebfa8b9eb112 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 14:02:03 -0700 Subject: [PATCH 08/25] feat(settings): date & time settings per design mock; handle mixed priced/unpriced usage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language tab (mock 2) - 'Date & time' section: Time format as a segmented control (Auto | 12-hour | 24-hour), Date format select (Auto, MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD) with a live example, 'Show relative times' checkbox, and a preview card showing the chat time, task-history time (relative or absolute) and hover text - New settings: dateFormat, showRelativeTimes - Task history honours them: relative ('5 min ago') or absolute date/time; the tooltip always shows the full date/time in the chosen formats - format.ts: formatDateOnly / formatTimeOnly; formatDateTime takes both formats Usage: mixed priced + unpriced providers - Cost stays the primary metric once any usage in the range is priced - Buckets are flagged priced/unpriced; in cost mode a subscription provider (e.g. GitHub Copilot) shows its tokens with a 'no price' tag, a muted bar and '–' instead of '$0.00 · 0%'; same for top tasks and the mode line Tests: segmented time format, date orders, relative/absolute history, formatDateOnly/formatTimeOnly, mixed-provider aggregation and rendering --- packages/types/src/global-settings.ts | 10 + packages/types/src/vscode-extension-host.ts | 2 + src/core/webview/ClineProvider.ts | 6 + .../src/components/history/TaskItemFooter.tsx | 11 +- .../history/__tests__/TaskItemFooter.spec.tsx | 32 +++ .../components/settings/LanguageSettings.tsx | 228 +++++++++++++----- .../src/components/settings/SettingsView.tsx | 6 + .../src/components/settings/UsageSettings.tsx | 49 ++-- .../LanguageSettings.timeFormat.spec.tsx | 95 ++++++-- .../settings/__tests__/UsageSettings.spec.tsx | 38 +++ .../src/context/ExtensionStateContext.tsx | 4 + webview-ui/src/i18n/locales/ca/settings.json | 32 ++- webview-ui/src/i18n/locales/de/settings.json | 32 ++- webview-ui/src/i18n/locales/en/settings.json | 32 ++- webview-ui/src/i18n/locales/es/settings.json | 32 ++- webview-ui/src/i18n/locales/fr/settings.json | 32 ++- webview-ui/src/i18n/locales/hi/settings.json | 32 ++- webview-ui/src/i18n/locales/id/settings.json | 32 ++- webview-ui/src/i18n/locales/it/settings.json | 32 ++- webview-ui/src/i18n/locales/ja/settings.json | 32 ++- webview-ui/src/i18n/locales/ko/settings.json | 32 ++- webview-ui/src/i18n/locales/nl/settings.json | 32 ++- webview-ui/src/i18n/locales/pl/settings.json | 32 ++- .../src/i18n/locales/pt-BR/settings.json | 32 ++- webview-ui/src/i18n/locales/ru/settings.json | 32 ++- webview-ui/src/i18n/locales/tr/settings.json | 32 ++- webview-ui/src/i18n/locales/vi/settings.json | 32 ++- .../src/i18n/locales/zh-CN/settings.json | 32 ++- .../src/i18n/locales/zh-TW/settings.json | 32 ++- webview-ui/src/utils/__tests__/format.spec.ts | 44 +++- webview-ui/src/utils/__tests__/usage.spec.ts | 38 +++ webview-ui/src/utils/format.ts | 38 ++- webview-ui/src/utils/usage.ts | 9 +- 33 files changed, 1027 insertions(+), 159 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 0b555f3927..674ba98771 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -301,6 +301,16 @@ export const globalSettingsSchema = z.object({ * @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) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 328eff8a1a..aa8d1d7945 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -346,6 +346,8 @@ export type ExtensionState = Pick< | "editorFontSize" | "spacingScale" | "timeFormat" + | "dateFormat" + | "showRelativeTimes" | "enterBehavior" | "includeCurrentTime" | "includeCurrentCost" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 32c40583f0..9c6d253285 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2815,6 +2815,8 @@ export class ClineProvider editorFontSize, spacingScale, timeFormat, + dateFormat, + showRelativeTimes, enterBehavior, cloudUserInfo, cloudIsAuthenticated, @@ -3001,6 +3003,8 @@ export class ClineProvider editorFontSize, spacingScale, timeFormat: timeFormat ?? "auto", + dateFormat: dateFormat ?? "auto", + showRelativeTimes: showRelativeTimes ?? true, enterBehavior: enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated: cloudIsAuthenticated ?? false, @@ -3239,6 +3243,8 @@ export class ClineProvider editorFontSize: stateValues.editorFontSize, spacingScale: stateValues.spacingScale, timeFormat: stateValues.timeFormat ?? "auto", + dateFormat: stateValues.dateFormat ?? "auto", + showRelativeTimes: stateValues.showRelativeTimes ?? true, enterBehavior: stateValues.enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated, diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index c43ce27ba2..c2ef925ff6 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -30,7 +30,10 @@ const TaskItemFooter: React.FC = ({ }) => { const { t } = useAppTranslation() // Optional read: this leaf renders standalone (tests, previews) without a provider. - const timeFormat = useContext(ExtensionStateContext)?.timeFormat + const ext = useContext(ExtensionStateContext) + const timeFormat = ext?.timeFormat + const dateFormat = ext?.dateFormat + const showRelativeTimes = ext?.showRelativeTimes ?? true return (
@@ -51,8 +54,10 @@ const TaskItemFooter: React.FC = ({ )} {/* Datetime with time-ago format */} - - {formatTimeAgo(item.ts)} + + + {showRelativeTimes ? formatTimeAgo(item.ts) : formatDateTime(item.ts, timeFormat, dateFormat)} + {/* Cost */} diff --git a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx index 5e5f428479..cf9f238072 100644 --- a/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx @@ -1,4 +1,6 @@ import { render, screen } from "@/utils/test-utils" +import { ExtensionStateContext } from "@/context/ExtensionStateContext" +import { formatDateTime } from "@/utils/format" import TaskItemFooter from "../TaskItemFooter" @@ -131,4 +133,34 @@ describe("TaskItemFooter", () => { expect(screen.queryByTestId("rename-task-button")).not.toBeInTheDocument() }) + + describe("time display settings", () => { + const renderWith = (state: Record) => + render( + + + , + ) + + it("shows a relative time by default", () => { + renderWith({}) + expect(screen.getByText("2 hours ago")).toBeInTheDocument() + }) + + it("shows a relative time when showRelativeTimes is on", () => { + renderWith({ showRelativeTimes: true }) + expect(screen.getByText("2 hours ago")).toBeInTheDocument() + }) + + it("shows the absolute date/time instead when showRelativeTimes is off", () => { + renderWith({ showRelativeTimes: false, timeFormat: "24h", dateFormat: "ymd" }) + expect(screen.queryByText("2 hours ago")).not.toBeInTheDocument() + expect(screen.getByText("Jan 15, 2024, 2:30 PM")).toBeInTheDocument() + }) + + it("passes the chosen time and date formats to the formatter", () => { + renderWith({ showRelativeTimes: false, timeFormat: "24h", dateFormat: "dmy" }) + expect(formatDateTime).toHaveBeenCalledWith(mockItem.ts, "24h", "dmy") + }) + }) }) diff --git a/webview-ui/src/components/settings/LanguageSettings.tsx b/webview-ui/src/components/settings/LanguageSettings.tsx index c96995ba5e..e9de59b253 100644 --- a/webview-ui/src/components/settings/LanguageSettings.tsx +++ b/webview-ui/src/components/settings/LanguageSettings.tsx @@ -1,4 +1,6 @@ import { HTMLAttributes, useMemo } from "react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" + import { useAppTranslation } from "@/i18n/TranslationContext" import type { Language } from "@roo-code/types" @@ -6,7 +8,7 @@ import type { Language } from "@roo-code/types" import { LANGUAGES } from "@roo/language" import { cn } from "@src/lib/utils" -import { formatDateTime, type TimeFormat } from "@src/utils/format" +import { formatDateOnly, formatDateTime, formatTimeOnly, type DateFormat, type TimeFormat } from "@src/utils/format" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { SetCachedStateField } from "./types" @@ -15,89 +17,195 @@ import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" const TIME_FORMATS: TimeFormat[] = ["auto", "12h", "24h"] +const DATE_FORMATS: DateFormat[] = ["auto", "mdy", "dmy", "ymd"] -// Fixed sample (14:41 local) so the preview clearly shows 12h vs 24h. -const PREVIEW_TS = (() => { - const d = new Date() - d.setHours(14, 41, 0, 0) - return d.getTime() -})() +// Fixed sample (Oct 27, 2026, 14:41 local) so the preview clearly shows 12h vs 24h and date order. +const SAMPLE_TS = new Date(2026, 9, 27, 14, 41, 0).getTime() type LanguageSettingsProps = HTMLAttributes & { language: string timeFormat?: TimeFormat - setCachedStateField: SetCachedStateField<"language" | "timeFormat"> + dateFormat?: DateFormat + showRelativeTimes?: boolean + setCachedStateField: SetCachedStateField<"language" | "timeFormat" | "dateFormat" | "showRelativeTimes"> } export const LanguageSettings = ({ language, timeFormat = "auto", + dateFormat = "auto", + showRelativeTimes = true, setCachedStateField, className, ...props }: LanguageSettingsProps) => { const { t } = useAppTranslation() - // Recompute when language changes too, since "auto" is locale-driven. - const preview = useMemo(() => formatDateTime(PREVIEW_TS, timeFormat), [timeFormat, language]) // eslint-disable-line react-hooks/exhaustive-deps + // Recompute when the language changes too, since "auto" is locale-driven. + /* eslint-disable react-hooks/exhaustive-deps */ + const preview = useMemo( + () => ({ + chatTime: formatTimeOnly(SAMPLE_TS, timeFormat), + dateExample: formatDateOnly(SAMPLE_TS, dateFormat), + hover: formatDateTime(SAMPLE_TS, timeFormat, dateFormat), + }), + [timeFormat, dateFormat, language], + ) + /* eslint-enable react-hooks/exhaustive-deps */ return (
- {t("settings:sections.language")} + + {t("settings:sections.language")} +
- - - - - -
- - -
- {t("settings:language.timeFormat.description")} -
-
- {t("settings:language.timeFormat.preview", { example: preview })} +
+
+ {t("settings:language.languageTitle")} + +
+ + +
+ {t("settings:language.displayLanguageDescription")} +
+
+
+
+ +
+ {t("settings:language.dateTimeTitle")} + + {/* Time format: segmented control, as in the design */} + +
+ +
+ {TIME_FORMATS.map((format) => ( + + ))} +
+
+ {t("settings:language.timeFormat.description")} +
+
+
+ + {/* Date format: select + live example */} + +
+ +
+ + + {t("settings:language.dateFormat.example", { example: preview.dateExample })} + +
+
+
+ + + setCachedStateField("showRelativeTimes", e.target.checked)} + data-testid="relative-times-checkbox"> + {t("settings:language.relativeTimes.label")} + + + + {/* Preview card: chat message time, task history (relative or absolute), hover text */} +
+
+ {t("settings:language.preview.title")} +
+
+ {t("settings:language.preview.chat")} · {preview.chatTime} +
+
+ {t("settings:language.preview.history")} ·{" "} + + {showRelativeTimes ? t("settings:language.preview.relative") : preview.hover} + + {showRelativeTimes && ( + + {t("settings:language.preview.hover", { value: preview.hover })} + + )} +
- +
) } + +const SubTitle = ({ children }: { children: React.ReactNode }) => ( +

{children}

+) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 129bde5cc6..083c435daa 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -222,6 +222,8 @@ const SettingsView = forwardRef(({ onDone, t editorFontSize, spacingScale, timeFormat, + dateFormat, + showRelativeTimes, enterBehavior, includeCurrentTime, includeCurrentCost, @@ -456,6 +458,8 @@ const SettingsView = forwardRef(({ onDone, t editorFontSize: editorFontSize ?? null, spacingScale: spacingScale ?? null, timeFormat: timeFormat ?? "auto", + dateFormat: dateFormat ?? "auto", + showRelativeTimes: showRelativeTimes ?? true, enterBehavior: enterBehavior ?? "send", includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, @@ -996,6 +1000,8 @@ const SettingsView = forwardRef(({ onDone, t )} diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 1ebf5ecfa0..8c03796a44 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -76,6 +76,19 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes (metric === "cost" ? formatCost(cost) : formatLargeNumber(tokens)) const total = value(summary.totalCost, summary.totalTokens) + // While showing cost, a bucket with no priced usage shows its tokens and a + // "no price" tag instead of a misleading "$0.00 · 0%". + const bucketRow = (b: { cost: number; tokens: number; priced: boolean }) => { + const unpriced = metric === "cost" && !b.priced + return { + value: value(b.cost, b.tokens), + unpriced, + label: unpriced + ? `${formatLargeNumber(b.tokens)} ${t("settings:usage.tokensShort")} · ${t("settings:usage.noPrice")}` + : fmt(b.cost, b.tokens), + } + } + const maxDaily = Math.max(0, ...summary.daily.map((d) => value(d.cost, d.tokens))) const showDayLabels = summary.daily.length <= 14 const isEmpty = summary.taskCount === 0 @@ -260,8 +273,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes ({ key: b.key, name: providerName(b.key), - value: value(b.cost, b.tokens), - label: fmt(b.cost, b.tokens), + ...bucketRow(b), }))} total={total} testId="usage-by-provider" @@ -272,8 +284,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes {t("settings:usage.byMode")}:{" "} - {summary.byMode.map((m) => `${m.key} ${fmt(m.cost, m.tokens)}`).join(" · ")} + {summary.byMode + .map((m) => `${m.key} ${bucketRow(m).label.split(" · ")[0]}`) + .join(" · ")}

- {fmt( - task.totalCost || 0, - (task.tokensIn || 0) + (task.tokensOut || 0), - )} + {metric === "cost" && !(task.totalCost > 0) + ? `${formatLargeNumber((task.tokensIn || 0) + (task.tokensOut || 0))} ${t("settings:usage.tokensShort")}` + : fmt( + task.totalCost || 0, + (task.tokensIn || 0) + (task.tokensOut || 0), + )} {formatTimeAgo(task.ts)} @@ -370,15 +385,15 @@ const Breakdown = ({ testId, }: { title: string - rows: { key: string; name: string; subtitle?: string; value: number; label: string }[] + rows: { key: string; name: string; subtitle?: string; value: number; label: string; unpriced?: boolean }[] total: number testId: string }) => (
{rows.slice(0, 5).map((row, i) => { - const pct = total > 0 ? Math.round((row.value / total) * 100) : 0 - const color = PALETTE[i % PALETTE.length] + const pct = row.unpriced ? 0 : total > 0 ? Math.round((row.value / total) * 100) : 0 + const color = row.unpriced ? "var(--vscode-descriptionForeground)" : PALETTE[i % PALETTE.length] return (
{row.subtitle} )} - {row.label} + + {row.label} + - {pct}% + + {row.unpriced ? "–" : `${pct}%`} +
) })} diff --git a/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx index beb82b36e3..ae29842acb 100644 --- a/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx @@ -3,8 +3,7 @@ import { describe, expect, it, vi } from "vitest" import { LanguageSettings } from "../LanguageSettings" -// The shared translation mock drops interpolation values; interpolate like i18next does -// so the preview text (the thing under test) is observable. +// Interpolate like i18next so the preview text (the thing under test) is observable. vi.mock("@/i18n/TranslationContext", () => ({ useAppTranslation: () => ({ t: (key: string, options?: Record) => @@ -12,29 +11,89 @@ vi.mock("@/i18n/TranslationContext", () => ({ }), })) -describe("LanguageSettings – time format", () => { - it("defaults to auto and shows a live example", () => { - render() - expect(screen.getByTestId("time-format-select")).toBeTruthy() - expect(screen.getByTestId("time-format-preview").textContent).toMatch(/\d{1,2}:41/) +const setup = (props: Partial> = {}) => { + const setCachedStateField = vi.fn() + render() + return { setCachedStateField } +} + +const clock = () => screen.getByTestId("preview-chat").textContent! + +describe("LanguageSettings – date & time (design mock 2)", () => { + describe("time format (segmented control)", () => { + it("offers Auto, 12-hour and 24-hour, with Auto active by default", () => { + setup() + expect(screen.getByTestId("time-format-auto").getAttribute("aria-checked")).toBe("true") + expect(screen.getByTestId("time-format-12h").getAttribute("aria-checked")).toBe("false") + expect(screen.getByTestId("time-format-24h").getAttribute("aria-checked")).toBe("false") + }) + + it.each(["auto", "12h", "24h"] as const)("saves %s when clicked", (format) => { + const { setCachedStateField } = setup({ timeFormat: format === "auto" ? "12h" : "auto" }) + fireEvent.click(screen.getByTestId(`time-format-${format}`)) + expect(setCachedStateField).toHaveBeenCalledWith("timeFormat", format) + }) + + it("marks the stored value as the active segment", () => { + setup({ timeFormat: "24h" }) + expect(screen.getByTestId("time-format-24h").getAttribute("aria-checked")).toBe("true") + }) + + it("previews 24-hour time without an AM/PM marker", () => { + setup({ timeFormat: "24h" }) + expect(clock()).toContain("14:41") + expect(clock()).not.toMatch(/\b(AM|PM)\b/i) + }) + + it("previews 12-hour time with an AM/PM marker", () => { + setup({ timeFormat: "12h" }) + expect(clock()).toMatch(/2:41\s?PM/i) + }) }) - it("previews 24-hour time without an AM/PM marker", () => { - render() - const text = screen.getByTestId("time-format-preview").textContent! - expect(text).toContain("14:41") - // Look only at the formatted example (after "example="), not the translation key text. - expect(text.split("example=")[1]).not.toMatch(/\b(AM|PM)\b/i) + describe("date format", () => { + it.each([ + ["mdy", "10/27/2026"], + ["dmy", "27/10/2026"], + ["ymd", "2026-10-27"], + ] as const)("shows %s as %s in the example", (format, expected) => { + setup({ dateFormat: format }) + expect(screen.getByTestId("date-format-example").textContent).toContain(expected) + }) + + it("renders the date format select", () => { + setup() + expect(screen.getByTestId("date-format-select")).toBeTruthy() + }) }) - it("previews 12-hour time with an AM/PM marker", () => { - render() - expect(screen.getByTestId("time-format-preview").textContent).toMatch(/2:41\s?PM/i) + describe("relative times checkbox", () => { + it("is checked by default and previews a relative time with the hover value", () => { + setup() + expect((screen.getByTestId("relative-times-checkbox") as HTMLInputElement).checked).toBe(true) + expect(screen.getByTestId("preview-history").textContent).toContain("relative") + expect(screen.getByTestId("preview-history").textContent).toContain("hover") + }) + + it("previews the absolute date/time when off, honouring both formats", () => { + setup({ showRelativeTimes: false, timeFormat: "24h", dateFormat: "ymd" }) + const text = screen.getByTestId("preview-history").textContent! + expect(text).toContain("2026-10-27, 14:41") + expect(text).not.toContain("relative") + }) + + it("saves the toggle", () => { + const { setCachedStateField } = setup() + fireEvent.click(screen.getByTestId("relative-times-checkbox")) + expect(setCachedStateField).toHaveBeenCalledWith("showRelativeTimes", false) + }) }) it("keeps the language selector", () => { - const { container } = render() + const { container } = (() => { + const r = render() + return r + })() expect(container.querySelectorAll("[role=combobox]").length).toBeGreaterThanOrEqual(2) - fireEvent.click(screen.getByTestId("time-format-select")) }) }) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx index 3522aaf9cd..659a63b184 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -120,6 +120,44 @@ describe("UsageSettings", () => { }) }) + describe("mixed priced and unpriced providers", () => { + beforeEach(() => { + state.taskHistory = [priced(1.5), copilot({ tokensIn: 16_000_000 })] + }) + + it("prioritises cost over tokens when any provider reports a price", () => { + render() + const stats = screen.getByTestId("usage-stats").textContent! + expect(stats).toContain("$1.50") + expect(screen.queryByTestId("usage-no-pricing-note")).toBeNull() + }) + + it("shows subscription providers as tokens with a no-price tag, not $0.00 · 0%", () => { + render() + const providers = screen.getByTestId("usage-by-provider") + const unpriced = within(providers).getByTestId("usage-unpriced") + expect(unpriced.textContent).toContain("noPrice") + expect(unpriced.textContent).not.toContain("$0.00") + // The unpriced row has no share: it shows "–", never "0%". + const row = unpriced.parentElement! + expect(row.textContent).toContain("–") + expect(row.textContent).not.toMatch(/(^|\D)0%/) + expect(row.querySelector("span[style*='width: 0%']")).not.toBeNull() + }) + + it("gives priced providers their cost and share", () => { + render() + const providers = screen.getByTestId("usage-by-provider") + expect(providers.textContent).toContain("$1.50") + expect(providers.textContent).toContain("100%") + }) + + it("shows an unpriced top task by tokens rather than $0.00", () => { + render() + expect(screen.getByTestId("usage-top-tasks").textContent).not.toContain("$0.00") + }) + }) + describe("attribution", () => { it("lists the provider and model that were recorded", () => { state.taskHistory = [copilot()] diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 4211f7ba8e..e42cdb32c1 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -138,6 +138,8 @@ export interface ExtensionStateContextType extends ExtensionState { editorFontSize?: number spacingScale?: number timeFormat?: "auto" | "12h" | "24h" + dateFormat?: "auto" | "mdy" | "dmy" | "ymd" + showRelativeTimes?: boolean enterBehavior?: "send" | "newline" setEnterBehavior: (value: "send" | "newline") => void autoCondenseContext: boolean @@ -560,6 +562,8 @@ export const ExtensionStateContextProvider: React.FC<{ editorFontSize: state.editorFontSize ?? undefined, spacingScale: state.spacingScale ?? undefined, timeFormat: state.timeFormat ?? "auto", + dateFormat: state.dateFormat ?? "auto", + showRelativeTimes: state.showRelativeTimes ?? true, reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true, didHydrateState, showWelcome, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index c8346cedd4..5e1e4d43c8 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1262,15 +1262,39 @@ "footer": "Crea les teves pròpies skills amb el mode Skill Writer, disponible al Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index a97a9a9c0f..9a825293ab 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1262,15 +1262,39 @@ "footer": "Erstelle deine eigenen Skills mit dem Skill Writer Modus, verfügbar im Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index f77803e174..1f04fd77f9 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1262,15 +1262,39 @@ } }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 0a18f2f566..9170557154 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1262,15 +1262,39 @@ "footer": "Crea tus propias skills con el modo Skill Writer, disponible en el Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index b66324e758..da87ce617c 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1262,15 +1262,39 @@ "footer": "Créez vos propres skills avec le mode Skill Writer, disponible dans le Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 5dc845b57c..73109c6722 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1262,15 +1262,39 @@ "footer": "Skill Writer मोड के साथ अपने स्वयं के Skills बनाएं, Modes Marketplace में उपलब्ध।" }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 6bc4bede57..ca51c9c242 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1262,15 +1262,39 @@ "footer": "Buat skills Anda sendiri dengan mode Skill Writer, tersedia di Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 0d456d14eb..4600ca08e6 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1262,15 +1262,39 @@ "footer": "Crea le tue skill con la modalità Skill Writer, disponibile in Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 4ea8d7685b..7fb22249e4 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1262,15 +1262,39 @@ "footer": "スキルライターモードで独自のスキルを作成します。モードマーケットプレイスで入手可能です。" }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 09e09bbdf8..0ec15070ca 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1262,15 +1262,39 @@ "footer": "스킬 작성자 모드로 자신만의 스킬을 만들 수 있습니다. 모드 마켓플레이스에서 사용 가능합니다." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index aa49991887..f06011a76a 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1262,15 +1262,39 @@ "footer": "Maak je eigen skills met de Skill Writer modus, beschikbaar in de Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index bcd4d5605b..053e867665 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1262,15 +1262,39 @@ "footer": "Twórz własne umiejętności za pomocą trybu Skill Writer, dostępnego w Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 55b138e9b9..5ac2c94856 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1262,15 +1262,39 @@ "footer": "Crie suas próprias skills com o modo Skill Writer, disponível em Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index a9c4a01e67..bf7120b9de 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1262,15 +1262,39 @@ "footer": "Создавайте собственные навыки с режимом Skill Writer, доступным в Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 6af6edfde6..9c880d5e0c 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1262,15 +1262,39 @@ "footer": "Skill Writer modu ile kendi becerilerinizi oluşturun. Modes Marketplace'de mevcut." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 932cc8ba0c..8a95d5d43e 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1262,15 +1262,39 @@ "footer": "Tạo các skill của riêng bạn với chế độ Skill Writer, có sẵn tại Modes Marketplace." }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c384767be6..c3ba3fc9d1 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1262,15 +1262,39 @@ "footer": "使用技能编写器模式创建您自己的技能,可在 模式市集 中获得。" }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 92f9750288..dd28b7c38f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1262,15 +1262,39 @@ "footer": "使用技能編寫器模式建立您自己的技能,可在 模式市集 中取得。" }, "language": { + "description": "Language and date/time display.", + "languageTitle": "Language", + "displayLanguage": "Display language", + "displayLanguageDescription": "Language for Zoo Code's interface.", + "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "How times are shown in Zoo Code. Auto follows the display language.", - "preview": "Example: {{example}}", + "description": "Auto follows your system locale.", "options": { "auto": "Auto", "12h": "12-hour", "24h": "24-hour" } + }, + "dateFormat": { + "label": "Date format", + "example": "e.g. {{example}}", + "options": { + "auto": "Auto", + "mdy": "MM/DD/YYYY", + "dmy": "DD/MM/YYYY", + "ymd": "YYYY-MM-DD" + } + }, + "relativeTimes": { + "label": "Show relative times (\"5 min ago\") in task history" + }, + "preview": { + "title": "Preview", + "chat": "Chat message", + "history": "Task history", + "relative": "5 min ago", + "hover": "(hover: {{value}})" } }, "usage": { @@ -1319,6 +1343,8 @@ "error": "Couldn't load" }, "byModel": "By model", - "notRecorded": "Not recorded" + "notRecorded": "Not recorded", + "noPrice": "no price", + "tokensShort": "tok" } } diff --git a/webview-ui/src/utils/__tests__/format.spec.ts b/webview-ui/src/utils/__tests__/format.spec.ts index 27970a513a..cc76ebced1 100644 --- a/webview-ui/src/utils/__tests__/format.spec.ts +++ b/webview-ui/src/utils/__tests__/format.spec.ts @@ -1,4 +1,12 @@ -import { formatLargeNumber, formatDate, formatDateTime, formatTimeAgo, hourCycleOptions } from "../format" +import { + formatLargeNumber, + formatDate, + formatDateOnly, + formatDateTime, + formatTimeAgo, + formatTimeOnly, + hourCycleOptions, +} from "../format" // Mock i18next vi.mock("i18next", () => ({ @@ -91,13 +99,45 @@ describe("hourCycleOptions", () => { }) }) +describe("formatDateOnly", () => { + const ts = new Date(2026, 9, 7, 14, 41).getTime() // 7 Oct 2026 + + it.each([ + ["mdy", "10/07/2026"], + ["dmy", "07/10/2026"], + ["ymd", "2026-10-07"], + ] as const)("writes %s as %s, zero-padded and locale-independent", (format, expected) => { + expect(formatDateOnly(ts, format)).toBe(expected) + }) + + it("follows the display language for auto", () => { + expect(formatDateOnly(ts, "auto")).toMatch(/2026/) + expect(formatDateOnly(ts)).toBe(formatDateOnly(ts, "auto")) + }) +}) + +describe("formatTimeOnly", () => { + const ts = new Date(2026, 9, 27, 14, 41).getTime() + + it("honours 24h and 12h", () => { + expect(formatTimeOnly(ts, "24h")).toBe("14:41") + expect(formatTimeOnly(ts, "12h")).toMatch(/^2:41\s?PM$/i) + }) +}) + describe("formatDateTime", () => { - it("includes date and time honouring the format", () => { + it("includes date and time honouring the formats", () => { const timestamp = new Date("2024-01-15T00:05:00").getTime() const result = formatDateTime(timestamp, "24h") expect(result).toMatch(/2024/) expect(result).toMatch(/00:05/) }) + + it("combines the chosen date order with the chosen clock", () => { + const ts = new Date(2026, 9, 27, 14, 41).getTime() + expect(formatDateTime(ts, "24h", "dmy")).toBe("27/10/2026, 14:41") + expect(formatDateTime(ts, "24h", "ymd")).toBe("2026-10-27, 14:41") + }) }) describe("formatTimeAgo", () => { diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 10317f5184..10b4160080 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -208,6 +208,44 @@ describe("summarizeUsage", () => { }) }) +describe("mixed priced and unpriced providers", () => { + const mixed = () => { + const mk = (provider: string, model: string, tokens: number, cost: number) => + item({ + tokensIn: tokens, + tokensOut: 0, + totalCost: cost, + usageByModel: [ + { apiProvider: provider, modelId: model, requests: 1, tokensIn: tokens, tokensOut: 0, cost }, + ], + }) + return summarizeUsage( + [ + mk(providerIdentifiers.githubCopilot, "claude-opus-5.5", 16_000_000, 0), + mk(providerIdentifiers.anthropic, "claude-sonnet-5", 200_000, 1.2), + ], + { range: "all", now: NOW }, + ) + } + + it("prioritises cost as soon as any usage in the range is priced", () => { + expect(pickMetric(mixed())).toBe("cost") + }) + + it("flags each bucket as priced or not so subscription usage isn't shown as $0.00", () => { + const s = mixed() + const byKey = Object.fromEntries(s.byProvider.map((b) => [b.key, b])) + expect(byKey[providerIdentifiers.anthropic].priced).toBe(true) + expect(byKey[providerIdentifiers.githubCopilot].priced).toBe(false) + expect(s.byModel.find((b) => modelIdFromKey(b.key) === "claude-opus-5.5")!.priced).toBe(false) + }) + + it("keeps all-unpriced ranges on tokens and all-priced ranges on cost", () => { + const unpriced = summarizeUsage([item({ tokensIn: 5, usageByModel: [] })], { range: "all", now: NOW }) + expect(pickMetric(unpriced)).toBe("tokens") + }) +}) + describe("primaryUsage", () => { it("returns the provider+model that used the most tokens", () => { const task = item({ diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts index 8b162fce74..a011d4f74c 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -1,6 +1,7 @@ import i18next from "i18next" export type TimeFormat = "auto" | "12h" | "24h" +export type DateFormat = "auto" | "mdy" | "dmy" | "ymd" export function formatLargeNumber(num: number): string { if (num >= 1e9) { @@ -46,16 +47,39 @@ export const formatDate = (timestamp: number, timeFormat: TimeFormat = "auto") = }) } -/** Full date + time, used for tooltips on relative timestamps. */ -export const formatDateTime = (timestamp: number, timeFormat: TimeFormat = "auto") => { - const locale = i18next.language || "en" +const pad = (n: number) => String(n).padStart(2, "0") - return new Date(timestamp).toLocaleString(locale, { - dateStyle: "medium", - timeStyle: "short", +/** + * Date only, in the chosen order. "auto" follows the display language; the + * explicit orders use a fixed numeric form (e.g. 10/27/2026, 27/10/2026, + * 2026-10-27) so the preview and the app agree regardless of locale. + */ +export const formatDateOnly = (timestamp: number, dateFormat: DateFormat = "auto") => { + const date = new Date(timestamp) + + switch (dateFormat) { + case "mdy": + return `${pad(date.getMonth() + 1)}/${pad(date.getDate())}/${date.getFullYear()}` + case "dmy": + return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}` + case "ymd": + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` + default: + return date.toLocaleDateString(i18next.language || "en", { dateStyle: "medium" }) + } +} + +/** Clock time only, honouring the 12h/24h choice. */ +export const formatTimeOnly = (timestamp: number, timeFormat: TimeFormat = "auto") => + new Date(timestamp).toLocaleTimeString(i18next.language || "en", { + hour: "numeric", + minute: "2-digit", ...hourCycleOptions(timeFormat), }) -} + +/** Full date + time, used for tooltips on relative timestamps and for absolute task times. */ +export const formatDateTime = (timestamp: number, timeFormat: TimeFormat = "auto", dateFormat: DateFormat = "auto") => + `${formatDateOnly(timestamp, dateFormat)}, ${formatTimeOnly(timestamp, timeFormat)}` export const formatTimeAgo = (timestamp: number) => { const now = Date.now() diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index c7d1d420d1..a65dcef1a5 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -28,6 +28,12 @@ export interface UsageBucket { tasks: number /** For model buckets: the provider that served it (undefined when unrecorded). */ apiProvider?: string + /** + * True when this bucket has any cost > 0. When the tab is showing cost, a bucket + * with `priced: false` (e.g. GitHub Copilot, billed by subscription) must not be + * shown as "$0.00 · 0%": it has no price, not zero spend. + */ + priced: boolean } export interface DailyUsage { @@ -114,9 +120,10 @@ function addTo( countedForTask: Set, extra?: Partial, ) { - const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0, ...extra } + const bucket = map.get(key) ?? { key, cost: 0, tokens: 0, tasks: 0, priced: false, ...extra } bucket.cost += cost bucket.tokens += tokens + if (cost > 0) bucket.priced = true // Count the task once per bucket even if several of its requests land here. if (!countedForTask.has(key)) { countedForTask.add(key) From fcd6d897f49f80eadd86b21df60203ee927dfb23 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 14:14:23 -0700 Subject: [PATCH 09/25] refactor: tighten quota services and usage view-model after review Security / correctness - Error messages from quota fetches no longer forward arbitrary Error text (network stack errors can embed URLs, headers or tokens). Only HTTP status, timeout and a generic 'Request failed' reach the webview; covered by tests that assert a planted secret never appears in results - Remove .scout/ config that was accidentally committed Design - quotaUtils.ts: shared toNumber/asRecord/clampPercent/formatMoney/ formatCredits/roundToTenth/safeErrorMessage (was duplicated across copilotQuota and providerQuotas) - providerQuotas.loadProfiles(): profile loading moved out of the message handler, which is now 4 lines and independently testable - usage.ts: bucketView() and metricTotal() own the priced/unpriced rule; UsageSettings no longer re-derives shares/flags inline - providerLabel util: one source of provider names (was a private map in UsageSettings, prop-drilled into ProviderLimits) - Remove dead exports (QUOTA_PROVIDER_IDS, spacing-density constants) and un-export unused CHAT_FONT_SIZE_* constants Accessibility - Daily chart is exposed as a single labelled image instead of unlabeled bars Tests - quotaUtils, loadProfiles, error-leak, malformed/trailing-slash base URLs, requestProviderQuotas handler (keys reach the service, never the webview), bucketView/metricTotal, providerLabel, tie-breaks, all-time range - Coverage of new code: webview 97% stmts, host 98% stmts --- .scout/scout.config.yaml | 16 -- ...bviewMessageHandler.providerQuotas.spec.ts | 102 ++++++++++++ src/core/webview/webviewMessageHandler.ts | 15 +- .../__tests__/copilotQuota.spec.ts | 19 ++- .../__tests__/providerQuotas.spec.ts | 149 ++++++++++++++++++ .../__tests__/quotaUtils.spec.ts | 86 ++++++++++ src/services/language-models/copilotQuota.ts | 25 +-- .../language-models/providerQuotas.ts | 69 ++++---- src/services/language-models/quotaUtils.ts | 51 ++++++ .../components/settings/ProviderLimits.tsx | 3 +- .../src/components/settings/UISettings.tsx | 6 +- .../src/components/settings/UsageSettings.tsx | 41 ++--- .../settings/__tests__/NumberStepper.spec.tsx | 7 + .../__tests__/ProviderLimits.spec.tsx | 21 ++- .../settings/__tests__/UsageSettings.spec.tsx | 15 ++ webview-ui/src/i18n/locales/ca/settings.json | 3 +- webview-ui/src/i18n/locales/de/settings.json | 3 +- webview-ui/src/i18n/locales/en/settings.json | 3 +- webview-ui/src/i18n/locales/es/settings.json | 3 +- webview-ui/src/i18n/locales/fr/settings.json | 3 +- webview-ui/src/i18n/locales/hi/settings.json | 3 +- webview-ui/src/i18n/locales/id/settings.json | 3 +- webview-ui/src/i18n/locales/it/settings.json | 3 +- webview-ui/src/i18n/locales/ja/settings.json | 3 +- webview-ui/src/i18n/locales/ko/settings.json | 3 +- webview-ui/src/i18n/locales/nl/settings.json | 3 +- webview-ui/src/i18n/locales/pl/settings.json | 3 +- .../src/i18n/locales/pt-BR/settings.json | 3 +- webview-ui/src/i18n/locales/ru/settings.json | 3 +- webview-ui/src/i18n/locales/tr/settings.json | 3 +- webview-ui/src/i18n/locales/vi/settings.json | 3 +- .../src/i18n/locales/zh-CN/settings.json | 3 +- .../src/i18n/locales/zh-TW/settings.json | 3 +- .../src/utils/__tests__/providerLabel.spec.ts | 14 ++ webview-ui/src/utils/__tests__/usage.spec.ts | 54 ++++++- webview-ui/src/utils/providerLabel.ts | 6 + webview-ui/src/utils/usage.ts | 35 ++++ 37 files changed, 652 insertions(+), 136 deletions(-) delete mode 100644 .scout/scout.config.yaml create mode 100644 src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts create mode 100644 src/services/language-models/__tests__/quotaUtils.spec.ts create mode 100644 src/services/language-models/quotaUtils.ts create mode 100644 webview-ui/src/utils/__tests__/providerLabel.spec.ts create mode 100644 webview-ui/src/utils/providerLabel.ts diff --git a/.scout/scout.config.yaml b/.scout/scout.config.yaml deleted file mode 100644 index cdb25bb166..0000000000 --- a/.scout/scout.config.yaml +++ /dev/null @@ -1,16 +0,0 @@ -# Scout per-repo config template. -# -# Seeded into `/.scout/scout.config.yaml` by `scout attach` when the -# repo has no config yet. Templates here only carry the repo-specific -# ignore / dampen / boost / tune overrides; defaults for -# repository/markdown/ranking/index/projects are filled in at load time. -ignore: - - '**/CHANGELOG.md' - - '**/CHANGELOG*' - - '**/CHANGES.md' - - '**/HISTORY.md' - -dampen: - - factor: 0.3 - patterns: - - '**/package.json' diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts new file mode 100644 index 0000000000..4b6332357e --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -0,0 +1,102 @@ +// npx vitest core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts + +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers } from "@roo-code/types" + +const quotas = vi.hoisted(() => ({ + getProviderQuotas: vi.fn(), +})) + +// Keep the real `loadProfiles` (it is the code under test alongside the handler) and stub only the network part. +vi.mock("../../../services/language-models/providerQuotas", async (importOriginal) => ({ + ...(await importOriginal()), + getProviderQuotas: quotas.getProviderQuotas, +})) + +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +const SECRET = "sk-super-secret" + +const makeProvider = (overrides: Record = {}) => + ({ + postMessageToWebview: vi.fn(), + providerSettingsManager: { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn(async ({ id }: { id: string }) => ({ + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: SECRET, + id, + })), + }, + ...overrides, + }) as unknown as ClineProvider & { postMessageToWebview: ReturnType } + +describe("webviewMessageHandler – requestProviderQuotas", () => { + beforeEach(() => { + quotas.getProviderQuotas.mockReset() + quotas.getProviderQuotas.mockResolvedValue([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt: 1, + windows: [{ id: "key_limit", valueLabel: "$5.00 / $10.00 used", usedPercent: 50 }], + }, + ]) + }) + + it("loads every saved profile and posts the formatted quotas back", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + expect(provider.providerSettingsManager.getProfile).toHaveBeenCalledTimes(2) + expect(quotas.getProviderQuotas.mock.calls[0][0]).toHaveLength(2) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: expect.arrayContaining([ + expect.objectContaining({ providerId: providerIdentifiers.openrouter }), + ]), + }) + }) + + it("does not force a refresh by default, and forces when the user presses refresh", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { force: false }) + + await webviewMessageHandler(provider, { type: "requestProviderQuotas", bool: true }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { force: true }) + }) + + it("passes API keys to the host service but never posts them to the webview", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + + // The service needs the key... + expect(JSON.stringify(quotas.getProviderQuotas.mock.calls[0][0])).toContain(SECRET) + // ...but nothing posted to the webview may contain it. + expect(JSON.stringify(provider.postMessageToWebview.mock.calls)).not.toContain(SECRET) + }) + + it("still answers when one profile cannot be loaded", async () => { + const provider = makeProvider() + provider.providerSettingsManager.getProfile = vi.fn(async ({ id }: { id: string }) => { + if (id === "a") throw new Error("corrupt secret storage") + return { apiProvider: providerIdentifiers.openrouter, openRouterApiKey: SECRET } as never + }) as never + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(quotas.getProviderQuotas.mock.calls[0][0]).toHaveLength(1) + expect(provider.postMessageToWebview).toHaveBeenCalled() + }) + + it("answers with an empty list when there are no profiles", async () => { + quotas.getProviderQuotas.mockResolvedValue([]) + const provider = makeProvider() + provider.providerSettingsManager.listConfig = vi.fn(async () => []) as never + + await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [] }) + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e0164b956e..6f994398ab 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1571,18 +1571,9 @@ export const webviewMessageHandler = async ( break } case VsCodeLmModelsMessageType.requestProviderQuotas: { - const { getProviderQuotas } = await import("../../services/language-models/providerQuotas") - // Load full settings (incl. API keys) host-side; keys never leave the extension host. - const entries = await provider.providerSettingsManager.listConfig() - const profiles = await Promise.all( - entries.map((entry) => - provider.providerSettingsManager.getProfile({ id: entry.id }).catch(() => undefined), - ), - ) - const providerQuotas = await getProviderQuotas( - profiles.filter((p): p is NonNullable => !!p), - { force: message.bool === true }, - ) + const { getProviderQuotas, loadProfiles } = await import("../../services/language-models/providerQuotas") + const profiles = await loadProfiles(provider.providerSettingsManager) + const providerQuotas = await getProviderQuotas(profiles, { force: message.bool === true }) await provider.postMessageToWebview({ type: VsCodeLmModelsMessageType.providerQuotas, providerQuotas }) break } diff --git a/src/services/language-models/__tests__/copilotQuota.spec.ts b/src/services/language-models/__tests__/copilotQuota.spec.ts index 08a9b4a688..946c4c9394 100644 --- a/src/services/language-models/__tests__/copilotQuota.spec.ts +++ b/src/services/language-models/__tests__/copilotQuota.spec.ts @@ -148,11 +148,26 @@ describe("getGitHubCopilotQuota", () => { expect(result).toMatchObject({ ok: false, reason: "noCopilot" }) }) - it("turns network errors into an error result instead of throwing", async () => { + it("turns network errors into a generic error result instead of throwing", async () => { const result = await getGitHubCopilotQuota({}, (async () => { throw new Error("offline") }) as never) - expect(result).toEqual({ ok: false, reason: "error", message: "offline" }) + // Arbitrary error text is never forwarded: it can embed URLs, headers or tokens. + expect(result).toEqual({ ok: false, reason: "error", message: "Request failed" }) + }) + + it("never leaks the access token through an error message", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw new Error("request to https://api.github.com failed: Authorization: token gho_token") + }) as never) + expect(JSON.stringify(result)).not.toContain("gho_token") + }) + + it("reports a timeout plainly", async () => { + const result = await getGitHubCopilotQuota({}, (async () => { + throw Object.assign(new Error("The operation timed out"), { name: "TimeoutError" }) + }) as never) + expect(result).toEqual({ ok: false, reason: "error", message: "Request timed out" }) }) it("caches successful results and bypasses the cache when forced", async () => { diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 9e6d0f4b7f..fffe45f63c 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -20,6 +20,7 @@ vi.mock("../copilotQuota", () => ({ import { getProviderQuotas, + loadProfiles, parseDeepSeekBalance, parseMoonshotBalance, parseOpenRouterKey, @@ -69,6 +70,22 @@ describe("parseDeepSeekBalance", () => { }) }) +describe("parseDeepSeekBalance – fallbacks", () => { + it("falls back to a zero USD balance, then to the first row", () => { + expect( + parseDeepSeekBalance({ + balance_infos: [ + { currency: "EUR", total_balance: "0" }, + { currency: "USD", total_balance: "0" }, + ], + }), + ).toEqual([{ id: "balance", valueLabel: "$0.00 left" }]) + expect(parseDeepSeekBalance({ balance_infos: [{ currency: "EUR", total_balance: "3" }] })).toEqual([ + { id: "balance", valueLabel: "$3.00 left" }, + ]) + }) +}) + describe("parseMoonshotBalance", () => { it("reads available_balance", () => { expect(parseMoonshotBalance({ data: { available_balance: 19.5 } })).toEqual([ @@ -104,6 +121,39 @@ describe("getProviderQuotas", () => { expect(okFetch).not.toHaveBeenCalled() }) + it("treats a malformed custom base URL as a different host and never sends the key", async () => { + const fetchSpy = vi.fn(okFetch) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "not a url", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: false, reason: "unsupported" }) + expect(fetchSpy).not.toHaveBeenCalled() + }) + + it("accepts the provider's own host written with a trailing path or slash", async () => { + const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const [q] = await getProviderQuotas( + [ + { + apiProvider: providerIdentifiers.openrouter, + openRouterApiKey: "sk", + openRouterBaseUrl: "https://openrouter.ai/api/v1/", + } as never, + ], + {}, + fetchSpy, + ) + expect(q).toMatchObject({ ok: true }) + }) + it("never sends a key to a custom base URL", async () => { const fetchSpy = vi.fn(okFetch) const [q] = await getProviderQuotas( @@ -154,3 +204,102 @@ describe("getProviderQuotas", () => { expect(fetchSpy).toHaveBeenCalledTimes(2) }) }) + +describe("loadProfiles", () => { + it("loads every saved profile through the manager, with keys", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "a" }, { id: "b" }]), + getProfile: vi.fn( + async ({ id }: { id: string }) => ({ apiProvider: "x", openRouterApiKey: `key-${id}` }) as never, + ), + } + const profiles = await loadProfiles(source) + expect(profiles).toEqual([ + { apiProvider: "x", openRouterApiKey: "key-a" }, + { apiProvider: "x", openRouterApiKey: "key-b" }, + ]) + expect(source.getProfile).toHaveBeenCalledWith({ id: "a" }) + }) + + it("skips a profile that fails to load instead of failing the whole refresh", async () => { + const source = { + listConfig: vi.fn(async () => [{ id: "ok" }, { id: "broken" }]), + getProfile: vi.fn(async ({ id }: { id: string }) => { + if (id === "broken") throw new Error("corrupt secret storage") + return { apiProvider: "x" } as never + }), + } + expect(await loadProfiles(source)).toEqual([{ apiProvider: "x" }]) + }) + + it("returns an empty list when there are no profiles", async () => { + expect(await loadProfiles({ listConfig: async () => [], getProfile: vi.fn() })).toEqual([]) + }) +}) + +describe("error handling never leaks credentials", () => { + beforeEach(() => resetProviderQuotaCache()) + + it("reports a generic message for network errors that embed the key", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "sk-secret" } as never], + {}, + vi.fn(async () => { + throw new Error("connect ECONNREFUSED https://api.deepseek.com Authorization: Bearer sk-secret") + }), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "Request failed" }) + expect(JSON.stringify(q)).not.toContain("sk-secret") + }) + + it("reports only the status for non-auth HTTP failures", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.moonshot, moonshotApiKey: "sk" } as never], + {}, + vi.fn(async () => new Response("secret internal page", { status: 502 })), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "HTTP 502" }) + }) + + it("treats an empty payload as an error", async () => { + const [q] = await getProviderQuotas( + [{ apiProvider: providerIdentifiers.deepseek, deepSeekApiKey: "sk" } as never], + {}, + vi.fn(async () => new Response(JSON.stringify({ balance_infos: [] }))), + ) + expect(q).toMatchObject({ ok: false, reason: "error", message: "No usage data" }) + }) + + it("surfaces Copilot failures with their reason", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ ok: false, reason: "signedOut" }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: false, reason: "signedOut" }) + }) + + it("shows Copilot unlimited plans without numbers", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ + ok: true, + quota: { plan: "enterprise", unlimited: true, fetchedAt: 1, resetAt: 2 }, + }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: true, windows: [{ id: "premium", valueLabel: "Unlimited", resetAt: 2 }] }) + }) + + it("falls back to a percent label when counts are missing", async () => { + const copilotMod = await import("../copilotQuota") + vi.mocked(copilotMod.getGitHubCopilotQuota).mockResolvedValueOnce({ + ok: true, + quota: { unlimited: false, usedPercent: 60, fetchedAt: 1 }, + }) + const [q] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never], { + force: true, + }) + expect(q).toMatchObject({ ok: true, windows: [{ valueLabel: "60% used", usedPercent: 60 }] }) + }) +}) diff --git a/src/services/language-models/__tests__/quotaUtils.spec.ts b/src/services/language-models/__tests__/quotaUtils.spec.ts new file mode 100644 index 0000000000..1c1cb45c00 --- /dev/null +++ b/src/services/language-models/__tests__/quotaUtils.spec.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from "vitest" + +import { + QuotaHttpError, + asRecord, + clampPercent, + formatCredits, + formatMoney, + roundToTenth, + safeErrorMessage, + toNumber, +} from "../quotaUtils" + +describe("toNumber", () => { + it.each([ + [5, 5], + [0, 0], + ["12.5", 12.5], + [" 7 ", 7], + ])("accepts %j", (input, expected) => expect(toNumber(input)).toBe(expected)) + + it.each([[NaN], [Infinity], [""], [" "], ["abc"], [null], [undefined], [{}], [[]]])("rejects %j", (input) => + expect(toNumber(input)).toBeUndefined(), + ) +}) + +describe("asRecord", () => { + it("returns plain objects only", () => { + expect(asRecord({ a: 1 })).toEqual({ a: 1 }) + for (const bad of [null, undefined, [], "x", 1]) expect(asRecord(bad)).toBeUndefined() + }) +}) + +describe("clampPercent", () => { + it.each([ + [-5, 0], + [0, 0], + [42.5, 42.5], + [100, 100], + [250, 100], + ])("%j → %j", (input, expected) => expect(clampPercent(input)).toBe(expected)) +}) + +describe("formatMoney", () => { + it("formats with two decimals and a sign before the symbol", () => { + expect(formatMoney(12.4)).toBe("$12.40") + expect(formatMoney(-0.5)).toBe("-$0.50") + expect(formatMoney(8, "¥")).toBe("¥8.00") + }) +}) + +describe("formatCredits", () => { + it("keeps whole numbers whole and fractions to one decimal", () => { + expect(formatCredits(140000)).toBe("140,000") + expect(formatCredits(39389.34)).toBe("39,389.3") + }) +}) + +describe("roundToTenth", () => { + it("rounds to one decimal", () => { + expect(roundToTenth(39389.26)).toBe(39389.3) + expect(roundToTenth(5)).toBe(5) + }) +}) + +describe("safeErrorMessage", () => { + it("keeps the status for HTTP errors", () => { + expect(safeErrorMessage(new QuotaHttpError(503))).toBe("HTTP 503") + }) + + it("normalises timeouts and aborts", () => { + const timeout = Object.assign(new Error("x"), { name: "TimeoutError" }) + const abort = Object.assign(new Error("x"), { name: "AbortError" }) + expect(safeErrorMessage(timeout)).toBe("Request timed out") + expect(safeErrorMessage(abort)).toBe("Request timed out") + }) + + it("never leaks the text of arbitrary errors (could embed URLs, headers or keys)", () => { + const leaky = new Error( + "fetch failed: https://api.example.com/v1?key=sk-secret Authorization: Bearer sk-secret", + ) + expect(safeErrorMessage(leaky)).toBe("Request failed") + expect(safeErrorMessage("sk-secret")).toBe("Request failed") + expect(safeErrorMessage(undefined)).toBe("Request failed") + }) +}) diff --git a/src/services/language-models/copilotQuota.ts b/src/services/language-models/copilotQuota.ts index 845aed6e62..6a8c6be07f 100644 --- a/src/services/language-models/copilotQuota.ts +++ b/src/services/language-models/copilotQuota.ts @@ -2,6 +2,8 @@ import * as vscode from "vscode" import { githubCopilotLanguageModel, type GitHubCopilotQuota, type GitHubCopilotQuotaResult } from "@roo-code/types" +import { asRecord, clampPercent, roundToTenth, safeErrorMessage, toNumber } from "./quotaUtils" + /** * GitHub Copilot premium-request allowance for the Usage tab. * @@ -18,21 +20,6 @@ const REQUEST_TIMEOUT_MS = 15_000 /** Avoid hammering the endpoint when the Usage tab is reopened repeatedly. */ const CACHE_TTL_MS = 60_000 -const toNumber = (value: unknown): number | undefined => { - if (typeof value === "number" && Number.isFinite(value)) return value - if (typeof value === "string" && value.trim() !== "") { - const parsed = Number(value) - return Number.isFinite(parsed) ? parsed : undefined - } - return undefined -} - -const asRecord = (value: unknown): Record | undefined => - value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined - -/** One decimal, matching GitHub's panel ("38,903.9 / 140,000"). */ -const roundCredits = (value: number) => Math.round(value * 10) / 10 - /** * Parses the endpoint payload. Pure, so it is unit-tested against recorded * shapes. Returns undefined when the payload has no premium snapshot at all. @@ -63,11 +50,11 @@ export function parseCopilotQuota(payload: unknown, now = Date.now()): GitHubCop let usedPercent: number | undefined if (entitlement !== undefined && entitlement > 0 && remaining !== undefined) { - used = roundCredits(Math.min(entitlement, Math.max(0, entitlement - remaining))) - usedPercent = Math.min(100, Math.max(0, (used / entitlement) * 100)) + used = roundToTenth(Math.min(entitlement, Math.max(0, entitlement - remaining))) + usedPercent = clampPercent((used / entitlement) * 100) } else { const percentRemaining = toNumber(snapshot.percent_remaining) - if (percentRemaining !== undefined) usedPercent = Math.min(100, Math.max(0, 100 - percentRemaining)) + if (percentRemaining !== undefined) usedPercent = clampPercent(100 - percentRemaining) } return { @@ -129,7 +116,7 @@ export function getGitHubCopilotQuota( if (result.ok) cache = { at: Date.now(), result } return result } catch (error) { - return { ok: false, reason: "error", message: error instanceof Error ? error.message : String(error) } + return { ok: false, reason: "error", message: safeErrorMessage(error) } } finally { inFlight = undefined } diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 181b148a70..f66151dfed 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -6,6 +6,15 @@ import { } from "@roo-code/types" import { getGitHubCopilotQuota } from "./copilotQuota" +import { + QuotaHttpError, + asRecord, + clampPercent, + formatCredits, + formatMoney, + safeErrorMessage, + toNumber, +} from "./quotaUtils" /** * Live limits/balances for every connected provider that exposes a usage API, @@ -19,25 +28,6 @@ import { getGitHubCopilotQuota } from "./copilotQuota" const REQUEST_TIMEOUT_MS = 15_000 const CACHE_TTL_MS = 60_000 -const toNumber = (value: unknown): number | undefined => { - if (typeof value === "number" && Number.isFinite(value)) return value - if (typeof value === "string" && value.trim() !== "") { - const n = Number(value) - return Number.isFinite(n) ? n : undefined - } - return undefined -} - -const asRecord = (value: unknown): Record | undefined => - value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined - -const money = (value: number, symbol = "$") => `${value < 0 ? "-" : ""}${symbol}${Math.abs(value).toFixed(2)}` - -const credits = (value: number) => - value.toLocaleString("en-US", { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) - -const clampPct = (value: number) => Math.min(100, Math.max(0, value)) - // Parsers (pure; unit-tested) ---------------------------------------------------------------------- /** OpenRouter `GET /api/v1/key` → key limit, or monthly spend when unlimited. */ @@ -53,14 +43,14 @@ export function parseOpenRouterKey(payload: unknown): ProviderQuotaWindow[] | un return [ { id: "key_limit", - valueLabel: `${money(used)} / ${money(limit)} used`, - usedPercent: clampPct((used / limit) * 100), + valueLabel: `${formatMoney(used)} / ${formatMoney(limit)} used`, + usedPercent: clampPercent((used / limit) * 100), }, ] } const usage = toNumber(data.usage_monthly) ?? toNumber(data.usage) - return usage === undefined ? undefined : [{ id: "spend", valueLabel: `${money(usage)} spent this month` }] + return usage === undefined ? undefined : [{ id: "spend", valueLabel: `${formatMoney(usage)} spent this month` }] } /** DeepSeek `GET /user/balance` → prepaid balance (prefers a positive USD balance). */ @@ -77,13 +67,13 @@ export function parseDeepSeekBalance(payload: unknown): ProviderQuotaWindow[] | const balance = toNumber(pick?.total_balance) if (balance === undefined) return undefined - return [{ id: "balance", valueLabel: `${money(balance, pick?.currency === "CNY" ? "¥" : "$")} left` }] + return [{ id: "balance", valueLabel: `${formatMoney(balance, pick?.currency === "CNY" ? "¥" : "$")} left` }] } /** Moonshot `GET /v1/users/me/balance` → available balance. */ export function parseMoonshotBalance(payload: unknown): ProviderQuotaWindow[] | undefined { const balance = toNumber(asRecord(asRecord(payload)?.data)?.available_balance) - return balance === undefined ? undefined : [{ id: "balance", valueLabel: `${money(balance)} left` }] + return balance === undefined ? undefined : [{ id: "balance", valueLabel: `${formatMoney(balance)} left` }] } // Fetchers ---------------------------------------------------------------------------------------- @@ -97,7 +87,7 @@ async function getJson(fetchImpl: Fetch, url: string, apiKey: string): Promise p.providerId), -] as string[] - const sameOrigin = (a: string, b: string) => { try { return new URL(a).origin === new URL(b).origin @@ -184,7 +168,7 @@ async function fetchKeyed( providerId: provider.providerId, ok: false, reason: error instanceof AuthError ? "noKey" : "error", - message: error instanceof Error ? error.message : String(error), + message: error instanceof AuthError ? undefined : safeErrorMessage(error), fetchedAt: now, } } @@ -203,7 +187,7 @@ async function fetchCopilot(force: boolean): Promise { id: "premium", valueLabel: q.used !== undefined && q.entitlement !== undefined - ? `${credits(q.used)} / ${credits(q.entitlement)} used` + ? `${formatCredits(q.used)} / ${formatCredits(q.entitlement)} used` : `${Math.round(q.usedPercent ?? 0)}% used`, usedPercent: q.usedPercent, resetAt: q.resetAt, @@ -213,6 +197,25 @@ async function fetchCopilot(force: boolean): Promise { // Entry point ------------------------------------------------------------------------------------- +/** The slice of `ProviderSettingsManager` this service needs (keeps it testable without the manager). */ +export interface ProfileSource { + listConfig(): Promise<{ id: string }[]> + getProfile(params: { id: string }): Promise +} + +/** + * Loads full profiles (including API keys) host-side. A profile that fails to + * load is skipped rather than failing the whole refresh. Keys never leave the + * extension host: only formatted quota rows are posted to the webview. + */ +export async function loadProfiles(source: ProfileSource): Promise { + const entries = await source.listConfig() + const profiles = await Promise.all( + entries.map((entry) => source.getProfile({ id: entry.id }).catch(() => undefined)), + ) + return profiles.filter((p): p is ProviderSettings => !!p) +} + let cache: { at: number; key: string; result: ProviderQuota[] } | undefined /** diff --git a/src/services/language-models/quotaUtils.ts b/src/services/language-models/quotaUtils.ts new file mode 100644 index 0000000000..768257e04a --- /dev/null +++ b/src/services/language-models/quotaUtils.ts @@ -0,0 +1,51 @@ +/** + * Small, pure helpers shared by the quota services (`copilotQuota`, + * `providerQuotas`). Kept in one place so payload parsing behaves identically + * everywhere and is tested once. + */ + +export const toNumber = (value: unknown): number | undefined => { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value) + return Number.isFinite(parsed) ? parsed : undefined + } + return undefined +} + +export const asRecord = (value: unknown): Record | undefined => + value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : undefined + +export const clampPercent = (value: number): number => Math.min(100, Math.max(0, value)) + +/** `$12.40`, `-$0.50`, `¥8.00`. */ +export const formatMoney = (value: number, symbol = "$"): string => + `${value < 0 ? "-" : ""}${symbol}${Math.abs(value).toFixed(2)}` + +/** `140,000` / `39,389.3`: whole numbers stay whole, fractions keep one decimal. */ +export const formatCredits = (value: number): string => + value.toLocaleString("en-US", { maximumFractionDigits: Number.isInteger(value) ? 0 : 1 }) + +/** One decimal, matching GitHub's own panel. */ +export const roundToTenth = (value: number): number => Math.round(value * 10) / 10 + +/** + * Turns an unknown failure into a short, safe string for the UI. Only the + * well-understood cases keep their text; anything else (network stack errors + * can embed URLs or headers) collapses to a generic message so credentials + * can never ride an error back to the webview. + */ +export class QuotaHttpError extends Error { + constructor(readonly status: number) { + super(`HTTP ${status}`) + this.name = "QuotaHttpError" + } +} + +export function safeErrorMessage(error: unknown): string { + if (error instanceof QuotaHttpError) return error.message + if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) { + return "Request timed out" + } + return "Request failed" +} diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index f90a0dce80..39cf205359 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -6,6 +6,7 @@ import { VsCodeLmModelsMessageType, type ProviderQuota } from "@roo-code/types" import { useAppTranslation } from "@/i18n/TranslationContext" import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" +import { providerLabel } from "@src/utils/providerLabel" const formatReset = (ts: number) => new Date(ts).toLocaleString(undefined, { weekday: "short", month: "short", day: "numeric" }) @@ -18,7 +19,7 @@ const barColor = (pct: number) => * exposes a usage API (GitHub Copilot, OpenRouter, DeepSeek, Moonshot), one * compact row each. Fetched by the extension host; keys never reach the webview. */ -export const ProviderLimits = ({ providerLabel }: { providerLabel: (id: string) => string }) => { +export const ProviderLimits = () => { const { t } = useAppTranslation() const [quotas, setQuotas] = useState() const [loading, setLoading] = useState(true) diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 40807c832e..ea730bb390 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -20,9 +20,9 @@ import { AppearancePreview } from "./AppearancePreview" const APPEARANCE_KEYS = Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[] -export const CHAT_FONT_SIZE_MIN = 8 -export const CHAT_FONT_SIZE_MAX = 32 -export const CHAT_FONT_SIZE_DEFAULT = 13 +const CHAT_FONT_SIZE_MIN = 8 +const CHAT_FONT_SIZE_MAX = 32 +const CHAT_FONT_SIZE_DEFAULT = 13 interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index 8c03796a44..7fe48fab58 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -6,12 +6,15 @@ import { useExtensionState } from "@/context/ExtensionStateContext" import { cn } from "@src/lib/utils" import { vscode } from "@src/utils/vscode" import { formatLargeNumber, formatTimeAgo } from "@src/utils/format" +import { providerLabel } from "@src/utils/providerLabel" import { UNRECORDED, USAGE_RANGES, type UsageMetric, type UsageRange, modelIdFromKey, + bucketView, + metricTotal, pickMetric, primaryUsage, summarizeUsage, @@ -22,12 +25,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { SearchableSetting } from "./SearchableSetting" -import { PROVIDERS } from "./constants" import { ProviderLimits } from "./ProviderLimits" -const PROVIDER_LABELS = new Map(PROVIDERS.map((p) => [p.value, p.label])) -const providerLabel = (id: string) => PROVIDER_LABELS.get(id) ?? id - const formatCost = (cost: number) => `$${cost.toFixed(2)}` const PALETTE = [ @@ -74,16 +73,14 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes (metric === "cost" ? cost : tokens) const fmt = (cost: number, tokens: number) => (metric === "cost" ? formatCost(cost) : formatLargeNumber(tokens)) - const total = value(summary.totalCost, summary.totalTokens) + const total = metricTotal(summary, metric) - // While showing cost, a bucket with no priced usage shows its tokens and a - // "no price" tag instead of a misleading "$0.00 · 0%". + // Row view-model: the priced/unpriced rule lives in `bucketView` (pure, tested). const bucketRow = (b: { cost: number; tokens: number; priced: boolean }) => { - const unpriced = metric === "cost" && !b.priced + const view = bucketView(b, metric, total) return { - value: value(b.cost, b.tokens), - unpriced, - label: unpriced + ...view, + label: view.unpriced ? `${formatLargeNumber(b.tokens)} ${t("settings:usage.tokensShort")} · ${t("settings:usage.noPrice")}` : fmt(b.cost, b.tokens), } @@ -109,7 +106,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes
- +
@@ -231,7 +228,17 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes -
+
{summary.daily.map((d) => { const v = value(d.cost, d.tokens) return ( @@ -275,7 +282,6 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes

(

{rows.slice(0, 5).map((row, i) => { - const pct = row.unpriced ? 0 : total > 0 ? Math.round((row.value / total) * 100) : 0 + const pct = row.percent const color = row.unpriced ? "var(--vscode-descriptionForeground)" : PALETTE[i % PALETTE.length] return (
{ expect(onChange).not.toHaveBeenCalledWith(NaN) }) + it("clamps a typed zero up to the minimum (a real 0, unlike an emptied box)", () => { + const { onChange, input } = setup({ value: 16 }) + fireEvent.change(input, { target: { value: "0" } }) + fireEvent.blur(input) + expect(onChange).toHaveBeenCalledWith(setting.min) + }) + it("reset clears the override", () => { const { onChange } = setup({ value: 20 }) fireEvent.click(screen.getByTestId("t-reset")) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index 9caea0810d..5db391a06d 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -13,25 +13,22 @@ const respond = (providerQuotas: ProviderQuota[]) => window.dispatchEvent(new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas } })) }) -const label = (id: string) => `Label:${id}` -const COPILOT = providerIdentifiers.githubCopilot - describe("ProviderLimits", () => { beforeEach(() => post.mockClear()) it("asks the extension for quotas on mount without forcing", () => { - render() + render() expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: false }) }) it("shows a loading row until the first response", () => { - render() + render() expect(screen.getByTestId("provider-limits")).toBeTruthy() expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`)).toBeNull() }) it("renders credits with a progress bar and reset date", () => { - render() + render() respond([ { providerId: providerIdentifiers.githubCopilot, @@ -50,7 +47,7 @@ describe("ProviderLimits", () => { ]) const row = screen.getByTestId(`provider-limit-${providerIdentifiers.githubCopilot}`) - expect(row.textContent).toContain(`Label:${COPILOT}`) + expect(row.textContent).toContain("GitHub Copilot") expect(row.textContent).toContain("business") expect(row.textContent).toContain("39,389.3 / 140,000 used") expect(row.textContent).toContain("28%") @@ -60,7 +57,7 @@ describe("ProviderLimits", () => { }) it("omits the bar for balance-only providers", () => { - render() + render() respond([ { providerId: providerIdentifiers.deepseek, @@ -75,7 +72,7 @@ describe("ProviderLimits", () => { }) it.each(["noKey", "signedOut", "unsupported", "error"] as const)("shows a quiet status for %s", (reason) => { - render() + render() respond([{ providerId: providerIdentifiers.openrouter, ok: false, reason, fetchedAt: 1 }]) const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) expect(row.textContent).toContain(`settings:usage.limits.${reason}`.split(".").pop()!) @@ -83,13 +80,13 @@ describe("ProviderLimits", () => { }) it("hides the whole section when nothing connected has a usage API", () => { - render() + render() respond([]) expect(screen.queryByTestId("provider-limits")).toBeNull() }) it("refresh forces a new request", () => { - render() + render() respond([ { providerId: providerIdentifiers.deepseek, @@ -104,7 +101,7 @@ describe("ProviderLimits", () => { }) it("ignores unrelated messages", () => { - render() + render() act(() => { window.dispatchEvent(new MessageEvent("message", { data: { type: "somethingElse" } })) }) diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx index 659a63b184..7f7c2acb54 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.spec.tsx @@ -207,6 +207,13 @@ describe("UsageSettings", () => { expect(screen.getByTestId("usage-stats").textContent).toContain("$1.00") }) + it("includes everything for the all-time range", () => { + state.taskHistory = [priced(1), priced(5, { ts: NOW - 200 * 24 * 60 * 60 * 1000 })] + render() + fireEvent.click(screen.getByTestId("usage-range-all")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$6.00") + }) + it("marks the active range for assistive tech", () => { render() expect(screen.getByTestId("usage-range-7d").getAttribute("aria-checked")).toBe("true") @@ -240,6 +247,14 @@ describe("UsageSettings", () => { }) }) + it("exposes the chart to assistive tech as a single labelled image", () => { + state.taskHistory = [priced(1)] + render() + const chart = screen.getByTestId("usage-daily-chart") + expect(chart.getAttribute("role")).toBe("img") + expect(chart.getAttribute("aria-label")).toBeTruthy() + }) + it("draws one chart bar per day for a bounded range", () => { state.taskHistory = [priced(1)] render() diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 5e1e4d43c8..47a663341d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 9a825293ab..567b418490 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 1f04fd77f9..11e74b33a6 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 9170557154..c9155f2470 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index da87ce617c..df92bdc82f 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 73109c6722..e5389ea8ee 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index ca51c9c242..c340d277ff 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 4600ca08e6..ec76f0e45d 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 7fb22249e4..952e664e61 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 0ec15070ca..d69467b792 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index f06011a76a..d7f13b262b 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 053e867665..936cd1b043 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 5ac2c94856..1d10ec5944 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index bf7120b9de..adfd978166 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 9c880d5e0c..b48dd84c32 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 8a95d5d43e..6ebdc4512c 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c3ba3fc9d1..231ee4dde3 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index dd28b7c38f..42d4055c4f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -1345,6 +1345,7 @@ "byModel": "By model", "notRecorded": "Not recorded", "noPrice": "no price", - "tokensShort": "tok" + "tokensShort": "tok", + "chartLabel": "{{metric}}, total {{total}}" } } diff --git a/webview-ui/src/utils/__tests__/providerLabel.spec.ts b/webview-ui/src/utils/__tests__/providerLabel.spec.ts new file mode 100644 index 0000000000..e2401a4f45 --- /dev/null +++ b/webview-ui/src/utils/__tests__/providerLabel.spec.ts @@ -0,0 +1,14 @@ +import { providerIdentifiers } from "@roo-code/types" + +import { providerLabel } from "../providerLabel" + +describe("providerLabel", () => { + it("returns the display name for a known provider", () => { + expect(providerLabel(providerIdentifiers.githubCopilot)).toBe("GitHub Copilot") + expect(providerLabel(providerIdentifiers.anthropic)).toBe("Anthropic") + }) + + it("falls back to the raw id for unknown or retired providers", () => { + expect(providerLabel("some-retired-provider")).toBe("some-retired-provider") + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 10b4160080..7ce3dd857c 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -1,6 +1,16 @@ import { providerIdentifiers, type HistoryItem } from "@roo-code/types" -import { UNRECORDED, modelIdFromKey, pickMetric, primaryUsage, rangeStart, summarizeUsage, usageToCsv } from "../usage" +import { + UNRECORDED, + bucketView, + metricTotal, + modelIdFromKey, + pickMetric, + primaryUsage, + rangeStart, + summarizeUsage, + usageToCsv, +} from "../usage" // Fixed "now": Wed 2026-10-14 15:00 local time. const NOW = new Date(2026, 9, 14, 15, 0, 0).getTime() @@ -182,6 +192,14 @@ describe("summarizeUsage", () => { expect(s.topTasks.map((t) => t.totalCost)).toEqual([99, 2]) }) + it("breaks cost ties by tokens so equal-priced tasks still rank meaningfully", () => { + const s = summarizeUsage( + [item({ id: "few", totalCost: 1, tokensIn: 10 }), item({ id: "many", totalCost: 1, tokensIn: 9_000 })], + { range: "all", now: NOW }, + ) + expect(s.topTasks.map((t) => t.id)).toEqual(["many", "few"]) + }) + it("is safe on empty history", () => { const s = summarizeUsage([], { range: "7d", now: NOW }) expect(s.taskCount).toBe(0) @@ -246,6 +264,40 @@ describe("mixed priced and unpriced providers", () => { }) }) +describe("bucketView", () => { + const priced = { cost: 3, tokens: 1000, priced: true } + const unpriced = { cost: 0, tokens: 16_000_000, priced: false } + + it("cost mode: a priced bucket is its cost and its share of total cost", () => { + expect(bucketView(priced, "cost", 12)).toEqual({ value: 3, unpriced: false, percent: 25 }) + }) + + it("cost mode: an unpriced bucket carries tokens, is flagged, and has no share (never 0%)", () => { + expect(bucketView(unpriced, "cost", 12)).toEqual({ value: 0, unpriced: true, percent: 0 }) + }) + + it("tokens mode: everything is tokens and nothing is flagged unpriced", () => { + expect(bucketView(unpriced, "tokens", 32_000_000)).toEqual({ value: 16_000_000, unpriced: false, percent: 50 }) + expect(bucketView(priced, "tokens", 2000)).toEqual({ value: 1000, unpriced: false, percent: 50 }) + }) + + it("guards division by zero", () => { + expect(bucketView(priced, "cost", 0).percent).toBe(0) + }) + + it("rounds shares to whole numbers", () => { + expect(bucketView({ cost: 1, tokens: 0, priced: true }, "cost", 3).percent).toBe(33) + }) +}) + +describe("metricTotal", () => { + it("selects the headline value for the metric", () => { + const s = { totalCost: 2.5, totalTokens: 900 } + expect(metricTotal(s, "cost")).toBe(2.5) + expect(metricTotal(s, "tokens")).toBe(900) + }) +}) + describe("primaryUsage", () => { it("returns the provider+model that used the most tokens", () => { const task = item({ diff --git a/webview-ui/src/utils/providerLabel.ts b/webview-ui/src/utils/providerLabel.ts new file mode 100644 index 0000000000..740e0aef02 --- /dev/null +++ b/webview-ui/src/utils/providerLabel.ts @@ -0,0 +1,6 @@ +import { PROVIDERS } from "@src/components/settings/constants" + +const LABELS = new Map(PROVIDERS.map((p) => [p.value, p.label])) + +/** Human label for a provider id, falling back to the id itself for unknown/retired providers. */ +export const providerLabel = (id: string): string => LABELS.get(id) ?? id diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index a65dcef1a5..602c69159c 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -54,6 +54,41 @@ export function pickMetric(summary: Pick 0 || summary.totalTokens === 0 ? "cost" : "tokens" } +/** + * What a breakdown row (provider / model / mode) should display for a bucket. + * + * Pure so the "priced vs unpriced" rule lives in one tested place instead of + * being re-derived by the component: + * - tokens mode: value = tokens + * - cost mode, priced bucket: value = cost, share of total cost + * - cost mode, unpriced: a subscription bucket (e.g. Copilot) has no price, + * not zero spend → value = tokens, `unpriced: true`, + * and no share (so no "$0.00 · 0%") + */ +export interface BucketView { + /** Numeric value the bar/share is computed from. */ + value: number + /** True when the bucket should be shown as tokens with a "no price" tag and no share. */ + unpriced: boolean + /** Whole-number share of the total, 0 for unpriced rows. */ + percent: number +} + +export function bucketView( + bucket: Pick, + metric: UsageMetric, + total: number, +): BucketView { + const unpriced = metric === "cost" && !bucket.priced + const value = metric === "cost" ? bucket.cost : bucket.tokens + return { value, unpriced, percent: unpriced || total <= 0 ? 0 : Math.round((value / total) * 100) } +} + +/** The metric's headline value for a summary (cost or tokens). */ +export function metricTotal(summary: Pick, metric: UsageMetric): number { + return metric === "cost" ? summary.totalCost : summary.totalTokens +} + export interface UsageSummary { totalCost: number tokensIn: number From b5fa87694f3254ae2193f2de17296eaf21c807f2 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 14:21:03 -0700 Subject: [PATCH 10/25] feat(chat): show when a message was sent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A muted clock + time line under each user message and under the task's first message: 🕒 Yesterday 11:43 AM - formatMessageTime(): Today / Yesterday (localised via Intl.RelativeTimeFormat, no new translation keys), weekday within the last week, then a date; year only when it isn't the current year. Day boundaries are local midnight, so 23:55 last night is still 'Yesterday' and 00:05 is 'Today' - Honours the Time format (Auto/12h/24h) and Date format settings; the tooltip always shows the full date and time - MessageTimestamp is presentational (formats come in as props), renders a semantic
+ {/* When the message was sent. Hidden while editing: the edit box replaces the bubble. */} + {!isEditing && ( +
+ +
+ )}
) case "user_feedback_diff": diff --git a/webview-ui/src/components/chat/MessageTimestamp.tsx b/webview-ui/src/components/chat/MessageTimestamp.tsx new file mode 100644 index 0000000000..f880b340d5 --- /dev/null +++ b/webview-ui/src/components/chat/MessageTimestamp.tsx @@ -0,0 +1,42 @@ +import { Clock } from "lucide-react" + +import { cn } from "@src/lib/utils" +import { formatDateTime, formatMessageTime, type DateFormat, type TimeFormat } from "@/utils/format" + +import { StandardTooltip } from "../ui/standard-tooltip" + +interface MessageTimestampProps { + /** Epoch ms the message was sent. */ + ts: number + /** From the user's Language settings; both default to "auto". */ + timeFormat?: TimeFormat + dateFormat?: DateFormat + className?: string +} + +/** + * "🕒 Yesterday 11:43 AM" under a sent message. + * + * Purely presentational: the caller supplies the user's time/date formats, so + * the component has no dependency on extension state and renders anywhere. The + * tooltip always carries the full date and time. A semantic
{task.images && task.images.length > 0 && } + {/* When the task's first message was sent (follows the Time/Date format settings). */} +
e.stopPropagation()}> + +
+
e.stopPropagation()}> ({ vscode: { postMessage: vi.fn() } })) + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ t: (key: string) => key, i18n: { exists: () => true } }), + Trans: ({ children }: { children?: React.ReactNode }) => <>{children}, + initReactI18next: { type: "3rdParty", init: () => {} }, +})) + +let mockState: Record = {} +vi.mock("@src/context/ExtensionStateContext", async (importOriginal) => ({ + ...(await importOriginal()), + useExtensionState: () => ({ + mcpServers: [], + alwaysAllowMcp: false, + currentCheckpoint: null, + mode: "code", + apiConfiguration: {}, + clineMessages: [], + currentTaskItem: undefined, + ...mockState, + }), +})) + +vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: () => ({ info: { supportsImages: true } }), +})) + +const renderRow = (message: ClineMessage, isStreaming = false) => + render( + {}} + onSuggestionClick={() => {}} + onBatchFileResponse={() => {}} + onFollowUpUnmount={() => {}} + isFollowUpAnswered={false} + />, + ) + +describe("ChatRow – message timestamp", () => { + beforeEach(() => { + mockState = {} + }) + + it("shows when a user message was sent", () => { + renderRow({ ts: Date.now(), type: "say", say: "user_feedback", text: "hello there" }) + expect(screen.getByTestId("message-timestamp").textContent).toContain("Today") + }) + + it("uses the message's own timestamp, not the current time", () => { + const d = new Date() + d.setDate(d.getDate() - 1) + d.setHours(11, 43, 0, 0) + renderRow({ ts: d.getTime(), type: "say", say: "user_feedback", text: "an older message" }) + expect(screen.getByTestId("message-timestamp").textContent).toContain("Yesterday") + }) + + it("is rendered after the message bubble (under it), not before", () => { + const { container } = renderRow({ ts: Date.now(), type: "say", say: "user_feedback", text: "ordering" }) + const html = container.innerHTML + expect(html.indexOf("ordering")).toBeGreaterThan(-1) + expect(html.indexOf("message-timestamp")).toBeGreaterThan(html.indexOf("ordering")) + }) + + it("applies the user's time and date format settings", () => { + const d = new Date() + d.setHours(14, 41, 0, 0) + mockState = { timeFormat: "24h", dateFormat: "ymd" } + renderRow({ ts: d.getTime(), type: "say", say: "user_feedback", text: "formats" }) + expect(screen.getByTestId("message-timestamp").textContent).toContain("14:41") + }) + + it("does not add a timestamp to assistant text", () => { + renderRow({ ts: Date.now(), type: "say", say: "text", text: "assistant reply" }) + expect(screen.queryByTestId("message-timestamp")).toBeNull() + }) + + it("keeps the message visible while streaming", () => { + renderRow({ ts: Date.now(), type: "say", say: "user_feedback", text: "streaming" }, true) + expect(screen.getByTestId("message-timestamp")).toBeTruthy() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/MessageTimestamp.spec.tsx b/webview-ui/src/components/chat/__tests__/MessageTimestamp.spec.tsx new file mode 100644 index 0000000000..b7520f2fb9 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/MessageTimestamp.spec.tsx @@ -0,0 +1,63 @@ +import { render, screen } from "@/utils/test-utils" + +import { MessageTimestamp } from "../MessageTimestamp" + +const at = (y: number, m: number, d: number, h: number, min = 0) => new Date(y, m, d, h, min).getTime() + +describe("MessageTimestamp", () => { + it("renders a semantic
) } -const QuotaRow = ({ quota, name }: { quota: ProviderQuota; name: string }) => { +const QuotaRow = ({ + quota, + name, + formatReset, +}: { + quota: ProviderQuota + name: string + formatReset: (ts: number) => string +}) => { const { t } = useAppTranslation() return ( diff --git a/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx b/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx new file mode 100644 index 0000000000..6f05c7cc64 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx @@ -0,0 +1,44 @@ +import { render, screen } from "@testing-library/react" +import { describe, expect, it } from "vitest" + +import { AppearancePreview } from "../AppearancePreview" + +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ t: (key: string) => key }), +})) + +const root = () => screen.getByTestId("appearance-preview") + +describe("AppearancePreview", () => { + it("sets nothing for an unset value, so the live CSS fallback chain applies (terminal follows VS Code's editor size)", () => { + render() + const style = root().style + expect(style.getPropertyValue("--zoo-terminal-font-size")).toBe("") + expect(style.getPropertyValue("--zoo-editor-font-size")).toBe("") + expect(style.getPropertyValue("--zoo-ui-scale")).toBe("") + expect(style.getPropertyValue("--zoo-spacing-scale")).toBe("") + }) + + it("sets only the values the user has set, converted the way the live UI converts them", () => { + render( + , + ) + const style = root().style + expect(style.getPropertyValue("--zoo-terminal-font-size")).toBe("18px") + expect(style.getPropertyValue("--zoo-ui-scale")).toBe("1.2") + expect(style.getPropertyValue("--zoo-spacing-scale")).toBe("") + expect(style.getPropertyValue("--zoo-editor-font-size")).toBe("") + }) + + it("clamps out-of-range values like the live UI", () => { + render() + expect(root().style.getPropertyValue("--zoo-spacing-scale")).toBe("2") + }) + + it("applies the chat font size only when one is set", () => { + const { rerender } = render() + expect(root().style.getPropertyValue("--zoo-chat-font-size")).toBe("") + rerender() + expect(root().style.getPropertyValue("--zoo-chat-font-size")).toBe("16px") + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index 3f2ea1d9ab..e8e7af4eaa 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -3,18 +3,35 @@ import { beforeEach, describe, expect, it, vi } from "vitest" import { providerIdentifiers, type ProviderQuota } from "@roo-code/types" +import { formatDateOnly, formatTimeOnly } from "@src/utils/format" + import { ProviderLimits } from "../ProviderLimits" const post = vi.hoisted(() => vi.fn()) vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) +const prefs = vi.hoisted(() => ({ timeFormat: "auto" as string, dateFormat: "auto" as string })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => prefs })) + +// Render the key plus its interpolation values so assertions can see the formatted output. +vi.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, options?: Record) => + options ? `${key} ${Object.values(options).join(" ")}` : key, + }), +})) + const respond = (providerQuotas: ProviderQuota[]) => act(() => { window.dispatchEvent(new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas } })) }) describe("ProviderLimits", () => { - beforeEach(() => post.mockClear()) + beforeEach(() => { + post.mockClear() + prefs.timeFormat = "auto" + prefs.dateFormat = "auto" + }) it("asks the extension for quotas on mount without forcing", () => { render() @@ -53,7 +70,10 @@ describe("ProviderLimits", () => { expect(row.textContent).toContain("28%") const bar = row.querySelector("[role=progressbar]")! expect(bar.getAttribute("aria-valuenow")).toBe("28") - expect(row.textContent).toContain("resets") + // The translation key plus the formatted reset date, not just the key. + expect(row.textContent).toContain( + `settings:usage.limits.resets ${formatDateOnly(Date.parse("2026-11-01T12:00:00Z"))}`, + ) }) it("omits the bar for balance-only providers", () => { @@ -71,14 +91,39 @@ describe("ProviderLimits", () => { expect(row.querySelector("[role=progressbar]")).toBeNull() }) - it.each(["noKey", "signedOut", "unsupported", "error"] as const)("shows a quiet status for %s", (reason) => { + it("formats the reset date and the updated time with the user's date and time preferences", () => { + prefs.dateFormat = "ymd" + prefs.timeFormat = "24h" render() - respond([{ providerId: providerIdentifiers.openrouter, ok: false, reason, fetchedAt: 1 }]) + const resetAt = new Date(2026, 10, 1, 12, 0).getTime() + const fetchedAt = new Date(2026, 10, 1, 14, 41).getTime() + respond([ + { + providerId: providerIdentifiers.openrouter, + ok: true, + fetchedAt, + windows: [{ id: "key_limit", valueLabel: "x", usedPercent: 10, resetAt }], + }, + ]) const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) - expect(row.textContent).toContain(`settings:usage.limits.${reason}`.split(".").pop()!) - expect(row.querySelector("[role=progressbar]")).toBeNull() + expect(row.textContent).toContain("settings:usage.limits.resets 2026-11-01") + expect(screen.getByTestId("provider-limits-refresh").textContent).toContain( + `settings:usage.limits.updated ${formatTimeOnly(fetchedAt, "24h")}`, + ) + expect(screen.getByTestId("provider-limits-refresh").textContent).not.toMatch(/\b(AM|PM)\b/i) }) + it.each(["noKey", "noCopilot", "signedOut", "unsupported", "error"] as const)( + "shows the quiet status text for %s", + (reason) => { + render() + respond([{ providerId: providerIdentifiers.openrouter, ok: false, reason, fetchedAt: 1 }]) + const row = screen.getByTestId(`provider-limit-${providerIdentifiers.openrouter}`) + expect(row.textContent).toContain(`settings:usage.limits.${reason}`) + expect(row.querySelector("[role=progressbar]")).toBeNull() + }, + ) + it.each([ [50, "bg-vscode-charts-green"], [80, "bg-vscode-charts-yellow"], diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 689b6ce4b4..f9758b2cda 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -43,7 +43,7 @@ "about": "Sobre Zoo Code", "skills": "Skills", "rules": "Regles", - "usage": "Usage" + "usage": "Ús" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Quan esta activat, les pestanyes dels fitxers que Zoo ha creat (en lloc de modificar) tambe es tanquen automaticament despres d'acceptar el canvi." }, "appearance": { - "reset": "Reset to default", + "reset": "Restableix el valor per defecte", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Mida de la lletra de la interfície", + "description": "Escala tot el text de Zoo Code. El 100% coincideix amb VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Mida de la lletra del terminal", + "description": "Mida de la lletra de la sortida d'ordres mostrada al xat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Mida de la lletra de l'editor", + "description": "Mida de la lletra dels blocs de codi i dels diffs mostrats al xat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Densitat de l'espaiat", + "description": "Farciment entre els missatges del xat. Un valor més baix és més compacte." }, - "preview": "Preview", + "preview": "Previsualització", "sample": { - "you": "You", + "you": "Tu", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Afegeix un indicador de càrrega al botó Desa", + "assistant": "Fet. He afegit un indicador que es mostra mentre la sol·licitud està en curs." } } }, @@ -1262,89 +1262,89 @@ "footer": "Crea les teves pròpies skills amb el mode Skill Writer, disponible al Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Idioma i visualització de data i hora.", + "languageTitle": "Idioma", + "displayLanguage": "Idioma de visualització", + "displayLanguageDescription": "Idioma de la interfície de Zoo Code.", + "dateTimeTitle": "Data i hora", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Format de l'hora", + "description": "Automàtic segueix l'idioma de visualització.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automàtic", + "12h": "12 hores", + "24h": "24 hores" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Format de la data", + "example": "p. ex. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automàtic", + "mdy": "MM/DD/AAAA", + "dmy": "DD/MM/AAAA", + "ymd": "AAAA-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Mostra els temps relatius (\"fa 5 min\") a l'historial de tasques" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Previsualització", + "chat": "Missatge del xat", + "history": "Historial de tasques", + "relative": "fa 5 min", + "hover": "(en passar el cursor: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens i cost de l'historial de tasques de Zoo Code en aquesta màquina.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Interval de temps", + "today": "Avui", + "7d": "7 dies", + "30d": "30 dies", + "all": "Tot el temps" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Mostra", + "workspace": "Aquest espai de treball", + "all": "Tots els espais de treball" }, - "empty": "No tasks in this time range.", + "empty": "No hi ha tasques en aquest interval de temps.", "stats": { "cost": "cost", "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", + "tasks": "tasques", + "cacheHit": "{{percent}}% de la memòria cau", + "inOut": "{{input}} d'entrada · {{output}} de sortida", "models": "models" }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "dailyCost": "Cost diari", + "byMode": "Per mode", + "topTasks": "Tasques més cares", + "disclaimer": "Els costos són estimacions de l'historial de tasques i poden diferir de la factura del teu proveïdor.", + "exportCsv": "Exporta CSV", + "noPricingNote": "Alguns proveïdors (p. ex. GitHub Copilot) facturen per subscripció i no informen d'un preu per tasca, per això aquest historial es mostra en tokens. Consulta els límits dels proveïdors a dalt per als crèdits.", + "dailyTokens": "Tokens diaris", + "topTasksTokens": "Tasques més grans", + "byProvider": "Per proveïdor", + "history": "Historial d'ús", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Límits dels proveïdors", + "refresh": "Actualitza", + "updated": "Actualitzat {{time}}", + "loading": "Comprovant els proveïdors connectats…", + "resets": "Es restableix {{date}}", + "signedOut": "No has iniciat sessió", + "noCopilot": "Sense quota de Copilot", + "noKey": "Clau d'API absent o no vàlida", + "unsupported": "URL base personalitzada — no comprovat", + "error": "No s'ha pogut carregar" + }, + "byModel": "Per model", + "notRecorded": "No registrat", + "noPrice": "sense preu", "tokensShort": "tok", "chartLabel": "{{metric}}, total {{total}}" } diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index d442504b44..ffff98452c 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -43,7 +43,7 @@ "about": "Über Zoo Code", "skills": "Skills", "rules": "Regeln", - "usage": "Usage" + "usage": "Nutzung" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Wenn aktiviert, werden Tabs für Dateien, die Zoo neu erstellt hat (anstatt sie zu bearbeiten), nach dem Akzeptieren der Änderung ebenfalls automatisch geschlossen." }, "appearance": { - "reset": "Reset to default", + "reset": "Auf Standard zurücksetzen", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Schriftgröße der Oberfläche", + "description": "Skaliert den gesamten Zoo-Code-Text. 100 % entspricht VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Schriftgröße des Terminals", + "description": "Schriftgröße für die im Chat angezeigte Befehlsausgabe." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Schriftgröße des Editors", + "description": "Schriftgröße für Codeblöcke und Diffs im Chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Abstandsdichte", + "description": "Innenabstand zwischen Chat-Nachrichten. Niedriger ist kompakter." }, - "preview": "Preview", + "preview": "Vorschau", "sample": { - "you": "You", + "you": "Du", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Füge der Schaltfläche „Speichern“ einen Ladeindikator hinzu", + "assistant": "Erledigt. Ich habe einen Spinner hinzugefügt, der angezeigt wird, solange die Anfrage läuft." } } }, @@ -1262,90 +1262,90 @@ "footer": "Erstelle deine eigenen Skills mit dem Skill Writer Modus, verfügbar im Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Sprache sowie Datums- und Zeitanzeige.", + "languageTitle": "Sprache", + "displayLanguage": "Anzeigesprache", + "displayLanguageDescription": "Sprache der Zoo-Code-Oberfläche.", + "dateTimeTitle": "Datum und Uhrzeit", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Zeitformat", + "description": "Automatisch folgt deiner Anzeigesprache.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automatisch", + "12h": "12-Stunden", + "24h": "24-Stunden" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Datumsformat", + "example": "z. B. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automatisch", + "mdy": "MM/TT/JJJJ", + "dmy": "TT/MM/JJJJ", + "ymd": "JJJJ-MM-TT" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Relative Zeiten („vor 5 Min.“) im Aufgabenverlauf anzeigen" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Vorschau", + "chat": "Chat-Nachricht", + "history": "Aufgabenverlauf", + "relative": "vor 5 Min.", + "hover": "(Hover: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens und Kosten aus deinem Zoo-Code-Aufgabenverlauf auf diesem Rechner.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Zeitraum", + "today": "Heute", + "7d": "7 Tage", + "30d": "30 Tage", + "all": "Gesamter Zeitraum" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Anzeigen", + "workspace": "Dieser Workspace", + "all": "Alle Workspaces" }, - "empty": "No tasks in this time range.", + "empty": "Keine Aufgaben in diesem Zeitraum.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "Kosten", + "tokens": "Tokens", + "tasks": "Aufgaben", + "cacheHit": "{{percent}} % aus dem Cache", + "inOut": "{{input}} ein · {{output}} aus", + "models": "Modelle" + }, + "dailyCost": "Tägliche Kosten", + "byMode": "Nach Modus", + "topTasks": "Teuerste Aufgaben", + "disclaimer": "Die Kosten sind Schätzungen aus dem Aufgabenverlauf und können von der Rechnung deines Anbieters abweichen.", + "exportCsv": "CSV exportieren", + "noPricingNote": "Einige Anbieter (z. B. GitHub Copilot) rechnen per Abonnement ab und melden keinen Preis pro Aufgabe, daher wird dieser Verlauf in Tokens angezeigt. Credits findest du oben unter Anbieterlimits.", + "dailyTokens": "Tägliche Tokens", + "topTasksTokens": "Größte Aufgaben", + "byProvider": "Nach Anbieter", + "history": "Nutzungsverlauf", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "Anbieterlimits", + "refresh": "Aktualisieren", + "updated": "Aktualisiert {{time}}", + "loading": "Verbundene Anbieter werden geprüft…", + "resets": "Zurücksetzung {{date}}", + "signedOut": "Nicht angemeldet", + "noCopilot": "Kein Copilot-Kontingent", + "noKey": "API-Schlüssel fehlt oder ist ungültig", + "unsupported": "Benutzerdefinierte Basis-URL – nicht geprüft", + "error": "Konnte nicht geladen werden" + }, + "byModel": "Nach Modell", + "notRecorded": "Nicht erfasst", + "noPrice": "kein Preis", + "tokensShort": "Tok.", + "chartLabel": "{{metric}}, insgesamt {{total}}" } } diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index fca8c2bf46..e26777e020 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -1269,7 +1269,7 @@ "dateTimeTitle": "Date & time", "timeFormat": { "label": "Time format", - "description": "Auto follows your system locale.", + "description": "Auto follows your display language.", "options": { "auto": "Auto", "12h": "12-hour", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 38bfb94a02..b5b16a8128 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -43,7 +43,7 @@ "about": "Acerca de Zoo Code", "skills": "Skills", "rules": "Reglas", - "usage": "Usage" + "usage": "Uso" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Cuando esta opcion esta activada, las pestanas de archivos que Zoo ha creado (en lugar de modificar) tambien se cierran automaticamente despues de aceptar la modificacion." }, "appearance": { - "reset": "Reset to default", + "reset": "Restablecer al valor predeterminado", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Tamaño de fuente de la interfaz", + "description": "Escala todo el texto de Zoo Code. El 100 % coincide con VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Tamaño de fuente del terminal", + "description": "Tamaño de fuente de la salida de comandos que se muestra en el chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Tamaño de fuente del editor", + "description": "Tamaño de fuente de los bloques de código y los diffs que se muestran en el chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Densidad de espaciado", + "description": "Relleno entre los mensajes del chat. Un valor menor es más compacto." }, - "preview": "Preview", + "preview": "Vista previa", "sample": { - "you": "You", + "you": "Tú", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Añade un indicador de carga al botón Guardar", + "assistant": "Listo. He añadido un indicador que se muestra mientras la solicitud está en curso." } } }, @@ -1262,89 +1262,89 @@ "footer": "Crea tus propias skills con el modo Skill Writer, disponible en el Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Idioma y visualización de fecha y hora.", + "languageTitle": "Idioma", + "displayLanguage": "Idioma de visualización", + "displayLanguageDescription": "Idioma de la interfaz de Zoo Code.", + "dateTimeTitle": "Fecha y hora", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Formato de hora", + "description": "Automático sigue el idioma de visualización.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automático", + "12h": "12 horas", + "24h": "24 horas" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Formato de fecha", + "example": "p. ej. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automático", + "mdy": "MM/DD/AAAA", + "dmy": "DD/MM/AAAA", + "ymd": "AAAA-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Mostrar tiempos relativos («hace 5 min») en el historial de tareas" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Vista previa", + "chat": "Mensaje del chat", + "history": "Historial de tareas", + "relative": "hace 5 min", + "hover": "(al pasar el cursor: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens y coste del historial de tareas de Zoo Code en este equipo.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Intervalo de tiempo", + "today": "Hoy", + "7d": "7 días", + "30d": "30 días", + "all": "Todo el tiempo" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Mostrar", + "workspace": "Este espacio de trabajo", + "all": "Todos los espacios de trabajo" }, - "empty": "No tasks in this time range.", + "empty": "No hay tareas en este intervalo de tiempo.", "stats": { - "cost": "cost", + "cost": "coste", "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "tasks": "tareas", + "cacheHit": "{{percent}} % desde la caché", + "inOut": "{{input}} de entrada · {{output}} de salida", + "models": "modelos" + }, + "dailyCost": "Coste diario", + "byMode": "Por modo", + "topTasks": "Tareas más caras", + "disclaimer": "Los costes son estimaciones del historial de tareas y pueden diferir de la factura de tu proveedor.", + "exportCsv": "Exportar CSV", + "noPricingNote": "Algunos proveedores (p. ej. GitHub Copilot) facturan por suscripción y no informan de un precio por tarea, por lo que este historial se muestra en tokens. Consulta los límites de los proveedores arriba para ver los créditos.", + "dailyTokens": "Tokens diarios", + "topTasksTokens": "Tareas más grandes", + "byProvider": "Por proveedor", + "history": "Historial de uso", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Límites de los proveedores", + "refresh": "Actualizar", + "updated": "Actualizado {{time}}", + "loading": "Comprobando los proveedores conectados…", + "resets": "Se restablece {{date}}", + "signedOut": "Sesión no iniciada", + "noCopilot": "Sin cuota de Copilot", + "noKey": "Falta la clave de API o no es válida", + "unsupported": "URL base personalizada: no comprobado", + "error": "No se pudo cargar" + }, + "byModel": "Por modelo", + "notRecorded": "No registrado", + "noPrice": "sin precio", "tokensShort": "tok", "chartLabel": "{{metric}}, total {{total}}" } diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 3fa3a8be70..5c94080e84 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -43,7 +43,7 @@ "about": "À propos de Zoo Code", "skills": "Skills", "rules": "Règles", - "usage": "Usage" + "usage": "Utilisation" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Quand cette option est activée, les onglets des fichiers que Zoo a créés (plutôt que modifiés) sont également fermés automatiquement après l'acceptation de la modification." }, "appearance": { - "reset": "Reset to default", + "reset": "Rétablir la valeur par défaut", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Taille de police de l'interface", + "description": "Met à l'échelle tout le texte de Zoo Code. 100 % correspond à VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Taille de police du terminal", + "description": "Taille de police de la sortie des commandes affichée dans le chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Taille de police de l'éditeur", + "description": "Taille de police des blocs de code et des diffs affichés dans le chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Densité d'espacement", + "description": "Marge intérieure entre les messages du chat. Plus bas, plus compact." }, - "preview": "Preview", + "preview": "Aperçu", "sample": { - "you": "You", + "you": "Vous", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Ajoute un indicateur de chargement au bouton Enregistrer", + "assistant": "Terminé. J'ai ajouté un indicateur qui s'affiche pendant que la requête est en cours." } } }, @@ -1262,89 +1262,89 @@ "footer": "Créez vos propres skills avec le mode Skill Writer, disponible dans le Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Langue et affichage de la date et de l'heure.", + "languageTitle": "Langue", + "displayLanguage": "Langue d'affichage", + "displayLanguageDescription": "Langue de l'interface de Zoo Code.", + "dateTimeTitle": "Date et heure", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Format de l'heure", + "description": "Auto suit votre langue d'affichage.", "options": { "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "12h": "12 heures", + "24h": "24 heures" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Format de la date", + "example": "p. ex. {{example}}", "options": { "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "mdy": "MM/JJ/AAAA", + "dmy": "JJ/MM/AAAA", + "ymd": "AAAA-MM-JJ" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Afficher les durées relatives (« il y a 5 min ») dans l'historique des tâches" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Aperçu", + "chat": "Message du chat", + "history": "Historique des tâches", + "relative": "il y a 5 min", + "hover": "(survol : {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens et coût de l'historique des tâches de Zoo Code sur cette machine.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Période", + "today": "Aujourd'hui", + "7d": "7 jours", + "30d": "30 jours", + "all": "Tout" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Afficher", + "workspace": "Cet espace de travail", + "all": "Tous les espaces de travail" }, - "empty": "No tasks in this time range.", + "empty": "Aucune tâche sur cette période.", "stats": { - "cost": "cost", + "cost": "coût", "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "tasks": "tâches", + "cacheHit": "{{percent}} % depuis le cache", + "inOut": "{{input}} en entrée · {{output}} en sortie", + "models": "modèles" + }, + "dailyCost": "Coût quotidien", + "byMode": "Par mode", + "topTasks": "Tâches les plus coûteuses", + "disclaimer": "Les coûts sont des estimations issues de l'historique des tâches et peuvent différer de la facture de votre fournisseur.", + "exportCsv": "Exporter en CSV", + "noPricingNote": "Certains fournisseurs (p. ex. GitHub Copilot) facturent par abonnement et ne communiquent pas de prix par tâche ; cet historique est donc affiché en tokens. Consultez les limites des fournisseurs ci-dessus pour les crédits.", + "dailyTokens": "Tokens quotidiens", + "topTasksTokens": "Plus grosses tâches", + "byProvider": "Par fournisseur", + "history": "Historique d'utilisation", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Limites des fournisseurs", + "refresh": "Actualiser", + "updated": "Mis à jour {{time}}", + "loading": "Vérification des fournisseurs connectés…", + "resets": "Réinitialisation {{date}}", + "signedOut": "Non connecté", + "noCopilot": "Aucun quota Copilot", + "noKey": "Clé API manquante ou invalide", + "unsupported": "URL de base personnalisée — non vérifiée", + "error": "Impossible de charger" + }, + "byModel": "Par modèle", + "notRecorded": "Non enregistré", + "noPrice": "sans prix", "tokensShort": "tok", "chartLabel": "{{metric}}, total {{total}}" } diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 484396c1ab..b1c10709ff 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -43,7 +43,7 @@ "about": "परिचय", "skills": "Skills", "rules": "नियम", - "usage": "Usage" + "usage": "उपयोग" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Saksham hone par, Zoo dvara banaayi gayi (sanshodhit nahin) files ki tabs ko bhi parivartan svikaar hone ke baad svachalit roop se band kar diya jaata hai." }, "appearance": { - "reset": "Reset to default", + "reset": "डिफ़ॉल्ट पर रीसेट करें", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "इंटरफ़ेस फ़ॉन्ट आकार", + "description": "Zoo Code के सारे टेक्स्ट को स्केल करता है। 100% VS Code के बराबर है।" }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "टर्मिनल फ़ॉन्ट आकार", + "description": "चैट में दिखाए गए कमांड आउटपुट का फ़ॉन्ट आकार।" }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "एडिटर फ़ॉन्ट आकार", + "description": "चैट में दिखाए गए कोड ब्लॉक और डिफ़ का फ़ॉन्ट आकार।" }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "स्पेसिंग घनत्व", + "description": "चैट संदेशों के बीच की पैडिंग। कम मान अधिक सघन होता है।" }, - "preview": "Preview", + "preview": "पूर्वावलोकन", "sample": { - "you": "You", + "you": "आप", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Save बटन में लोडिंग स्पिनर जोड़ें", + "assistant": "हो गया। मैंने एक स्पिनर जोड़ा है जो अनुरोध चलने के दौरान दिखता है।" } } }, @@ -1262,90 +1262,90 @@ "footer": "Skill Writer मोड के साथ अपने स्वयं के Skills बनाएं, Modes Marketplace में उपलब्ध।" }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "भाषा और तारीख़/समय का प्रदर्शन।", + "languageTitle": "भाषा", + "displayLanguage": "प्रदर्शन भाषा", + "displayLanguageDescription": "Zoo Code के इंटरफ़ेस की भाषा।", + "dateTimeTitle": "तारीख़ और समय", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "समय प्रारूप", + "description": "ऑटो आपकी प्रदर्शन भाषा का पालन करता है।", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "ऑटो", + "12h": "12-घंटे", + "24h": "24-घंटे" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "तारीख़ प्रारूप", + "example": "उदा. {{example}}", "options": { - "auto": "Auto", + "auto": "ऑटो", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "टास्क इतिहास में सापेक्ष समय (\"5 मिनट पहले\") दिखाएँ" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "पूर्वावलोकन", + "chat": "चैट संदेश", + "history": "टास्क इतिहास", + "relative": "5 मिनट पहले", + "hover": "(होवर: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "इस मशीन पर आपके Zoo Code टास्क इतिहास से टोकन और लागत।", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "समय सीमा", + "today": "आज", + "7d": "7 दिन", + "30d": "30 दिन", + "all": "सभी समय" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "दिखाएँ", + "workspace": "यह वर्कस्पेस", + "all": "सभी वर्कस्पेस" }, - "empty": "No tasks in this time range.", + "empty": "इस समय सीमा में कोई टास्क नहीं।", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "लागत", + "tokens": "टोकन", + "tasks": "टास्क", + "cacheHit": "{{percent}}% कैश से", + "inOut": "{{input}} इनपुट · {{output}} आउटपुट", + "models": "मॉडल" + }, + "dailyCost": "दैनिक लागत", + "byMode": "मोड के अनुसार", + "topTasks": "सबसे महंगे टास्क", + "disclaimer": "लागत टास्क इतिहास से अनुमान हैं और आपके प्रदाता के बिल से भिन्न हो सकती हैं।", + "exportCsv": "CSV निर्यात करें", + "noPricingNote": "कुछ प्रदाता (जैसे GitHub Copilot) सदस्यता के आधार पर बिल करते हैं और प्रति-टास्क मूल्य नहीं बताते, इसलिए यह इतिहास टोकन में दिखाया गया है। क्रेडिट के लिए ऊपर प्रदाता सीमाएँ देखें।", + "dailyTokens": "दैनिक टोकन", + "topTasksTokens": "सबसे बड़े टास्क", + "byProvider": "प्रदाता के अनुसार", + "history": "उपयोग इतिहास", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "प्रदाता सीमाएँ", + "refresh": "रीफ़्रेश करें", + "updated": "{{time}} को अपडेट किया गया", + "loading": "कनेक्टेड प्रदाताओं की जाँच हो रही है…", + "resets": "{{date}} को रीसेट होगा", + "signedOut": "साइन इन नहीं है", + "noCopilot": "कोई Copilot कोटा नहीं", + "noKey": "API कुंजी अनुपलब्ध या अमान्य", + "unsupported": "कस्टम बेस URL — जाँच नहीं की गई", + "error": "लोड नहीं हो सका" + }, + "byModel": "मॉडल के अनुसार", + "notRecorded": "दर्ज नहीं", + "noPrice": "कोई मूल्य नहीं", + "tokensShort": "टोकन", + "chartLabel": "{{metric}}, कुल {{total}}" } } diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 6ced9b69d1..beadd74d58 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -43,7 +43,7 @@ "about": "Tentang Zoo Code", "skills": "Skills", "rules": "Aturan", - "usage": "Usage" + "usage": "Penggunaan" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Saat diaktifkan, tab untuk file yang dibuat Zoo (bukan dimodifikasi) juga ditutup secara otomatis setelah perubahan diterima." }, "appearance": { - "reset": "Reset to default", + "reset": "Setel ulang ke default", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Ukuran font antarmuka", + "description": "Menskalakan semua teks Zoo Code. 100% sama dengan VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Ukuran font terminal", + "description": "Ukuran font untuk output perintah yang ditampilkan di chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Ukuran font editor", + "description": "Ukuran font untuk blok kode dan diff yang ditampilkan di chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Kerapatan spasi", + "description": "Padding antar pesan chat. Nilai lebih rendah lebih ringkas." }, - "preview": "Preview", + "preview": "Pratinjau", "sample": { - "you": "You", + "you": "Anda", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Tambahkan spinner pemuatan ke tombol Simpan", + "assistant": "Selesai. Saya menambahkan spinner yang tampil selama permintaan berlangsung." } } }, @@ -1262,89 +1262,89 @@ "footer": "Buat skills Anda sendiri dengan mode Skill Writer, tersedia di Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Bahasa dan tampilan tanggal/waktu.", + "languageTitle": "Bahasa", + "displayLanguage": "Bahasa tampilan", + "displayLanguageDescription": "Bahasa untuk antarmuka Zoo Code.", + "dateTimeTitle": "Tanggal & waktu", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Format waktu", + "description": "Otomatis mengikuti bahasa tampilan Anda.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Otomatis", + "12h": "12 jam", + "24h": "24 jam" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Format tanggal", + "example": "mis. {{example}}", "options": { - "auto": "Auto", + "auto": "Otomatis", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Tampilkan waktu relatif (\"5 menit lalu\") di riwayat tugas" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", + "title": "Pratinjau", + "chat": "Pesan chat", + "history": "Riwayat tugas", + "relative": "5 menit lalu", "hover": "(hover: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Token dan biaya dari riwayat tugas Zoo Code Anda di mesin ini.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Rentang waktu", + "today": "Hari ini", + "7d": "7 hari", + "30d": "30 hari", + "all": "Sepanjang waktu" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Tampilkan", + "workspace": "Workspace ini", + "all": "Semua workspace" }, - "empty": "No tasks in this time range.", + "empty": "Tidak ada tugas dalam rentang waktu ini.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "biaya", + "tokens": "token", + "tasks": "tugas", + "cacheHit": "{{percent}}% dari cache", + "inOut": "{{input}} masuk · {{output}} keluar", + "models": "model" + }, + "dailyCost": "Biaya harian", + "byMode": "Menurut mode", + "topTasks": "Tugas termahal", + "disclaimer": "Biaya adalah perkiraan dari riwayat tugas dan bisa berbeda dari tagihan penyedia Anda.", + "exportCsv": "Ekspor CSV", + "noPricingNote": "Beberapa penyedia (mis. GitHub Copilot) menagih berdasarkan langganan dan tidak melaporkan harga per tugas, sehingga riwayat ini ditampilkan dalam token. Lihat Batas penyedia di atas untuk kredit.", + "dailyTokens": "Token harian", + "topTasksTokens": "Tugas terbesar", + "byProvider": "Menurut penyedia", + "history": "Riwayat penggunaan", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Batas penyedia", + "refresh": "Segarkan", + "updated": "Diperbarui {{time}}", + "loading": "Memeriksa penyedia yang terhubung…", + "resets": "Diatur ulang {{date}}", + "signedOut": "Belum masuk", + "noCopilot": "Tidak ada kuota Copilot", + "noKey": "Kunci API hilang atau tidak valid", + "unsupported": "URL dasar kustom — tidak diperiksa", + "error": "Tidak dapat dimuat" + }, + "byModel": "Menurut model", + "notRecorded": "Tidak dicatat", + "noPrice": "tanpa harga", "tokensShort": "tok", "chartLabel": "{{metric}}, total {{total}}" } diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index efd74490bf..6e28b47ac1 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -43,7 +43,7 @@ "about": "Informazioni su Zoo Code", "skills": "Skills", "rules": "Regole", - "usage": "Usage" + "usage": "Utilizzo" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Quando abilitata, le schede dei file che Zoo ha creato (invece di modificare) vengono chiuse automaticamente dopo l'accettazione della modifica." }, "appearance": { - "reset": "Reset to default", + "reset": "Ripristina predefinito", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Dimensione carattere dell'interfaccia", + "description": "Ridimensiona tutto il testo di Zoo Code. 100% corrisponde a VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Dimensione carattere del terminale", + "description": "Dimensione carattere dell'output dei comandi mostrato nella chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Dimensione carattere dell'editor", + "description": "Dimensione carattere dei blocchi di codice e dei diff mostrati nella chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Densità della spaziatura", + "description": "Spaziatura interna tra i messaggi della chat. Un valore più basso è più compatto." }, - "preview": "Preview", + "preview": "Anteprima", "sample": { - "you": "You", + "you": "Tu", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Aggiungi un indicatore di caricamento al pulsante Salva", + "assistant": "Fatto. Ho aggiunto un indicatore che compare mentre la richiesta è in corso." } } }, @@ -1262,90 +1262,90 @@ "footer": "Crea le tue skill con la modalità Skill Writer, disponibile in Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Lingua e visualizzazione di data e ora.", + "languageTitle": "Lingua", + "displayLanguage": "Lingua di visualizzazione", + "displayLanguageDescription": "Lingua dell'interfaccia di Zoo Code.", + "dateTimeTitle": "Data e ora", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Formato dell'ora", + "description": "Auto segue la lingua di visualizzazione.", "options": { "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "12h": "12 ore", + "24h": "24 ore" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Formato della data", + "example": "es. {{example}}", "options": { "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "mdy": "MM/GG/AAAA", + "dmy": "GG/MM/AAAA", + "ymd": "AAAA-MM-GG" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Mostra i tempi relativi (\"5 min fa\") nella cronologia delle attività" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", + "title": "Anteprima", + "chat": "Messaggio della chat", + "history": "Cronologia delle attività", + "relative": "5 min fa", "hover": "(hover: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Token e costi dalla cronologia delle attività di Zoo Code su questo computer.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Intervallo di tempo", + "today": "Oggi", + "7d": "7 giorni", + "30d": "30 giorni", + "all": "Sempre" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Mostra", + "workspace": "Questo workspace", + "all": "Tutti i workspace" }, - "empty": "No tasks in this time range.", + "empty": "Nessuna attività in questo intervallo di tempo.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "costo", + "tokens": "token", + "tasks": "attività", + "cacheHit": "{{percent}}% dalla cache", + "inOut": "{{input}} in ingresso · {{output}} in uscita", + "models": "modelli" + }, + "dailyCost": "Costo giornaliero", + "byMode": "Per modalità", + "topTasks": "Attività più costose", + "disclaimer": "I costi sono stime basate sulla cronologia delle attività e possono differire dalla fattura del tuo provider.", + "exportCsv": "Esporta CSV", + "noPricingNote": "Alcuni provider (es. GitHub Copilot) fatturano tramite abbonamento e non riportano un prezzo per attività, quindi questa cronologia è mostrata in token. Vedi i limiti dei provider sopra per i crediti.", + "dailyTokens": "Token giornalieri", + "topTasksTokens": "Attività più grandi", + "byProvider": "Per provider", + "history": "Cronologia di utilizzo", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Limiti dei provider", + "refresh": "Aggiorna", + "updated": "Aggiornato {{time}}", + "loading": "Controllo dei provider collegati…", + "resets": "Si reimposta {{date}}", + "signedOut": "Accesso non effettuato", + "noCopilot": "Nessuna quota Copilot", + "noKey": "Chiave API mancante o non valida", + "unsupported": "URL di base personalizzato — non controllato", + "error": "Impossibile caricare" + }, + "byModel": "Per modello", + "notRecorded": "Non registrato", + "noPrice": "senza prezzo", "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "chartLabel": "{{metric}}, totale {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 05a4b917e8..170e90dc5e 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -43,7 +43,7 @@ "about": "Zoo Codeについて", "skills": "Skills", "rules": "ルール", - "usage": "Usage" + "usage": "使用状況" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "有効にすると、Zooが作成した(変更ではなく)ファイルのタブも、変更を承認した後に自動的に閉じられます。" }, "appearance": { - "reset": "Reset to default", + "reset": "デフォルトに戻す", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "インターフェースのフォントサイズ", + "description": "Zoo Code のすべてのテキストを拡大縮小します。100% は VS Code と同じです。" }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "ターミナルのフォントサイズ", + "description": "チャットに表示されるコマンド出力のフォントサイズ。" }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "エディターのフォントサイズ", + "description": "チャットに表示されるコードブロックと diff のフォントサイズ。" }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "余白の密度", + "description": "チャットメッセージ間の余白。値が小さいほどコンパクトになります。" }, - "preview": "Preview", + "preview": "プレビュー", "sample": { - "you": "You", + "you": "あなた", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "保存ボタンに読み込み中スピナーを追加して", + "assistant": "完了しました。リクエスト処理中に表示されるスピナーを追加しました。" } } }, @@ -1262,90 +1262,90 @@ "footer": "スキルライターモードで独自のスキルを作成します。モードマーケットプレイスで入手可能です。" }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "言語と日付/時刻の表示。", + "languageTitle": "言語", + "displayLanguage": "表示言語", + "displayLanguageDescription": "Zoo Code のインターフェースの言語。", + "dateTimeTitle": "日付と時刻", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "時刻形式", + "description": "自動は表示言語に従います。", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "自動", + "12h": "12時間制", + "24h": "24時間制" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "日付形式", + "example": "例: {{example}}", "options": { - "auto": "Auto", + "auto": "自動", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "タスク履歴に相対時間(「5分前」)を表示する" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "プレビュー", + "chat": "チャットメッセージ", + "history": "タスク履歴", + "relative": "5分前", + "hover": "(ホバー: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "このマシン上の Zoo Code のタスク履歴によるトークンとコスト。", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "期間", + "today": "今日", + "7d": "7日間", + "30d": "30日間", + "all": "全期間" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "表示", + "workspace": "このワークスペース", + "all": "すべてのワークスペース" }, - "empty": "No tasks in this time range.", + "empty": "この期間にタスクはありません。", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "コスト", + "tokens": "トークン", + "tasks": "タスク", + "cacheHit": "{{percent}}% がキャッシュから", + "inOut": "入力 {{input}} · 出力 {{output}}", + "models": "モデル" + }, + "dailyCost": "日別コスト", + "byMode": "モード別", + "topTasks": "コストの高いタスク", + "disclaimer": "コストはタスク履歴からの推定値であり、プロバイダーの請求額と異なる場合があります。", + "exportCsv": "CSV をエクスポート", + "noPricingNote": "一部のプロバイダー(GitHub Copilot など)はサブスクリプション課金で、タスクごとの価格を報告しないため、この履歴はトークンで表示されます。クレジットについては上のプロバイダーの上限を参照してください。", + "dailyTokens": "日別トークン", + "topTasksTokens": "大きいタスク", + "byProvider": "プロバイダー別", + "history": "使用履歴", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "プロバイダーの上限", + "refresh": "更新", + "updated": "{{time}} に更新", + "loading": "接続済みのプロバイダーを確認中…", + "resets": "{{date}} にリセット", + "signedOut": "サインインしていません", + "noCopilot": "Copilot のクォータなし", + "noKey": "API キーがないか無効です", + "unsupported": "カスタムベース URL — 確認していません", + "error": "読み込めませんでした" + }, + "byModel": "モデル別", + "notRecorded": "記録なし", + "noPrice": "価格なし", + "tokensShort": "トークン", + "chartLabel": "{{metric}}、合計 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index e2d6e44cd2..664dbc8aad 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -43,7 +43,7 @@ "about": "Zoo Code 정보", "skills": "Skills", "rules": "규칙", - "usage": "Usage" + "usage": "사용량" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "활성화 시, Zoo가 수정한 것이 아니라 새로 만든 파일의 탭도 변경 사항을 승인한 후 자동으로 닫힙니다." }, "appearance": { - "reset": "Reset to default", + "reset": "기본값으로 재설정", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "인터페이스 글꼴 크기", + "description": "모든 Zoo Code 텍스트의 크기를 조절합니다. 100%는 VS Code와 같습니다." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "터미널 글꼴 크기", + "description": "채팅에 표시되는 명령 출력의 글꼴 크기입니다." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "편집기 글꼴 크기", + "description": "채팅에 표시되는 코드 블록과 diff의 글꼴 크기입니다." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "간격 밀도", + "description": "채팅 메시지 사이의 여백입니다. 낮을수록 더 촘촘합니다." }, - "preview": "Preview", + "preview": "미리보기", "sample": { - "you": "You", + "you": "나", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "저장 버튼에 로딩 스피너를 추가해 줘", + "assistant": "완료했습니다. 요청이 진행되는 동안 표시되는 스피너를 추가했습니다." } } }, @@ -1262,90 +1262,90 @@ "footer": "스킬 작성자 모드로 자신만의 스킬을 만들 수 있습니다. 모드 마켓플레이스에서 사용 가능합니다." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "언어 및 날짜/시간 표시.", + "languageTitle": "언어", + "displayLanguage": "표시 언어", + "displayLanguageDescription": "Zoo Code 인터페이스 언어입니다.", + "dateTimeTitle": "날짜 및 시간", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "시간 형식", + "description": "자동은 표시 언어를 따릅니다.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "자동", + "12h": "12시간제", + "24h": "24시간제" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "날짜 형식", + "example": "예: {{example}}", "options": { - "auto": "Auto", + "auto": "자동", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "작업 기록에 상대 시간(\"5분 전\") 표시" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "미리보기", + "chat": "채팅 메시지", + "history": "작업 기록", + "relative": "5분 전", + "hover": "(호버: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "이 컴퓨터의 Zoo Code 작업 기록에서 집계한 토큰과 비용입니다.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "기간", + "today": "오늘", + "7d": "7일", + "30d": "30일", + "all": "전체 기간" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "표시", + "workspace": "이 워크스페이스", + "all": "모든 워크스페이스" }, - "empty": "No tasks in this time range.", + "empty": "이 기간에 작업이 없습니다.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "비용", + "tokens": "토큰", + "tasks": "작업", + "cacheHit": "{{percent}}% 캐시에서", + "inOut": "입력 {{input}} · 출력 {{output}}", + "models": "모델" + }, + "dailyCost": "일별 비용", + "byMode": "모드별", + "topTasks": "비용이 큰 작업", + "disclaimer": "비용은 작업 기록을 기반으로 한 추정치이며 공급자의 청구서와 다를 수 있습니다.", + "exportCsv": "CSV 내보내기", + "noPricingNote": "일부 공급자(예: GitHub Copilot)는 구독 방식으로 청구하며 작업별 가격을 보고하지 않으므로 이 기록은 토큰으로 표시됩니다. 크레딧은 위의 공급자 한도를 확인하세요.", + "dailyTokens": "일별 토큰", + "topTasksTokens": "큰 작업", + "byProvider": "공급자별", + "history": "사용 기록", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "공급자 한도", + "refresh": "새로 고침", + "updated": "{{time}}에 업데이트됨", + "loading": "연결된 공급자를 확인하는 중…", + "resets": "{{date}}에 초기화", + "signedOut": "로그인하지 않음", + "noCopilot": "Copilot 할당량 없음", + "noKey": "API 키가 없거나 올바르지 않음", + "unsupported": "사용자 지정 기본 URL — 확인하지 않음", + "error": "불러올 수 없음" + }, + "byModel": "모델별", + "notRecorded": "기록되지 않음", + "noPrice": "가격 없음", + "tokensShort": "토큰", + "chartLabel": "{{metric}}, 합계 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 6528984ea2..66a9748f26 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -43,7 +43,7 @@ "about": "Over Zoo Code", "skills": "Skills", "rules": "Regels", - "usage": "Usage" + "usage": "Gebruik" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Wanneer ingeschakeld, worden tabbladen van bestanden die Zoo heeft aangemaakt (in plaats van gewijzigd) ook automatisch gesloten nadat de wijziging is geaccepteerd." }, "appearance": { - "reset": "Reset to default", + "reset": "Standaardwaarde herstellen", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Lettergrootte van de interface", + "description": "Schaalt alle Zoo Code-tekst. 100% komt overeen met VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Lettergrootte van de terminal", + "description": "Lettergrootte voor de opdrachtuitvoer die in de chat wordt getoond." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Lettergrootte van de editor", + "description": "Lettergrootte voor codeblokken en diffs in de chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Dichtheid van de witruimte", + "description": "Opvulling tussen chatberichten. Lager is compacter." }, - "preview": "Preview", + "preview": "Voorbeeld", "sample": { - "you": "You", + "you": "Jij", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Voeg een laadspinner toe aan de knop Opslaan", + "assistant": "Klaar. Ik heb een spinner toegevoegd die wordt getoond zolang het verzoek loopt." } } }, @@ -1262,90 +1262,90 @@ "footer": "Maak je eigen skills met de Skill Writer modus, beschikbaar in de Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Taal en weergave van datum en tijd.", + "languageTitle": "Taal", + "displayLanguage": "Weergavetaal", + "displayLanguageDescription": "Taal van de Zoo Code-interface.", + "dateTimeTitle": "Datum en tijd", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Tijdnotatie", + "description": "Automatisch volgt je weergavetaal.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automatisch", + "12h": "12-uurs", + "24h": "24-uurs" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Datumnotatie", + "example": "bijv. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automatisch", + "mdy": "MM/DD/JJJJ", + "dmy": "DD/MM/JJJJ", + "ymd": "JJJJ-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Relatieve tijden (\"5 min geleden\") tonen in de takengeschiedenis" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", + "title": "Voorbeeld", + "chat": "Chatbericht", + "history": "Takengeschiedenis", + "relative": "5 min geleden", "hover": "(hover: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens en kosten uit je Zoo Code-takengeschiedenis op deze machine.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Tijdsbereik", + "today": "Vandaag", + "7d": "7 dagen", + "30d": "30 dagen", + "all": "Alle tijd" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Tonen", + "workspace": "Deze werkruimte", + "all": "Alle werkruimtes" }, - "empty": "No tasks in this time range.", + "empty": "Geen taken in dit tijdsbereik.", "stats": { - "cost": "cost", + "cost": "kosten", "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "tasks": "taken", + "cacheHit": "{{percent}}% uit cache", + "inOut": "{{input}} in · {{output}} uit", + "models": "modellen" + }, + "dailyCost": "Dagelijkse kosten", + "byMode": "Per modus", + "topTasks": "Duurste taken", + "disclaimer": "Kosten zijn schattingen op basis van de takengeschiedenis en kunnen afwijken van de rekening van je provider.", + "exportCsv": "CSV exporteren", + "noPricingNote": "Sommige providers (bijv. GitHub Copilot) factureren via een abonnement en melden geen prijs per taak, daarom wordt deze geschiedenis in tokens getoond. Zie hierboven de providerlimieten voor credits.", + "dailyTokens": "Dagelijkse tokens", + "topTasksTokens": "Grootste taken", + "byProvider": "Per provider", + "history": "Gebruiksgeschiedenis", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Providerlimieten", + "refresh": "Vernieuwen", + "updated": "Bijgewerkt {{time}}", + "loading": "Verbonden providers controleren…", + "resets": "Reset {{date}}", + "signedOut": "Niet aangemeld", + "noCopilot": "Geen Copilot-quotum", + "noKey": "API-sleutel ontbreekt of is ongeldig", + "unsupported": "Aangepaste basis-URL — niet gecontroleerd", + "error": "Kon niet laden" + }, + "byModel": "Per model", + "notRecorded": "Niet vastgelegd", + "noPrice": "geen prijs", "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "chartLabel": "{{metric}}, totaal {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 2dc6f4dd57..c08fef91a1 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -43,7 +43,7 @@ "about": "O Zoo Code", "skills": "Skills", "rules": "Reguły", - "usage": "Usage" + "usage": "Użycie" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Gdy wlaczone, karty plikow, ktore Zoo utworzylo (zamiast zmodyfikowalo), sa rowniez automatycznie zamykane po zaakceptowaniu zmiany." }, "appearance": { - "reset": "Reset to default", + "reset": "Przywróć domyślne", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Rozmiar czcionki interfejsu", + "description": "Skaluje cały tekst Zoo Code. 100% odpowiada VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Rozmiar czcionki terminala", + "description": "Rozmiar czcionki danych wyjściowych poleceń wyświetlanych w czacie." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Rozmiar czcionki edytora", + "description": "Rozmiar czcionki bloków kodu i różnic wyświetlanych w czacie." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Gęstość odstępów", + "description": "Odstęp między wiadomościami czatu. Niższa wartość oznacza zwartszy układ." }, - "preview": "Preview", + "preview": "Podgląd", "sample": { - "you": "You", + "you": "Ty", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Dodaj wskaźnik ładowania do przycisku Zapisz", + "assistant": "Gotowe. Dodałem wskaźnik, który jest widoczny, gdy żądanie jest w toku." } } }, @@ -1262,90 +1262,90 @@ "footer": "Twórz własne umiejętności za pomocą trybu Skill Writer, dostępnego w Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Język oraz wyświetlanie daty i godziny.", + "languageTitle": "Język", + "displayLanguage": "Język wyświetlania", + "displayLanguageDescription": "Język interfejsu Zoo Code.", + "dateTimeTitle": "Data i godzina", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Format godziny", + "description": "Automatyczny podąża za językiem wyświetlania.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automatyczny", + "12h": "12-godzinny", + "24h": "24-godzinny" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Format daty", + "example": "np. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automatyczny", + "mdy": "MM/DD/RRRR", + "dmy": "DD/MM/RRRR", + "ymd": "RRRR-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Pokazuj czasy względne („5 min temu”) w historii zadań" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Podgląd", + "chat": "Wiadomość czatu", + "history": "Historia zadań", + "relative": "5 min temu", + "hover": "(po najechaniu: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokeny i koszty z historii zadań Zoo Code na tym komputerze.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Zakres czasu", + "today": "Dzisiaj", + "7d": "7 dni", + "30d": "30 dni", + "all": "Cały czas" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Pokaż", + "workspace": "Ten obszar roboczy", + "all": "Wszystkie obszary robocze" }, - "empty": "No tasks in this time range.", + "empty": "Brak zadań w tym zakresie czasu.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "koszt", + "tokens": "tokeny", + "tasks": "zadania", + "cacheHit": "{{percent}}% z pamięci podręcznej", + "inOut": "{{input}} wejście · {{output}} wyjście", + "models": "modele" + }, + "dailyCost": "Koszt dzienny", + "byMode": "Według trybu", + "topTasks": "Najdroższe zadania", + "disclaimer": "Koszty są szacunkami z historii zadań i mogą różnić się od rachunku od dostawcy.", + "exportCsv": "Eksportuj CSV", + "noPricingNote": "Niektórzy dostawcy (np. GitHub Copilot) rozliczają się w ramach subskrypcji i nie podają ceny za zadanie, dlatego ta historia jest pokazana w tokenach. Kredyty znajdziesz wyżej w limitach dostawców.", + "dailyTokens": "Tokeny dzienne", + "topTasksTokens": "Największe zadania", + "byProvider": "Według dostawcy", + "history": "Historia użycia", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "Limity dostawców", + "refresh": "Odśwież", + "updated": "Zaktualizowano {{time}}", + "loading": "Sprawdzanie połączonych dostawców…", + "resets": "Reset {{date}}", + "signedOut": "Nie zalogowano", + "noCopilot": "Brak limitu Copilot", + "noKey": "Brak klucza API lub jest nieprawidłowy", + "unsupported": "Niestandardowy bazowy URL — nie sprawdzono", + "error": "Nie udało się wczytać" + }, + "byModel": "Według modelu", + "notRecorded": "Nie zapisano", + "noPrice": "brak ceny", + "tokensShort": "tok.", + "chartLabel": "{{metric}}, łącznie {{total}}" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 70ac21dfa1..3da89d4114 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -43,7 +43,7 @@ "about": "Sobre", "skills": "Skills", "rules": "Regras", - "usage": "Usage" + "usage": "Uso" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Quando ativada, as abas de arquivos que o Zoo criou (em vez de modificar) tambem sao fechadas automaticamente apos aceitar a modificacao." }, "appearance": { - "reset": "Reset to default", + "reset": "Restaurar padrão", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Tamanho da fonte da interface", + "description": "Dimensiona todo o texto do Zoo Code. 100% corresponde ao VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Tamanho da fonte do terminal", + "description": "Tamanho da fonte da saída de comandos exibida no chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Tamanho da fonte do editor", + "description": "Tamanho da fonte dos blocos de código e diffs exibidos no chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Densidade de espaçamento", + "description": "Preenchimento entre as mensagens do chat. Menor é mais compacto." }, - "preview": "Preview", + "preview": "Pré-visualização", "sample": { - "you": "You", + "you": "Você", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Adicione um indicador de carregamento ao botão Salvar", + "assistant": "Pronto. Adicionei um indicador que aparece enquanto a solicitação está em andamento." } } }, @@ -1262,89 +1262,89 @@ "footer": "Crie suas próprias skills com o modo Skill Writer, disponível em Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Idioma e exibição de data e hora.", + "languageTitle": "Idioma", + "displayLanguage": "Idioma de exibição", + "displayLanguageDescription": "Idioma da interface do Zoo Code.", + "dateTimeTitle": "Data e hora", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Formato de hora", + "description": "Automático segue o idioma de exibição.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Automático", + "12h": "12 horas", + "24h": "24 horas" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Formato de data", + "example": "ex.: {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Automático", + "mdy": "MM/DD/AAAA", + "dmy": "DD/MM/AAAA", + "ymd": "AAAA-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Mostrar horários relativos (\"há 5 min\") no histórico de tarefas" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Pré-visualização", + "chat": "Mensagem do chat", + "history": "Histórico de tarefas", + "relative": "há 5 min", + "hover": "(ao passar o mouse: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Tokens e custo do histórico de tarefas do Zoo Code nesta máquina.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Intervalo de tempo", + "today": "Hoje", + "7d": "7 dias", + "30d": "30 dias", + "all": "Todo o período" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Mostrar", + "workspace": "Este espaço de trabalho", + "all": "Todos os espaços de trabalho" }, - "empty": "No tasks in this time range.", + "empty": "Nenhuma tarefa neste intervalo de tempo.", "stats": { - "cost": "cost", + "cost": "custo", "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "tasks": "tarefas", + "cacheHit": "{{percent}}% do cache", + "inOut": "{{input}} de entrada · {{output}} de saída", + "models": "modelos" + }, + "dailyCost": "Custo diário", + "byMode": "Por modo", + "topTasks": "Tarefas mais caras", + "disclaimer": "Os custos são estimativas do histórico de tarefas e podem diferir da fatura do seu provedor.", + "exportCsv": "Exportar CSV", + "noPricingNote": "Alguns provedores (ex.: GitHub Copilot) cobram por assinatura e não informam um preço por tarefa, por isso este histórico é mostrado em tokens. Veja os limites dos provedores acima para os créditos.", + "dailyTokens": "Tokens diários", + "topTasksTokens": "Maiores tarefas", + "byProvider": "Por provedor", + "history": "Histórico de uso", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Limites dos provedores", + "refresh": "Atualizar", + "updated": "Atualizado {{time}}", + "loading": "Verificando os provedores conectados…", + "resets": "Redefine {{date}}", + "signedOut": "Sem login", + "noCopilot": "Sem cota do Copilot", + "noKey": "Chave de API ausente ou inválida", + "unsupported": "URL base personalizada — não verificada", + "error": "Não foi possível carregar" + }, + "byModel": "Por modelo", + "notRecorded": "Não registrado", + "noPrice": "sem preço", "tokensShort": "tok", "chartLabel": "{{metric}}, total {{total}}" } diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 612ab715b2..754125214e 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -43,7 +43,7 @@ "about": "О Zoo Code", "skills": "Skills", "rules": "Правила", - "usage": "Usage" + "usage": "Использование" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Если включено, вкладки файлов, которые Zoo создал (а не изменил), также автоматически закрываются после принятия изменения." }, "appearance": { - "reset": "Reset to default", + "reset": "Сбросить по умолчанию", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Размер шрифта интерфейса", + "description": "Масштабирует весь текст Zoo Code. 100% соответствует VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Размер шрифта терминала", + "description": "Размер шрифта вывода команд, показываемого в чате." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Размер шрифта редактора", + "description": "Размер шрифта блоков кода и диффов, показываемых в чате." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Плотность отступов", + "description": "Отступы между сообщениями чата. Чем меньше значение, тем компактнее." }, - "preview": "Preview", + "preview": "Предпросмотр", "sample": { - "you": "You", + "you": "Вы", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Добавь индикатор загрузки на кнопку «Сохранить»", + "assistant": "Готово. Я добавил индикатор, который отображается, пока выполняется запрос." } } }, @@ -1262,90 +1262,90 @@ "footer": "Создавайте собственные навыки с режимом Skill Writer, доступным в Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Язык и отображение даты и времени.", + "languageTitle": "Язык", + "displayLanguage": "Язык интерфейса", + "displayLanguageDescription": "Язык интерфейса Zoo Code.", + "dateTimeTitle": "Дата и время", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Формат времени", + "description": "Авто следует языку интерфейса.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Авто", + "12h": "12-часовой", + "24h": "24-часовой" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Формат даты", + "example": "напр. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Авто", + "mdy": "ММ/ДД/ГГГГ", + "dmy": "ДД/ММ/ГГГГ", + "ymd": "ГГГГ-ММ-ДД" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Показывать относительное время («5 мин назад») в истории задач" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Предпросмотр", + "chat": "Сообщение чата", + "history": "История задач", + "relative": "5 мин назад", + "hover": "(при наведении: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Токены и стоимость по истории задач Zoo Code на этом компьютере.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Период", + "today": "Сегодня", + "7d": "7 дней", + "30d": "30 дней", + "all": "Всё время" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Показать", + "workspace": "Эта рабочая область", + "all": "Все рабочие области" }, - "empty": "No tasks in this time range.", + "empty": "В этом периоде нет задач.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "стоимость", + "tokens": "токены", + "tasks": "задачи", + "cacheHit": "{{percent}}% из кэша", + "inOut": "{{input}} вход · {{output}} выход", + "models": "модели" + }, + "dailyCost": "Стоимость по дням", + "byMode": "По режимам", + "topTasks": "Самые дорогие задачи", + "disclaimer": "Стоимость — это оценка по истории задач, она может отличаться от счёта вашего провайдера.", + "exportCsv": "Экспорт в CSV", + "noPricingNote": "Некоторые провайдеры (например, GitHub Copilot) тарифицируют по подписке и не сообщают цену за задачу, поэтому эта история показана в токенах. Кредиты смотрите выше в лимитах провайдеров.", + "dailyTokens": "Токены по дням", + "topTasksTokens": "Самые крупные задачи", + "byProvider": "По провайдерам", + "history": "История использования", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "Лимиты провайдеров", + "refresh": "Обновить", + "updated": "Обновлено {{time}}", + "loading": "Проверка подключённых провайдеров…", + "resets": "Сброс {{date}}", + "signedOut": "Вход не выполнен", + "noCopilot": "Нет квоты Copilot", + "noKey": "Ключ API отсутствует или недействителен", + "unsupported": "Пользовательский базовый URL — не проверено", + "error": "Не удалось загрузить" + }, + "byModel": "По моделям", + "notRecorded": "Не записано", + "noPrice": "без цены", + "tokensShort": "ток.", + "chartLabel": "{{metric}}, всего {{total}}" } } diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index c7d1127d57..6c6f36bb8a 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -43,7 +43,7 @@ "about": "Zoo Code Hakkında", "skills": "Skills", "rules": "Kurallar", - "usage": "Usage" + "usage": "Kullanım" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Etkinlestirildiginde, Zoo'nun olusturdugu (degistirmedigi) dosyalarin sekmeleri de degisiklik kabul edildikten sonra otomatik olarak kapatilir." }, "appearance": { - "reset": "Reset to default", + "reset": "Varsayılana sıfırla", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Arayüz yazı tipi boyutu", + "description": "Tüm Zoo Code metnini ölçekler. %100, VS Code ile aynıdır." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Terminal yazı tipi boyutu", + "description": "Sohbette gösterilen komut çıktısının yazı tipi boyutu." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Düzenleyici yazı tipi boyutu", + "description": "Sohbette gösterilen kod blokları ve diff'lerin yazı tipi boyutu." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Boşluk yoğunluğu", + "description": "Sohbet mesajları arasındaki dolgu. Düşük değer daha sıkışık görünür." }, - "preview": "Preview", + "preview": "Önizleme", "sample": { - "you": "You", + "you": "Sen", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Kaydet düğmesine bir yükleniyor göstergesi ekle", + "assistant": "Tamam. İstek sürerken görünen bir gösterge ekledim." } } }, @@ -1262,90 +1262,90 @@ "footer": "Skill Writer modu ile kendi becerilerinizi oluşturun. Modes Marketplace'de mevcut." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Dil ile tarih/saat gösterimi.", + "languageTitle": "Dil", + "displayLanguage": "Görüntüleme dili", + "displayLanguageDescription": "Zoo Code arayüzünün dili.", + "dateTimeTitle": "Tarih ve saat", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Saat biçimi", + "description": "Otomatik, görüntüleme dilini izler.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Otomatik", + "12h": "12 saatlik", + "24h": "24 saatlik" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Tarih biçimi", + "example": "ör. {{example}}", "options": { - "auto": "Auto", - "mdy": "MM/DD/YYYY", - "dmy": "DD/MM/YYYY", - "ymd": "YYYY-MM-DD" + "auto": "Otomatik", + "mdy": "AA/GG/YYYY", + "dmy": "GG/AA/YYYY", + "ymd": "YYYY-AA-GG" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Görev geçmişinde göreli zamanları (\"5 dk önce\") göster" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Önizleme", + "chat": "Sohbet mesajı", + "history": "Görev geçmişi", + "relative": "5 dk önce", + "hover": "(üzerine gelince: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Bu makinedeki Zoo Code görev geçmişinizden token ve maliyet.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Zaman aralığı", + "today": "Bugün", + "7d": "7 gün", + "30d": "30 gün", + "all": "Tüm zamanlar" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Göster", + "workspace": "Bu çalışma alanı", + "all": "Tüm çalışma alanları" }, - "empty": "No tasks in this time range.", + "empty": "Bu zaman aralığında görev yok.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "maliyet", + "tokens": "token", + "tasks": "görev", + "cacheHit": "%{{percent}} önbellekten", + "inOut": "{{input}} giriş · {{output}} çıkış", + "models": "model" + }, + "dailyCost": "Günlük maliyet", + "byMode": "Moda göre", + "topTasks": "En pahalı görevler", + "disclaimer": "Maliyetler görev geçmişinden yapılan tahminlerdir ve sağlayıcınızın faturasından farklı olabilir.", + "exportCsv": "CSV olarak dışa aktar", + "noPricingNote": "Bazı sağlayıcılar (ör. GitHub Copilot) abonelik üzerinden faturalandırır ve görev başına fiyat bildirmez; bu yüzden bu geçmiş token cinsinden gösterilir. Krediler için yukarıdaki sağlayıcı limitlerine bakın.", + "dailyTokens": "Günlük token", + "topTasksTokens": "En büyük görevler", + "byProvider": "Sağlayıcıya göre", + "history": "Kullanım geçmişi", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Sağlayıcı limitleri", + "refresh": "Yenile", + "updated": "Güncellendi: {{time}}", + "loading": "Bağlı sağlayıcılar kontrol ediliyor…", + "resets": "Sıfırlanma: {{date}}", + "signedOut": "Oturum açılmadı", + "noCopilot": "Copilot kotası yok", + "noKey": "API anahtarı eksik veya geçersiz", + "unsupported": "Özel temel URL — kontrol edilmedi", + "error": "Yüklenemedi" + }, + "byModel": "Modele göre", + "notRecorded": "Kaydedilmedi", + "noPrice": "fiyat yok", "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "chartLabel": "{{metric}}, toplam {{total}}" } } diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index d9f7e48b33..4bcc725732 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -43,7 +43,7 @@ "about": "Giới thiệu", "skills": "Skills", "rules": "Quy tắc", - "usage": "Usage" + "usage": "Mức sử dụng" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "Khi bật, các tab của các tệp mà Zoo đã tạo (thay vì chỉnh sửa) cũng sẽ tự động đóng sau khi chấp nhận thay đổi." }, "appearance": { - "reset": "Reset to default", + "reset": "Đặt lại về mặc định", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "Cỡ chữ giao diện", + "description": "Thu phóng toàn bộ văn bản của Zoo Code. 100% khớp với VS Code." }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "Cỡ chữ terminal", + "description": "Cỡ chữ cho kết quả lệnh hiển thị trong chat." }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "Cỡ chữ trình soạn thảo", + "description": "Cỡ chữ cho khối mã và diff hiển thị trong chat." }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "Mật độ khoảng cách", + "description": "Khoảng đệm giữa các tin nhắn chat. Giá trị thấp hơn sẽ gọn hơn." }, - "preview": "Preview", + "preview": "Xem trước", "sample": { - "you": "You", + "you": "Bạn", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "Thêm biểu tượng đang tải vào nút Lưu", + "assistant": "Xong. Tôi đã thêm một biểu tượng hiển thị khi yêu cầu đang được xử lý." } } }, @@ -1262,90 +1262,90 @@ "footer": "Tạo các skill của riêng bạn với chế độ Skill Writer, có sẵn tại Modes Marketplace." }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "Ngôn ngữ và cách hiển thị ngày/giờ.", + "languageTitle": "Ngôn ngữ", + "displayLanguage": "Ngôn ngữ hiển thị", + "displayLanguageDescription": "Ngôn ngữ của giao diện Zoo Code.", + "dateTimeTitle": "Ngày và giờ", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "Định dạng giờ", + "description": "Tự động sẽ theo ngôn ngữ hiển thị của bạn.", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "Tự động", + "12h": "12 giờ", + "24h": "24 giờ" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "Định dạng ngày", + "example": "ví dụ: {{example}}", "options": { - "auto": "Auto", + "auto": "Tự động", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "Hiển thị thời gian tương đối (\"5 phút trước\") trong lịch sử nhiệm vụ" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "Xem trước", + "chat": "Tin nhắn chat", + "history": "Lịch sử nhiệm vụ", + "relative": "5 phút trước", + "hover": "(di chuột: {{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "Token và chi phí từ lịch sử nhiệm vụ Zoo Code của bạn trên máy này.", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "Khoảng thời gian", + "today": "Hôm nay", + "7d": "7 ngày", + "30d": "30 ngày", + "all": "Mọi lúc" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "Hiển thị", + "workspace": "Không gian làm việc này", + "all": "Tất cả không gian làm việc" }, - "empty": "No tasks in this time range.", + "empty": "Không có nhiệm vụ nào trong khoảng thời gian này.", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "chi phí", + "tokens": "token", + "tasks": "nhiệm vụ", + "cacheHit": "{{percent}}% từ bộ nhớ đệm", + "inOut": "{{input}} vào · {{output}} ra", + "models": "mô hình" + }, + "dailyCost": "Chi phí hằng ngày", + "byMode": "Theo chế độ", + "topTasks": "Nhiệm vụ tốn kém nhất", + "disclaimer": "Chi phí là ước tính từ lịch sử nhiệm vụ và có thể khác với hóa đơn của nhà cung cấp.", + "exportCsv": "Xuất CSV", + "noPricingNote": "Một số nhà cung cấp (ví dụ GitHub Copilot) tính phí theo gói đăng ký và không báo cáo giá cho từng nhiệm vụ, nên lịch sử này được hiển thị bằng token. Xem Giới hạn nhà cung cấp ở trên để biết tín dụng.", + "dailyTokens": "Token hằng ngày", + "topTasksTokens": "Nhiệm vụ lớn nhất", + "byProvider": "Theo nhà cung cấp", + "history": "Lịch sử sử dụng", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", + "title": "Giới hạn nhà cung cấp", + "refresh": "Làm mới", + "updated": "Cập nhật {{time}}", + "loading": "Đang kiểm tra các nhà cung cấp đã kết nối…", + "resets": "Đặt lại {{date}}", + "signedOut": "Chưa đăng nhập", + "noCopilot": "Không có hạn mức Copilot", + "noKey": "Thiếu khóa API hoặc không hợp lệ", + "unsupported": "URL cơ sở tùy chỉnh — không kiểm tra", + "error": "Không thể tải" + }, + "byModel": "Theo mô hình", + "notRecorded": "Chưa ghi nhận", + "noPrice": "không có giá", "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "chartLabel": "{{metric}}, tổng {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 4757066930..b6697f61be 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -43,7 +43,7 @@ "about": "关于 Zoo Code", "skills": "Skills", "rules": "规则", - "usage": "Usage" + "usage": "用量" }, "about": { "bugReport": { @@ -1182,29 +1182,29 @@ "description": "启用后,Zoo 新建(而非修改)的文件标签页在接受更改后也会自动关闭。" }, "appearance": { - "reset": "Reset to default", + "reset": "恢复默认", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "界面字体大小", + "description": "缩放所有 Zoo Code 文本。100% 与 VS Code 一致。" }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "终端字体大小", + "description": "聊天中显示的命令输出的字体大小。" }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "编辑器字体大小", + "description": "聊天中显示的代码块和 diff 的字体大小。" }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "间距密度", + "description": "聊天消息之间的内边距。数值越小越紧凑。" }, - "preview": "Preview", + "preview": "预览", "sample": { - "you": "You", + "you": "你", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "给保存按钮添加一个加载动画", + "assistant": "完成。我添加了一个在请求进行时显示的加载动画。" } } }, @@ -1262,90 +1262,90 @@ "footer": "使用技能编写器模式创建您自己的技能,可在 模式市集 中获得。" }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "语言和日期/时间显示。", + "languageTitle": "语言", + "displayLanguage": "显示语言", + "displayLanguageDescription": "Zoo Code 界面的语言。", + "dateTimeTitle": "日期和时间", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "时间格式", + "description": "自动将跟随显示语言。", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "自动", + "12h": "12 小时制", + "24h": "24 小时制" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "日期格式", + "example": "例如 {{example}}", "options": { - "auto": "Auto", + "auto": "自动", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "在任务历史中显示相对时间(“5 分钟前”)" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "预览", + "chat": "聊天消息", + "history": "任务历史", + "relative": "5 分钟前", + "hover": "(悬停:{{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "来自此设备上 Zoo Code 任务历史的 Token 和费用。", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "时间范围", + "today": "今天", + "7d": "7 天", + "30d": "30 天", + "all": "全部时间" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "显示", + "workspace": "此工作区", + "all": "所有工作区" }, - "empty": "No tasks in this time range.", + "empty": "此时间范围内没有任务。", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "费用", + "tokens": "Token", + "tasks": "任务", + "cacheHit": "{{percent}}% 来自缓存", + "inOut": "输入 {{input}} · 输出 {{output}}", + "models": "模型" + }, + "dailyCost": "每日费用", + "byMode": "按模式", + "topTasks": "费用最高的任务", + "disclaimer": "费用为根据任务历史得出的估算值,可能与提供商的账单不同。", + "exportCsv": "导出 CSV", + "noPricingNote": "部分提供商(如 GitHub Copilot)按订阅计费,不报告每个任务的价格,因此此历史以 Token 显示。额度请参见上方的提供商限额。", + "dailyTokens": "每日 Token", + "topTasksTokens": "最大的任务", + "byProvider": "按提供商", + "history": "用量历史", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "提供商限额", + "refresh": "刷新", + "updated": "更新于 {{time}}", + "loading": "正在检查已连接的提供商…", + "resets": "重置于 {{date}}", + "signedOut": "未登录", + "noCopilot": "无 Copilot 配额", + "noKey": "API 密钥缺失或无效", + "unsupported": "自定义基础 URL — 未检查", + "error": "无法加载" + }, + "byModel": "按模型", + "notRecorded": "未记录", + "noPrice": "无价格", + "tokensShort": "Token", + "chartLabel": "{{metric}},共 {{total}}" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index f0e1c15578..82e583e929 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -43,7 +43,7 @@ "about": "關於 Zoo Code", "skills": "Skills", "rules": "規則", - "usage": "Usage" + "usage": "用量" }, "about": { "bugReport": { @@ -192,29 +192,29 @@ "description": "啟用後,Zoo 新建(而非修改)的檔案分頁在接受變更後也會自動關閉。" }, "appearance": { - "reset": "Reset to default", + "reset": "還原為預設值", "interfaceFontScale": { - "label": "Interface font size", - "description": "Scales all Zoo Code text. 100% matches VS Code." + "label": "介面字型大小", + "description": "縮放所有 Zoo Code 文字。100% 與 VS Code 相同。" }, "terminalFontSize": { - "label": "Terminal font size", - "description": "Font size for command output shown in chat." + "label": "終端機字型大小", + "description": "聊天中顯示的指令輸出的字型大小。" }, "editorFontSize": { - "label": "Editor font size", - "description": "Font size for code blocks and diffs shown in chat." + "label": "編輯器字型大小", + "description": "聊天中顯示的程式碼區塊與 diff 的字型大小。" }, "spacingScale": { - "label": "Spacing density", - "description": "Padding between chat messages. Lower is more compact." + "label": "間距密度", + "description": "聊天訊息之間的內距。數值越小越緊湊。" }, - "preview": "Preview", + "preview": "預覽", "sample": { - "you": "You", + "you": "你", "zoo": "Zoo", - "user": "Add a loading spinner to the Save button", - "assistant": "Done. I added a spinner that shows while the request is in flight." + "user": "為儲存按鈕加上載入動畫", + "assistant": "完成。我加入了一個在請求進行中顯示的載入動畫。" } } }, @@ -1262,90 +1262,90 @@ "footer": "使用技能編寫器模式建立您自己的技能,可在 模式市集 中取得。" }, "language": { - "description": "Language and date/time display.", - "languageTitle": "Language", - "displayLanguage": "Display language", - "displayLanguageDescription": "Language for Zoo Code's interface.", - "dateTimeTitle": "Date & time", + "description": "語言與日期/時間顯示。", + "languageTitle": "語言", + "displayLanguage": "顯示語言", + "displayLanguageDescription": "Zoo Code 介面的語言。", + "dateTimeTitle": "日期與時間", "timeFormat": { - "label": "Time format", - "description": "Auto follows your system locale.", + "label": "時間格式", + "description": "自動會跟隨顯示語言。", "options": { - "auto": "Auto", - "12h": "12-hour", - "24h": "24-hour" + "auto": "自動", + "12h": "12 小時制", + "24h": "24 小時制" } }, "dateFormat": { - "label": "Date format", - "example": "e.g. {{example}}", + "label": "日期格式", + "example": "例如 {{example}}", "options": { - "auto": "Auto", + "auto": "自動", "mdy": "MM/DD/YYYY", "dmy": "DD/MM/YYYY", "ymd": "YYYY-MM-DD" } }, "relativeTimes": { - "label": "Show relative times (\"5 min ago\") in task history" + "label": "在任務歷史中顯示相對時間(「5 分鐘前」)" }, "preview": { - "title": "Preview", - "chat": "Chat message", - "history": "Task history", - "relative": "5 min ago", - "hover": "(hover: {{value}})" + "title": "預覽", + "chat": "聊天訊息", + "history": "任務歷史", + "relative": "5 分鐘前", + "hover": "(懸停:{{value}})" } }, "usage": { - "description": "Tokens and cost from your Zoo Code task history on this machine.", + "description": "來自此裝置上 Zoo Code 任務歷史的 Token 與費用。", "range": { - "label": "Time range", - "today": "Today", - "7d": "7 days", - "30d": "30 days", - "all": "All time" + "label": "時間範圍", + "today": "今天", + "7d": "7 天", + "30d": "30 天", + "all": "全部時間" }, "scope": { - "label": "Show", - "workspace": "This workspace", - "all": "All workspaces" + "label": "顯示", + "workspace": "此工作區", + "all": "所有工作區" }, - "empty": "No tasks in this time range.", + "empty": "此時間範圍內沒有任務。", "stats": { - "cost": "cost", - "tokens": "tokens", - "tasks": "tasks", - "cacheHit": "{{percent}}% from cache", - "inOut": "{{input}} in · {{output}} out", - "models": "models" - }, - "dailyCost": "Daily cost", - "byMode": "By mode", - "topTasks": "Most expensive tasks", - "disclaimer": "Costs are estimates from task history and may differ from your provider's bill.", - "exportCsv": "Export CSV", - "noPricingNote": "Some providers (e.g. GitHub Copilot) bill by subscription and don't report a per-task price, so this history is shown in tokens. See Provider limits above for credits.", - "dailyTokens": "Daily tokens", - "topTasksTokens": "Largest tasks", - "byProvider": "By provider", - "history": "Usage history", + "cost": "費用", + "tokens": "Token", + "tasks": "任務", + "cacheHit": "{{percent}}% 來自快取", + "inOut": "輸入 {{input}} · 輸出 {{output}}", + "models": "模型" + }, + "dailyCost": "每日費用", + "byMode": "依模式", + "topTasks": "費用最高的任務", + "disclaimer": "費用為根據任務歷史得出的估算值,可能與供應商的帳單不同。", + "exportCsv": "匯出 CSV", + "noPricingNote": "部分供應商(如 GitHub Copilot)按訂閱計費,不回報每個任務的價格,因此此歷史以 Token 顯示。額度請參閱上方的供應商限額。", + "dailyTokens": "每日 Token", + "topTasksTokens": "最大的任務", + "byProvider": "依供應商", + "history": "用量歷史", "limits": { - "title": "Provider limits", - "refresh": "Refresh", - "updated": "Updated {{time}}", - "loading": "Checking connected providers…", - "resets": "Resets {{date}}", - "signedOut": "Not signed in", - "noCopilot": "No Copilot quota", - "noKey": "API key missing or invalid", - "unsupported": "Custom base URL — not checked", - "error": "Couldn't load" - }, - "byModel": "By model", - "notRecorded": "Not recorded", - "noPrice": "no price", - "tokensShort": "tok", - "chartLabel": "{{metric}}, total {{total}}" + "title": "供應商限額", + "refresh": "重新整理", + "updated": "更新於 {{time}}", + "loading": "正在檢查已連線的供應商…", + "resets": "重設於 {{date}}", + "signedOut": "未登入", + "noCopilot": "無 Copilot 配額", + "noKey": "API 金鑰遺失或無效", + "unsupported": "自訂基底 URL — 未檢查", + "error": "無法載入" + }, + "byModel": "依模型", + "notRecorded": "未記錄", + "noPrice": "無價格", + "tokensShort": "Token", + "chartLabel": "{{metric}},共 {{total}}" } } diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index 61bd10cf6a..d0bbc95f66 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -409,6 +409,61 @@ describe("usage attribution edge cases", () => { }) }) +describe("range bounds across a DST change", () => { + const midnight = (y: number, m: number, d: number) => new Date(y, m, d).getTime() + + it("starts 7d and 30d at local midnight even when the window crosses a clock change", () => { + // US clocks go forward on 2026-03-08 and back on 2026-11-01: these windows contain a 23h and a 25h day. + const afterSpring = new Date(2026, 2, 11, 15, 0).getTime() + expect(rangeStart("7d", afterSpring)).toBe(midnight(2026, 2, 5)) + expect(new Date(rangeStart("7d", afterSpring)!).getHours()).toBe(0) + + const afterFall = new Date(2026, 10, 3, 15, 0).getTime() + expect(rangeStart("7d", afterFall)).toBe(midnight(2026, 9, 28)) + expect(new Date(rangeStart("7d", afterFall)!).getHours()).toBe(0) + expect(rangeStart("30d", afterFall)).toBe(midnight(2026, 9, 5)) + expect(new Date(rangeStart("30d", afterFall)!).getHours()).toBe(0) + }) + + it("includes a task from the first hour of the first day, and excludes the hour before it", () => { + const now = new Date(2026, 10, 3, 15, 0).getTime() + const start = midnight(2026, 9, 28) + const csv = usageToCsv( + [ + item({ task: "first-hour", ts: start + 30 * 60 * 1000 }), + item({ task: "just-before", ts: start - 30 * 60 * 1000 }), + ], + { range: "7d", now }, + ) + expect(csv).toContain("first-hour") + expect(csv).not.toContain("just-before") + }) +}) + +describe("usageToCsv formula safety", () => { + it.each(['=HYPERLINK("http://x")', "+1+1", "-2+3", "@SUM(A1)", "\tcmd", "\rcmd"])( + "neutralises a cell that starts with %j", + (text) => { + const csv = usageToCsv([item({ task: text, mode: text })], { range: "all", now: NOW }) + const row = csv.split("\n")[1] + // Each text cell is quoted and prefixed with an apostrophe, so it cannot start with a trigger character. + expect(row).toMatch(/^[^,]+,"'[=+\-@\t\r]/) + expect(row).not.toMatch(/,"[=+\-@\t\r]/) + }, + ) + + it("leaves ordinary text and the numeric columns alone", () => { + const csv = usageToCsv([item({ task: "Fix the = sign", totalCost: 1.5, tokensIn: 10 })], { + range: "all", + now: NOW, + }) + const row = csv.split("\n")[1] + expect(row).toContain('"Fix the = sign"') + expect(row).not.toContain("'Fix") + expect(row.endsWith(",1.5000")).toBe(true) + }) +}) + describe("usageToCsv", () => { it("sorts oldest first and leaves provider, model and counters blank or zero when nothing was recorded", () => { const csv = usageToCsv( diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 602c69159c..1be237e43a 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -131,17 +131,23 @@ function startOfLocalDay(ts: number): number { return d.getTime() } +/** Local midnight `days` calendar days before `ts` (not `days * 24h`, which is off by an hour across a DST change). */ +function startOfLocalDayMinus(ts: number, days: number): number { + const d = new Date(ts) + d.setHours(0, 0, 0, 0) + d.setDate(d.getDate() - days) + return d.getTime() +} + /** Inclusive lower bound for a range, or `undefined` for "all". */ export function rangeStart(range: UsageRange, now: number): number | undefined { - const today = startOfLocalDay(now) - switch (range) { case "today": - return today + return startOfLocalDay(now) case "7d": - return today - 6 * DAY_MS + return startOfLocalDayMinus(now, 6) case "30d": - return today - 29 * DAY_MS + return startOfLocalDayMinus(now, 29) default: return undefined } @@ -299,7 +305,12 @@ export function usageToCsv(history: HistoryItem[], options: UsageOptions): strin "cost", ] - const escape = (value: string) => `"${value.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` + // Spreadsheet apps evaluate a quoted cell that starts with = + - @ (or tab/CR) as a formula, and the + // task text is user- and model-controlled. A leading apostrophe makes the cell inert text. + const escape = (value: string) => { + const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value + return `"${safe.replace(/"/g, '""').replace(/\r?\n/g, " ")}"` + } const rows = history .filter( From 96f41c693994105004157f3399d7857cd516b45b Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 16:21:56 -0700 Subject: [PATCH 20/25] test(visual): update UI settings baselines for the preview font fallback --- .../__screenshots__/ui-settings-dark.png | Bin 85555 -> 85593 bytes .../ui-settings-high-contrast-light.png | Bin 87619 -> 87531 bytes .../ui-settings-high-contrast.png | Bin 87653 -> 87566 bytes .../__screenshots__/ui-settings-light.png | Bin 86630 -> 86537 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png index be05124bf335cc391d86bb38263cbe2607a22677..43037ef226d51d5c8c7d1fba8cb73e5355d7c34f 100644 GIT binary patch delta 39696 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 z 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 delta 39608 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%0A?jkP=Cu73*Q;_OxCK+406 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 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png index 66f47a06b799d25b6f86c8e5b6c1ebd97496b3af..46b1c0ab3cc2c8e21f4aae08a0ba6674ec5f18e0 100644 GIT binary patch delta 42438 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𽿝nl(b$~9rhAYAlnraFifx&w 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 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png index b9c3c4e279f3fd3e300477c1b083280fd7f055f4..f67020542210e48b4c42f18c1a13315e8d45ce90 100644 GIT binary patch delta 41748 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˶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 LGSaNnaEkpGEY!6= From fa442612acaddca303552f92f76eb80b1be7f09f Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 19:12:04 -0700 Subject: [PATCH 21/25] fix(quotas): compare the API key directly instead of hashing it for the cache --- .../__tests__/providerQuotas.spec.ts | 25 +++++++++++- .../language-models/providerQuotas.ts | 40 +++++++++---------- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index e000f51b27..9030446c2b 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -710,7 +710,7 @@ describe("getProviderQuotas", () => { expect(fetchSpy).toHaveBeenCalledTimes(2) }) - it("refetches when the API key changes, without keeping the key in the cache key", async () => { + it("refetches when the API key changes", async () => { const fetchSpy = vi.fn(async () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) await getProviderQuotas( [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never], @@ -725,6 +725,29 @@ describe("getProviderQuotas", () => { expect(fetchSpy).toHaveBeenCalledTimes(2) }) + it("does not hand a result fetched with one key to a caller using another, even while the first is in flight", async () => { + const releases: Array<(r: Response) => void> = [] + const fetchSpy = vi.fn(() => new Promise((resolve) => releases.push(resolve))) + const ok = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + + const first = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never], + {}, + fetchSpy, + ) + const second = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-two" } as never], + {}, + fetchSpy, + ) + await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2)) + releases.forEach((release) => release(ok())) + await Promise.all([first, second]) + + // Each key was fetched on its own; neither piggy-backed on the other's request. + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) + it("shares one request between concurrent callers for the same credentials", async () => { let release: (r: Response) => void = () => {} const fetchSpy = vi.fn(() => new Promise((resolve) => (release = resolve))) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index c2130b691b..c8a346ec96 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -5,8 +5,6 @@ import { type ProviderSettings, } from "@roo-code/types" -import { createHash } from "node:crypto" - import { getGitHubCopilotQuota } from "./copilotQuota" import { QuotaHttpError, @@ -554,19 +552,16 @@ export async function loadProfiles(source: ProfileSource): Promise !!p) } -// Successful results only, per provider and per credential set. A missing key, a -// sign-out or a transient error is never pinned, so fixing it shows up on the -// next open instead of after the TTL. -const cache = new Map() -const inflight = new Map>() - -/** Identifies the key and URLs a result was fetched with, without keeping the key itself. */ -function fingerprint(provider: KeyedProvider, creds: KeyedCredentials): string { - const digest = createHash("sha256") - .update(`${creds.apiKey}\n${(creds.urls ?? []).join("\n")}`) - .digest("hex") - .slice(0, 16) - return `${provider.providerId}:${digest}` +// Successful results only, per provider and URL set. A missing key, a sign-out or a +// transient error is never pinned, so fixing it shows up on the next open instead of +// after the TTL. An entry is reused only for the exact key it was fetched with, so +// switching keys refetches. The key is compared, never hashed or used in a map key. +const cache = new Map() +const inflight = new Map }>() + +/** Identifies a provider and the URLs it is queried at (the credential itself is not part of it). */ +function cacheKey(provider: KeyedProvider, creds: KeyedCredentials): string { + return `${provider.providerId}|${(creds.urls ?? []).join(",")}` } function quotaForKeyedProvider( @@ -580,21 +575,24 @@ function quotaForKeyedProvider( if (skipped) return Promise.resolve(skipped) const resolved = creds as KeyedCredentials - const key = fingerprint(provider, resolved) + const apiKey = resolved.apiKey as string + const key = cacheKey(provider, resolved) const hit = cache.get(key) - if (!force && hit && Date.now() - hit.at < CACHE_TTL_MS) return Promise.resolve(hit.result) + if (!force && hit && hit.apiKey === apiKey && Date.now() - hit.at < CACHE_TTL_MS) return Promise.resolve(hit.result) // Concurrent callers (e.g. a refresh while the first load is still running) share one request. const pending = inflight.get(key) - if (pending) return pending + if (pending && pending.apiKey === apiKey) return pending.request const request = fetchKeyed(provider, resolved, fetchImpl) .then((result) => { - if (result.ok) cache.set(key, { at: Date.now(), result }) + if (result.ok) cache.set(key, { at: Date.now(), apiKey, result }) return result }) - .finally(() => inflight.delete(key)) - inflight.set(key, request) + .finally(() => { + if (inflight.get(key)?.request === request) inflight.delete(key) + }) + inflight.set(key, { apiKey, request }) return request } From e4378beb22ff6c0e7b1c728e81361c73f6fdae3a Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 19:20:55 -0700 Subject: [PATCH 22/25] fix: address second review round (preview isolation, locale-aware preview, DST chart, midnight rollover, Copilot unknown usage) --- .../__tests__/providerQuotas.spec.ts | 4 +- .../language-models/providerQuotas.ts | 5 +- webview-ui/playwright/gallery/stories.tsx | 5 + .../components/settings/AppearancePreview.tsx | 15 ++- .../components/settings/LanguageSettings.tsx | 9 +- .../src/components/settings/UsageSettings.tsx | 18 ++- .../__tests__/AppearancePreview.spec.tsx | 40 +++++-- .../AppearancePreview.visual.fixture.tsx | 33 ++++++ .../__tests__/AppearancePreview.visual.tsx | 38 ++++++ .../LanguageSettings.timeFormat.spec.tsx | 29 +++++ .../__tests__/UsageSettings.branches.spec.tsx | 13 ++- .../__tests__/UsageSettings.midnight.spec.tsx | 103 +++++++++++++++++ .../src/utils/__tests__/usage.dst.spec.ts | 109 ++++++++++++++++++ webview-ui/src/utils/__tests__/usage.spec.ts | 31 ----- webview-ui/src/utils/format.ts | 29 +++-- webview-ui/src/utils/usage.ts | 14 ++- 16 files changed, 425 insertions(+), 70 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/AppearancePreview.visual.fixture.tsx create mode 100644 webview-ui/src/components/settings/__tests__/AppearancePreview.visual.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx create mode 100644 webview-ui/src/utils/__tests__/usage.dst.spec.ts diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index 9030446c2b..ffb94364ee 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -384,7 +384,9 @@ describe("getProviderQuotas – Copilot window and misc", () => { resetProviderQuotaCache() mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: false, fetchedAt: 1 } }) const [none] = await getProviderQuotas([{ apiProvider: providerIdentifiers.githubCopilot } as never]) - expect(none).toMatchObject({ ok: true, windows: [{ valueLabel: "0% used" }] }) + // Unknown usage is reported as unknown (no bar), never as a measured 0%. + expect(none).toMatchObject({ ok: true, windows: [{ valueLabel: "Usage not reported" }] }) + expect((none as { windows: Array<{ usedPercent?: number }> }).windows[0].usedPercent).toBeUndefined() resetProviderQuotaCache() mocked.mockResolvedValueOnce({ ok: true, quota: { unlimited: true, resetAt: 7, fetchedAt: 1 } }) diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index c8a346ec96..2cb2e67d01 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -524,7 +524,10 @@ async function fetchCopilot(force: boolean): Promise { valueLabel: q.used !== undefined && q.entitlement !== undefined ? `${formatCredits(q.used)} / ${formatCredits(q.entitlement)} used` - : `${Math.round(q.usedPercent ?? 0)}% used`, + : q.usedPercent !== undefined + ? `${Math.round(q.usedPercent)}% used` + : // Neither counts nor a percentage were reported: that is unknown, not a measured zero. + "Usage not reported", usedPercent: q.usedPercent, resetAt: q.resetAt, } diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index 85d6919db4..dd6bc511ab 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -419,6 +419,11 @@ export const stories: Record = { const { UISettingsStory } = await import("@/components/settings/__tests__/UISettings.visual.fixture") return }, + "appearance-preview": async () => { + const { AppearancePreviewStory } = + await import("@/components/settings/__tests__/AppearancePreview.visual.fixture") + return + }, "ui-settings-long-locale": async () => { const [{ UISettingsStory }, { default: i18next }] = await Promise.all([ import("@/components/settings/__tests__/UISettings.visual.fixture"), diff --git a/webview-ui/src/components/settings/AppearancePreview.tsx b/webview-ui/src/components/settings/AppearancePreview.tsx index 8961a326c0..eff255e47b 100644 --- a/webview-ui/src/components/settings/AppearancePreview.tsx +++ b/webview-ui/src/components/settings/AppearancePreview.tsx @@ -18,16 +18,18 @@ interface AppearancePreviewProps { export const AppearancePreview = ({ values, chatFontSize }: AppearancePreviewProps) => { const { t } = useAppTranslation() - // Only set what the user has set. An unset value must resolve through the same CSS fallback the - // live UI uses (e.g. the terminal falls back to VS Code's editor font size), so the preview - // never shows a size the chat would not. + // Custom properties inherit, and the saved values live on . An unset value is therefore set + // to `initial` (the guaranteed-invalid value) rather than left out: leaving it out would inherit + // the saved value, so a reset would not show in the preview until after Save. `initial` makes + // var() fall through to the same fallback the live UI uses (e.g. the terminal follows VS Code's + // editor font size), so the preview never shows a size the chat would not. const vars: Record = {} for (const key of Object.keys(APPEARANCE_SETTINGS) as AppearanceKey[]) { const setting = APPEARANCE_SETTINGS[key] const v = clampAppearance(key, values[key]) - if (v !== undefined) vars[setting.cssVar] = setting.toCss(v) + vars[setting.cssVar] = v === undefined ? "initial" : setting.toCss(v) } - if (chatFontSize) vars["--zoo-chat-font-size"] = `${chatFontSize}px` + vars["--zoo-chat-font-size"] = chatFontSize ? `${chatFontSize}px` : "initial" const uiText: CSSProperties = { fontSize: "calc(var(--zoo-chat-font-size, var(--vscode-font-size)) * var(--zoo-ui-scale, 1))", @@ -72,7 +74,8 @@ export const AppearancePreview = ({ values, chatFontSize }: AppearancePreviewPro

+					// Same fallback as CodeBlock: relative to the chat text when no editor size is set.
+					style={{ ...mono, fontSize: "var(--zoo-editor-font-size, 0.95em)" }}>
 					{'const isLoading = status === "saving"'}
 				
diff --git a/webview-ui/src/components/settings/LanguageSettings.tsx b/webview-ui/src/components/settings/LanguageSettings.tsx index 329f90ad0b..b1d19e42de 100644 --- a/webview-ui/src/components/settings/LanguageSettings.tsx +++ b/webview-ui/src/components/settings/LanguageSettings.tsx @@ -41,11 +41,12 @@ export const LanguageSettings = ({ }: LanguageSettingsProps) => { const { t } = useAppTranslation() - // Three short Intl calls: computed on every render, so "auto" follows a language change too. + // Three short Intl calls, computed on every render. They use the language selected above, not the + // saved one, so "auto" previews the new language before Save. const preview = { - chatTime: formatTimeOnly(SAMPLE_TS, timeFormat), - dateExample: formatDateOnly(SAMPLE_TS, dateFormat), - hover: formatDateTime(SAMPLE_TS, timeFormat, dateFormat), + chatTime: formatTimeOnly(SAMPLE_TS, timeFormat, language), + dateExample: formatDateOnly(SAMPLE_TS, dateFormat, language), + hover: formatDateTime(SAMPLE_TS, timeFormat, dateFormat, language), } return ( diff --git a/webview-ui/src/components/settings/UsageSettings.tsx b/webview-ui/src/components/settings/UsageSettings.tsx index a5c3a3ef97..1db7ecd019 100644 --- a/webview-ui/src/components/settings/UsageSettings.tsx +++ b/webview-ui/src/components/settings/UsageSettings.tsx @@ -1,4 +1,4 @@ -import { HTMLAttributes, useMemo, useState } from "react" +import { HTMLAttributes, useEffect, useMemo, useState } from "react" import { Download } from "lucide-react" import { useAppTranslation } from "@/i18n/TranslationContext" @@ -64,7 +64,19 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes taskHistory ?? [], [taskHistory]) - const summary = useMemo(() => summarizeUsage(history, { range, workspace }), [history, range, workspace]) + + // One "now" for the view and for the export, rolled over at the next local midnight. Without it a + // tab left open across midnight keeps yesterday's window while Export (computed at click time) + // would use today's, and the two would disagree. + const [now, setNow] = useState(() => Date.now()) + useEffect(() => { + const nextMidnight = new Date(now) + nextMidnight.setHours(24, 0, 0, 0) + const timer = setTimeout(() => setNow(Date.now()), nextMidnight.getTime() - now + 1000) + return () => clearTimeout(timer) + }, [now]) + + const summary = useMemo(() => summarizeUsage(history, { range, workspace, now }), [history, range, workspace, now]) const providerName = (key: string) => (key === UNRECORDED ? t("settings:usage.notRecorded") : providerLabel(key)) @@ -89,7 +101,7 @@ export const UsageSettings = ({ className, ...props }: HTMLAttributes { - const csv = usageToCsv(history, { range, workspace }) + const csv = usageToCsv(history, { range, workspace, now }) const url = URL.createObjectURL(new Blob([csv], { type: "text/csv" })) const a = document.createElement("a") a.href = url diff --git a/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx b/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx index 6f05c7cc64..57e1ad2991 100644 --- a/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/AppearancePreview.spec.tsx @@ -10,24 +10,40 @@ vi.mock("@/i18n/TranslationContext", () => ({ const root = () => screen.getByTestId("appearance-preview") describe("AppearancePreview", () => { - it("sets nothing for an unset value, so the live CSS fallback chain applies (terminal follows VS Code's editor size)", () => { + it("resets every unset value to `initial`, so the live CSS fallback chain applies and nothing is inherited from ", () => { render() const style = root().style - expect(style.getPropertyValue("--zoo-terminal-font-size")).toBe("") - expect(style.getPropertyValue("--zoo-editor-font-size")).toBe("") - expect(style.getPropertyValue("--zoo-ui-scale")).toBe("") - expect(style.getPropertyValue("--zoo-spacing-scale")).toBe("") + for (const cssVar of [ + "--zoo-terminal-font-size", + "--zoo-editor-font-size", + "--zoo-ui-scale", + "--zoo-spacing-scale", + "--zoo-chat-font-size", + ]) { + expect(style.getPropertyValue(cssVar)).toBe("initial") + } }) - it("sets only the values the user has set, converted the way the live UI converts them", () => { + it("is not affected by a saved value on the root: a reset shows before Save", () => { + document.documentElement.style.setProperty("--zoo-terminal-font-size", "30px") + try { + render() + // The preview sets its own value for the property, so the root's 30px cannot reach it. + expect(root().style.getPropertyValue("--zoo-terminal-font-size")).toBe("initial") + } finally { + document.documentElement.style.removeProperty("--zoo-terminal-font-size") + } + }) + + it("sets the values the user has set, converted the way the live UI converts them, and resets the rest", () => { render( , ) const style = root().style expect(style.getPropertyValue("--zoo-terminal-font-size")).toBe("18px") expect(style.getPropertyValue("--zoo-ui-scale")).toBe("1.2") - expect(style.getPropertyValue("--zoo-spacing-scale")).toBe("") - expect(style.getPropertyValue("--zoo-editor-font-size")).toBe("") + expect(style.getPropertyValue("--zoo-spacing-scale")).toBe("initial") + expect(style.getPropertyValue("--zoo-editor-font-size")).toBe("initial") }) it("clamps out-of-range values like the live UI", () => { @@ -37,8 +53,14 @@ describe("AppearancePreview", () => { it("applies the chat font size only when one is set", () => { const { rerender } = render() - expect(root().style.getPropertyValue("--zoo-chat-font-size")).toBe("") + expect(root().style.getPropertyValue("--zoo-chat-font-size")).toBe("initial") rerender() expect(root().style.getPropertyValue("--zoo-chat-font-size")).toBe("16px") }) + + it("sizes the editor sample like the live code block when no editor size is set (0.95em of the chat text)", () => { + const { container } = render() + const sample = container.querySelector("pre")! + expect(sample.style.fontSize).toBe("var(--zoo-editor-font-size, 0.95em)") + }) }) diff --git a/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.fixture.tsx new file mode 100644 index 0000000000..f96bbab205 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.fixture.tsx @@ -0,0 +1,33 @@ +import { AppearancePreview } from "../AppearancePreview" +import { AppProviders } from "../../../../playwright/AppProviders" + +/** + * Two previews, one above the other: + * - "set": explicit sizes, so the stepper values show up in the sample. + * - "reset": every value unset while carries saved values. The preview must ignore the saved + * values (a reset shows before Save) and fall back to VS Code's sizes, like the live UI does. + */ +export function AppearancePreviewStory() { + return ( + +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.tsx b/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.tsx new file mode 100644 index 0000000000..8420999716 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/AppearancePreview.visual.tsx @@ -0,0 +1,38 @@ +import { expect, test } from "../../../../playwright/coverage-fixture" +import { mountedStory } from "../../../../playwright/mounted-story" +import { applyVisualTheme, visualThemes } from "../../../../playwright/themes" + +for (const theme of visualThemes) { + test(`renders the appearance preview in the VS Code ${theme.name} theme`, async ({ mount, page }) => { + const component = mountedStory(await mount("appearance-preview")) + await applyVisualTheme(page, theme) + // Saved values on , as the app writes them. The "reset" preview must not pick these up. + await page.evaluate(() => { + const root = document.documentElement + root.style.setProperty("--zoo-terminal-font-size", "30px") + root.style.setProperty("--zoo-editor-font-size", "30px") + root.style.setProperty("--zoo-ui-scale", "2") + root.style.setProperty("--zoo-spacing-scale", "2") + root.style.setProperty("--zoo-chat-font-size", "30px") + }) + const story = component.getByTestId("appearance-preview-story") + await expect(story.getByTestId("appearance-preview-set")).toBeVisible() + await expect(story.getByTestId("appearance-preview-reset")).toBeVisible() + await expect(story).toHaveScreenshot(`appearance-preview-${theme.name}.png`) + }) +} + +test("a reset preview ignores saved root values and uses the live fallbacks", async ({ mount, page }) => { + const component = mountedStory(await mount("appearance-preview")) + await page.evaluate(() => { + const root = document.documentElement + root.style.setProperty("--zoo-terminal-font-size", "30px") + root.style.setProperty("--zoo-ui-scale", "2") + }) + const reset = component.getByTestId("appearance-preview-reset").getByTestId("appearance-preview") + const terminal = reset.locator("pre").nth(1) + const size = await terminal.evaluate((el) => parseFloat(getComputedStyle(el).fontSize)) + // Not the saved 30px: the terminal falls back to VS Code's editor font size. + expect(size).toBeLessThan(30) + expect(await reset.evaluate((el) => getComputedStyle(el).getPropertyValue("--zoo-ui-scale").trim())).toBe("") +}) diff --git a/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx index ae29842acb..988bfb2ca4 100644 --- a/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/LanguageSettings.timeFormat.spec.tsx @@ -51,6 +51,35 @@ describe("LanguageSettings – date & time (design mock 2)", () => { }) }) + describe("display language (before Save)", () => { + it("previews 'auto' in the language chosen in the picker, not the saved one", () => { + // German writes 24-hour time, English 12-hour; the i18n language here is untouched. + setup({ language: "de" }) + expect(clock()).toContain("14:41") + expect(clock()).not.toMatch(/\b(AM|PM)\b/i) + }) + + it("changes the 'auto' preview when the picked language changes", () => { + const setCachedStateField = vi.fn() + const { rerender } = render() + expect(clock()).toMatch(/2:41\s?PM/i) + rerender() + expect(clock()).toContain("14:41") + }) + + it("still honours an explicit 12-hour choice whatever language is picked", () => { + setup({ language: "de", timeFormat: "12h" }) + expect(clock()).toMatch(/2:41\s?PM/i) + }) + + it("writes the date example in the picked language for 'auto'", () => { + setup({ language: "de", dateFormat: "auto" }) + expect(screen.getByTestId("date-format-example").textContent).toContain( + new Date(2026, 9, 27).toLocaleDateString("de", { dateStyle: "medium" }), + ) + }) + }) + describe("date format", () => { it.each([ ["mdy", "10/27/2026"], diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx index 45e19b0fcf..f7518755ad 100644 --- a/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.branches.spec.tsx @@ -132,9 +132,16 @@ describe("UsageSettings – branches", () => { }), ] render() - const rows = screen.getByTestId("usage-top-tasks").textContent! - expect(rows).toContain("$2.00") - expect(rows).toContain("settings:usage.tokensShort") + // Each task is one button; find it by its title and check that row on its own. + const rowFor = (title: string) => screen.getByText(title).closest("button")!.textContent! + + const priced = rowFor("priced task") + expect(priced).toContain("$2.00") + expect(priced).not.toContain("settings:usage.tokensShort") + + const unpriced = rowFor("unpriced task") + expect(unpriced).toContain("settings:usage.tokensShort") + expect(unpriced).not.toContain("$") }) it("shows only the model when no provider was recorded, and nothing when no usage was recorded", () => { diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx new file mode 100644 index 0000000000..f688f68287 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.midnight.spec.tsx @@ -0,0 +1,103 @@ +import { act, fireEvent, render, screen } from "@testing-library/react" +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" + +import { providerIdentifiers, type HistoryItem } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" + +const post = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: post } })) + +const state = vi.hoisted(() => ({ taskHistory: [] as HistoryItem[], cwd: "/ws/a" })) +vi.mock("@/context/ExtensionStateContext", () => ({ useExtensionState: () => state })) + +vi.mock("../ProviderLimits", () => ({ ProviderLimits: () =>
)}
})) + +// Spy on the real export so the test can see which "now" it was given. +const csvSpy = vi.hoisted(() => vi.fn()) +vi.mock("@src/utils/usage", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + usageToCsv: (...args: Parameters) => { + csvSpy(...args) + return actual.usageToCsv(...args) + }, + } +}) + +const MINUTE = 60_000 +// 23:50 local on 2026-10-27: ten minutes before midnight. +const BEFORE_MIDNIGHT = new Date(2026, 9, 27, 23, 50).getTime() + +const todaysTask: HistoryItem = { + id: "late", + number: 1, + ts: new Date(2026, 9, 27, 23, 0).getTime(), + task: "Late task", + tokensIn: 1000, + tokensOut: 100, + totalCost: 4, + workspace: "/ws/a", + usageByModel: [ + { + apiProvider: providerIdentifiers.anthropic, + modelId: "claude-sonnet-5", + requests: 1, + tokensIn: 1000, + tokensOut: 100, + cost: 4, + }, + ], +} + +describe("UsageSettings – across local midnight", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] }) + vi.setSystemTime(BEFORE_MIDNIGHT) + post.mockClear() + csvSpy.mockClear() + state.taskHistory = [todaysTask] + state.cwd = "/ws/a" + }) + + afterEach(() => { + vi.useRealTimers() + }) + + it("rolls the Today view over at midnight instead of keeping yesterday's tasks", () => { + render() + fireEvent.click(screen.getByTestId("usage-range-today")) + expect(screen.getByTestId("usage-stats").textContent).toContain("$4.00") + + // Midnight plus the one-second margin: the view must re-evaluate "today" on its own. + act(() => { + vi.advanceTimersByTime(10 * MINUTE + 5_000) + }) + + expect(screen.queryByTestId("usage-stats")).toBeNull() + expect(screen.getByTestId("usage-empty")).toBeTruthy() + }) + + it("keeps the view and the export on the same day, even if the clock moves past midnight first", () => { + render() + fireEvent.click(screen.getByTestId("usage-range-today")) + Object.assign(URL, { createObjectURL: vi.fn(() => "blob:x"), revokeObjectURL: vi.fn() }) + const click = vi.spyOn(HTMLAnchorElement.prototype, "click").mockImplementation(() => {}) + + // The wall clock passes midnight but the roll-over timer has not fired yet: the view still shows the + // old day, so Export must use that same day rather than a fresh Date.now(). + vi.setSystemTime(BEFORE_MIDNIGHT + 20 * MINUTE) + fireEvent.click(screen.getByTestId("usage-export-csv")) + + expect(csvSpy).toHaveBeenCalledTimes(1) + expect(csvSpy.mock.calls[0][1]).toMatchObject({ range: "today", now: BEFORE_MIDNIGHT }) + click.mockRestore() + }) + + it("does not leave a timer running after unmount", () => { + const { unmount } = render() + unmount() + expect(vi.getTimerCount()).toBe(0) + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.dst.spec.ts b/webview-ui/src/utils/__tests__/usage.dst.spec.ts new file mode 100644 index 0000000000..4b2a58c027 --- /dev/null +++ b/webview-ui/src/utils/__tests__/usage.dst.spec.ts @@ -0,0 +1,109 @@ +import { afterAll, describe, expect, it } from "vitest" + +import type { HistoryItem } from "@roo-code/types" + +import { rangeStart, summarizeUsage, usageToCsv } from "../usage" + +// These cases only mean something in a zone that observes DST: in UTC a fixed 24h step and a calendar +// day are the same thing, so the old implementation would pass. The zone is pinned here, and every test +// asserts that its fixture really crosses an offset change, so a runner that ignores TZ fails loudly +// instead of passing vacuously. +// +// The zone must be set before any fixture below is computed (they are built at collection time), so it +// is set at module level rather than in beforeAll. +const previousZone = process.env.TZ +process.env.TZ = "America/New_York" + +afterAll(() => { + if (previousZone === undefined) delete process.env.TZ + else process.env.TZ = previousZone +}) + +const at = (y: number, m: number, d: number, h = 0, min = 0) => new Date(y, m, d, h, min).getTime() +const offsetOf = (ts: number) => new Date(ts).getTimezoneOffset() + +let counter = 0 +const item = (overrides: Partial): HistoryItem => ({ + id: `dst${++counter}`, + number: counter, + ts: 0, + task: `task ${counter}`, + tokensIn: 100, + tokensOut: 0, + totalCost: 1, + workspace: "/ws/a", + ...overrides, +}) + +// US clocks go forward on 2026-03-08 (a 23h day) and back on 2026-11-01 (a 25h day). +const SPRING = { now: at(2026, 2, 11, 15), firstDay: at(2026, 2, 5), lastDay: at(2026, 2, 11) } +const FALL = { now: at(2026, 10, 4, 15), firstDay: at(2026, 9, 29), lastDay: at(2026, 10, 4) } + +describe.each([ + ["spring forward (23h day)", SPRING], + ["fall back (25h day)", FALL], +])("a 7-day window across %s", (_name, w) => { + it("really crosses a clock change in this runner", () => { + expect(offsetOf(w.firstDay)).not.toBe(offsetOf(w.lastDay)) + }) + + it("starts at local midnight", () => { + const start = rangeStart("7d", w.now)! + expect(start).toBe(w.firstDay) + expect(new Date(start).getHours()).toBe(0) + expect(new Date(start).getMinutes()).toBe(0) + }) + + it("charts one bar per calendar day, ending on today", () => { + const { daily } = summarizeUsage([item({ ts: w.now - 60_000 })], { range: "7d", now: w.now }) + + expect(daily).toHaveLength(7) + // Every bar is a local midnight, on consecutive calendar dates, with no day skipped or repeated. + const dates = daily.map((d) => new Date(d.day)) + dates.forEach((d) => expect([d.getHours(), d.getMinutes()]).toEqual([0, 0])) + dates.slice(1).forEach((d, i) => { + const prev = new Date(dates[i]) + prev.setDate(prev.getDate() + 1) + expect(d.getTime()).toBe(prev.getTime()) + }) + expect(daily[0].day).toBe(w.firstDay) + expect(daily.at(-1)!.day).toBe(w.lastDay) + }) + + it("puts today's spend in today's bar, so the bars add up to the total", () => { + const history = [ + item({ ts: w.firstDay + 30 * 60_000, totalCost: 2 }), + item({ ts: w.lastDay + 30 * 60_000, totalCost: 3 }), + ] + const s = summarizeUsage(history, { range: "7d", now: w.now }) + expect(s.totalCost).toBe(5) + expect(s.daily.at(-1)).toMatchObject({ day: w.lastDay, cost: 3 }) + expect(s.daily[0]).toMatchObject({ day: w.firstDay, cost: 2 }) + expect(s.daily.reduce((sum, d) => sum + d.cost, 0)).toBe(s.totalCost) + }) + + it("includes a task from the first minute of the first day, and excludes the minute before it", () => { + const csv = usageToCsv( + [ + item({ task: "first-minute", ts: w.firstDay + 60_000 }), + item({ task: "just-before", ts: w.firstDay - 60_000 }), + ], + { range: "7d", now: w.now }, + ) + expect(csv).toContain("first-minute") + expect(csv).not.toContain("just-before") + }) +}) + +describe("a 30-day window across a clock change", () => { + it("charts 30 consecutive local days ending on today", () => { + const now = at(2026, 10, 3, 15) + expect(offsetOf(rangeStart("30d", now)!)).not.toBe(offsetOf(now)) + + const { daily } = summarizeUsage([item({ ts: now - 60_000 })], { range: "30d", now }) + expect(daily).toHaveLength(30) + expect(daily.at(-1)!.day).toBe(at(2026, 10, 3)) + daily.forEach((d) => expect(new Date(d.day).getHours()).toBe(0)) + expect(new Set(daily.map((d) => d.day)).size).toBe(30) + }) +}) diff --git a/webview-ui/src/utils/__tests__/usage.spec.ts b/webview-ui/src/utils/__tests__/usage.spec.ts index d0bbc95f66..7f5b8435b3 100644 --- a/webview-ui/src/utils/__tests__/usage.spec.ts +++ b/webview-ui/src/utils/__tests__/usage.spec.ts @@ -409,37 +409,6 @@ describe("usage attribution edge cases", () => { }) }) -describe("range bounds across a DST change", () => { - const midnight = (y: number, m: number, d: number) => new Date(y, m, d).getTime() - - it("starts 7d and 30d at local midnight even when the window crosses a clock change", () => { - // US clocks go forward on 2026-03-08 and back on 2026-11-01: these windows contain a 23h and a 25h day. - const afterSpring = new Date(2026, 2, 11, 15, 0).getTime() - expect(rangeStart("7d", afterSpring)).toBe(midnight(2026, 2, 5)) - expect(new Date(rangeStart("7d", afterSpring)!).getHours()).toBe(0) - - const afterFall = new Date(2026, 10, 3, 15, 0).getTime() - expect(rangeStart("7d", afterFall)).toBe(midnight(2026, 9, 28)) - expect(new Date(rangeStart("7d", afterFall)!).getHours()).toBe(0) - expect(rangeStart("30d", afterFall)).toBe(midnight(2026, 9, 5)) - expect(new Date(rangeStart("30d", afterFall)!).getHours()).toBe(0) - }) - - it("includes a task from the first hour of the first day, and excludes the hour before it", () => { - const now = new Date(2026, 10, 3, 15, 0).getTime() - const start = midnight(2026, 9, 28) - const csv = usageToCsv( - [ - item({ task: "first-hour", ts: start + 30 * 60 * 1000 }), - item({ task: "just-before", ts: start - 30 * 60 * 1000 }), - ], - { range: "7d", now }, - ) - expect(csv).toContain("first-hour") - expect(csv).not.toContain("just-before") - }) -}) - describe("usageToCsv formula safety", () => { it.each(['=HYPERLINK("http://x")', "+1+1", "-2+3", "@SUM(A1)", "\tcmd", "\rcmd"])( "neutralises a cell that starts with %j", diff --git a/webview-ui/src/utils/format.ts b/webview-ui/src/utils/format.ts index 55c9cd88fd..f0ed61f101 100644 --- a/webview-ui/src/utils/format.ts +++ b/webview-ui/src/utils/format.ts @@ -54,7 +54,11 @@ const pad = (n: number) => String(n).padStart(2, "0") * explicit orders use a fixed numeric form (e.g. 10/27/2026, 27/10/2026, * 2026-10-27) so the preview and the app agree regardless of locale. */ -export const formatDateOnly = (timestamp: number, dateFormat: DateFormat = "auto") => { +export const formatDateOnly = ( + timestamp: number, + dateFormat: DateFormat = "auto", + locale: string = i18next.language || "en", +) => { const date = new Date(timestamp) switch (dateFormat) { @@ -65,21 +69,32 @@ export const formatDateOnly = (timestamp: number, dateFormat: DateFormat = "auto case "ymd": return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` default: - return date.toLocaleDateString(i18next.language || "en", { dateStyle: "medium" }) + return date.toLocaleDateString(locale, { dateStyle: "medium" }) } } -/** Clock time only, honouring the 12h/24h choice. */ -export const formatTimeOnly = (timestamp: number, timeFormat: TimeFormat = "auto") => - new Date(timestamp).toLocaleTimeString(i18next.language || "en", { +/** + * Clock time only, honouring the 12h/24h choice. `locale` defaults to the active display language; + * pass one to preview a language that has not been saved yet. + */ +export const formatTimeOnly = ( + timestamp: number, + timeFormat: TimeFormat = "auto", + locale: string = i18next.language || "en", +) => + new Date(timestamp).toLocaleTimeString(locale, { hour: "numeric", minute: "2-digit", ...hourCycleOptions(timeFormat), }) /** Full date + time, used for tooltips on relative timestamps and for absolute task times. */ -export const formatDateTime = (timestamp: number, timeFormat: TimeFormat = "auto", dateFormat: DateFormat = "auto") => - `${formatDateOnly(timestamp, dateFormat)}, ${formatTimeOnly(timestamp, timeFormat)}` +export const formatDateTime = ( + timestamp: number, + timeFormat: TimeFormat = "auto", + dateFormat: DateFormat = "auto", + locale: string = i18next.language || "en", +) => `${formatDateOnly(timestamp, dateFormat, locale)}, ${formatTimeOnly(timestamp, timeFormat, locale)}` export const formatTimeAgo = (timestamp: number) => { const now = Date.now() diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index 1be237e43a..e46058d662 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -244,11 +244,15 @@ export function summarizeUsage(history: HistoryItem[], options: UsageOptions): U // (including zero-spend days). "all" stays sparse to avoid huge arrays. const dailySeries: DailyUsage[] = [] if (start !== undefined) { - for (let day = start; day <= startOfLocalDay(now); day += DAY_MS) { - // Re-normalise each step to survive DST transitions. - const normalised = startOfLocalDay(day + DAY_MS / 2) - const entry = daily.get(normalised) - dailySeries.push({ day: normalised, cost: entry?.cost ?? 0, tokens: entry?.tokens ?? 0 }) + // Step by calendar date, not by 24h: a 23h or 25h day (DST) would otherwise drift the + // cursor off local midnight and drop today's bar while today's tasks are still counted. + const cursor = new Date(start) + const last = startOfLocalDay(now) + for (let day = cursor.getTime(); day <= last; day = cursor.getTime()) { + const entry = daily.get(day) + dailySeries.push({ day, cost: entry?.cost ?? 0, tokens: entry?.tokens ?? 0 }) + cursor.setDate(cursor.getDate() + 1) + cursor.setHours(0, 0, 0, 0) } } else { for (const [day, entry] of [...daily.entries()].sort(([a], [b]) => a - b)) { From b1c73f7584a9f877910be46c1641770d26e5c8aa Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 19:26:16 -0700 Subject: [PATCH 23/25] test(visual): add appearance preview baselines and refresh UI settings --- .../appearance-preview-dark.png | Bin 0 -> 34940 bytes ...appearance-preview-high-contrast-light.png | Bin 0 -> 35880 bytes .../appearance-preview-high-contrast.png | Bin 0 -> 35088 bytes .../appearance-preview-light.png | Bin 0 -> 35300 bytes .../__screenshots__/ui-settings-dark.png | Bin 85593 -> 86256 bytes .../ui-settings-high-contrast-light.png | Bin 87531 -> 88320 bytes .../ui-settings-high-contrast.png | Bin 87566 -> 88266 bytes .../__screenshots__/ui-settings-light.png | Bin 86537 -> 87280 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-dark.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-high-contrast-light.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-high-contrast.png create mode 100644 webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-light.png diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f57aca01b60ace43a918d7a5d94cb10fa427621f GIT binary patch literal 34940 zcmb@OWmH^km!@%dcXx;25TtMjPJrMN2o@x`yL*BK*8sttP`FEgg5d5F+#TldPJcbq zYfVr0tTq3Dsyg+Y^T^)!weL-ν7c3JD4n6coCGytD=s6m%gJ6wFs77~n5B+k&c4 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 literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-high-contrast-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-high-contrast-light.png new file mode 100644 index 0000000000000000000000000000000000000000..751c957b97cc84e8e6ad1463b5cc3c270809fc56 GIT binary patch literal 35880 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�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 oWEt{*0myZ-Q~&?~ literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/appearance-preview-light.png new file mode 100644 index 0000000000000000000000000000000000000000..78f4089f8d8b4b85a02d907d464fe6e16ef54c53 GIT binary patch literal 35300 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 bNY`GKJ5zU_pg)Cwa!pZIO{P}LJoJA7q$AI0 literal 0 HcmV?d00001 diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png index 43037ef226d51d5c8c7d1fba8cb73e5355d7c34f..fcd5bd829940e64ca30fe22bf514bdbbba78ea13 100644 GIT binary patch delta 40494 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�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 delta 39872 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 delta 42640 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 KRV`An4*n0Ue&$;M diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png index 46b1c0ab3cc2c8e21f4aae08a0ba6674ec5f18e0..e370f0d287d767ef13c396bd1a388f72516ac0ad 100644 GIT binary patch delta 43170 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 delta 42426 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 W!Y@XzcZdl8;jp!Guq-{~mGD0y9y{g$ diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png index f67020542210e48b4c42f18c1a13315e8d45ce90..71fbf8be63ef477fc4b48011595511c30c12f854 100644 GIT binary patch delta 42443 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 delta 41736 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 Date: Tue, 27 Oct 2026 19:33:25 -0700 Subject: [PATCH 24/25] fix(lint): drop the unused DAY_MS constant --- webview-ui/src/utils/usage.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/webview-ui/src/utils/usage.ts b/webview-ui/src/utils/usage.ts index e46058d662..3659198f8d 100644 --- a/webview-ui/src/utils/usage.ts +++ b/webview-ui/src/utils/usage.ts @@ -18,8 +18,6 @@ export const UNRECORDED = "__unrecorded__" export const USAGE_RANGES = ["today", "7d", "30d", "all"] as const export type UsageRange = (typeof USAGE_RANGES)[number] -const DAY_MS = 24 * 60 * 60 * 1000 - export interface UsageBucket { key: string cost: number From 02d82a53321830108b32dccd3a40fcf0ab7c29b0 Mon Sep 17 00:00:00 2001 From: Karthick Mahalingam Date: Tue, 27 Oct 2026 22:23:14 -0700 Subject: [PATCH 25/25] fix: atomic settings save with host acknowledgement, cancellable quota requests, assistant timestamps, Usage visual story --- packages/types/src/providers/vscode-llm.ts | 1 + packages/types/src/vscode-extension-host.ts | 2 + ...bviewMessageHandler.providerQuotas.spec.ts | 99 +++++++++++- .../__tests__/webviewMessageHandler.spec.ts | 140 +++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 110 +++++++++++--- src/i18n/locales/ca/common.json | 1 + src/i18n/locales/de/common.json | 1 + src/i18n/locales/en/common.json | 1 + src/i18n/locales/es/common.json | 1 + src/i18n/locales/fr/common.json | 1 + src/i18n/locales/hi/common.json | 1 + src/i18n/locales/id/common.json | 1 + src/i18n/locales/it/common.json | 1 + src/i18n/locales/ja/common.json | 1 + src/i18n/locales/ko/common.json | 1 + src/i18n/locales/nl/common.json | 1 + src/i18n/locales/pl/common.json | 1 + src/i18n/locales/pt-BR/common.json | 1 + src/i18n/locales/ru/common.json | 1 + src/i18n/locales/tr/common.json | 1 + src/i18n/locales/vi/common.json | 1 + src/i18n/locales/zh-CN/common.json | 1 + src/i18n/locales/zh-TW/common.json | 1 + .../__tests__/providerQuotas.spec.ts | 108 ++++++++++++- .../language-models/providerQuotas.ts | 85 ++++++++--- webview-ui/playwright/gallery/stories.tsx | 12 ++ webview-ui/src/components/chat/ChatRow.tsx | 7 + .../chat/__tests__/ChatRow.timestamp.spec.tsx | 49 +++++- .../components/settings/ProviderLimits.tsx | 46 ++++-- .../src/components/settings/SettingsView.tsx | 55 ++++++- .../__tests__/ProviderLimits.spec.tsx | 90 ++++++++++- .../SettingsView.change-detection.spec.tsx | 7 +- .../SettingsView.saveResult.spec.tsx | 119 +++++++++++++++ .../settings/__tests__/SettingsView.spec.tsx | 13 +- .../SettingsView.unsaved-changes.spec.tsx | 7 +- .../UsageSettings.visual.fixture.tsx | 143 ++++++++++++++++++ .../__tests__/UsageSettings.visual.tsx | 59 ++++++++ .../settings/__tests__/saveAcknowledgement.ts | 28 ++++ webview-ui/src/i18n/locales/id/settings.json | 2 +- webview-ui/src/i18n/locales/it/settings.json | 2 +- webview-ui/src/i18n/locales/nl/settings.json | 2 +- 41 files changed, 1115 insertions(+), 89 deletions(-) create mode 100644 webview-ui/src/components/settings/__tests__/SettingsView.saveResult.spec.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx create mode 100644 webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx create mode 100644 webview-ui/src/components/settings/__tests__/saveAcknowledgement.ts diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index 078ad0b999..91e74e54fd 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -12,6 +12,7 @@ export const vsCodeLmModelsMessageTypes = [ "githubCopilotSignInResult", "githubCopilotModels", "requestProviderQuotas", + "cancelProviderQuotas", "providerQuotas", ] as const diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index aa8d1d7945..0921418960 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -51,6 +51,7 @@ export interface ExtensionMessage { | typeof VsCodeLmModelsMessageType.githubCopilotModels | typeof VsCodeLmModelsMessageType.githubCopilotSignInResult | typeof VsCodeLmModelsMessageType.providerQuotas + | "settingsSaveResult" | "vsCodeLmApiAvailable" | "updatePrompt" | "systemPrompt" @@ -526,6 +527,7 @@ export interface WebviewMessage { | typeof VsCodeLmModelsMessageType.githubCopilotReconnect | typeof VsCodeLmModelsMessageType.githubCopilotManageAccount | typeof VsCodeLmModelsMessageType.requestProviderQuotas + | typeof VsCodeLmModelsMessageType.cancelProviderQuotas | "openImage" | "saveImage" | "openFile" diff --git a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts index 14afe19e6c..d83aa92aac 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.providerQuotas.spec.ts @@ -57,16 +57,23 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { providerQuotas: expect.arrayContaining([ expect.objectContaining({ providerId: providerIdentifiers.openrouter }), ]), + requestId: undefined, }) }) it("does not force a refresh by default, and forces when the user presses refresh", async () => { const provider = makeProvider() await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) - expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { force: false }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: false, + signal: expect.any(AbortSignal), + }) await webviewMessageHandler(provider, { type: "requestProviderQuotas", bool: true }) - expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { force: true }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: true, + signal: expect.any(AbortSignal), + }) }) it("passes API keys to the host service but never posts them to the webview", async () => { @@ -100,7 +107,11 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) - expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [] }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) expect(log).toHaveBeenCalledWith(expect.stringContaining("secret storage unavailable")) }) @@ -111,7 +122,11 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) - expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [] }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) expect(log).toHaveBeenCalledWith(expect.stringContaining("Failed to load provider limits")) }) @@ -121,6 +136,80 @@ describe("webviewMessageHandler – requestProviderQuotas", () => { provider.providerSettingsManager.listConfig = vi.fn(async () => []) as never await webviewMessageHandler(provider, { type: "requestProviderQuotas" }) - expect(provider.postMessageToWebview).toHaveBeenCalledWith({ type: "providerQuotas", providerQuotas: [] }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith({ + type: "providerQuotas", + providerQuotas: [], + requestId: undefined, + }) + }) + + describe("request lifecycle", () => { + it("echoes the request id on the answer", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-1" }) + expect(provider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "providerQuotas", requestId: "req-1" }), + ) + }) + + it("passes an abort signal to the quota service", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-2" }) + expect(quotas.getProviderQuotas).toHaveBeenLastCalledWith(expect.any(Array), { + force: false, + signal: expect.any(AbortSignal), + }) + }) + + it("aborts the work and posts nothing when the request is cancelled while it is running", async () => { + let signal: AbortSignal | undefined + let finish: (value: never[]) => void = () => {} + quotas.getProviderQuotas.mockImplementation((_profiles, options: { signal: AbortSignal }) => { + signal = options.signal + return new Promise((resolve) => (finish = resolve)) + }) + const provider = makeProvider() + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-3" }) + await vi.waitFor(() => expect(signal).toBeDefined()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-3" }) + + expect(signal!.aborted).toBe(true) + finish([]) + await running + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("does not post when the request was cancelled before the profiles finished loading", async () => { + const provider = makeProvider() + let release: (entries: { id: string }[]) => void = () => {} + provider.providerSettingsManager.listConfig = vi.fn( + () => new Promise((resolve) => (release = resolve)), + ) as never + + const running = webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-4" }) + await vi.waitFor(() => expect(provider.providerSettingsManager.listConfig).toHaveBeenCalled()) + await webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-4" }) + release([{ id: "a" }]) + await running + + expect(quotas.getProviderQuotas).not.toHaveBeenCalled() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("ignores a cancel for an unknown or missing id, and for a request that already finished", async () => { + const provider = makeProvider() + await webviewMessageHandler(provider, { type: "requestProviderQuotas", requestId: "req-5" }) + provider.postMessageToWebview.mockClear() + + await expect( + webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "req-5" }), + ).resolves.not.toThrow() + await expect( + webviewMessageHandler(provider, { type: "cancelProviderQuotas", requestId: "nope" }), + ).resolves.not.toThrow() + await expect(webviewMessageHandler(provider, { type: "cancelProviderQuotas" })).resolves.not.toThrow() + expect(provider.postMessageToWebview).not.toHaveBeenCalled() + }) }) }) diff --git a/src/core/webview/__tests__/webviewMessageHandler.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.spec.ts index 685a7b11c1..8fd3239449 100644 --- a/src/core/webview/__tests__/webviewMessageHandler.spec.ts +++ b/src/core/webview/__tests__/webviewMessageHandler.spec.ts @@ -1905,6 +1905,146 @@ describe("webviewMessageHandler - mcpEnabled", () => { }) }) +describe("webviewMessageHandler - updateSettings is all-or-nothing", () => { + const stored: Record = {} + + beforeEach(() => { + vi.clearAllMocks() + for (const key of Object.keys(stored)) delete stored[key] + Object.assign(stored, { soundEnabled: false, writeDelayMs: 1000, language: "en" }) + vi.mocked(mockClineProvider.contextProxy.getValue).mockImplementation((key: string) => stored[key] as never) + vi.mocked(mockClineProvider.contextProxy.setValue).mockImplementation(async (key: string, value: unknown) => { + stored[key] = value + }) + }) + + it("stores every setting and confirms the save by request id", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + requestId: "save-1", + updatedSettings: { soundEnabled: true, writeDelayMs: 2000 }, + }) + + expect(stored).toMatchObject({ soundEnabled: true, writeDelayMs: 2000 }) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "settingsSaveResult", + requestId: "save-1", + success: true, + }) + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + it("restores the settings already written when a later one fails, and reports the failure", async () => { + const real = vi.mocked(mockClineProvider.contextProxy.setValue).getMockImplementation()! + vi.mocked(mockClineProvider.contextProxy.setValue).mockImplementation(async (key: string, value: unknown) => { + if (key === "writeDelayMs" && value === 2000) throw new Error("storage unavailable") + return real(key as never, value as never) + }) + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + requestId: "save-2", + updatedSettings: { soundEnabled: true, writeDelayMs: 2000 }, + }) + + // soundEnabled was written first, then rolled back; writeDelayMs never changed. + expect(stored).toMatchObject({ soundEnabled: false, writeDelayMs: 1000 }) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "settingsSaveResult", + requestId: "save-2", + success: false, + error: "storage unavailable", + }) + expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalledWith( + expect.objectContaining({ success: true }), + ) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.settings_save_failed") + expect(mockClineProvider.log).toHaveBeenCalledWith(expect.stringContaining("rolled back")) + // The webview is re-synced with what is really stored. + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + }) + + it("rolls back in reverse order, so the first-written value is restored last", async () => { + const order: string[] = [] + const real = vi.mocked(mockClineProvider.contextProxy.setValue).getMockImplementation()! + vi.mocked(mockClineProvider.contextProxy.setValue).mockImplementation(async (key: string, value: unknown) => { + order.push(`${key}=${String(value)}`) + if (key === "terminalCommandDelay" && value === 50) throw new Error("boom") + return real(key as never, value as never) + }) + stored.terminalCommandDelay = 0 + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + requestId: "save-3", + updatedSettings: { soundEnabled: true, writeDelayMs: 2000, terminalCommandDelay: 50 }, + }) + + expect(stored).toMatchObject({ soundEnabled: false, writeDelayMs: 1000, terminalCommandDelay: 0 }) + + // Writes go forward in order; restores come back in the opposite order. + const failedAt = order.indexOf("terminalCommandDelay=50") + const restoreOrder = order + .slice(failedAt + 1) + .map((entry) => entry.split("=")[0]) + .filter((key, index, all) => all.indexOf(key) === index) + expect(restoreOrder.indexOf("writeDelayMs")).toBeGreaterThanOrEqual(0) + expect(restoreOrder.indexOf("soundEnabled")).toBeGreaterThan(restoreOrder.indexOf("writeDelayMs")) + }) + + it("keeps going and still reports the failure when a restore itself fails", async () => { + const real = vi.mocked(mockClineProvider.contextProxy.setValue).getMockImplementation()! + vi.mocked(mockClineProvider.contextProxy.setValue).mockImplementation(async (key: string, value: unknown) => { + if (key === "writeDelayMs") throw new Error("writeDelayMs is broken") + if (key === "soundEnabled" && value === false) throw new Error("cannot restore") + return real(key as never, value as never) + }) + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + requestId: "save-4", + updatedSettings: { soundEnabled: true, writeDelayMs: 2000 }, + }) + + expect(mockClineProvider.log).toHaveBeenCalledWith(expect.stringContaining("Could not restore soundEnabled")) + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ type: "settingsSaveResult", requestId: "save-4", success: false }), + ) + }) + + it("reports a non-Error failure by its text", async () => { + vi.mocked(mockClineProvider.contextProxy.setValue).mockRejectedValue("plain string failure") + + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + requestId: "save-5", + updatedSettings: { soundEnabled: true }, + }) + + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith( + expect.objectContaining({ success: false, error: "plain string failure" }), + ) + }) + + it("still saves and re-syncs, without a result message, when the request has no id", async () => { + await webviewMessageHandler(mockClineProvider, { + type: "updateSettings", + updatedSettings: { soundEnabled: true }, + }) + + expect(stored.soundEnabled).toBe(true) + expect(mockClineProvider.postStateToWebview).toHaveBeenCalled() + expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled() + }) + + it("does nothing when there are no settings to save", async () => { + await webviewMessageHandler(mockClineProvider, { type: "updateSettings", requestId: "save-6" }) + expect(mockClineProvider.contextProxy.setValue).not.toHaveBeenCalled() + expect(mockClineProvider.postMessageToWebview).not.toHaveBeenCalled() + }) +}) + describe("webviewMessageHandler - destructiveCommandGuardEnabled", () => { beforeEach(() => { vi.clearAllMocks() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 7df9970f7c..e98d1820c1 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -131,6 +131,9 @@ import { const handlerFor = (apiConfiguration: ProviderSettings, provider: RouterName): ApiHandler => buildApiHandler({ ...apiConfiguration, apiProvider: provider }) +/** In-flight `requestProviderQuotas` work by request id, so a cancel message can abort it. */ +const pendingProviderQuotaRequests = new Map() + export const webviewMessageHandler = async ( provider: ClineProvider, message: WebviewMessage, @@ -738,18 +741,21 @@ export const webviewMessageHandler = async ( } break - case "updateSettings": - if (message.updatedSettings) { - if (message.updatedSettings.destructiveCommandGuardEnabled === true) { + case "updateSettings": { + const updatedSettings = message.updatedSettings + const saveRequestId = message.requestId + if (!updatedSettings) break + { + if (updatedSettings.destructiveCommandGuardEnabled === true) { try { const { ensureDcgInstalled } = await import("../../services/destructive-command-guard") const binaryPath = await ensureDcgInstalled(provider.context.globalStorageUri.fsPath) if (!binaryPath) { - message.updatedSettings.destructiveCommandGuardEnabled = false + updatedSettings.destructiveCommandGuardEnabled = false vscode.window.showErrorMessage(t("common:errors.destructiveCommandGuard.unavailable")) } } catch (error) { - message.updatedSettings.destructiveCommandGuardEnabled = false + updatedSettings.destructiveCommandGuardEnabled = false vscode.window.showErrorMessage( t("common:errors.destructiveCommandGuard.enableFailed", { error: error instanceof Error ? error.message : String(error), @@ -758,7 +764,8 @@ export const webviewMessageHandler = async ( } } - for (const [key, value] of Object.entries(message.updatedSettings)) { + /** Applies one setting (its side effect, then the stored value). Returns whether a value was stored. */ + const applySetting = async (key: string, value: unknown): Promise => { let newValue = value if (key === "language") { @@ -855,7 +862,7 @@ export const webviewMessageHandler = async ( } } else if (key === "experiments") { if (!value) { - continue + return false } newValue = { @@ -864,17 +871,60 @@ export const webviewMessageHandler = async ( } } else if (key === "customSupportPrompts") { if (!value) { - continue + return false } } - await provider.contextProxy.setValue(key as keyof RooCodeSettings, newValue) + await provider.contextProxy.setValue(key as keyof RooCodeSettings, newValue as never) + return true } - await provider.postStateToWebview() - } + // Every key is snapshotted before it is written. If any write fails, the ones already applied are + // restored (stored value and side effect), so a save is all-or-nothing and the webview can keep + // the user's edits and let them retry. + const written: Array<{ key: string; previous: unknown }> = [] + try { + for (const [key, value] of Object.entries(updatedSettings)) { + written.push({ key, previous: provider.contextProxy.getValue(key as keyof RooCodeSettings) }) + await applySetting(key, value) + } + await provider.postStateToWebview() + if (saveRequestId) { + await provider.postMessageToWebview({ + type: "settingsSaveResult", + requestId: saveRequestId, + success: true, + }) + } + } catch (error) { + const reason = error instanceof Error ? error.message : String(error) + for (const { key, previous } of written.reverse()) { + try { + await applySetting(key, previous) + // Put back exactly what was stored, whatever defaulting the side effect applied. + await provider.contextProxy.setValue(key as keyof RooCodeSettings, previous as never) + } catch (rollbackError) { + provider.log( + `[updateSettings] Could not restore ${key}: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, + ) + } + } + provider.log(`[updateSettings] Save failed and was rolled back: ${reason}`) + vscode.window.showErrorMessage(t("common:errors.settings_save_failed", { error: reason })) + await provider.postStateToWebview() + if (saveRequestId) { + await provider.postMessageToWebview({ + type: "settingsSaveResult", + requestId: saveRequestId, + success: false, + error: reason, + }) + } + } + } break + } case "terminalOperation": if (message.terminalOperation) { @@ -1571,21 +1621,45 @@ export const webviewMessageHandler = async ( } break } + case VsCodeLmModelsMessageType.cancelProviderQuotas: + // The Usage tab was closed (or the request superseded): stop the profile load and any fetches. + if (message.requestId) pendingProviderQuotaRequests.get(message.requestId)?.abort() + break case VsCodeLmModelsMessageType.requestProviderQuotas: { - // Always answer: the webview shows a spinner until a response arrives and disables - // refresh while it waits, so a thrown error here would leave it stuck. + // Always answer (unless cancelled): the webview shows a spinner until a response arrives and + // disables refresh while it waits, so a thrown error here would leave it stuck. The answer + // carries the request's id so the webview can ignore one that has been superseded. + const requestId = message.requestId + const controller = new AbortController() + if (requestId) pendingProviderQuotaRequests.set(requestId, controller) let providerQuotas: ProviderQuota[] = [] try { const { getProviderQuotas, loadProfiles } = await import("../../services/language-models/providerQuotas") const profiles = await loadProfiles(provider.providerSettingsManager) - providerQuotas = await getProviderQuotas(profiles, { force: message.bool === true }) + if (!controller.signal.aborted) { + providerQuotas = await getProviderQuotas(profiles, { + force: message.bool === true, + signal: controller.signal, + }) + } } catch (error) { - provider.log( - `[requestProviderQuotas] ${error instanceof Error ? error.message : "Failed to load provider limits"}`, - ) + if (!controller.signal.aborted) { + provider.log( + `[requestProviderQuotas] ${error instanceof Error ? error.message : "Failed to load provider limits"}`, + ) + } + } finally { + if (requestId) pendingProviderQuotaRequests.delete(requestId) + } + // Nobody is waiting for a cancelled request: skip the post. + if (!controller.signal.aborted) { + await provider.postMessageToWebview({ + type: VsCodeLmModelsMessageType.providerQuotas, + providerQuotas, + requestId, + }) } - await provider.postMessageToWebview({ type: VsCodeLmModelsMessageType.providerQuotas, providerQuotas }) break } case VsCodeLmModelsMessageType.githubCopilotManageAccount: diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 579cd6b7d7..bc6f64b125 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -62,6 +62,7 @@ "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}", "path_outside_workspace": "La ruta és fora de l'espai de treball", "settings_import_failed": "Ha fallat la importació de la configuració: {{error}}.", + "settings_save_failed": "No s'ha pogut desar la configuració i s'han restaurat els valors anteriors: {{error}}", "mistake_limit_guidance": "Això pot indicar un error en el procés de pensament del model o la incapacitat d'utilitzar una eina correctament, que es pot mitigar amb orientació de l'usuari (p. ex. \"Prova de dividir la tasca en passos més petits\").", "violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual no és compatible amb la configuració de la teva organització", "condense_failed": "Ha fallat la condensació del context", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 3d8175a77e..8ac78a072f 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}", "path_outside_workspace": "Pfad liegt außerhalb des Arbeitsbereichs", "settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}.", + "settings_save_failed": "Die Einstellungen konnten nicht gespeichert werden und wurden auf die vorherigen Werte zurückgesetzt: {{error}}", "mistake_limit_guidance": "Dies kann auf einen Fehler im Denkprozess des Modells oder die Unfähigkeit hinweisen, ein Tool richtig zu verwenden, was durch Benutzerführung behoben werden kann (z.B. \"Versuche, die Aufgabe in kleinere Schritte zu unterteilen\").", "violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil ist nicht kompatibel mit den Einstellungen deiner Organisation", "condense_failed": "Fehler beim Verdichten des Kontexts", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index a7d616028f..a16574650b 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Cannot access path {{path}}: {{error}}", "path_outside_workspace": "Path is outside the workspace", "settings_import_failed": "Settings import failed: {{error}}.", + "settings_save_failed": "Settings could not be saved and were restored to their previous values: {{error}}", "mistake_limit_guidance": "This may indicate a failure in the model's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. \"Try breaking down the task into smaller steps\").", "violated_organization_allowlist": "Failed to run task: the current profile isn't compatible with your organization settings", "condense_failed": "Failed to condense context", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 2eb6848f11..f5d2d1b060 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}", "path_outside_workspace": "La ruta está fuera del espacio de trabajo", "settings_import_failed": "Error al importar la configuración: {{error}}.", + "settings_save_failed": "No se pudieron guardar los ajustes y se restauraron los valores anteriores: {{error}}", "mistake_limit_guidance": "Esto puede indicar un fallo en el proceso de pensamiento del modelo o la incapacidad de usar una herramienta correctamente, lo cual puede mitigarse con orientación del usuario (ej. \"Intenta dividir la tarea en pasos más pequeños\").", "violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual no es compatible con la configuración de tu organización", "condense_failed": "Error al condensar el contexto", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index b6b65a88de..3cfeeb4664 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}", "path_outside_workspace": "Le chemin est à l'extérieur de l'espace de travail", "settings_import_failed": "Échec de l'importation des paramètres : {{error}}", + "settings_save_failed": "Impossible d'enregistrer les paramètres ; les valeurs précédentes ont été rétablies : {{error}}", "mistake_limit_guidance": "Cela peut indiquer un échec dans le processus de réflexion du modèle ou une incapacité à utiliser un outil correctement, ce qui peut être atténué avec des conseils de l'utilisateur (par ex. \"Essaie de diviser la tâche en étapes plus petites\").", "violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel n'est pas compatible avec les paramètres de votre organisation", "condense_failed": "Échec de la condensation du contexte", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index f69b5adccf..d77a44f18e 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}", "path_outside_workspace": "पथ वर्कस्पेस से बाहर है", "settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।", + "settings_save_failed": "सेटिंग्स सहेजी नहीं जा सकीं और पिछले मान बहाल कर दिए गए: {{error}}", "mistake_limit_guidance": "यह मॉडल की सोच प्रक्रिया में विफलता या किसी टूल का सही उपयोग न कर पाने का संकेत हो सकता है, जिसे उपयोगकर्ता के मार्गदर्शन से ठीक किया जा सकता है (जैसे \"कार्य को छोटे चरणों में बांटने की कोशिश करें\")।", "violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स के साथ संगत नहीं है", "condense_failed": "संदर्भ को संक्षिप्त करने में विफल", diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index e401612bf3..a9a78a8c28 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Tidak dapat mengakses path {{path}}: {{error}}", "path_outside_workspace": "Path berada di luar workspace", "settings_import_failed": "Impor pengaturan gagal: {{error}}.", + "settings_save_failed": "Pengaturan tidak dapat disimpan dan dikembalikan ke nilai sebelumnya: {{error}}", "mistake_limit_guidance": "Ini mungkin menunjukkan kegagalan dalam proses pemikiran model atau ketidakmampuan untuk menggunakan tool dengan benar, yang dapat diatasi dengan beberapa panduan pengguna (misalnya \"Coba bagi tugas menjadi langkah-langkah yang lebih kecil\").", "violated_organization_allowlist": "Gagal menjalankan tugas: profil saat ini tidak kompatibel dengan pengaturan organisasi kamu", "condense_failed": "Gagal mengompres konteks", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index aeb984940a..ead87cfa11 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}", "path_outside_workspace": "Il percorso è fuori dall'area di lavoro", "settings_import_failed": "Importazione delle impostazioni fallita: {{error}}.", + "settings_save_failed": "Impossibile salvare le impostazioni; sono stati ripristinati i valori precedenti: {{error}}", "mistake_limit_guidance": "Questo può indicare un fallimento nel processo di pensiero del modello o l'incapacità di utilizzare correttamente uno strumento, che può essere mitigato con la guida dell'utente (ad es. \"Prova a suddividere l'attività in passaggi più piccoli\").", "violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente non è compatibile con le impostazioni della tua organizzazione", "condense_failed": "Impossibile condensare il contesto", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index a7ec0c4de3..d5532ea227 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}", "path_outside_workspace": "パスはワークスペースの外にあります", "settings_import_failed": "設定のインポートに失敗しました:{{error}}", + "settings_save_failed": "設定を保存できなかったため、以前の値に戻しました:{{error}}", "mistake_limit_guidance": "これは、モデルの思考プロセスの失敗やツールを適切に使用できないことを示している可能性があり、ユーザーのガイダンスによって軽減できます(例:「タスクをより小さなステップに分割してみてください」)。", "violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定と互換性がありません", "condense_failed": "コンテキストの圧縮に失敗しました", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 906652ac14..ff55cfc8f9 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}", "path_outside_workspace": "경로가 작업 영역 밖에 있습니다", "settings_import_failed": "설정 가져오기 실패: {{error}}.", + "settings_save_failed": "설정을 저장할 수 없어 이전 값으로 복원했습니다: {{error}}", "mistake_limit_guidance": "이는 모델의 사고 과정 실패나 도구를 제대로 사용하지 못하는 것을 나타낼 수 있으며, 사용자 가이드를 통해 완화할 수 있습니다 (예: \"작업을 더 작은 단계로 나누어 시도해보세요\").", "violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정과 호환되지 않습니다", "condense_failed": "컨텍스트 압축에 실패했습니다", diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 34cdbbbe89..a3a5b61595 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Kan pad {{path}} niet openen: {{error}}", "path_outside_workspace": "Pad is buiten de werkomgeving", "settings_import_failed": "Importeren van instellingen mislukt: {{error}}.", + "settings_save_failed": "De instellingen konden niet worden opgeslagen en zijn teruggezet naar de vorige waarden: {{error}}", "mistake_limit_guidance": "Dit kan duiden op een fout in het denkproces van het model of het onvermogen om een tool correct te gebruiken, wat kan worden verminderd met gebruikersbegeleiding (bijv. \"Probeer de taak op te delen in kleinere stappen\").", "violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel is niet compatibel met de instellingen van uw organisatie", "condense_failed": "Comprimeren van context mislukt", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 2a743fc009..cfdd0b67e0 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}", "path_outside_workspace": "Ścieżka znajduje się poza obszarem roboczym", "settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}.", + "settings_save_failed": "Nie udało się zapisać ustawień; przywrócono poprzednie wartości: {{error}}", "mistake_limit_guidance": "To może wskazywać na błąd w procesie myślowym modelu lub niezdolność do prawidłowego użycia narzędzia, co można złagodzić poprzez wskazówki użytkownika (np. \"Spróbuj podzielić zadanie na mniejsze kroki\").", "violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil nie jest kompatybilny z ustawieniami Twojej organizacji", "condense_failed": "Nie udało się skondensować kontekstu", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index eaf1cae06b..bbe3a0193b 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -62,6 +62,7 @@ "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}", "path_outside_workspace": "O caminho está fora do espaço de trabalho", "settings_import_failed": "Falha ao importar configurações: {{error}}", + "settings_save_failed": "Não foi possível salvar as configurações; os valores anteriores foram restaurados: {{error}}", "mistake_limit_guidance": "Isso pode indicar uma falha no processo de pensamento do modelo ou incapacidade de usar uma ferramenta adequadamente, o que pode ser mitigado com orientação do usuário (ex. \"Tente dividir a tarefa em etapas menores\").", "violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual não é compatível com as configurações da sua organização", "condense_failed": "Falha ao condensar o contexto", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 6f48365ec1..a020b49dcd 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Невозможно получить доступ к пути {{path}}: {{error}}", "path_outside_workspace": "Путь находится вне рабочего пространства", "settings_import_failed": "Не удалось импортировать настройки: {{error}}.", + "settings_save_failed": "Не удалось сохранить настройки; восстановлены прежние значения: {{error}}", "mistake_limit_guidance": "Это может указывать на сбой в процессе мышления модели или неспособность правильно использовать инструмент, что можно смягчить с помощью руководства пользователя (например, \"Попробуйте разбить задачу на более мелкие шаги\").", "violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль несовместим с настройками вашей организации", "condense_failed": "Не удалось сжать контекст", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index b5c543c405..a60d5ff7a9 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}", "path_outside_workspace": "Yol çalışma alanının dışında", "settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}.", + "settings_save_failed": "Ayarlar kaydedilemedi ve önceki değerlere geri döndürüldü: {{error}}", "mistake_limit_guidance": "Bu, modelin düşünce sürecindeki bir başarısızlığı veya bir aracı düzgün kullanamama durumunu gösterebilir, bu da kullanıcı rehberliği ile hafifletilebilir (örn. \"Görevi daha küçük adımlara bölmeyi deneyin\").", "violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarıyla uyumlu değil", "condense_failed": "Bağlam sıkıştırılamadı", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index cb2ed8e9b8..8c072c9b5b 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}", "path_outside_workspace": "Đường dẫn nằm ngoài không gian làm việc", "settings_import_failed": "Nhập cài đặt thất bại: {{error}}.", + "settings_save_failed": "Không thể lưu cài đặt và đã khôi phục về giá trị trước đó: {{error}}", "mistake_limit_guidance": "Điều này có thể cho thấy sự thất bại trong quá trình suy nghĩ của mô hình hoặc không thể sử dụng công cụ đúng cách, có thể được giảm thiểu bằng hướng dẫn của người dùng (ví dụ: \"Hãy thử chia nhỏ nhiệm vụ thành các bước nhỏ hơn\").", "violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại không tương thích với cài đặt của tổ chức của bạn", "condense_failed": "Không thể nén ngữ cảnh", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 08cfe736b5..7ed9f1b617 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -63,6 +63,7 @@ "cannot_access_path": "无法访问路径 {{path}}:{{error}}", "path_outside_workspace": "路径位于工作区之外", "settings_import_failed": "设置导入失败:{{error}}。", + "settings_save_failed": "无法保存设置,已恢复为之前的值:{{error}}", "mistake_limit_guidance": "这可能表明模型思维过程失败或无法正确使用工具,可通过用户指导来缓解(例如\"尝试将任务分解为更小的步骤\")。", "violated_organization_allowlist": "执行任务失败:当前配置文件与您的组织设置不兼容", "condense_failed": "压缩上下文失败", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 92fdbaeb96..3d9c8d9858 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -58,6 +58,7 @@ "cannot_access_path": "無法存取路徑 {{path}}:{{error}}", "path_outside_workspace": "路徑位於工作區之外", "settings_import_failed": "設定匯入失敗:{{error}}。", + "settings_save_failed": "無法儲存設定,已還原為之前的值:{{error}}", "mistake_limit_guidance": "這可能表明模型思維過程失敗或無法正確使用工具,可透過使用者指導來緩解(例如「嘗試將工作分解為更小的步驟」)。", "violated_organization_allowlist": "執行工作失敗:目前設定檔與您的組織設定不相容", "condense_failed": "壓縮上下文失敗", diff --git a/src/services/language-models/__tests__/providerQuotas.spec.ts b/src/services/language-models/__tests__/providerQuotas.spec.ts index ffb94364ee..b27a7830aa 100644 --- a/src/services/language-models/__tests__/providerQuotas.spec.ts +++ b/src/services/language-models/__tests__/providerQuotas.spec.ts @@ -728,9 +728,15 @@ describe("getProviderQuotas", () => { }) it("does not hand a result fetched with one key to a caller using another, even while the first is in flight", async () => { - const releases: Array<(r: Response) => void> = [] - const fetchSpy = vi.fn(() => new Promise((resolve) => releases.push(resolve))) - const ok = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + const pending = new Map void>() + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((resolve) => { + pending.set(String((init?.headers as Record).Authorization), resolve) + }), + ) + const body = (remaining: number) => + new Response(JSON.stringify({ data: { limit: 10, limit_remaining: remaining } })) const first = getProviderQuotas( [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk-one" } as never], @@ -743,11 +749,99 @@ describe("getProviderQuotas", () => { fetchSpy, ) await vi.waitFor(() => expect(fetchSpy).toHaveBeenCalledTimes(2)) - releases.forEach((release) => release(ok())) - await Promise.all([first, second]) + // Each request went out with its own key, and nothing else. + expect([...pending.keys()].sort()).toEqual(["Bearer sk-one", "Bearer sk-two"]) - // Each key was fetched on its own; neither piggy-backed on the other's request. - expect(fetchSpy).toHaveBeenCalledTimes(2) + // Answer them with different data, in the opposite order, so a mix-up would show. + pending.get("Bearer sk-two")!(body(2)) + pending.get("Bearer sk-one")!(body(9)) + + const [[a], [b]] = await Promise.all([first, second]) + expect(a).toMatchObject({ ok: true, windows: [{ usedPercent: 10 }] }) + expect(b).toMatchObject({ ok: true, windows: [{ usedPercent: 80 }] }) + }) + + it("drops a cached success when a forced refresh fails, so the next normal call retries", async () => { + const ok = () => new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } })) + const fetchSpy = vi + .fn() + .mockResolvedValueOnce(ok()) + .mockResolvedValueOnce(new Response("", { status: 500 })) + .mockResolvedValueOnce(ok()) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + + await getProviderQuotas(profiles, {}, fetchSpy) + const [failed] = await getProviderQuotas(profiles, { force: true }, fetchSpy) + expect(failed).toMatchObject({ ok: false, reason: "error" }) + + // The old success must not be served as if it were current. + const [retry] = await getProviderQuotas(profiles, {}, fetchSpy) + expect(retry).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(3) + }) + + it("aborts the network request when the only caller goes away", async () => { + let seen: AbortSignal | undefined + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((_resolve, reject) => { + seen = init?.signal ?? undefined + seen?.addEventListener("abort", () => reject(new DOMException("Aborted", "AbortError"))) + }), + ) + const controller = new AbortController() + const result = getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never], + { signal: controller.signal }, + fetchSpy, + ) + await vi.waitFor(() => expect(seen).toBeDefined()) + expect(seen!.aborted).toBe(false) + + controller.abort() + await expect(result).rejects.toMatchObject({ name: "AbortError" }) + expect(seen!.aborted).toBe(true) + }) + + it("keeps the shared request running until its last caller leaves", async () => { + let seen: AbortSignal | undefined + let release: (r: Response) => void = () => {} + const fetchSpy = vi.fn( + (_url: RequestInfo | URL, init?: RequestInit) => + new Promise((resolve) => { + seen = init?.signal ?? undefined + release = resolve + }), + ) + const profiles = [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never] + const leaver = new AbortController() + + const staying = getProviderQuotas(profiles, {}, fetchSpy) + const leaving = getProviderQuotas(profiles, { signal: leaver.signal }, fetchSpy) + await vi.waitFor(() => expect(seen).toBeDefined()) + + leaver.abort() + await expect(leaving).rejects.toMatchObject({ name: "AbortError" }) + // Someone is still waiting, so the shared fetch must keep going. + expect(seen!.aborted).toBe(false) + + release(new Response(JSON.stringify({ data: { limit: 10, limit_remaining: 5 } }))) + const [quota] = await staying + expect(quota).toMatchObject({ ok: true }) + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it("rejects at once with an already-aborted signal, without keeping a request alive", async () => { + const fetchSpy = vi.fn(() => new Promise(() => {})) + const controller = new AbortController() + controller.abort() + await expect( + getProviderQuotas( + [{ apiProvider: providerIdentifiers.openrouter, openRouterApiKey: "sk" } as never], + { signal: controller.signal }, + fetchSpy, + ), + ).rejects.toMatchObject({ name: "AbortError" }) }) it("shares one request between concurrent callers for the same credentials", async () => { diff --git a/src/services/language-models/providerQuotas.ts b/src/services/language-models/providerQuotas.ts index 2cb2e67d01..4286b4f6d1 100644 --- a/src/services/language-models/providerQuotas.ts +++ b/src/services/language-models/providerQuotas.ts @@ -316,11 +316,12 @@ export function parseOpenCodeGoUsage(payload: unknown): ProviderQuotaWindow[] | type Fetch = typeof fetch -async function getJson(fetchImpl: Fetch, url: string, apiKey: string): Promise { +async function getJson(fetchImpl: Fetch, url: string, apiKey: string, signal?: AbortSignal): Promise { + const timeout = AbortSignal.timeout(REQUEST_TIMEOUT_MS) const response = await fetchImpl(url, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }, - signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + signal: signal ? AbortSignal.any([signal, timeout]) : timeout, }) if (response.status === 401 || response.status === 403) throw new AuthError() if (!response.ok) throw new QuotaHttpError(response.status) @@ -476,7 +477,12 @@ function unqueryable(provider: KeyedProvider, creds: KeyedCredentials | undefine return undefined } -async function fetchKeyed(provider: KeyedProvider, creds: KeyedCredentials, fetchImpl: Fetch): Promise { +async function fetchKeyed( + provider: KeyedProvider, + creds: KeyedCredentials, + fetchImpl: Fetch, + signal?: AbortSignal, +): Promise { const now = Date.now() if (!creds.apiKey || !creds.urls?.length) { return { providerId: provider.providerId, ok: false, reason: "unsupported", fetchedAt: now } @@ -488,7 +494,7 @@ async function fetchKeyed(provider: KeyedProvider, creds: KeyedCredentials, fetc let lastError: unknown for (const [index, url] of creds.urls.entries()) { try { - windows = provider.parse(await getJson(fetchImpl, url, creds.apiKey), index) + windows = provider.parse(await getJson(fetchImpl, url, creds.apiKey, signal), index) if (windows?.length) break } catch (error) { if (error instanceof AuthError) throw error @@ -560,18 +566,40 @@ export async function loadProfiles(source: ProfileSource): Promise() -const inflight = new Map }>() +interface Inflight { + apiKey: string + request: Promise + /** Callers still waiting on this request. The shared fetch is aborted only when the last one leaves. */ + waiters: number + controller: AbortController +} +const inflight = new Map() /** Identifies a provider and the URLs it is queried at (the credential itself is not part of it). */ function cacheKey(provider: KeyedProvider, creds: KeyedCredentials): string { return `${provider.providerId}|${(creds.urls ?? []).join(",")}` } +/** Rejects as soon as `signal` aborts, without cancelling `work` (which other callers may still be using). */ +function until(work: Promise, signal: AbortSignal | undefined, onAbort: () => void): Promise { + if (!signal) return work + return new Promise((resolve, reject) => { + const abort = () => { + onAbort() + reject(new DOMException("Aborted", "AbortError")) + } + if (signal.aborted) return abort() + signal.addEventListener("abort", abort, { once: true }) + work.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort)) + }) +} + function quotaForKeyedProvider( provider: KeyedProvider, profiles: ProviderSettings[], force: boolean, fetchImpl: Fetch, + signal?: AbortSignal, ): Promise { const creds = pickCredentials(provider, profiles) const skipped = unqueryable(provider, creds) @@ -584,19 +612,36 @@ function quotaForKeyedProvider( if (!force && hit && hit.apiKey === apiKey && Date.now() - hit.at < CACHE_TTL_MS) return Promise.resolve(hit.result) // Concurrent callers (e.g. a refresh while the first load is still running) share one request. - const pending = inflight.get(key) - if (pending && pending.apiKey === apiKey) return pending.request + let entry = inflight.get(key) + if (!entry || entry.apiKey !== apiKey) { + const controller = new AbortController() + const request: Promise = fetchKeyed(provider, resolved, fetchImpl, controller.signal) + .then((result) => { + if (result.ok) { + cache.set(key, { at: Date.now(), apiKey, result }) + } else { + // A failed refresh must not leave an older success to be served as if it were current. + cache.delete(key) + } + return result + }) + .finally(() => { + if (inflight.get(key)?.request === request) inflight.delete(key) + }) + entry = { apiKey, request, waiters: 0, controller } + inflight.set(key, entry) + } - const request = fetchKeyed(provider, resolved, fetchImpl) - .then((result) => { - if (result.ok) cache.set(key, { at: Date.now(), apiKey, result }) - return result - }) - .finally(() => { - if (inflight.get(key)?.request === request) inflight.delete(key) - }) - inflight.set(key, { apiKey, request }) - return request + const shared = entry + shared.waiters++ + let left = false + const leave = () => { + if (left) return + left = true + // Last one out cancels the network work; earlier leavers just stop waiting. + if (--shared.waiters === 0) shared.controller.abort() + } + return until(shared.request, signal, leave).finally(leave) } /** @@ -605,7 +650,7 @@ function quotaForKeyedProvider( */ export async function getProviderQuotas( profiles: ProviderSettings[], - { force = false }: { force?: boolean } = {}, + { force = false, signal }: { force?: boolean; signal?: AbortSignal } = {}, fetchImpl: Fetch = fetch, ): Promise { const connected = new Set(profiles.map((p) => p.apiProvider).filter(Boolean) as string[]) @@ -613,7 +658,9 @@ export async function getProviderQuotas( const tasks: Promise[] = [] if (connected.has(providerIdentifiers.githubCopilot)) tasks.push(fetchCopilot(force)) for (const provider of KEYED_PROVIDERS) { - if (connected.has(provider.providerId)) tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl)) + if (connected.has(provider.providerId)) { + tasks.push(quotaForKeyedProvider(provider, profiles, force, fetchImpl, signal)) + } } return Promise.all(tasks) diff --git a/webview-ui/playwright/gallery/stories.tsx b/webview-ui/playwright/gallery/stories.tsx index dd6bc511ab..6fa9cbd314 100644 --- a/webview-ui/playwright/gallery/stories.tsx +++ b/webview-ui/playwright/gallery/stories.tsx @@ -419,6 +419,18 @@ export const stories: Record = { const { UISettingsStory } = await import("@/components/settings/__tests__/UISettings.visual.fixture") return }, + "usage-settings": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, + "usage-settings-degraded": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, + "usage-settings-empty": async () => { + const { UsageSettingsStory } = await import("@/components/settings/__tests__/UsageSettings.visual.fixture") + return + }, "appearance-preview": async () => { const { AppearancePreviewStory } = await import("@/components/settings/__tests__/AppearancePreview.visual.fixture") diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 407af0d605..138b505643 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -1227,6 +1227,13 @@ export const ChatRowContent = ({

+ {/* When the reply was sent. Held back while it streams, so the line does not appear and + shift as the text grows. */} + {!message.partial && ( +
+ +
+ )}
) case "user_feedback": diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.timestamp.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.timestamp.spec.tsx index d19a0bcaa7..2ed98c4620 100644 --- a/webview-ui/src/components/chat/__tests__/ChatRow.timestamp.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatRow.timestamp.spec.tsx @@ -79,9 +79,52 @@ describe("ChatRow – message timestamp", () => { expect(screen.getByTestId("message-timestamp").textContent).toContain("14:41") }) - it("does not add a timestamp to assistant text", () => { - renderRow({ ts: Date.now(), type: "say", say: "text", text: "assistant reply" }) - expect(screen.queryByTestId("message-timestamp")).toBeNull() + describe("assistant text", () => { + it("shows when the reply was sent, under the reply", () => { + const { container } = renderRow({ ts: Date.now(), type: "say", say: "text", text: "assistant reply" }) + expect(screen.getByTestId("message-timestamp").textContent).toContain("Today") + const html = container.innerHTML + expect(html.indexOf("message-timestamp")).toBeGreaterThan(html.indexOf("assistant reply")) + }) + + it("uses the reply's own timestamp and the user's formats", () => { + const d = new Date() + d.setHours(14, 41, 0, 0) + mockState = { timeFormat: "24h", dateFormat: "ymd" } + renderRow({ ts: d.getTime(), type: "say", say: "text", text: "formatted reply" }) + expect(screen.getByTestId("message-timestamp").textContent).toContain("14:41") + }) + + it("waits until the reply has finished streaming", () => { + const { rerender } = renderRow({ + ts: Date.now(), + type: "say", + say: "text", + text: "still typing", + partial: true, + }) + expect(screen.queryByTestId("message-timestamp")).toBeNull() + + rerender( + {}} + onSuggestionClick={() => {}} + onBatchFileResponse={() => {}} + onFollowUpUnmount={() => {}} + isFollowUpAnswered={false} + />, + ) + expect(screen.getByTestId("message-timestamp")).toBeTruthy() + }) + + it("shows nothing for an assistant message with an invalid timestamp", () => { + renderRow({ ts: 0, type: "say", say: "text", text: "no time" }) + expect(screen.queryByTestId("message-timestamp")).toBeNull() + }) }) it("keeps the message visible while streaming", () => { diff --git a/webview-ui/src/components/settings/ProviderLimits.tsx b/webview-ui/src/components/settings/ProviderLimits.tsx index f12f3a6841..9b90fa78ef 100644 --- a/webview-ui/src/components/settings/ProviderLimits.tsx +++ b/webview-ui/src/components/settings/ProviderLimits.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react" +import { useCallback, useEffect, useRef, useState } from "react" import { RefreshCw } from "lucide-react" import { VsCodeLmModelsMessageType, type ProviderQuota } from "@roo-code/types" @@ -25,22 +25,48 @@ export const ProviderLimits = () => { const [quotas, setQuotas] = useState() const [loading, setLoading] = useState(true) - const request = useCallback((force: boolean) => { - setLoading(true) - vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force }) + // Only the newest request counts: an answer for an older one (a slow first load that a refresh + // overtook, or one that outlived this component) is ignored. + const latestRequestId = useRef(undefined) + const counter = useRef(0) + + const cancelLatest = useCallback(() => { + if (latestRequestId.current) { + vscode.postMessage({ + type: VsCodeLmModelsMessageType.cancelProviderQuotas, + requestId: latestRequestId.current, + }) + latestRequestId.current = undefined + } }, []) + const request = useCallback( + (force: boolean) => { + cancelLatest() + const requestId = `provider-quotas-${++counter.current}` + latestRequestId.current = requestId + setLoading(true) + vscode.postMessage({ type: VsCodeLmModelsMessageType.requestProviderQuotas, bool: force, requestId }) + }, + [cancelLatest], + ) + useEffect(() => { const onMessage = (event: MessageEvent) => { - if (event.data?.type === VsCodeLmModelsMessageType.providerQuotas) { - setQuotas(event.data.providerQuotas ?? []) - setLoading(false) - } + if (event.data?.type !== VsCodeLmModelsMessageType.providerQuotas) return + if (event.data.requestId !== latestRequestId.current) return + latestRequestId.current = undefined + setQuotas(event.data.providerQuotas ?? []) + setLoading(false) } window.addEventListener("message", onMessage) request(false) - return () => window.removeEventListener("message", onMessage) - }, [request]) + return () => { + window.removeEventListener("message", onMessage) + // Tell the host nobody is waiting any more so it can stop the profile load and any fetches. + cancelLatest() + } + }, [request, cancelLatest]) // Nothing connected that has a usage API: hide the section entirely. if (quotas && quotas.length === 0) return null diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 083c435daa..16ae51d8eb 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -137,6 +137,18 @@ const SettingsView = forwardRef(({ onDone, t const [isDiscardDialogShow, setDiscardDialogShow] = useState(false) const [isChangeDetected, setChangeDetected] = useState(false) + // The save currently waiting for the host's answer. The "unsaved changes" flag stays on until it succeeds. + const [pendingSaveId, setPendingSaveId] = useState(undefined) + const saveCounter = useRef(0) + // What the pending save must send once its settings have been stored. + const pendingSaveRef = useRef< + | { + apiConfiguration: typeof apiConfiguration + telemetrySetting: typeof telemetrySetting + debug: boolean | undefined + } + | undefined + >(undefined) const [errorMessage, setErrorMessage] = useState(undefined) const [activeTab, setActiveTab] = useState( targetSection && sectionNames.includes(targetSection as SectionName) @@ -391,9 +403,13 @@ const SettingsView = forwardRef(({ onDone, t const isSettingValid = !errorMessage const handleSubmit = () => { - if (isSettingValid) { + if (isSettingValid && !pendingSaveId) { + const saveId = `settings-save-${++saveCounter.current}` + pendingSaveRef.current = { apiConfiguration, telemetrySetting, debug: cachedState.debug } + setPendingSaveId(saveId) vscode.postMessage({ type: "updateSettings", + requestId: saveId, updatedSettings: { language, alwaysAllowReadOnly: alwaysAllowReadOnly ?? undefined, @@ -477,15 +493,40 @@ const SettingsView = forwardRef(({ onDone, t }, }) + // The rest of the save (API configuration, telemetry, debug) is sent once the host confirms the + // settings were stored, and the "unsaved changes" flag is cleared only then: see the + // `settingsSaveResult` listener below. A failed save leaves the user's edits in place to retry. + } + } + + useEffect(() => { + if (!pendingSaveId) return + + const onResult = (event: MessageEvent) => { + const message = event.data + if (message?.type !== "settingsSaveResult" || message.requestId !== pendingSaveId) return + + const saved = pendingSaveRef.current + pendingSaveRef.current = undefined + setPendingSaveId(undefined) + + if (!message.success || !saved) return + // These have more complex logic so they aren't (yet) handled // by the `updateSettings` message. - vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) - vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) - vscode.postMessage({ type: "debugSetting", bool: cachedState.debug }) - + vscode.postMessage({ + type: "upsertApiConfiguration", + text: currentApiConfigName, + apiConfiguration: saved.apiConfiguration, + }) + vscode.postMessage({ type: "telemetrySetting", text: saved.telemetrySetting }) + vscode.postMessage({ type: "debugSetting", bool: saved.debug }) setChangeDetected(false) } - } + + window.addEventListener("message", onResult) + return () => window.removeEventListener("message", onResult) + }, [pendingSaveId, currentApiConfigName]) const checkUnsaveChanges = useCallback( (then: () => void) => { @@ -715,7 +756,7 @@ const SettingsView = forwardRef(({ onDone, t variant={isSettingValid ? "primary" : "secondary"} className={!isSettingValid ? "!border-vscode-errorForeground" : ""} onClick={handleSubmit} - disabled={!isChangeDetected || !isSettingValid} + disabled={!isChangeDetected || !isSettingValid || !!pendingSaveId} data-testid="save-button"> {t("settings:common.save")} diff --git a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx index e8e7af4eaa..e502574293 100644 --- a/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ProviderLimits.spec.tsx @@ -21,9 +21,19 @@ vi.mock("@/i18n/TranslationContext", () => ({ }), })) -const respond = (providerQuotas: ProviderQuota[]) => +/** The id the component put on its most recent request (the host echoes it back). */ +const lastRequestId = () => { + const requests = post.mock.calls.map((c) => c[0]).filter((m) => m.type === "requestProviderQuotas") + return requests.at(-1)?.requestId as string +} + +/** Answers the most recent request, or the given id. Pass `null` to send an answer with no id at all. */ +const respond = (providerQuotas: ProviderQuota[], id?: string | null) => act(() => { - window.dispatchEvent(new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas } })) + const requestId = id === null ? undefined : (id ?? lastRequestId()) + window.dispatchEvent( + new MessageEvent("message", { data: { type: "providerQuotas", providerQuotas, requestId } }), + ) }) describe("ProviderLimits", () => { @@ -35,7 +45,11 @@ describe("ProviderLimits", () => { it("asks the extension for quotas on mount without forcing", () => { render() - expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: false }) + expect(post).toHaveBeenCalledWith({ + type: "requestProviderQuotas", + bool: false, + requestId: expect.stringMatching(/^provider-quotas-\d+$/), + }) }) it("shows a loading row until the first response", () => { @@ -147,7 +161,9 @@ describe("ProviderLimits", () => { it("treats a response without a providerQuotas list as empty", () => { render() act(() => { - window.dispatchEvent(new MessageEvent("message", { data: { type: "providerQuotas" } })) + window.dispatchEvent( + new MessageEvent("message", { data: { type: "providerQuotas", requestId: lastRequestId() } }), + ) }) expect(screen.queryByTestId("provider-limits")).toBeNull() }) @@ -170,7 +186,11 @@ describe("ProviderLimits", () => { ]) post.mockClear() fireEvent.click(screen.getByTestId("provider-limits-refresh")) - expect(post).toHaveBeenCalledWith({ type: "requestProviderQuotas", bool: true }) + expect(post).toHaveBeenCalledWith({ + type: "requestProviderQuotas", + bool: true, + requestId: expect.stringMatching(/^provider-quotas-\d+$/), + }) }) it("ignores unrelated messages", () => { @@ -180,4 +200,64 @@ describe("ProviderLimits", () => { }) expect(screen.getByTestId("provider-limits")).toBeTruthy() }) + + describe("request lifecycle", () => { + const ok = (providerId: string = providerIdentifiers.deepseek): ProviderQuota => ({ + providerId, + ok: true, + fetchedAt: 1, + windows: [{ id: "b", valueLabel: "$1.00 left" }], + }) + + it("ignores an answer for a request that a refresh has overtaken", () => { + render() + const first = lastRequestId() + respond([ok(providerIdentifiers.deepseek)]) + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + const second = lastRequestId() + expect(second).not.toBe(first) + + // A late answer to the first request must not replace the newer state or end the spinner. + respond([ok(providerIdentifiers.moonshot)], first) + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.moonshot}`)).toBeNull() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(true) + + respond([ok(providerIdentifiers.moonshot)], second) + expect(screen.getByTestId(`provider-limit-${providerIdentifiers.moonshot}`)).toBeTruthy() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(false) + }) + + it("ignores an answer that carries no request id or an unknown one", () => { + render() + respond([ok()], null) + respond([ok()], "provider-quotas-999") + expect(screen.queryByTestId(`provider-limit-${providerIdentifiers.deepseek}`)).toBeNull() + }) + + it("does not offer a refresh while a request is pending, so a newer one never races an older one", () => { + render() + expect(screen.getByTestId("provider-limits-refresh").hasAttribute("disabled")).toBe(true) + post.mockClear() + fireEvent.click(screen.getByTestId("provider-limits-refresh")) + expect(post).not.toHaveBeenCalled() + }) + + it("cancels the in-flight request on unmount and ignores a late answer", () => { + const { unmount } = render() + const pending = lastRequestId() + post.mockClear() + unmount() + expect(post).toHaveBeenCalledWith({ type: "cancelProviderQuotas", requestId: pending }) + // Must not throw or update state after unmount. + expect(() => respond([ok()], pending)).not.toThrow() + }) + + it("does not cancel a request that has already been answered", () => { + const { unmount } = render() + respond([ok()]) + post.mockClear() + unmount() + expect(post).not.toHaveBeenCalledWith(expect.objectContaining({ type: "cancelProviderQuotas" })) + }) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx index 6704aa1cf0..9f95ee3c47 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.change-detection.spec.tsx @@ -5,6 +5,7 @@ import { renderWithExtensionState } from "@/utils/test-utils" import { vi, describe, it, expect, beforeEach, beforeAll } from "vitest" import { QueryClient } from "@tanstack/react-query" import React from "react" +import { saveAndConfirm } from "./saveAcknowledgement" // Increase timeout for slow CI environments configure({ asyncUtilTimeout: 10000 }) @@ -505,7 +506,7 @@ describe("SettingsView - Change Detection Fix", () => { fireEvent.click(screen.getByTestId("set-reasoning-default")) expect(screen.getByTestId("save-button")).toBeEnabled() - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(mockPostMessage) expect(mockPostMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", text: "default", @@ -557,7 +558,7 @@ describe("SettingsView - Change Detection Fix", () => { expect(screen.getByTestId("provider-value")).toHaveTextContent("baseten") mockPostMessage.mockClear() - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(mockPostMessage) expect(mockPostMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", text: "default", @@ -594,7 +595,7 @@ describe("SettingsView - Change Detection Fix", () => { expect(screen.getByTestId("provider-value")).toHaveTextContent("deepseek") mockPostMessage.mockClear() - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(mockPostMessage) expect(mockPostMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.saveResult.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.saveResult.spec.tsx new file mode 100644 index 0000000000..71642926ba --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/SettingsView.saveResult.spec.tsx @@ -0,0 +1,119 @@ +import { fireEvent, screen, waitFor } from "@testing-library/react" +import { QueryClient } from "@tanstack/react-query" +import { beforeEach, describe, expect, it, vi } from "vitest" + +import { renderWithExtensionState } from "@/utils/test-utils" +import { vscode } from "@src/utils/vscode" + +import SettingsView from "../SettingsView" +import { answerSave, lastSaveRequestId } from "./saveAcknowledgement" + +vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } })) + +vi.mock("@/utils/TelemetryClient", () => ({ telemetryClient: { capture: vi.fn() } })) + +const postMessage = vi.mocked(vscode.postMessage) + +const posted = (type: string) => + postMessage.mock.calls.map((call) => call[0] as { type: string }).filter((message) => message.type === type) + +const renderUiTab = () => + renderWithExtensionState(, { + queryClient: new QueryClient({ defaultOptions: { queries: { retry: false } } }), + }) + +/** Edit a setting so Save becomes available. */ +const makeAnEdit = async () => { + const checkbox = await screen.findByTestId("collapse-thinking-checkbox") + fireEvent.click(checkbox) + await waitFor(() => expect(screen.getByTestId("save-button")).toBeEnabled()) +} + +describe("SettingsView – save acknowledgement", () => { + beforeEach(() => postMessage.mockClear()) + + it("sends the settings with a request id and holds back the follow-up messages until the host confirms", async () => { + renderUiTab() + await makeAnEdit() + + fireEvent.click(screen.getByTestId("save-button")) + + const [save] = posted("updateSettings") as Array<{ type: string; requestId?: string }> + expect(save.requestId).toMatch(/^settings-save-\d+$/) + // Nothing else is sent, and the unsaved flag stays on, until the host answers. + expect(posted("upsertApiConfiguration")).toHaveLength(0) + expect(posted("telemetrySetting")).toHaveLength(0) + expect(posted("debugSetting")).toHaveLength(0) + expect(screen.getByTestId("save-button")).toBeDisabled() + }) + + it("sends the rest of the save and clears the unsaved flag once the host confirms", async () => { + renderUiTab() + await makeAnEdit() + fireEvent.click(screen.getByTestId("save-button")) + + answerSave(lastSaveRequestId(postMessage), { success: true }) + + await waitFor(() => expect(posted("upsertApiConfiguration")).toHaveLength(1)) + expect(posted("telemetrySetting")).toHaveLength(1) + expect(posted("debugSetting")).toHaveLength(1) + // Saved: nothing left to save. + expect(screen.getByTestId("save-button")).toBeDisabled() + }) + + it("keeps the edits and the unsaved flag when the host reports a failure, so the user can retry", async () => { + renderUiTab() + await makeAnEdit() + fireEvent.click(screen.getByTestId("save-button")) + + answerSave(lastSaveRequestId(postMessage), { success: false, error: "disk full" }) + + // No follow-up writes after a failed save. + expect(posted("upsertApiConfiguration")).toHaveLength(0) + expect(posted("telemetrySetting")).toHaveLength(0) + // The edit is still there, still unsaved, and Save is available again. + await waitFor(() => expect(screen.getByTestId("save-button")).toBeEnabled()) + // The checkbox defaults to checked; the user unchecked it, and that edit survives the failed save. + expect((screen.getByTestId("collapse-thinking-checkbox") as HTMLInputElement).checked).toBe(false) + + // Retrying sends a fresh request with a new id. + const firstId = lastSaveRequestId(postMessage) + fireEvent.click(screen.getByTestId("save-button")) + expect(lastSaveRequestId(postMessage)).not.toBe(firstId) + expect(posted("updateSettings")).toHaveLength(2) + }) + + it("does not send a second save while one is waiting for the host", async () => { + renderUiTab() + await makeAnEdit() + + fireEvent.click(screen.getByTestId("save-button")) + fireEvent.click(screen.getByTestId("save-button")) + fireEvent.click(screen.getByTestId("save-button")) + + expect(posted("updateSettings")).toHaveLength(1) + }) + + it("ignores an answer for a different save, or one that arrives with no id", async () => { + renderUiTab() + await makeAnEdit() + fireEvent.click(screen.getByTestId("save-button")) + + answerSave("settings-save-999", { success: true }) + answerSave(undefined, { success: true }) + + expect(posted("upsertApiConfiguration")).toHaveLength(0) + expect(screen.getByTestId("save-button")).toBeDisabled() + + // The real answer still works afterwards. + answerSave(lastSaveRequestId(postMessage), { success: true }) + await waitFor(() => expect(posted("upsertApiConfiguration")).toHaveLength(1)) + }) + + it("ignores a save result when nothing is being saved", async () => { + renderUiTab() + await screen.findByTestId("collapse-thinking-checkbox") + answerSave("settings-save-1", { success: true }) + expect(posted("upsertApiConfiguration")).toHaveLength(0) + }) +}) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 0835f255f9..16668b5064 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -8,6 +8,7 @@ import { DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, type ProviderSettings } from "@roo- import type { ApiOptionsProps } from "../ApiOptions" import SettingsView from "../SettingsView" +import { saveAndConfirm } from "./saveAcknowledgement" vi.mock("@src/utils/vscode", () => ({ vscode: { postMessage: vi.fn() } })) @@ -479,7 +480,7 @@ describe("SettingsView - Sound Settings", () => { // Pick a size, then Save — the boundary should forward it to the host. fireEvent.change(slider, { target: { value: "18" } }) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(vscode.postMessage).toHaveBeenCalledWith( expect.objectContaining({ @@ -490,7 +491,7 @@ describe("SettingsView - Sound Settings", () => { // Reset clears the override; it is persisted as null (not undefined). fireEvent.click(within(getSettingsContent()).getByTestId("chat-font-size-reset")) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(vscode.postMessage).toHaveBeenCalledWith( expect.objectContaining({ @@ -1010,7 +1011,7 @@ describe("SettingsView - openAiStrictToolSchemas save round trip", () => { fireEvent.click(screen.getByTestId("set-strict-false")) // Toggling buffers in cachedState; nothing is persisted before Save. expect(vscode.postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(posted()).toEqual( expect.objectContaining({ type: "upsertApiConfiguration", @@ -1025,7 +1026,7 @@ describe("SettingsView - openAiStrictToolSchemas save round trip", () => { // toggling on from a stored false and saving has to put true in the payload. renderWithConfig({ openAiStrictToolSchemas: false }) fireEvent.click(screen.getByTestId("set-strict-true")) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBe(true) }) @@ -1033,7 +1034,7 @@ describe("SettingsView - openAiStrictToolSchemas save round trip", () => { renderWithConfig({ openAiStrictToolSchemas: false }) fireEvent.click(screen.getByTestId("set-strict-true")) fireEvent.click(screen.getByTestId("set-strict-false")) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBe(false) }) @@ -1044,7 +1045,7 @@ describe("SettingsView - openAiStrictToolSchemas save round trip", () => { expect(screen.getByTestId("received-strict").textContent).toBe("undefined") // Make a change elsewhere so Save is enabled, then save. fireEvent.click(screen.getByTestId("set-other-field")) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(vi.mocked(vscode.postMessage)) expect(posted()?.apiConfiguration?.openAiBaseUrl).toBe("https://example.test") expect(posted()?.apiConfiguration?.openAiStrictToolSchemas).toBeUndefined() }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 1bc4ab1afb..ff99be5002 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -7,6 +7,7 @@ import { QueryClient } from "@tanstack/react-query" import React from "react" import SettingsView from "../SettingsView" +import { saveAndConfirm } from "./saveAcknowledgement" import { vscode } from "@src/utils/vscode" const postMessage = vi.spyOn(vscode, "postMessage").mockImplementation(() => {}) @@ -664,7 +665,7 @@ describe("SettingsView - Unsaved Changes Detection", () => { }) expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(postMessage) expect(postMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", @@ -717,7 +718,7 @@ describe("SettingsView - Unsaved Changes Detection", () => { expect(configuration.openAiCustomModelInfo?.reasoningEffort).toBe("low") expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(postMessage) expect(postMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", text: "default", @@ -798,7 +799,7 @@ describe("SettingsView - Unsaved Changes Detection", () => { }) expect(postMessage).not.toHaveBeenCalledWith(expect.objectContaining({ type: "upsertApiConfiguration" })) - fireEvent.click(screen.getByTestId("save-button")) + saveAndConfirm(postMessage) expect(postMessage).toHaveBeenCalledWith({ type: "upsertApiConfiguration", diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx new file mode 100644 index 0000000000..1dbb6e3c9c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.fixture.tsx @@ -0,0 +1,143 @@ +import { useEffect } from "react" + +import { providerIdentifiers, VsCodeLmModelsMessageType, type HistoryItem, type ProviderQuota } from "@roo-code/types" + +import { UsageSettings } from "../UsageSettings" +import { AppProviders } from "../../../../playwright/AppProviders" +import { vscode } from "@src/utils/vscode" + +/** Matches the clock the visual test pins, so the history and the "updated" label are stable. */ +export const USAGE_NOW = new Date("2026-06-01T12:00:00Z").getTime() +const DAY = 24 * 60 * 60 * 1000 +const WORKSPACE = "/work/project" + +const task = (n: number, daysAgo: number, overrides: Partial): HistoryItem => ({ + id: `usage-task-${n}`, + number: n, + ts: USAGE_NOW - daysAgo * DAY, + task: `Task ${n}`, + tokensIn: 0, + tokensOut: 0, + totalCost: 0, + workspace: WORKSPACE, + mode: "code", + ...overrides, +}) + +const used = (apiProvider: string, modelId: string, tokensIn: number, tokensOut: number, cost: number) => [ + { apiProvider, modelId, requests: 4, tokensIn, tokensOut, cost }, +] + +/** A week of mixed priced (Anthropic) and unpriced (Copilot) work. */ +const history: HistoryItem[] = [ + task(1, 0, { + task: "Add a loading spinner to the Save button", + tokensIn: 52_000, + tokensOut: 4_100, + totalCost: 1.84, + cacheReads: 31_000, + usageByModel: used(providerIdentifiers.anthropic, "claude-sonnet-5", 52_000, 4_100, 1.84), + }), + task(2, 1, { + task: "Refactor the auth middleware", + tokensIn: 91_000, + tokensOut: 8_800, + totalCost: 3.27, + cacheReads: 60_000, + mode: "architect", + usageByModel: used(providerIdentifiers.anthropic, "claude-sonnet-5", 91_000, 8_800, 3.27), + }), + task(3, 2, { + task: "Write unit tests for the billing service", + tokensIn: 210_000, + tokensOut: 15_000, + totalCost: 0, + usageByModel: used(providerIdentifiers.githubCopilot, "gpt-5", 210_000, 15_000, 0), + }), + task(4, 4, { + task: "Investigate the flaky checkout test", + tokensIn: 34_000, + tokensOut: 2_900, + totalCost: 0.61, + mode: "debug", + usageByModel: used(providerIdentifiers.anthropic, "claude-haiku-5", 34_000, 2_900, 0.61), + }), +] + +const healthyQuotas: ProviderQuota[] = [ + { + providerId: providerIdentifiers.githubCopilot, + ok: true, + plan: "business", + fetchedAt: USAGE_NOW, + windows: [ + { + id: "premium", + valueLabel: "39,389 / 140,000 used", + usedPercent: 28, + resetAt: new Date("2026-07-01T00:00:00Z").getTime(), + }, + ], + }, + { + providerId: providerIdentifiers.deepseek, + ok: true, + fetchedAt: USAGE_NOW, + windows: [{ id: "balance", valueLabel: "$4.25 left" }], + }, +] + +const degradedQuotas: ProviderQuota[] = [ + { + providerId: providerIdentifiers.githubCopilot, + ok: false, + reason: "signedOut", + fetchedAt: USAGE_NOW, + }, + { providerId: providerIdentifiers.openrouter, ok: false, reason: "noKey", fetchedAt: USAGE_NOW }, + { providerId: providerIdentifiers.deepseek, ok: false, reason: "error", message: "HTTP 500", fetchedAt: USAGE_NOW }, +] + +/** Plays the extension host: answers the component's quota request, echoing its request id. */ +function FakeHost({ quotas }: { quotas: ProviderQuota[] }) { + useEffect(() => { + const original = vscode.postMessage.bind(vscode) + vscode.postMessage = ((message: { type: string; requestId?: string }) => { + if (message.type === VsCodeLmModelsMessageType.requestProviderQuotas) { + queueMicrotask(() => + window.dispatchEvent( + new MessageEvent("message", { + data: { + type: VsCodeLmModelsMessageType.providerQuotas, + providerQuotas: quotas, + requestId: message.requestId, + }, + }), + ), + ) + return + } + return original(message as never) + }) as typeof vscode.postMessage + return () => { + vscode.postMessage = original as typeof vscode.postMessage + } + }, [quotas]) + return null +} + +export type UsageStoryVariant = "populated" | "degraded" | "empty" + +export function UsageSettingsStory({ variant = "populated" }: { variant?: UsageStoryVariant }) { + const quotas = variant === "degraded" ? degradedQuotas : variant === "empty" ? [] : healthyQuotas + return ( + + +
+ +
+
+ ) +} diff --git a/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx new file mode 100644 index 0000000000..5b227ac29c --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/UsageSettings.visual.tsx @@ -0,0 +1,59 @@ +import { expect, test } from "../../../../playwright/coverage-fixture" +import { expectBoundedLayout } from "../../../../playwright/layout-contracts" +import { mountedStory } from "../../../../playwright/mounted-story" +import { applyVisualTheme, visualThemes } from "../../../../playwright/themes" + +const NOW = new Date("2026-06-01T12:00:00Z") + +for (const theme of visualThemes) { + test(`renders the Usage tab with provider limits and history in the VS Code ${theme.name} theme`, async ({ + mount, + page, + }) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings")) + await applyVisualTheme(page, theme) + const story = component.getByTestId("usage-story") + + // Limits arrive from the (fake) host; wait for them so the screenshot is the settled state. + await expect(story.getByTestId(`provider-limit-github-copilot`)).toBeVisible() + await expect(story.getByTestId("usage-stats")).toBeVisible() + await expect(story.getByTestId("usage-daily-chart")).toBeVisible() + + await expect(story).toHaveScreenshot(`usage-settings-${theme.name}.png`) + const exportButton = story.getByTestId("usage-export-csv") + await expectBoundedLayout(page, story, { focusedControl: exportButton }) + }) +} + +test("shows quiet statuses for provider limits that cannot be read, and keeps the history", async ({ mount, page }) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings-degraded")) + await applyVisualTheme(page, visualThemes[0]) + const story = component.getByTestId("usage-story") + + await expect(story.getByTestId("provider-limit-github-copilot")).toBeVisible() + await expect(story.getByTestId("provider-limit-openrouter")).toBeVisible() + await expect(story.getByTestId("provider-limit-deepseek")).toBeVisible() + // No bars for providers that report nothing, but the usage history is unaffected. + await expect(story.locator("[role=progressbar]")).toHaveCount(0) + await expect(story.getByTestId("usage-stats")).toBeVisible() + + await expect(story).toHaveScreenshot("usage-settings-degraded.png") +}) + +test("shows the empty state, and hides the limits section when no provider has a usage API", async ({ + mount, + page, +}) => { + await page.clock.setFixedTime(NOW) + const component = mountedStory(await mount("usage-settings-empty")) + await applyVisualTheme(page, visualThemes[0]) + const story = component.getByTestId("usage-story") + + await expect(story.getByTestId("usage-empty")).toBeVisible() + await expect(story.getByTestId("provider-limits")).toHaveCount(0) + await expect(story.getByTestId("usage-export-csv")).toHaveCount(0) + + await expect(story).toHaveScreenshot("usage-settings-empty.png") +}) diff --git a/webview-ui/src/components/settings/__tests__/saveAcknowledgement.ts b/webview-ui/src/components/settings/__tests__/saveAcknowledgement.ts new file mode 100644 index 0000000000..2f24c431e4 --- /dev/null +++ b/webview-ui/src/components/settings/__tests__/saveAcknowledgement.ts @@ -0,0 +1,28 @@ +import { act, fireEvent, screen } from "@testing-library/react" + +// Only the recorded calls are read, so any `vi.fn()` or `vi.mocked(fn)` works. +interface PostMessage { + mock: { calls: ReadonlyArray> } +} + +/** The request id of the most recent `updateSettings` the component posted. */ +export const lastSaveRequestId = (postMessage: PostMessage): string | undefined => { + const saves = postMessage.mock.calls + .map((call) => call[0] as { type?: string; requestId?: string }) + .filter((message) => message?.type === "updateSettings") + return saves.at(-1)?.requestId +} + +/** Plays the extension host's answer to a save. */ +export const answerSave = (requestId: string | undefined, result: { success: boolean; error?: string }) => + act(() => { + window.dispatchEvent( + new MessageEvent("message", { data: { type: "settingsSaveResult", requestId, ...result } }), + ) + }) + +/** Clicks Save, then reports the host's success, so the follow-up messages (API config, telemetry) go out. */ +export const saveAndConfirm = (postMessage: PostMessage) => { + fireEvent.click(screen.getByTestId("save-button")) + answerSave(lastSaveRequestId(postMessage), { success: true }) +} diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index beadd74d58..5f28aea29c 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1294,7 +1294,7 @@ "chat": "Pesan chat", "history": "Riwayat tugas", "relative": "5 menit lalu", - "hover": "(hover: {{value}})" + "hover": "(arahkan kursor: {{value}})" } }, "usage": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 6e28b47ac1..69321bc238 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1294,7 +1294,7 @@ "chat": "Messaggio della chat", "history": "Cronologia delle attività", "relative": "5 min fa", - "hover": "(hover: {{value}})" + "hover": "(al passaggio del mouse: {{value}})" } }, "usage": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 66a9748f26..4ff2e24df9 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1294,7 +1294,7 @@ "chat": "Chatbericht", "history": "Takengeschiedenis", "relative": "5 min geleden", - "hover": "(hover: {{value}})" + "hover": "(bij aanwijzen: {{value}})" } }, "usage": {