From 0b166bfebd4873447746dc5a7c2f20055d53aa1c Mon Sep 17 00:00:00 2001 From: Kevin Yin <182213728+yinkev@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:05:54 -0700 Subject: [PATCH] fix(desktop): pin English number formatting Signed-off-by: Kevin Yin <182213728+yinkev@users.noreply.github.com> --- .../hooks/use-prompt-actions/utils.test.ts | 14 +++++++++- .../session/hooks/use-prompt-actions/utils.ts | 2 +- .../settings/billing/billing-amounts.test.ts | 27 +++++++++++++++++++ .../app/settings/billing/billing-amounts.ts | 2 +- .../assistant-ui/tool/fallback-model.test.ts | 11 +++++++- .../tool/fallback-model/format.ts | 2 +- 6 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src/app/settings/billing/billing-amounts.test.ts diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.test.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.test.ts index 061fd16210d72..56b27939c6416 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.test.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.test.ts @@ -1,5 +1,5 @@ import type { AppendMessage } from '@assistant-ui/react' -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import type { ChatMessage } from '@/lib/chat-messages' @@ -207,6 +207,18 @@ describe('renderRpcResult', () => { ) }) + it('pins English usage copy to en-US number formatting', () => { + const localeSpy = vi.spyOn(Number.prototype, 'toLocaleString').mockImplementation(function (this: number) { + return String(this) + }) + + renderRpcResult({ calls: 12, input: 1_234, output: 56, total: 1_290 }, 'usage') + + expect(localeSpy).toHaveBeenCalledTimes(4) + expect(localeSpy.mock.calls).toEqual([['en-US'], ['en-US'], ['en-US'], ['en-US']]) + localeSpy.mockRestore() + }) + it('appends credits_lines when present', () => { const body = renderRpcResult( { diff --git a/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.ts b/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.ts index ad9167ebb0508..757eb31c552fb 100644 --- a/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.ts +++ b/apps/desktop/src/app/session/hooks/use-prompt-actions/utils.ts @@ -271,7 +271,7 @@ export function renderRpcResult(response: unknown, name: string): string { const total = Number(r.total ?? 0) const lines: string[] = [ - `Usage: ${calls.toLocaleString()} calls · ${input.toLocaleString()} in / ${output.toLocaleString()} out · ${total.toLocaleString()} total` + `Usage: ${calls.toLocaleString('en-US')} calls · ${input.toLocaleString('en-US')} in / ${output.toLocaleString('en-US')} out · ${total.toLocaleString('en-US')} total` ] if (Array.isArray(r.credits_lines)) { diff --git a/apps/desktop/src/app/settings/billing/billing-amounts.test.ts b/apps/desktop/src/app/settings/billing/billing-amounts.test.ts new file mode 100644 index 0000000000000..9ed5630340ca0 --- /dev/null +++ b/apps/desktop/src/app/settings/billing/billing-amounts.test.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { formatMoney } from './billing-amounts' + +afterEach(() => { + vi.restoreAllMocks() +}) + +describe('formatMoney', () => { + it('pins hard-coded USD copy to en-US formatting', () => { + const OriginalNumberFormat = Intl.NumberFormat + const seenLocales: Array = [] + + vi.spyOn(Intl, 'NumberFormat').mockImplementation( + class { + constructor(locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions) { + seenLocales.push(locales) + + return new OriginalNumberFormat(locales ?? 'en-DE', options) + } + } as typeof Intl.NumberFormat + ) + + expect(formatMoney(25)).toBe('$25') + expect(seenLocales).toEqual(['en-US']) + }) +}) diff --git a/apps/desktop/src/app/settings/billing/billing-amounts.ts b/apps/desktop/src/app/settings/billing/billing-amounts.ts index 0b0e5517b92e1..3bc90e754959c 100644 --- a/apps/desktop/src/app/settings/billing/billing-amounts.ts +++ b/apps/desktop/src/app/settings/billing/billing-amounts.ts @@ -114,7 +114,7 @@ export function formatMoney(value?: null | number | string): string { return EMPTY_BILLING_VALUE } - return new Intl.NumberFormat(undefined, { + return new Intl.NumberFormat('en-US', { currency: 'USD', maximumFractionDigits: amount % 1 === 0 ? 0 : 2, minimumFractionDigits: amount % 1 === 0 ? 0 : 2, diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts index 275cc13fb4f64..2f50caf9b6839 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { setRuntimeI18nLocale } from '@/i18n' @@ -372,6 +372,15 @@ describe('clampForDisplay', () => { expect(clamped).toContain('5,000 more characters truncated') expect(clamped).toContain('Copy') }) + + it('pins the English truncation copy to en-US number formatting', () => { + const localeSpy = vi.spyOn(Number.prototype, 'toLocaleString').mockReturnValue('5,000') + + clampForDisplay('x'.repeat(MAX_TOOL_RENDER_CHARS + 5_000)) + + expect(localeSpy).toHaveBeenCalledWith('en-US') + localeSpy.mockRestore() + }) }) // A large tool result (e.g. a 100KB read_file during a `/learn` run) must not diff --git a/apps/desktop/src/components/assistant-ui/tool/fallback-model/format.ts b/apps/desktop/src/components/assistant-ui/tool/fallback-model/format.ts index ba7581461b870..76c7ae2ad6ec1 100644 --- a/apps/desktop/src/components/assistant-ui/tool/fallback-model/format.ts +++ b/apps/desktop/src/components/assistant-ui/tool/fallback-model/format.ts @@ -55,7 +55,7 @@ export function clampForDisplay(value: string, max = MAX_TOOL_RENDER_CHARS): str const omitted = value.length - max - return `${value.slice(0, max)}\n\n… ${omitted.toLocaleString()} more characters truncated — use Copy for the full output.` + return `${value.slice(0, max)}\n\n… ${omitted.toLocaleString('en-US')} more characters truncated — use Copy for the full output.` } export function prettyJson(value: unknown): string {