Skip to content
26 changes: 15 additions & 11 deletions webview-ui/src/__tests__/ContextWindowProgress.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,22 @@ describe("ContextWindowProgress", () => {
it("calculates percentages correctly", () => {
renderComponent({ contextTokens: 1000, contextWindow: 4000 })

// Instead of checking the title attribute, verify the data-test-id
// which identifies the element containing info about the percentage of tokens used
const tokenUsageDiv = screen.getByTestId("context-tokens-used")
expect(tokenUsageDiv).toBeInTheDocument()
// Verify that the token count and window size are displayed correctly
const tokenCount = screen.getByTestId("context-tokens-count")
const windowSize = screen.getByTestId("context-window-size")

// Just verify that the element has a title attribute (the actual text is translated and may vary)
expect(tokenUsageDiv).toHaveAttribute("title")
expect(tokenCount).toBeInTheDocument()
expect(tokenCount).toHaveTextContent("1000")

// We can't reliably test computed styles in JSDOM, so we'll just check
// that the component appears to be working correctly by checking for expected elements
// The context-window-label is not part of the ContextWindowProgress component
expect(screen.getByTestId("context-tokens-count")).toBeInTheDocument()
expect(screen.getByTestId("context-tokens-count")).toHaveTextContent("1000")
expect(windowSize).toBeInTheDocument()
expect(windowSize).toHaveTextContent("4000")

// The progress bar is now wrapped in tooltips, but we can verify the structure exists
// by checking for the progress bar container
const progressBarContainer = screen.getByTestId("context-tokens-count").parentElement
expect(progressBarContainer).toBeInTheDocument()

// Verify the flex container has the expected structure
expect(progressBarContainer?.querySelector(".flex-1.relative")).toBeInTheDocument()
})
})
39 changes: 21 additions & 18 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
SearchResult,
} from "@src/utils/context-mentions"
import { convertToMentionPath } from "@/utils/path-mentions"
import { SelectDropdown, DropdownOptionType, Button } from "@/components/ui"
import { SelectDropdown, DropdownOptionType, Button, StandardTooltip } from "@/components/ui"

import Thumbnails from "../common/Thumbnails"
import ModeSelector from "./ModeSelector"
Expand Down Expand Up @@ -1094,8 +1094,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<div
className={cn("truncate min-w-0 overflow-hidden", {
"font-medium": isCurrentConfig,
})}
title={label}>
})}>
{label}
</div>
<div className="flex justify-end w-10 flex-shrink-0">
Expand All @@ -1106,21 +1105,25 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
})}>
<Check className="size-3" />
</div>
<Button
variant="ghost"
size="icon"
title={pinned ? t("chat:unpin") : t("chat:pin")}
onClick={(e) => {
e.stopPropagation()
togglePinnedApiConfig(value)
vscode.postMessage({ type: "toggleApiConfigPin", text: value })
}}
className={cn("size-5", {
"hidden group-hover:flex": !pinned,
"bg-accent": pinned,
})}>
<Pin className="size-3 p-0.5 opacity-50" />
</Button>
<StandardTooltip content={pinned ? t("chat:unpin") : t("chat:pin")}>
<Button
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
togglePinnedApiConfig(value)
vscode.postMessage({
type: "toggleApiConfigPin",
text: value,
})
}}
className={cn("size-5", {
"hidden group-hover:flex": !pinned,
"bg-accent": pinned,
})}>
<Pin className="size-3 p-0.5 opacity-50" />
</Button>
</StandardTooltip>
</div>
</div>
)
Expand Down
66 changes: 36 additions & 30 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import RooHero from "@src/components/welcome/RooHero"
import RooTips from "@src/components/welcome/RooTips"
import { StandardTooltip } from "@src/components/ui"

import TelemetryBanner from "../common/TelemetryBanner"
import { useTaskSearch } from "../history/useTaskSearch"
Expand Down Expand Up @@ -730,7 +731,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
},
50,
[isHidden, sendingDisabled, enableButtons]
[isHidden, sendingDisabled, enableButtons],
)

