From fe4dd91c464b6f9e6c8494daf6efebcbc3399b8c Mon Sep 17 00:00:00 2001 From: Lia Date: Mon, 12 Oct 2026 13:30:34 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=97=9E=EF=B8=8F=20feat:=20Make=20Cont?= =?UTF-8?q?ext=20Usage=20Responsive=20and=20Condensed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Chat/Input/TokenUsage/Breakdown.spec.tsx | 47 +- .../Chat/Input/TokenUsage/Breakdown.tsx | 588 ++++++++---------- .../Chat/Input/TokenUsage/Usage.spec.tsx | 112 ++++ .../Chat/Input/TokenUsage/Usage.tsx | 126 ++++ .../Chat/Input/TokenUsage/index.spec.tsx | 26 + .../Chat/Input/TokenUsage/index.tsx | 15 +- 6 files changed, 555 insertions(+), 359 deletions(-) create mode 100644 client/src/components/Chat/Input/TokenUsage/Usage.spec.tsx create mode 100644 client/src/components/Chat/Input/TokenUsage/Usage.tsx diff --git a/client/src/components/Chat/Input/TokenUsage/Breakdown.spec.tsx b/client/src/components/Chat/Input/TokenUsage/Breakdown.spec.tsx index b89c15a5a0e..eff714fb353 100644 --- a/client/src/components/Chat/Input/TokenUsage/Breakdown.spec.tsx +++ b/client/src/components/Chat/Input/TokenUsage/Breakdown.spec.tsx @@ -556,7 +556,7 @@ describe('TokenUsage Breakdown', () => { const totals = within(screen.getByTestId('token-usage-totals')); expect( - totals.getByText('com_ui_context_subagents_all').parentElement?.nextElementSibling, + totals.getByText('com_ui_context_subagents_all').nextElementSibling, ).toHaveTextContent('1K'); }); }); @@ -594,11 +594,11 @@ describe('TokenUsage Breakdown', () => { expect(screen.queryByTestId('token-usage-totals')).not.toBeInTheDocument(); expect(screen.getByTestId('token-usage-all-branches')).toHaveTextContent('800'); if (showCost) { - expect(screen.getByTestId('token-usage-cost')).toHaveTextContent( - 'com_ui_context_cost_total', - ); + expect( + within(screen.getByTestId('token-usage-all-branches')).getByTestId('usage-cost'), + ).toHaveTextContent('com_ui_context_cost_total'); } else { - expect(screen.queryByTestId('token-usage-cost')).not.toBeInTheDocument(); + expect(screen.queryByTestId('usage-cost')).not.toBeInTheDocument(); } }, ); @@ -655,10 +655,9 @@ describe('TokenUsage Breakdown', () => { expect(all.getByText('com_ui_cache_read').parentElement?.parentElement).toHaveTextContent( '900', ); - const cost = within(screen.getByTestId('token-usage-cost')); - expect(cost.getByText('com_ui_context_cost_last_turn')).toBeInTheDocument(); - expect(cost.getByText('com_ui_context_cost_branch')).toBeInTheDocument(); - expect(cost.getByText('com_ui_context_cost_total')).toBeInTheDocument(); + expect(last.getByTestId('usage-cost')).toHaveTextContent('com_ui_context_cost_last_turn'); + expect(totals.getByTestId('usage-cost')).toHaveTextContent('com_ui_context_cost_branch'); + expect(all.getByTestId('usage-cost')).toHaveTextContent('com_ui_context_cost_total'); }); it('exposes all-branches token differences without requiring cost coverage', async () => { @@ -680,12 +679,12 @@ describe('TokenUsage Breakdown', () => { }); await userEvent.click(toggle()); expect(screen.getByTestId('token-usage-all-branches')).toHaveTextContent('25'); - expect(screen.getByTestId('token-usage-cost')).toHaveTextContent( - 'com_ui_context_cost_last_turn', - ); - expect(screen.getByTestId('token-usage-cost')).not.toHaveTextContent( - 'com_ui_context_cost_branch', - ); + expect( + within(screen.getByTestId('token-usage-last-turn')).getByTestId('usage-cost'), + ).toHaveTextContent('com_ui_context_cost_last_turn'); + expect( + within(screen.getByTestId('token-usage-totals')).queryByTestId('usage-cost'), + ).not.toBeInTheDocument(); }); it('shows waiting and unavailable states without presenting old totals as the current turn', async () => { @@ -728,9 +727,9 @@ describe('TokenUsage Breakdown', () => { expect(screen.getByTestId('token-usage-last-turn')).toHaveTextContent( 'com_ui_context_current_turn', ); - expect(screen.getByTestId('token-usage-cost')).toHaveTextContent( - 'com_ui_context_cost_current_turn', - ); + expect( + within(screen.getByTestId('token-usage-last-turn')).getByTestId('usage-cost'), + ).toHaveTextContent('com_ui_context_cost_current_turn'); }); it('keeps a known zero cost and hides a missing cost', async () => { @@ -753,12 +752,12 @@ describe('TokenUsage Breakdown', () => { expect(screen.getByTestId('token-usage-last-turn')).toHaveTextContent( 'com_ui_context_last_turn', ); - expect(screen.getByTestId('token-usage-cost')).toHaveTextContent( - 'com_ui_context_cost_last_turn', - ); - expect(screen.getByTestId('token-usage-cost')).not.toHaveTextContent( - 'com_ui_context_cost_branch', - ); + expect( + within(screen.getByTestId('token-usage-last-turn')).getByTestId('usage-cost'), + ).toHaveTextContent('com_ui_context_cost_last_turn'); + expect( + within(screen.getByTestId('token-usage-totals')).queryByTestId('usage-cost'), + ).not.toBeInTheDocument(); }); }); diff --git a/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx b/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx index 540f1c95d30..2509da18fc9 100644 --- a/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx +++ b/client/src/components/Chat/Input/TokenUsage/Breakdown.tsx @@ -16,9 +16,10 @@ import { import type { MeterSegment } from '@librechat/client'; import type { TokenUsageView } from '~/hooks/Chat/useTokenUsage'; import type { CurrencyConfig } from '~/utils'; -import { groupToolTokens, formatTokens, formatCost, normalizeTokenCount, cn } from '~/utils'; +import { groupToolTokens, formatTokens, normalizeTokenCount, cn } from '~/utils'; import { contextBreakdownExpandedAtom } from './store'; import { useLocalize } from '~/hooks'; +import Usage from './Usage'; interface RowProps { label: string; @@ -351,7 +352,7 @@ export default function Breakdown({ hoveredSegment != null && hoverIsValid(hoveredSegment) ? hoveredSegment : null; return ( -
+
-
- {(pressure !== 'none' || insights.length > 0) && ( -
- {pressure !== 'none' ? ( -

-

- ) : ( - - )} - {insights.length > 0 && ( - - - - - - - {insights.map((insight, index) => ( -

- {insight} -

- ))} -
-
-
- )} -
- )} -
- {breakdown ? ( - <> - {segments.map(({ id, label, value, ...segment }) => { - const shouldRender = - value > 0 || (id === 'tool-calls' && toolCallTokens != null); - if (!shouldRender) { - return null; - } - return ( - - {id === 'tool-calls' && hasToolCounts ? ( +
+
+ {(pressure !== 'none' || insights.length > 0) && ( +
+ {pressure !== 'none' ? ( +

+

+ ) : ( + + )} + {insights.length > 0 && ( + + + + + + + {insights.map((insight, index) => ( +

+ {insight} +

+ ))} +
+
+
+ )} +
+ )} +
+ {breakdown ? ( + <> + {segments.map(({ id, label, value, ...segment }) => { + const shouldRender = + value > 0 || (id === 'tool-calls' && toolCallTokens != null); + if (!shouldRender) { + return null; + } + return ( + + {id === 'tool-calls' && hasToolCounts ? ( + setToolsExpanded((open) => !open)} + expanded={toolsExpanded} + controls={toolsExpanded ? toolBreakdownId : undefined} + /> + ) : ( + + )} + {id === 'tool-calls' && toolsExpanded && hasToolCounts && ( +
+

+ {localize('com_ui_context_tool_breakdown')} +

+ {toolBreakdown.map((tool) => ( + + ))} +
+ )} + {id === 'system' && dynamicInstructionTokens > 0 && ( +
+ +
+ )} +
+ ); + })} + {/** The reconciling call's cached prompt share is already inside + * the segments above (reconciliation counted it in `usedTokens`), + * so it renders indented, like the estimate path's tool-call + * share — as peer rows a fully cached prompt would show its + * tokens twice and the visible rows would sum past the meter. */} + {(normalizeTokenCount(view.cacheRead) > 0 || + normalizeTokenCount(view.cacheWrite) > 0) && ( +
+

+ {localize('com_ui_context_cache_last_call')} +

+ {normalizeTokenCount(view.cacheRead) > 0 && ( setToolsExpanded((open) => !open)} - expanded={toolsExpanded} - controls={toolsExpanded ? toolBreakdownId : undefined} /> - ) : ( + )} + {normalizeTokenCount(view.cacheWrite) > 0 && ( )} - {id === 'tool-calls' && toolsExpanded && hasToolCounts && ( -
-

- {localize('com_ui_context_tool_breakdown')} -

- {toolBreakdown.map((tool) => ( - - ))} -
- )} - {id === 'system' && dynamicInstructionTokens > 0 && ( -
- -
- )} - - ); - })} - {/** The reconciling call's cached prompt share is already inside - * the segments above (reconciliation counted it in `usedTokens`), - * so it renders indented, like the estimate path's tool-call - * share — as peer rows a fully cached prompt would show its - * tokens twice and the visible rows would sum past the meter. */} - {(normalizeTokenCount(view.cacheRead) > 0 || - normalizeTokenCount(view.cacheWrite) > 0) && ( -
-

- {localize('com_ui_context_cache_last_call')} -

- {normalizeTokenCount(view.cacheRead) > 0 && ( - - )} - {normalizeTokenCount(view.cacheWrite) > 0 && ( - - )} -
- )} - {freeTokens != null && ( - - )} - - ) : ( - <> - {normalizeTokenCount(view.branchTotals.summaryBaseline) > 0 && ( - - )} - {view.messagesPruned ? ( - /** Over-window: the per-category split no longer describes what's - * sent, so show the pruned message total (incl. in-flight). */ - - ) : ( - <> - +
+ )} + {freeTokens != null && ( - {normalizeTokenCount(view.estimatedTokens) > 0 && ( - - )} - - )} - {/** Retained tool traffic is already inside the message rows above - * (their clamped tool-content share), so it renders as an indented - * subtotal — as a peer row the visible rows would sum past - * `usedTokens`. The snapshot path instead subtracts it from - * Messages, which is exact there; here the counted Input/Output - * rows are provider counts and this share is a char estimate, so - * subtracting it would corrupt them. */} - {normalizeTokenCount(view.toolCallTokens) > 0 && ( -
+ )} + + ) : ( + <> + {normalizeTokenCount(view.branchTotals.summaryBaseline) > 0 && ( -
- )} - {view.overheadTokens > 0 && ( - - )} - {maxTokens == null && ( -

- {localize('com_ui_context_unknown')} + )} + {view.messagesPruned ? ( + /** Over-window: the per-category split no longer describes what's + * sent, so show the pruned message total (incl. in-flight). */ + + ) : ( + <> + + + {normalizeTokenCount(view.estimatedTokens) > 0 && ( + + )} + + )} + {/** Retained tool traffic is already inside the message rows above + * (their clamped tool-content share), so it renders as an indented + * subtotal — as a peer row the visible rows would sum past + * `usedTokens`. The snapshot path instead subtracts it from + * Messages, which is exact there; here the counted Input/Output + * rows are provider counts and this share is a char estimate, so + * subtracting it would corrupt them. */} + {normalizeTokenCount(view.toolCallTokens) > 0 && ( +

+ +
+ )} + {view.overheadTokens > 0 && ( + + )} + {maxTokens == null && ( +

+ {localize('com_ui_context_unknown')} +

+ )} +

+ {localize('com_ui_estimated')}

- )} -

- {localize('com_ui_estimated')} -

- - )} -
+ + )} +
-
-
-

- {view.turnInProgress - ? localize('com_ui_context_current_turn') - : localize('com_ui_context_last_turn')} -

- {view.lastTurnUsage != null ? ( - <> - - - - - - ) : ( -

- {view.turnInProgress - ? localize('com_ui_context_waiting_usage') - : localize('com_ui_context_unavailable_usage')} -

- )}
- - {hasUsage && ( - <> -
-
-
-

- {localize('com_ui_context_totals')} -

- - {localize('com_ui_context_this_branch')} - -
- - - {normalizeTokenCount(branchUsage.cacheRead) > 0 && ( - - )} - {normalizeTokenCount(branchUsage.cacheWrite) > 0 && ( - - )} - {/** Subagent calls are accumulated per conversation for the session - * and are not attributed to a response; label them all-branches. */} - {subagentTokens > 0 && ( - - )} -
- - )} - {showTotal && ( -
-

- {localize('com_ui_context_cost_total')} -

- - - {normalizeTokenCount(view.totalUsage.cacheRead) > 0 && ( - +
+ 0 && ( - + usage={view.lastTurnUsage} + unavailable={localize( + view.turnInProgress + ? 'com_ui_context_waiting_usage' + : 'com_ui_context_unavailable_usage', )} -
- )} - - {showCost && - (view.lastTurnUsage?.costKnown === true || - (hasUsage && branchUsage.costKnown) || - (showTotal && view.totalUsage.costKnown)) && ( - <> -
-
- {view.lastTurnUsage?.costKnown === true && ( -
- - {localize( - view.turnInProgress - ? 'com_ui_context_cost_current_turn' - : 'com_ui_context_cost_last_turn', - )} - - - {formatCost(view.lastTurnUsage.cost, currency)} - -
- )} - {hasUsage && branchUsage.costKnown && ( -
- - {showTotal - ? localize('com_ui_context_cost_branch') - : localize('com_ui_context_cost')} - - - {formatCost(view.branchCost, currency)} - -
- )} - {showTotal && view.totalUsage.costKnown && ( -
- - {localize('com_ui_context_cost_total')} - - - {formatCost(view.totalCost, currency)} - -
- )} -
- + cost={showCost && view.lastTurnUsage?.costKnown ? view.lastTurnUsage.cost : undefined} + costLabel={localize( + view.turnInProgress + ? 'com_ui_context_cost_current_turn' + : 'com_ui_context_cost_last_turn', + )} + currency={currency} + testId="token-usage-last-turn" + /> + {hasUsage && ( + + )} + {showTotal && ( + )} +
{langfuseSessionUrl && ( <> -
-