From 4bba75f7655dc9826465cde65e28846dc1e1df6d Mon Sep 17 00:00:00 2001
From: JerryLee <223425819+Jerry2003826@users.noreply.github.com>
Date: Mon, 11 May 2026 22:35:55 +1000
Subject: [PATCH] fix(cli): keep long model stats header on one line (#4032)
* fix(cli): keep long model stats header on one line
* test(cli): cover fixed model stats columns
---
.../ui/components/ModelStatsDisplay.test.tsx | 85 ++++++++++++++++++-
.../src/ui/components/ModelStatsDisplay.tsx | 47 ++++++++--
2 files changed, 126 insertions(+), 6 deletions(-)
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
index 51dd755f2..73f7c1a15 100644
--- a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
@@ -39,6 +39,7 @@ const renderWithMockedStats = (
string,
{ inputPerMillionTokens?: number; outputPerMillionTokens?: number }
>,
+ width?: number,
) => {
useSessionStatsMock.mockReturnValue({
stats: {
@@ -60,7 +61,7 @@ const renderWithMockedStats = (
return render(
-
+
,
);
};
@@ -275,6 +276,88 @@ describe('', () => {
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,
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
index 563630fe5..953f91df9 100644
--- a/packages/cli/src/ui/components/ModelStatsDisplay.tsx
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.tsx
@@ -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;
+ modelColWidth: number;
isSubtle?: boolean;
isSection?: boolean;
}
@@ -37,6 +42,7 @@ interface StatRowProps {
const StatRow: React.FC = ({
title,
values,
+ modelColWidth,
isSubtle = false,
isSection = false,
}) => (
@@ -50,7 +56,7 @@ const StatRow: React.FC = ({
{values.map((value, index) => (
-
+
{value}
))}
@@ -94,6 +100,13 @@ export const ModelStatsDisplay: React.FC = ({
({ 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 = ({
{entries.map(({ key, label }) => (
-
+
{label}
@@ -147,10 +160,16 @@ export const ModelStatsDisplay: React.FC = ({
/>
{/* API Section */}
-
+
m.api.totalRequests.toLocaleString())}
+ modelColWidth={modelColWidth}
/>
= ({
);
})}
+ modelColWidth={modelColWidth}
/>
= ({
const avgLatency = calculateAverageLatency(m);
return formatDuration(avgLatency);
})}
+ modelColWidth={modelColWidth}
/>
{/* Tokens Section */}
-
+
(
@@ -186,11 +212,13 @@ export const ModelStatsDisplay: React.FC = ({
{m.tokens.total.toLocaleString()}
))}
+ modelColWidth={modelColWidth}
/>
m.tokens.prompt.toLocaleString())}
+ modelColWidth={modelColWidth}
/>
{hasCached && (
= ({
);
})}
+ modelColWidth={modelColWidth}
/>
)}
{hasThoughts && (
@@ -211,17 +240,24 @@ export const ModelStatsDisplay: React.FC = ({
title={t('Thoughts')}
isSubtle
values={getModelValues((m) => m.tokens.thoughts.toLocaleString())}
+ modelColWidth={modelColWidth}
/>
)}
m.tokens.candidates.toLocaleString())}
+ modelColWidth={modelColWidth}
/>
{hasPricing && (
<>
-
+
{
@@ -233,6 +269,7 @@ export const ModelStatsDisplay: React.FC = ({
});
return cost != null ? `$${cost.toFixed(4)}` : 'N/A';
})}
+ modelColWidth={modelColWidth}
/>
>
)}