Files
openclaude/src/components/BuiltinStatusLine.test.tsx
626c4873ab feat(statusline): show token counts in context bar (ctx 74K/200K (37%)) (#1967)
* feat(statusline): show token counts in context bar (ctx 74K/200K (37%))

Instead of just percentage, show actual token usage and context window
size with integer K/M prefixes (uppercase, en-US locale).

- BuiltinStatusLine: added contextInputTokens + contextWindow fields
- buildBuiltinStatusSegments: formats "ctx {used}/{window} ({pct}%)"
- format.ts: added formatTokenCount() — integer, uppercase K/M
- All 18 tests pass

* fix(statusline): call getCurrentUsage once and reuse result

* fix(statusline): mark estimated token counts with ~ prefix

When getCurrentUsage() returns is_estimated:true (provider reported
all-zero usage), show "ctx ~74K/200K (37%)" instead of "ctx 74K/200K
(37%)" so built-in statusline preserves the estimate distinction
exposed by the custom-statusline contract.

* fix(openai-shim): send stream_options for non-Ollama local providers

Previously stream_options was disabled for all local URLs (127.0.0.1,
192.168.x.x, etc.), preventing llama-server and other self-hosted
OpenAI-compatible servers from returning usage in SSE streams. Now only
Ollama (localhost:11434) is excluded, since it rejects stream_options.
All other providers including llama-server receive stream_options and
their prompt_tokens/completion_tokens are correctly mapped via
buildAnthropicUsageFromRawUsage.

---------

Co-authored-by: Andrey Bezborodov <andrey@getdataflow.ru>
2026-07-15 23:58:44 +08:00

215 lines
6.7 KiB
TypeScript

import { describe, expect, it } from 'bun:test'
import { DEFAULT_GLOBAL_CONFIG } from '../utils/config.js'
import {
type BuiltinStatusData,
buildBuiltinStatusSegments,
builtinStatusLineShouldDisplay,
fitSegments,
} from './BuiltinStatusLine.js'
const fullData: BuiltinStatusData = {
modelName: 'Opus 4.8',
contextUsedPercent: 37.4,
contextInputTokens: 74000,
contextWindow: 200000,
costUSD: 1.234,
rateLimit: { label: '5h', usedPercent: 42 },
}
describe('buildBuiltinStatusSegments', () => {
it('builds all segments when every datum is present', () => {
const segments = buildBuiltinStatusSegments(fullData)
expect(segments.map(s => s.key)).toEqual([
'model',
'context',
'cost',
'rateLimit',
])
expect(segments.map(s => s.text)).toEqual([
'Opus 4.8',
'ctx 74K/200K (37%)',
'$1.23',
'5h 42%',
])
})
it('omits context before the first assistant turn', () => {
const segments = buildBuiltinStatusSegments({
...fullData,
contextUsedPercent: null,
contextInputTokens: null,
contextWindow: null,
})
expect(segments.find(s => s.key === 'context')).toBeUndefined()
})
it('omits cost at $0 and rate limit without utilization data', () => {
const segments = buildBuiltinStatusSegments({
...fullData,
costUSD: 0,
rateLimit: null,
})
expect(segments.map(s => s.key)).toEqual(['model', 'context'])
})
it('colors context by usage thresholds', () => {
const at = (pct: number) =>
buildBuiltinStatusSegments({ ...fullData, contextUsedPercent: pct }).find(
s => s.key === 'context',
)?.color
expect(at(50)).toBeUndefined()
expect(at(70)).toBe('warning')
expect(at(90)).toBe('error')
})
it('shows token counts in context segment', () => {
const ctx = buildBuiltinStatusSegments({
...fullData,
contextUsedPercent: 37.4,
contextInputTokens: 74000,
contextWindow: 200000,
}).find(s => s.key === 'context')
expect(ctx?.text).toBe('ctx 74K/200K (37%)')
expect(ctx?.shortText).toBe('ctx 74K/200K')
})
it('shows sub-one-percent context usage as nonzero', () => {
const ctx = buildBuiltinStatusSegments({
...fullData,
contextUsedPercent: 0.01,
contextInputTokens: 20,
contextWindow: 200000,
}).find(s => s.key === 'context')
expect(ctx?.text).toBe('ctx 20/200K (<1%)')
})
it('colors context by the displayed rounded percentage', () => {
const at = (pct: number) =>
buildBuiltinStatusSegments({ ...fullData, contextUsedPercent: pct }).find(
s => s.key === 'context',
)
expect(at(69.6)).toMatchObject({ text: 'ctx 74K/200K (70%)', color: 'warning' })
expect(at(89.6)).toMatchObject({ text: 'ctx 74K/200K (90%)', color: 'error' })
})
it('colors rate limit by usage thresholds', () => {
const at = (pct: number) =>
buildBuiltinStatusSegments({
...fullData,
rateLimit: { label: '7d', usedPercent: pct },
}).find(s => s.key === 'rateLimit')?.color
expect(at(30)).toBeUndefined()
expect(at(60)).toBe('warning')
expect(at(85)).toBe('error')
})
it('prefixes estimated tokens with ~ when contextIsEstimated is true', () => {
const ctx = buildBuiltinStatusSegments({
...fullData,
contextUsedPercent: 37,
contextInputTokens: 74000,
contextWindow: 200000,
contextIsEstimated: true,
}).find(s => s.key === 'context')
expect(ctx?.text).toBe('ctx ~74K/200K (37%)')
expect(ctx?.shortText).toBe('ctx ~74K/200K')
})
it('does not show ~ when contextIsEstimated is false or absent', () => {
const ctxWithFlag = buildBuiltinStatusSegments({
...fullData,
contextIsEstimated: false,
}).find(s => s.key === 'context')
const ctxWithoutFlag = buildBuiltinStatusSegments({
...fullData,
}).find(s => s.key === 'context')
expect(ctxWithFlag?.text).toBe('ctx 74K/200K (37%)')
expect(ctxWithFlag?.shortText).toBe('ctx 74K/200K')
expect(ctxWithoutFlag?.text).toBe('ctx 74K/200K (37%)')
expect(ctxWithoutFlag?.shortText).toBe('ctx 74K/200K')
})
})
describe('fitSegments', () => {
const segments = buildBuiltinStatusSegments(fullData)
// 'Opus 4.8 · ctx 74K/200K (37%) · $1.23 · 5h 42%' = 46 cols
it('keeps everything when the line fits', () => {
expect(fitSegments(segments, 120)).toHaveLength(4)
})
it('degrades segments to short forms before dropping any', () => {
// Full: 'Opus 4.8 · ctx 74K/200K (37%) · $1.23 · 5h 42%' = 46 cols
// Degraded shortText: 'Opus 4.8 · ctx 74K/200K · $1 · 5h 42%' = 39 cols
const fitted = fitSegments(segments, 40)
expect(fitted.map(s => s.key)).toEqual([
'model',
'context',
'cost',
'rateLimit',
])
expect(fitted.find(s => s.key === 'context')?.text).toBe('ctx 74K/200K')
expect(fitted.find(s => s.key === 'cost')?.text).toBe('$1')
})
it('marks dropped segments with a trailing ellipsis', () => {
// Too narrow for all four even degraded; hidden data must be visible as hidden
// Full is ~46 cols, degraded shortText is ~39 cols — try width 35
const fitted = fitSegments(segments, 35)
expect(fitted.at(-1)?.key).toBe('truncated')
expect(fitted.at(-1)?.text).toBe('…')
})
it('keeps only the model at very narrow widths, skipping the marker if it will not fit', () => {
const fitted = fitSegments(segments, 10)
expect(fitted.map(s => s.key)).toEqual(['model'])
})
it('does not mutate the caller-visible segment text', () => {
const before = segments.map(s => s.text)
fitSegments(segments, 22)
expect(segments.map(s => s.text)).toEqual(before)
})
it('returns empty when even the model does not fit', () => {
expect(fitSegments(segments, 3)).toEqual([])
})
})
describe('builtinStatusLineShouldDisplay', () => {
it('yields to a configured custom statusline', () => {
expect(
builtinStatusLineShouldDisplay({
statusLine: { type: 'command', command: 'echo custom' },
}),
).toBe(false)
})
it('displays by default when no custom statusline is configured', () => {
expect(builtinStatusLineShouldDisplay({})).toBe(true)
})
it('hides when defaultStatusLineEnabled is false', () => {
expect(
builtinStatusLineShouldDisplay(
{},
{ ...DEFAULT_GLOBAL_CONFIG, defaultStatusLineEnabled: false },
),
).toBe(false)
})
it('config off still yields to nothing — custom statusline wins regardless', () => {
expect(
builtinStatusLineShouldDisplay(
{ statusLine: { type: 'command', command: 'echo custom' } },
{ ...DEFAULT_GLOBAL_CONFIG, defaultStatusLineEnabled: false },
),
).toBe(false)
})
})