const visibleMessages = useMemo(() => {
Expand Down Expand Up @@ -1095,8 +1096,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro

useEffect(() => {
return () => {
if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === 'function') {
(scrollToBottomSmooth as any).cancel()
if (scrollToBottomSmooth && typeof (scrollToBottomSmooth as any).cancel === "function") {
;(scrollToBottomSmooth as any).cancel()
}
}
}, [scrollToBottomSmooth])
Expand Down Expand Up @@ -1477,15 +1478,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
<AutoApproveMenu />
{showScrollToBottom ? (
<div className="flex px-[15px] pt-[10px]">
<div
className="bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_55%,_transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_90%,_transparent)] active:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_70%,_transparent)]"
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}
title={t("chat:scrollToBottom")}>
<span className="codicon codicon-chevron-down text-[18px]"></span>
</div>
<StandardTooltip content={t("chat:scrollToBottom")}>
<div
className="bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_55%,_transparent)] rounded-[3px] overflow-hidden cursor-pointer flex justify-center items-center flex-1 h-[25px] hover:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_90%,_transparent)] active:bg-[color-mix(in_srgb,_var(--vscode-toolbar-hoverBackground)_70%,_transparent)]"
onClick={() => {
scrollToBottomSmooth()
disableAutoScrollRef.current = false
}}>
<span className="codicon codicon-chevron-down text-[18px]"></span>
</div>
</StandardTooltip>
</div>
) : (
<div
Expand All @@ -1499,11 +1501,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
: "opacity-0"
}`}>
{primaryButtonText && !isStreaming && (
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
title={
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
? t("chat:retry.tooltip")
: primaryButtonText === t("chat:save.title")
Expand All @@ -1522,17 +1521,19 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
t("chat:proceedWhileRunning.title")
? t("chat:proceedWhileRunning.tooltip")
: undefined
}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
}>
<VSCodeButton
appearance="primary"
disabled={!enableButtons}
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
{primaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
{(secondaryButtonText || isStreaming) && (
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
title={
<StandardTooltip
content={
isStreaming
? t("chat:cancel.tooltip")
: secondaryButtonText === t("chat:startNewTask.title")
Expand All @@ -1542,10 +1543,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
: secondaryButtonText === t("chat:terminate.title")
? t("chat:terminate.tooltip")
: undefined
}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
</VSCodeButton>
}>
<VSCodeButton
appearance="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
</VSCodeButton>
</StandardTooltip>
)}
</div>
)}
Expand Down
26 changes: 14 additions & 12 deletions webview-ui/src/components/chat/CodebaseSearchResult.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from "react"
import { vscode } from "@src/utils/vscode"
import { StandardTooltip } from "@/components/ui"

interface CodebaseSearchResultProps {
filePath: string
Expand All @@ -23,19 +24,20 @@ const CodebaseSearchResult: React.FC<CodebaseSearchResultProps> = ({ filePath, s
}

return (
<div
onClick={handleClick}
className="mb-1 p-2 border border-primary rounded cursor-pointer hover:bg-secondary hover:text-white"
title={`Score: ${score.toFixed(2)}`}>
<div className="flex gap-2 items-center overflow-hidden">
<span className="text-primary-300 whitespace-nowrap flex-shrink-0">
{filePath.split("/").at(-1)}:{startLine}-{endLine}
</span>
<span className="text-gray-500 truncate min-w-0 flex-1">
{filePath.split("/").slice(0, -1).join("/")}
</span>
<StandardTooltip content={`Score: ${score.toFixed(2)}`}>
<div
onClick={handleClick}
className="mb-1 p-2 border border-primary rounded cursor-pointer hover:bg-secondary hover:text-white">
<div className="flex gap-2 items-center overflow-hidden">
<span className="text-primary-300 whitespace-nowrap flex-shrink-0">
{filePath.split("/").at(-1)}:{startLine}-{endLine}
</span>
<span className="text-gray-500 truncate min-w-0 flex-1">
{filePath.split("/").slice(0, -1).join("/")}
</span>
</div>
</div>
</div>
</StandardTooltip>
)
}

Expand Down
88 changes: 44 additions & 44 deletions webview-ui/src/components/chat/ContextWindowProgress.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"

import { formatLargeNumber } from "@/utils/format"
import { calculateTokenDistribution } from "@/utils/model-utils"
import { StandardTooltip } from "@/components/ui"

interface ContextWindowProgressProps {
contextWindow: number
Expand Down Expand Up @@ -30,60 +31,59 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
<>
<div className="flex items-center gap-2 flex-1 whitespace-nowrap px-2">
<div data-testid="context-tokens-count">{formatLargeNumber(safeContextTokens)}</div>
<div className="flex-1 relative">
{/* Invisible overlay for hover area */}
<div
className="absolute w-full h-4 -top-[7px] z-5"
title={t("chat:tokenProgress.availableSpace", { amount: formatLargeNumber(availableSize) })}
data-testid="context-available-space"
/>

{/* Main progress bar container */}
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
{/* Current tokens container */}
<div className="relative h-full" style={{ width: `${currentPercent}%` }}>
{/* Invisible overlay for current tokens section */}
<div
className="absolute h-4 -top-[7px] w-full z-6"
title={t("chat:tokenProgress.tokensUsed", {
<StandardTooltip
content={t("chat:tokenProgress.availableSpace", { amount: formatLargeNumber(availableSize) })}
side="top"
sideOffset={8}>
<div className="flex-1 relative">
{/* Main progress bar container */}
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
{/* Current tokens container */}
<StandardTooltip
content={t("chat:tokenProgress.tokensUsed", {
used: formatLargeNumber(safeContextTokens),
total: formatLargeNumber(safeContextWindow),
})}
data-testid="context-tokens-used"
/>
{/* Current tokens used - darkest */}
<div className="h-full w-full bg-[var(--vscode-foreground)] transition-width duration-300 ease-out" />
</div>
side="top"
sideOffset={8}
asChild={false}>
<div className="relative h-full" style={{ width: `${currentPercent}%` }}>
{/* Current tokens used - darkest */}
<div className="h-full w-full bg-[var(--vscode-foreground)] transition-width duration-300 ease-out" />
</div>
</StandardTooltip>

{/* Container for reserved tokens */}
<div className="relative h-full" style={{ width: `${reservedPercent}%` }}>
{/* Invisible overlay for reserved section */}
<div
className="absolute h-4 -top-[7px] w-full z-6"
title={t("chat:tokenProgress.reservedForResponse", {
{/* Container for reserved tokens */}
<StandardTooltip
content={t("chat:tokenProgress.reservedForResponse", {
amount: formatLargeNumber(reservedForOutput),
})}
data-testid="context-reserved-tokens"
/>
{/* Reserved for output section - medium gray */}
<div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
</div>
side="top"
sideOffset={8}
asChild={false}>
<div className="relative h-full" style={{ width: `${reservedPercent}%` }}>
{/* Reserved for output section - medium gray */}
<div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
</div>
</StandardTooltip>

{/* Empty section (if any) */}
{availablePercent > 0 && (
<div className="relative h-full" style={{ width: `${availablePercent}%` }}>
{/* Invisible overlay for available space */}
<div
className="absolute h-4 -top-[7px] w-full z-6"
title={t("chat:tokenProgress.availableSpace", {
{/* Empty section (if any) */}
{availablePercent > 0 && (
<StandardTooltip
content={t("chat:tokenProgress.availableSpace", {
amount: formatLargeNumber(availableSize),
})}
data-testid="context-available-space-section"
/>
</div>
)}
side="top"
sideOffset={8}
asChild={false}>
<div className="relative h-full" style={{ width: `${availablePercent}%` }}>
{/* Available space - transparent */}
</div>
</StandardTooltip>
)}
</div>
</div>
</div>
</StandardTooltip>
<div data-testid="context-window-size">{formatLargeNumber(safeContextWindow)}</div>
</div>
</>
Expand Down
Loading
Loading