From d56b3dac0119481b7f9e2a8808f18572b04d0dc7 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 28 Jan 2026 19:46:48 +0000 Subject: [PATCH 1/2] fix: calculate header percentage based on available input space The percentage in the collapsed task header was showing 45% when it should show ~19%. This was because the formula incorrectly calculated: (tokensUsed + reservedForOutput) / contextWindow * 100 Instead of the correct formula: tokensUsed / (contextWindow - reservedForOutput) * 100 The correct formula shows the percentage of available input space that is currently used. The reserved output tokens should not be counted towards the used percentage since they are reserved for the model response. Example with 50.4k tokens, 128k reserved, 400k context window: - Old (incorrect): (50.4k + 128k) / 400k = 44.6% - New (correct): 50.4k / (400k - 128k) = 18.5% Fixes EXT-675 --- webview-ui/src/components/chat/TaskHeader.tsx | 10 +++++--- .../chat/__tests__/TaskHeader.spec.tsx | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/chat/TaskHeader.tsx b/webview-ui/src/components/chat/TaskHeader.tsx index 8948302ce17..d5424b74221 100644 --- a/webview-ui/src/components/chat/TaskHeader.tsx +++ b/webview-ui/src/components/chat/TaskHeader.tsx @@ -282,9 +282,13 @@ const TaskHeader = ({ sideOffset={8}> {(() => { - const percentage = Math.round( - (((contextTokens || 0) + reservedForOutput) / contextWindow) * 100, - ) + // Calculate percentage of available input space used + // Available input space = context window - reserved for output + const availableInputSpace = contextWindow - reservedForOutput + const percentage = + availableInputSpace > 0 + ? Math.round(((contextTokens || 0) / availableInputSpace) * 100) + : 0 return ( <> diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 07aa5480aff..43a2e7525f9 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -402,4 +402,27 @@ describe("TaskHeader", () => { expect(backButton?.querySelector("svg.lucide-arrow-left")).toBeInTheDocument() }) }) + + describe("Context window percentage calculation", () => { + // The percentage should be calculated as: + // contextTokens / (contextWindow - reservedForOutput) * 100 + // This represents the percentage of AVAILABLE input space used, + // not the percentage of the total context window. + + it("should calculate percentage based on available input space, not total context window", () => { + // With the formula: contextTokens / (contextWindow - reservedForOutput) * 100 + // If contextTokens = 200, contextWindow = 1000, reservedForOutput = 200 + // Then available input space = 1000 - 200 = 800 + // Percentage = 200 / 800 * 100 = 25% + // + // Old (incorrect) formula would have been: (200 + 200) / 1000 * 100 = 40% + + renderTaskHeader({ contextTokens: 200 }) + + // Look for the percentage text + // The exact value depends on the mocked model info + // Since we don't have a detailed mock, we just verify the component renders + expect(screen.getByText("Test task")).toBeInTheDocument() + }) + }) }) From 51e062c2a6197edb9abf5c7de1680984e10c821c Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 28 Jan 2026 20:07:05 +0000 Subject: [PATCH 2/2] test: improve percentage calculation test with proper mocks and assertions --- .../chat/__tests__/TaskHeader.spec.tsx | 53 +++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx index 43a2e7525f9..c4ebe06973a 100644 --- a/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx @@ -91,6 +91,26 @@ vi.mock("@roo/array", () => ({ }, })) +// Create a variable to hold the mock model info for useSelectedModel +let mockModelInfo: { contextWindow: number; maxTokens: number } | undefined = undefined + +// Mock useSelectedModel hook +vi.mock("@/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: () => ({ + provider: "anthropic", + id: "test-model", + info: mockModelInfo, + isLoading: false, + isError: false, + }), +})) + +// Mock getModelMaxOutputTokens from @roo/api +let mockMaxOutputTokens = 0 +vi.mock("@roo/api", () => ({ + getModelMaxOutputTokens: () => mockMaxOutputTokens, +})) + describe("TaskHeader", () => { const defaultProps: TaskHeaderProps = { task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, @@ -409,6 +429,19 @@ describe("TaskHeader", () => { // This represents the percentage of AVAILABLE input space used, // not the percentage of the total context window. + beforeEach(() => { + // Set up mock model with known contextWindow + mockModelInfo = { contextWindow: 1000, maxTokens: 200 } + // Set up mock for getModelMaxOutputTokens to return reservedForOutput + mockMaxOutputTokens = 200 + }) + + afterEach(() => { + // Reset mocks + mockModelInfo = undefined + mockMaxOutputTokens = 0 + }) + it("should calculate percentage based on available input space, not total context window", () => { // With the formula: contextTokens / (contextWindow - reservedForOutput) * 100 // If contextTokens = 200, contextWindow = 1000, reservedForOutput = 200 @@ -419,10 +452,22 @@ describe("TaskHeader", () => { renderTaskHeader({ contextTokens: 200 }) - // Look for the percentage text - // The exact value depends on the mocked model info - // Since we don't have a detailed mock, we just verify the component renders - expect(screen.getByText("Test task")).toBeInTheDocument() + // The percentage should be rendered in the collapsed header state + // Verify that 25% is displayed (correct formula) and NOT 40% (old incorrect formula) + expect(screen.getByText("25%")).toBeInTheDocument() + expect(screen.queryByText("40%")).not.toBeInTheDocument() + }) + + it("should handle edge case when available input space is zero", () => { + // When contextWindow equals reservedForOutput, available space is 0 + // The percentage should be 0 to avoid division by zero + mockModelInfo = { contextWindow: 200, maxTokens: 200 } + mockMaxOutputTokens = 200 + + renderTaskHeader({ contextTokens: 100 }) + + // Should show 0% when available input space is 0 + expect(screen.getByText("0%")).toBeInTheDocument() }) }) })