fix(cli): keep long model stats header on one line (#4032)
Some checks failed
Qwen Code CI / Classify PR (push) Waiting to run
Qwen Code CI / Lint (push) Blocked by required conditions
Qwen Code CI / Test (macos-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (ubuntu-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Test (windows-latest, Node 22.x) (push) Blocked by required conditions
Qwen Code CI / Post Coverage Comment (push) Blocked by required conditions
Qwen Code CI / CodeQL (push) Blocked by required conditions
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
SDK Python / Classify PR (push) Has been cancelled
SDK Python / SDK Python (3.10) (push) Has been cancelled
SDK Python / SDK Python (3.11) (push) Has been cancelled
SDK Python / SDK Python (3.12) (push) Has been cancelled

* fix(cli): keep long model stats header on one line

* test(cli): cover fixed model stats columns
This commit is contained in:
JerryLee 2026-05-11 22:35:55 +10:00 committed by GitHub
parent bdd5b602de
commit 4bba75f765
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 126 additions and 6 deletions

View file

@ -39,6 +39,7 @@ const renderWithMockedStats = (
string,
{ inputPerMillionTokens?: number; outputPerMillionTokens?: number }
>,
width?: number,
) => {
useSessionStatsMock.mockReturnValue({
stats: {
@ -60,7 +61,7 @@ const renderWithMockedStats = (
return render(
<SettingsContext.Provider value={mockSettings}>
<ModelStatsDisplay />
<ModelStatsDisplay width={width} />
</SettingsContext.Provider>,
);
};
@ -275,6 +276,88 @@ describe('<ModelStatsDisplay />', () => {
expect(output).toMatchSnapshot();
});
it('keeps a long single model name on the metric header line when space is available', () => {
const modelName = 'ggml-org/gemma-4-E4B-it-GGUF';
const { lastFrame } = renderWithMockedStats(
{
models: {
[modelName]: mainOnly({
api: { totalRequests: 2, totalErrors: 1, totalLatencyMs: 220000 },
tokens: {
prompt: 17953,
candidates: 225,
total: 18178,
cached: 0,
thoughts: 0,
},
}),
},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0, auto_accept: 0 },
byName: {},
},
files: { totalLinesAdded: 0, totalLinesRemoved: 0 },
},
undefined,
100,
);
expect(lastFrame()).toContain(`Metric ${modelName}`);
});
it('keeps fixed model column widths for multiple models even when space is available', () => {
const { lastFrame } = renderWithMockedStats(
{
models: {
'model-a': mainOnly({
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
tokens: {
prompt: 10,
candidates: 20,
total: 30,
cached: 0,
thoughts: 0,
},
}),
'model-b': mainOnly({
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
tokens: {
prompt: 10,
candidates: 20,
total: 30,
cached: 0,
thoughts: 0,
},
}),
},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: { accept: 0, reject: 0, modify: 0, auto_accept: 0 },
byName: {},
},
files: { totalLinesAdded: 0, totalLinesRemoved: 0 },
},
undefined,
120,
);
const headerLine = lastFrame()
?.split('\n')
.find((line) => line.includes('Metric') && line.includes('model-a'));
expect(headerLine).toBeDefined();
expect(
headerLine!.indexOf('model-b') - headerLine!.indexOf('model-a'),
).toBe(24);
});
describe('Subagent source attribution', () => {
const baseTools: SessionMetrics['tools'] = {
totalCalls: 0,

View file

@ -26,10 +26,15 @@ const METRIC_COL_WIDTH = 28;
// Sessions with three or more sources will exceed the panel — acceptable per
// the design doc, which accepts the crowded layout for many-subagent cases.
const MODEL_COL_WIDTH = 24;
// Keep this in sync with the surrounding Box borderStyle and paddingX:
// Ink's round border consumes 2 border columns plus 2 columns of horizontal
// padding on each side.
const PANEL_HORIZONTAL_CHROME_WIDTH = 6;
interface StatRowProps {
title: string;
values: Array<string | React.ReactElement>;
modelColWidth: number;
isSubtle?: boolean;
isSection?: boolean;
}
@ -37,6 +42,7 @@ interface StatRowProps {
const StatRow: React.FC<StatRowProps> = ({
title,
values,
modelColWidth,
isSubtle = false,
isSection = false,
}) => (
@ -50,7 +56,7 @@ const StatRow: React.FC<StatRowProps> = ({
</Text>
</Box>
{values.map((value, index) => (
<Box width={MODEL_COL_WIDTH} key={index}>
<Box width={modelColWidth} key={index}>
<Text color={theme.text.primary}>{value}</Text>
</Box>
))}
@ -94,6 +100,13 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
({ metrics }) => metrics.tokens.thoughts > 0,
);
const hasCached = entries.some(({ metrics }) => metrics.tokens.cached > 0);
const modelColWidth =
entries.length === 1 && width
? Math.max(
MODEL_COL_WIDTH,
width - PANEL_HORIZONTAL_CHROME_WIDTH - METRIC_COL_WIDTH,
)
: MODEL_COL_WIDTH;
const getModelName = (key: string): string => key.split('::')[0];
@ -128,7 +141,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
</Text>
</Box>
{entries.map(({ key, label }) => (
<Box width={MODEL_COL_WIDTH} key={key}>
<Box width={modelColWidth} key={key}>
<Text bold color={theme.text.primary}>
{label}
</Text>
@ -147,10 +160,16 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
/>
{/* API Section */}
<StatRow title={t('API')} values={[]} isSection />
<StatRow
title={t('API')}
values={[]}
modelColWidth={modelColWidth}
isSection
/>
<StatRow
title={t('Requests')}
values={getModelValues((m) => m.api.totalRequests.toLocaleString())}
modelColWidth={modelColWidth}
/>
<StatRow
title={t('Errors')}
@ -166,6 +185,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
</Text>
);
})}
modelColWidth={modelColWidth}
/>
<StatRow
title={t('Avg Latency')}
@ -173,12 +193,18 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
const avgLatency = calculateAverageLatency(m);
return formatDuration(avgLatency);
})}
modelColWidth={modelColWidth}
/>
<Box height={1} />
{/* Tokens Section */}
<StatRow title={t('Tokens')} values={[]} isSection />
<StatRow
title={t('Tokens')}
values={[]}
modelColWidth={modelColWidth}
isSection
/>
<StatRow
title={t('Total')}
values={getModelValues((m) => (
@ -186,11 +212,13 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
{m.tokens.total.toLocaleString()}
</Text>
))}
modelColWidth={modelColWidth}
/>
<StatRow
title={t('Prompt')}
isSubtle
values={getModelValues((m) => m.tokens.prompt.toLocaleString())}
modelColWidth={modelColWidth}
/>
{hasCached && (
<StatRow
@ -204,6 +232,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
</Text>
);
})}
modelColWidth={modelColWidth}
/>
)}
{hasThoughts && (
@ -211,17 +240,24 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
title={t('Thoughts')}
isSubtle
values={getModelValues((m) => m.tokens.thoughts.toLocaleString())}
modelColWidth={modelColWidth}
/>
)}
<StatRow
title={t('Output')}
isSubtle
values={getModelValues((m) => m.tokens.candidates.toLocaleString())}
modelColWidth={modelColWidth}
/>
{hasPricing && (
<>
<Box height={1} />
<StatRow title={t('Cost')} values={[]} isSection />
<StatRow
title={t('Cost')}
values={[]}
modelColWidth={modelColWidth}
isSection
/>
<StatRow
title={t('Estimated')}
values={entries.map(({ key, metrics }) => {
@ -233,6 +269,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
});
return cost != null ? `$${cost.toFixed(4)}` : 'N/A';
})}
modelColWidth={modelColWidth}
/>
</>
)}