Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 22 additions & 10 deletions src/tui/chrome.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import process from 'node:process'
import type { RuntimeConfig } from '../config.js'
import type { SlashCommand } from '../cli-commands.js'
import type { PermissionRequest } from '../permissions.js'
import type { ContextStats } from '../utils/token-estimator.js'

const RESET = '\u001b[0m'
const DIM = '\u001b[2m'
Expand Down Expand Up @@ -114,6 +115,23 @@ function colorBadge(
return `${color}[${label}]${RESET} ${BOLD}${value}${RESET}`
}

function formatCompactTokenCount(tokens: number): string {
const value = Math.max(0, Math.floor(tokens))
if (value < 1_000) return String(value)
if (value < 1_000_000) {
const thousands = Math.round(value / 1_000)
return thousands >= 1_000 ? formatCompactMillionTokenCount(value) : `${thousands}K`
}

return formatCompactMillionTokenCount(value)
}

function formatCompactMillionTokenCount(value: number): string {
const millions = value / 1_000_000
const formatted = millions.toFixed(1).replace(/\.0$/, '')
return `${formatted}M`
}

function joinSegmentsWithinWidth(
segments: string[],
separator: string,
Expand Down Expand Up @@ -232,6 +250,7 @@ export function renderPanel(
export function renderContextBadge(stats: {
utilization: number
warningLevel: 'normal' | 'warning' | 'critical' | 'blocked'
remainingTokens: number
accounting?: {
providerUsageTokens: number
estimatedTokens: number
Expand All @@ -251,6 +270,7 @@ export function renderContextBadge(stats: {

const filled = Math.round(utilization * 10)
const bar = '\u2593'.repeat(filled) + '\u2591'.repeat(10 - filled)
const headroom = ` ${formatCompactTokenCount(stats.remainingTokens)} left`
const sourceLabel =
accounting?.source === 'provider_usage'
? 'usage'
Expand All @@ -261,7 +281,7 @@ export function renderContextBadge(stats: {
: ''
const suffix = sourceLabel ? ` ${sourceLabel}` : ''

return colorBadge('ctx', `${percent}% ${bar}${suffix}`, color)
return colorBadge('ctx', `${percent}% ${bar}${headroom}${suffix}`, color)
}

export function renderBanner(
Expand All @@ -276,15 +296,7 @@ export function renderBanner(
mcpConnectedCount: number
mcpConnectingCount: number
mcpErrorCount: number
contextStats?: {
utilization: number
warningLevel: 'normal' | 'warning' | 'critical' | 'blocked'
accounting?: {
providerUsageTokens: number
estimatedTokens: number
source: 'provider_usage' | 'provider_usage_plus_estimate' | 'estimate_only'
}
} | null
contextStats?: ContextStats | null
},
): string {
const panelWidth = Math.max(60, process.stdout.columns ?? 100)
Expand Down
3 changes: 3 additions & 0 deletions src/utils/token-estimator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type ContextStats = {
providerUsageTokens: number
contextWindow: number
effectiveInput: number
remainingTokens: number
utilization: number
warningLevel: 'normal' | 'warning' | 'critical' | 'blocked'
accounting: TokenAccountingResult
Expand Down Expand Up @@ -181,6 +182,7 @@ export function computeContextStats(
const window = getModelContextWindow(model)
const accounting = tokenCountWithEstimation(messages)
const utilization = Math.min(1, accounting.totalTokens / window.effectiveInput)
const remainingTokens = Math.max(0, window.effectiveInput - accounting.totalTokens)

let warningLevel: ContextStats['warningLevel']
if (utilization >= 0.95) {
Expand All @@ -199,6 +201,7 @@ export function computeContextStats(
providerUsageTokens: accounting.providerUsageTokens,
contextWindow: window.contextWindow,
effectiveInput: window.effectiveInput,
remainingTokens,
utilization,
warningLevel,
accounting,
Expand Down
94 changes: 87 additions & 7 deletions test/context-badge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ function stripAnsi(input: string): string {

describe('renderContextBadge', () => {
it('renders normal level badge', () => {
const result = renderContextBadge({ utilization: 0.23, warningLevel: 'normal' })
const result = renderContextBadge({
utilization: 0.23,
warningLevel: 'normal',
remainingTokens: 142_000,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('ctx'), 'should contain ctx label')
assert.ok(plain.includes('23%'), 'should show 23%')
Expand All @@ -17,32 +21,52 @@ describe('renderContextBadge', () => {
})

it('renders warning level badge', () => {
const result = renderContextBadge({ utilization: 0.68, warningLevel: 'warning' })
const result = renderContextBadge({
utilization: 0.68,
warningLevel: 'warning',
remainingTokens: 59_000,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('68%'))
assert.ok(plain.includes('ctx'))
})

it('renders critical level badge', () => {
const result = renderContextBadge({ utilization: 0.89, warningLevel: 'critical' })
const result = renderContextBadge({
utilization: 0.89,
warningLevel: 'critical',
remainingTokens: 20_000,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('89%'))
})

it('renders blocked level badge', () => {
const result = renderContextBadge({ utilization: 0.96, warningLevel: 'blocked' })
const result = renderContextBadge({
utilization: 0.96,
warningLevel: 'blocked',
remainingTokens: 0,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('96%'))
})

it('renders 0% utilization correctly', () => {
const result = renderContextBadge({ utilization: 0, warningLevel: 'normal' })
const result = renderContextBadge({
utilization: 0,
warningLevel: 'normal',
remainingTokens: 184_000,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('0%'))
})

it('renders 100% utilization correctly', () => {
const result = renderContextBadge({ utilization: 1, warningLevel: 'blocked' })
const result = renderContextBadge({
utilization: 1,
warningLevel: 'blocked',
remainingTokens: 0,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('100%'))
})
Expand All @@ -51,6 +75,7 @@ describe('renderContextBadge', () => {
const result = renderContextBadge({
utilization: 0.82,
warningLevel: 'warning',
remainingTokens: 18_000,
accounting: {
providerUsageTokens: 70_000,
estimatedTokens: 12_000,
Expand All @@ -59,11 +84,66 @@ describe('renderContextBadge', () => {
})
const plain = stripAnsi(result)
assert.ok(plain.includes('82%'))
assert.ok(plain.includes('18K left'))
assert.ok(plain.includes('usage+est'))
})

it('shows compact remaining context headroom', () => {
const result = renderContextBadge({
utilization: 0.68,
warningLevel: 'warning',
remainingTokens: 59_200,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('59K left'))
})

it('shows small remaining context headroom without a suffix', () => {
const result = renderContextBadge({
utilization: 0.94,
warningLevel: 'critical',
remainingTokens: 999,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('999 left'))
})

it('shows million-scale remaining context headroom compactly', () => {
const result = renderContextBadge({
utilization: 0.12,
warningLevel: 'normal',
remainingTokens: 1_234_000,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('1.2M left'))
})

it('promotes rounded 1000K headroom to million-scale display', () => {
const result = renderContextBadge({
utilization: 0.12,
warningLevel: 'normal',
remainingTokens: 999_500,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('1M left'))
})

it('shows zero remaining headroom when context is blocked', () => {
const result = renderContextBadge({
utilization: 1,
warningLevel: 'blocked',
remainingTokens: 0,
})
const plain = stripAnsi(result)
assert.ok(plain.includes('0 left'))
})

it('uses correct block characters for utilization', () => {
const result = renderContextBadge({ utilization: 0.5, warningLevel: 'warning' })
const result = renderContextBadge({
utilization: 0.5,
warningLevel: 'warning',
remainingTokens: 92_000,
})
const plain = stripAnsi(result)
// 50% → 5 filled blocks out of 10
const filledCount = (plain.match(/\u2593/g) || []).length
Expand Down
1 change: 1 addition & 0 deletions test/session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function contextStats(messages: ChatMessage[], effectiveInput = 20_000): Context
providerUsageTokens: accounting.providerUsageTokens,
contextWindow: effectiveInput,
effectiveInput,
remainingTokens: Math.max(0, effectiveInput - accounting.totalTokens),
utilization,
warningLevel:
utilization >= 0.95
Expand Down
1 change: 1 addition & 0 deletions test/snip-compact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function contextStats(messages: ChatMessage[], effectiveInput = 20_000): Context
providerUsageTokens: 0,
contextWindow: effectiveInput,
effectiveInput,
remainingTokens: Math.max(0, effectiveInput - totalTokens),
utilization,
warningLevel:
utilization >= 0.95
Expand Down
3 changes: 3 additions & 0 deletions test/token-estimator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ describe('computeContextStats', () => {
assert.ok(stats.estimatedTokens > 0)
assert.equal(stats.contextWindow, 200_000)
assert.equal(stats.effectiveInput, 184_000)
assert.equal(stats.remainingTokens, stats.effectiveInput - stats.totalTokens)
})

it('computes blocked warning level for large messages', () => {
Expand All @@ -109,6 +110,7 @@ describe('computeContextStats', () => {
`expected blocked or critical, got ${stats.warningLevel}`,
)
assert.equal(stats.utilization, 1, 'utilization should be capped at 1')
assert.equal(stats.remainingTokens, 0)
})

it('computes warning level for medium messages', () => {
Expand All @@ -133,6 +135,7 @@ describe('computeContextStats', () => {
]
const stats = computeContextStats(messages, 'deepseek-chat')
assert.equal(stats.utilization, 1)
assert.equal(stats.remainingTokens, 0)
})
})

Expand Down