diff --git a/src/ccstatusline.ts b/src/ccstatusline.ts index 6fe1c22..0225140 100644 --- a/src/ccstatusline.ts +++ b/src/ccstatusline.ts @@ -38,6 +38,14 @@ import { } from './utils/speed-window'; import { prefetchUsageDataIfNeeded } from './utils/usage-prefetch'; +function formatModelDisplayName(modelId: string): string { + return modelId + .replace(/^claude-/, '') + .replace(/-(\d{8})$/, '') + .replace(/-/g, ' ') + .replace(/\b\w/g, c => c.toUpperCase()); +} + function hasSessionDurationInStatusJson(data: StatusJSON): boolean { const durationMs = data.cost?.total_duration_ms; return typeof durationMs === 'number' && Number.isFinite(durationMs) && durationMs >= 0; @@ -114,6 +122,18 @@ async function renderMultipleLines(data: StatusJSON) { let tokenMetrics: TokenMetrics | null = null; if (data.transcript_path) { tokenMetrics = await getTokenMetrics(data.transcript_path); + + if (tokenMetrics.model) { + const stdinModelId = typeof data.model === 'string' + ? data.model + : data.model?.id; + if (stdinModelId !== tokenMetrics.model) { + data.model = { + id: tokenMetrics.model, + display_name: formatModelDisplayName(tokenMetrics.model) + }; + } + } } let sessionDuration: string | null = null; diff --git a/src/types/TokenMetrics.ts b/src/types/TokenMetrics.ts index cb63cac..eb97d9a 100644 --- a/src/types/TokenMetrics.ts +++ b/src/types/TokenMetrics.ts @@ -6,7 +6,7 @@ export interface TokenUsage { } export interface TranscriptLine { - message?: { usage?: TokenUsage }; + message?: { usage?: TokenUsage; model?: string }; isSidechain?: boolean; timestamp?: string; isApiErrorMessage?: boolean; @@ -19,4 +19,5 @@ export interface TokenMetrics { cachedTokens: number; totalTokens: number; contextLength: number; + model: string | null; } \ No newline at end of file diff --git a/src/utils/__tests__/context-percentage.test.ts b/src/utils/__tests__/context-percentage.test.ts index 8f85c78..f789698 100644 --- a/src/utils/__tests__/context-percentage.test.ts +++ b/src/utils/__tests__/context-percentage.test.ts @@ -23,7 +23,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 100000 + contextLength: 100000, + model: null } }; @@ -61,7 +62,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -79,7 +81,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -95,7 +98,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 2000000 + contextLength: 2000000, + model: null } }; @@ -111,7 +115,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -127,7 +132,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -148,7 +154,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -166,7 +173,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; @@ -188,7 +196,8 @@ describe('calculateContextPercentage', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; diff --git a/src/utils/__tests__/jsonl-metrics.test.ts b/src/utils/__tests__/jsonl-metrics.test.ts index 92d9f63..84702f9 100644 --- a/src/utils/__tests__/jsonl-metrics.test.ts +++ b/src/utils/__tests__/jsonl-metrics.test.ts @@ -46,6 +46,7 @@ function makeTranscriptLine(params: { output?: number; isSidechain?: boolean; isApiErrorMessage?: boolean; + model?: string; }): string { return JSON.stringify({ timestamp: params.timestamp, @@ -57,7 +58,8 @@ function makeTranscriptLine(params: { usage: { input_tokens: params.input ?? 0, output_tokens: params.output ?? 0 - } + }, + model: params.model } : undefined }); @@ -155,7 +157,8 @@ describe('jsonl transcript metrics', () => { outputTokens: 141, cachedTokens: 92, totalTokens: 2032, - contextLength: 250 + contextLength: 250, + model: null }); }); @@ -166,10 +169,80 @@ describe('jsonl transcript metrics', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 0 + contextLength: 0, + model: null }); }); + it('returns the model from the most recent assistant entry', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-jsonl-metrics-')); + tempRoots.push(root); + const transcriptPath = path.join(root, 'model.jsonl'); + fs.writeFileSync(transcriptPath, [ + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:00.000Z', + type: 'user' + }), + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:05.000Z', + type: 'assistant', + input: 100, + output: 50, + model: 'claude-sonnet-4-5-20250929' + }) + ].join('\n')); + + const metrics = await getTokenMetrics(transcriptPath); + expect(metrics.model).toBe('claude-sonnet-4-5-20250929'); + }); + + it('returns null model when the last entry is a user message (API in progress)', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-jsonl-metrics-')); + tempRoots.push(root); + const transcriptPath = path.join(root, 'model-pending.jsonl'); + fs.writeFileSync(transcriptPath, [ + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:00.000Z', + type: 'user' + }), + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:05.000Z', + type: 'assistant', + input: 100, + output: 50, + model: 'claude-sonnet-4-5-20250929' + }), + makeTranscriptLine({ + timestamp: '2026-01-01T10:01:00.000Z', + type: 'user' + }) + ].join('\n')); + + const metrics = await getTokenMetrics(transcriptPath); + expect(metrics.model).toBeNull(); + }); + + it('returns null model when transcript has no model field', async () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-jsonl-metrics-')); + tempRoots.push(root); + const transcriptPath = path.join(root, 'no-model.jsonl'); + fs.writeFileSync(transcriptPath, [ + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:00.000Z', + type: 'user' + }), + makeTranscriptLine({ + timestamp: '2026-01-01T10:00:05.000Z', + type: 'assistant', + input: 100, + output: 50 + }) + ].join('\n')); + + const metrics = await getTokenMetrics(transcriptPath); + expect(metrics.model).toBeNull(); + }); + it('calculates speed metrics from user-to-assistant processing windows', async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-jsonl-speed-')); tempRoots.push(root); diff --git a/src/utils/jsonl-metrics.ts b/src/utils/jsonl-metrics.ts index aacdefb..cca96b0 100644 --- a/src/utils/jsonl-metrics.ts +++ b/src/utils/jsonl-metrics.ts @@ -152,7 +152,7 @@ export async function getTokenMetrics(transcriptPath: string): Promise= 0; i--) { + const line = lines[i]; + if (!line) { + continue; + } + const last = parseJsonlLine(line) as TranscriptLine | null; + if (last?.type === 'assistant' || last?.type === 'user') { + lastEntryIsUser = last.type === 'user'; + break; + } + } + if (!lastEntryIsUser) { + model = mostRecentMainChainEntry.message.model; + } + } + const totalTokens = inputTokens + outputTokens + cachedTokens; - return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength }; + return { inputTokens, outputTokens, cachedTokens, totalTokens, contextLength, model }; } catch { - return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0 }; + return { inputTokens: 0, outputTokens: 0, cachedTokens: 0, totalTokens: 0, contextLength: 0, model: null }; } } diff --git a/src/widgets/__tests__/ContextBar.test.ts b/src/widgets/__tests__/ContextBar.test.ts index 7f277f0..045b175 100644 --- a/src/widgets/__tests__/ContextBar.test.ts +++ b/src/widgets/__tests__/ContextBar.test.ts @@ -49,7 +49,8 @@ describe('ContextBarWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 50000 + contextLength: 50000, + model: null } }; const widget = new ContextBarWidget(); @@ -65,7 +66,8 @@ describe('ContextBarWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 50000 + contextLength: 50000, + model: null } }; const widget = new ContextBarWidget(); @@ -81,7 +83,8 @@ describe('ContextBarWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 50000 + contextLength: 50000, + model: null } }; const widget = new ContextBarWidget(); diff --git a/src/widgets/__tests__/ContextPercentage.test.ts b/src/widgets/__tests__/ContextPercentage.test.ts index beba7d8..2a9444a 100644 --- a/src/widgets/__tests__/ContextPercentage.test.ts +++ b/src/widgets/__tests__/ContextPercentage.test.ts @@ -20,7 +20,8 @@ function render(modelId: string | undefined, contextLength: number, rawValue = f outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength + contextLength, + model: null } }; const item: WidgetItem = { @@ -72,7 +73,8 @@ describe('ContextPercentageWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 100000 + contextLength: 100000, + model: null } }; diff --git a/src/widgets/__tests__/ContextPercentageUsable.test.ts b/src/widgets/__tests__/ContextPercentageUsable.test.ts index 98945af..ed424fe 100644 --- a/src/widgets/__tests__/ContextPercentageUsable.test.ts +++ b/src/widgets/__tests__/ContextPercentageUsable.test.ts @@ -20,7 +20,8 @@ function render(modelId: string | undefined, contextLength: number, rawValue = f outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength + contextLength, + model: null } }; const item: WidgetItem = { @@ -76,7 +77,8 @@ describe('ContextPercentageUsableWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 200000 + contextLength: 200000, + model: null } }; @@ -99,7 +101,8 @@ describe('ContextPercentageUsableWidget', () => { outputTokens: 0, cachedTokens: 0, totalTokens: 0, - contextLength: 42000 + contextLength: 42000, + model: null } }; diff --git a/src/widgets/__tests__/TokensWidgets.test.ts b/src/widgets/__tests__/TokensWidgets.test.ts index 29b9fb5..b7fe7c3 100644 --- a/src/widgets/__tests__/TokensWidgets.test.ts +++ b/src/widgets/__tests__/TokensWidgets.test.ts @@ -57,7 +57,8 @@ describe('Token widgets', () => { outputTokens: 9999, cachedTokens: 9999, totalTokens: 9999, - contextLength: 9999 + contextLength: 9999, + model: null } }; @@ -75,7 +76,8 @@ describe('Token widgets', () => { outputTokens: 3400, cachedTokens: 560, totalTokens: 5160, - contextLength: 0 + contextLength: 0, + model: null } }; @@ -105,7 +107,8 @@ describe('Token widgets', () => { outputTokens: 3400, cachedTokens: 560, totalTokens: 5160, - contextLength: 20000 + contextLength: 20000, + model: null } };