Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,9 +282,13 @@ const TaskHeader = ({
sideOffset={8}>
<span className="flex items-center gap-1.5">
{(() => {
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 (
<>
<CircularProgress percentage={percentage} />
Expand Down
68 changes: 68 additions & 0 deletions webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [] },
Expand Down Expand Up @@ -402,4 +422,52 @@ 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.

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
// 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 })

// 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()
})
})
})
Loading