fix: standardize tooltip delays to 300ms (#5090) (#5098)

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-06-26 13:41:32 -06:00 committed by GitHub
parent c797c9a1d1
commit 3318366b66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 1242 additions and 798 deletions

View file

@ -20,6 +20,8 @@ import ModesView from "./components/modes/ModesView"
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
import { AccountView } from "./components/account/AccountView"
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
import { TooltipProvider } from "./components/ui/tooltip"
import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
@ -215,7 +217,9 @@ const AppWithProviders = () => (
<ExtensionStateContextProvider>
<TranslationProvider>
<QueryClientProvider client={queryClient}>
<App />
<TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
<App />
</TooltipProvider>
</QueryClientProvider>
</TranslationProvider>
</ExtensionStateContextProvider>

View file

@ -1,7 +1,7 @@
// npx vitest run src/__tests__/App.spec.tsx
import React from "react"
import { render, screen, act, cleanup } from "@testing-library/react"
import { render, screen, act, cleanup } from "@/utils/test-utils"
import AppWithProviders from "../App"

View file

@ -1,6 +1,6 @@
// npm run test ContextWindowProgress.spec.tsx
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import TaskHeader from "@src/components/chat/TaskHeader"
@ -102,18 +102,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()
})
})

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import { describe, it, expect, vi } from "vitest"
import { AccountView } from "../AccountView"

View file

@ -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"
@ -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">
@ -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>
)

View file

@ -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"
@ -730,7 +731,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
},
50,
[isHidden, sendingDisabled, enableButtons]
[isHidden, sendingDisabled, enableButtons],
)
const visibleMessages = useMemo(() => {
@ -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])
@ -1476,15 +1477,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
@ -1498,11 +1500,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")
@ -1521,17 +1520,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")
@ -1541,10 +1542,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>
)}

View file

@ -1,5 +1,6 @@
import React from "react"
import { vscode } from "@src/utils/vscode"
import { StandardTooltip } from "@/components/ui"
interface CodebaseSearchResultProps {
filePath: string
@ -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>
)
}

View file

@ -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
@ -26,64 +27,70 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
const safeContextWindow = Math.max(0, contextWindow)
const safeContextTokens = Math.max(0, contextTokens)
// Combine all tooltip content into a single tooltip
const tooltipContent = (
<div className="space-y-1">
<div>
{t("chat:tokenProgress.tokensUsed", {
used: formatLargeNumber(safeContextTokens),
total: formatLargeNumber(safeContextWindow),
})}
</div>
{reservedForOutput > 0 && (
<div>
{t("chat:tokenProgress.reservedForResponse", {
amount: formatLargeNumber(reservedForOutput),
})}
</div>
)}
{availableSize > 0 && (
<div>
{t("chat:tokenProgress.availableSpace", {
amount: formatLargeNumber(availableSize),
})}
</div>
)}
</div>
)
return (
<>
<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 */}
<StandardTooltip content={tooltipContent} 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 */}
<div
className="absolute h-4 -top-[7px] w-full z-6"
title={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>
{/* 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", {
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>
{/* 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", {
amount: formatLargeNumber(availableSize),
})}
data-testid="context-available-space-section"
/>
className="relative h-full"
style={{ width: `${currentPercent}%` }}
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>
)}
{/* Container for reserved tokens */}
<div
className="relative h-full"
style={{ width: `${reservedPercent}%` }}
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>
{/* Empty section (if any) */}
{availablePercent > 0 && (
<div
className="relative h-full"
style={{ width: `${availablePercent}%` }}
data-testid="context-available-space-section">
{/* Available space - transparent */}
</div>
)}
</div>
</div>
</div>
</StandardTooltip>
<div data-testid="context-window-size">{formatLargeNumber(safeContextWindow)}</div>
</div>
</>

View file

@ -1,7 +1,7 @@
import { useCallback } from "react"
import { Edit } from "lucide-react"
import { Button } from "@/components/ui"
import { Button, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@src/i18n/TranslationContext"
@ -36,18 +36,19 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
aria-label={suggestion}>
<div>{suggestion}</div>
</Button>
<div
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
// Simulate shift-click by directly calling the handler with shiftKey=true.
onSuggestionClick?.(suggestion, { ...e, shiftKey: true })
}}
title={t("chat:followUpSuggest.copyToInput")}>
<Button variant="ghost" size="icon">
<Edit />
</Button>
</div>
<StandardTooltip content={t("chat:followUpSuggest.copyToInput")}>
<div
className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
// Simulate shift-click by directly calling the handler with shiftKey=true.
onSuggestionClick?.(suggestion, { ...e, shiftKey: true })
}}>
<Button variant="ghost" size="icon">
<Edit />
</Button>
</div>
</StandardTooltip>
</div>
))}
</div>

View file

@ -1,4 +1,5 @@
import { cn } from "@/lib/utils"
import { StandardTooltip } from "@/components/ui"
interface IconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
iconClass: string
@ -35,10 +36,9 @@ export const IconButton: React.FC<IconButtonProps> = ({
const iconClasses = cn("codicon", iconClass, isLoading && "codicon-modifier-spin")
return (
const button = (
<button
aria-label={title}
title={title}
className={buttonClasses}
disabled={disabled}
onClick={!disabled ? onClick : undefined}
@ -47,4 +47,6 @@ export const IconButton: React.FC<IconButtonProps> = ({
<span className={iconClasses} />
</button>
)
return <StandardTooltip content={title}>{button}</StandardTooltip>
}

View file

@ -2,6 +2,7 @@ import { memo, useState } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { useCopyToClipboard } from "@src/utils/clipboard"
import { StandardTooltip } from "@src/components/ui"
import MarkdownBlock from "../common/MarkdownBlock"
@ -34,30 +35,31 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia
borderRadius: "4px",
}}>
<style>{`@keyframes fadeIn { from { opacity: 0; } to { opacity: 1.0; } }`}</style>
<VSCodeButton
className="copy-button"
appearance="icon"
style={{
height: "24px",
border: "none",
background: "var(--vscode-editor-background)",
transition: "background 0.2s ease-in-out",
}}
onClick={async () => {
const success = await copyWithFeedback(markdown)
if (success) {
const button = document.activeElement as HTMLElement
if (button) {
button.style.background = "var(--vscode-button-background)"
setTimeout(() => {
button.style.background = ""
}, 200)
<StandardTooltip content="Copy as markdown">
<VSCodeButton
className="copy-button"
appearance="icon"
style={{
height: "24px",
border: "none",
background: "var(--vscode-editor-background)",
transition: "background 0.2s ease-in-out",
}}
onClick={async () => {
const success = await copyWithFeedback(markdown)
if (success) {
const button = document.activeElement as HTMLElement
if (button) {
button.style.background = "var(--vscode-button-background)"
setTimeout(() => {
button.style.background = ""
}, 200)
}
}
}
}}
title="Copy as markdown">
<span className="codicon codicon-copy" />
</VSCodeButton>
}}>
<span className="codicon codicon-copy" />
</VSCodeButton>
</StandardTooltip>
</div>
)}
</div>

View file

@ -2,7 +2,7 @@ import React from "react"
import { ChevronUp, Check } from "lucide-react"
import { cn } from "@/lib/utils"
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import { IconButton } from "./IconButton"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
@ -61,6 +61,27 @@ export const ModeSelector = ({
// Find the selected mode
const selectedMode = React.useMemo(() => modes.find((mode) => mode.slug === value), [modes, value])
const trigger = (
<PopoverTrigger
disabled={disabled}
data-testid="mode-selector-trigger"
className={cn(
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
!disabled && !hasOpenedModeSelector
? "bg-primary opacity-90 hover:bg-primary-hover text-vscode-button-foreground"
: null,
)}>
<ChevronUp className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
<span className="truncate">{selectedMode?.name || ""}</span>
</PopoverTrigger>
)
return (
<Popover
open={open}
@ -69,25 +90,7 @@ export const ModeSelector = ({
setOpen(isOpen)
}}
data-testid="mode-selector-root">
<PopoverTrigger
disabled={disabled}
title={title}
data-testid="mode-selector-trigger"
className={cn(
"inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
!disabled && !hasOpenedModeSelector
? "bg-primary opacity-90 hover:bg-primary-hover text-vscode-button-foreground"
: null,
)}>
<ChevronUp className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
<span className="truncate">{selectedMode?.name || ""}</span>
</PopoverTrigger>
{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
<PopoverContent
align="start"

View file

@ -20,6 +20,7 @@ import {
DialogContent,
DialogHeader,
DialogTitle,
StandardTooltip,
} from "@/components/ui"
interface ShareButtonProps {
@ -144,15 +145,16 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
{shareButtonState.showPopover ? (
<Popover open={shareDropdownOpen} onOpenChange={setShareDropdownOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
title={shareButtonState.title}
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
<StandardTooltip content={shareButtonState.title}>
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
</StandardTooltip>
</PopoverTrigger>
<PopoverContent className="w-56 p-0" align="start">
{shareSuccess ? (
@ -205,15 +207,16 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
</PopoverContent>
</Popover>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
title={shareButtonState.title}
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
<StandardTooltip content={shareButtonState.title}>
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
</StandardTooltip>
)}
{/* Connect to Cloud Modal */}

View file

@ -10,7 +10,7 @@ import { getModelMaxOutputTokens } from "@roo/api"
import { formatLargeNumber } from "@src/utils/format"
import { cn } from "@src/lib/utils"
import { Button } from "@src/components/ui"
import { Button, StandardTooltip } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
@ -58,13 +58,14 @@ const TaskHeader = ({
const { width: windowWidth } = useWindowSize()
const condenseButton = (
<button
title={t("chat:task.condenseContext")}
disabled={buttonsDisabled}
onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer disabled:cursor-not-allowed opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
<FoldVertical size={16} />
</button>
<StandardTooltip content={t("chat:task.condenseContext")}>
<button
disabled={buttonsDisabled}
onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer disabled:cursor-not-allowed opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
<FoldVertical size={16} />
</button>
</StandardTooltip>
)
return (
@ -95,14 +96,11 @@ const TaskHeader = ({
)}
</div>
</div>
<Button
variant="ghost"
size="icon"
onClick={onClose}
title={t("chat:task.closeAndStart")}
className="shrink-0 w-5 h-5">
<span className="codicon codicon-close" />
</Button>
<StandardTooltip content={t("chat:task.closeAndStart")}>
<Button variant="ghost" size="icon" onClick={onClose} className="shrink-0 w-5 h-5">
<span className="codicon codicon-close" />
</Button>
</StandardTooltip>
</div>
{/* Collapsed state: Track context and cost if we have any */}
{!isTaskExpanded && contextWindow > 0 && (

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import { Package } from "@roo/package"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"

View file

@ -1,4 +1,4 @@
import { render, fireEvent, screen } from "@testing-library/react"
import { render, fireEvent, screen } from "@/utils/test-utils"
import { defaultModeSlug } from "@roo/modes"
@ -761,7 +761,7 @@ describe("ChatTextArea", () => {
describe("selectApiConfig", () => {
// Helper function to get the API config dropdown
const getApiConfigDropdown = () => {
return screen.getByTitle("chat:selectApiConfig")
return screen.getByTestId("dropdown-trigger")
}
it("should be enabled independently of sendingDisabled", () => {
render(<ChatTextArea {...defaultProps} sendingDisabled={true} selectApiConfigDisabled={false} />)

View file

@ -1,6 +1,6 @@
// npx vitest run src/components/chat/__tests__/ChatView.auto-approve.spec.tsx
import { render, waitFor } from "@testing-library/react"
import { render, waitFor } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"

View file

@ -1,7 +1,7 @@
// npx vitest run src/components/chat/__tests__/ChatView.spec.tsx
import React from "react"
import { render, waitFor, act } from "@testing-library/react"
import { render, waitFor, act } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { ExtensionStateContextProvider } from "@src/context/ExtensionStateContext"

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"
import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils"
import { vscode } from "@src/utils/vscode"

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import { describe, test, expect, vi } from "vitest"
import ModeSelector from "../ModeSelector"
import { Mode } from "@roo/modes"

View file

@ -1,5 +1,5 @@
import { describe, test, expect, vi, beforeEach } from "vitest"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { ShareButton } from "../ShareButton"
import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { vi, describe, it, expect, beforeEach } from "vitest"
import { TaskActions } from "../TaskActions"
import type { HistoryItem } from "@roo-code/types"
@ -89,15 +89,19 @@ describe("TaskActions", () => {
it("renders share button when item has id", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeInTheDocument()
})
it("does not render share button when item has no id", () => {
render(<TaskActions item={undefined} buttonsDisabled={false} />)
const shareButton = screen.queryByTitle("Share task")
expect(shareButton).not.toBeInTheDocument()
// Find button by its icon class
const buttons = screen.queryAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).not.toBeDefined()
})
it("renders share button even when not authenticated", () => {
@ -108,7 +112,9 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeInTheDocument()
})
})
@ -117,8 +123,11 @@ describe("TaskActions", () => {
it("shows organization and public share options when authenticated and sharing enabled", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
expect(screen.getByText("Share with Organization")).toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
@ -127,8 +136,11 @@ describe("TaskActions", () => {
it("sends shareCurrentTask message when organization option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
const orgOption = screen.getByText("Share with Organization")
fireEvent.click(orgOption)
@ -142,8 +154,11 @@ describe("TaskActions", () => {
it("sends shareCurrentTask message when public option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
const publicOption = screen.getByText("Share Publicly")
fireEvent.click(publicOption)
@ -165,8 +180,11 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
@ -184,8 +202,11 @@ describe("TaskActions", () => {
it("shows connect to cloud option when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument()
expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument()
@ -195,8 +216,11 @@ describe("TaskActions", () => {
it("does not show organization and public options when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument()
@ -205,8 +229,11 @@ describe("TaskActions", () => {
it("sends rooCloudSignIn message when connect to cloud is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
const connectOption = screen.getByText("Connect")
fireEvent.click(connectOption)
@ -226,12 +253,14 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Sharing disabled by organization")
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeInTheDocument()
expect(shareButton).toBeDisabled()
// Should not have a popover when sharing is disabled
fireEvent.click(shareButton)
fireEvent.click(shareButton!)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.queryByText("Connect to Cloud")).not.toBeInTheDocument()
})
@ -269,14 +298,14 @@ describe("TaskActions", () => {
it("renders export button", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const exportButton = screen.getByTitle("Export task history")
const exportButton = screen.getByLabelText("Export task history")
expect(exportButton).toBeInTheDocument()
})
it("sends exportCurrentTask message when export button is clicked", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const exportButton = screen.getByTitle("Export task history")
const exportButton = screen.getByLabelText("Export task history")
fireEvent.click(exportButton)
expect(mockPostMessage).toHaveBeenCalledWith({
@ -287,7 +316,7 @@ describe("TaskActions", () => {
it("renders delete button and file size when item has size", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const deleteButton = screen.getByTitle("Delete Task (Shift + Click to skip confirmation)")
const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)")
expect(deleteButton).toBeInTheDocument()
expect(screen.getByText("1024 B")).toBeInTheDocument()
})
@ -296,7 +325,7 @@ describe("TaskActions", () => {
const itemWithoutSize = { ...mockItem, size: 0 }
render(<TaskActions item={itemWithoutSize} buttonsDisabled={false} />)
const deleteButton = screen.queryByTitle("Delete Task (Shift + Click to skip confirmation)")
const deleteButton = screen.queryByLabelText("Delete Task (Shift + Click to skip confirmation)")
expect(deleteButton).not.toBeInTheDocument()
})
})
@ -305,8 +334,10 @@ describe("TaskActions", () => {
it("disables buttons when buttonsDisabled is true", () => {
render(<TaskActions item={mockItem} buttonsDisabled={true} />)
const shareButton = screen.getByTitle("Share task")
const exportButton = screen.getByTitle("Export task history")
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
const exportButton = screen.getByLabelText("Export task history")
expect(shareButton).toBeDisabled()
expect(exportButton).toBeDisabled()

View file

@ -1,7 +1,7 @@
// npx vitest src/components/chat/__tests__/TaskHeader.spec.tsx
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import type { ProviderSettings } from "@roo-code/types"
@ -93,22 +93,33 @@ describe("TaskHeader", () => {
it("should render the condense context button", () => {
renderTaskHeader()
expect(screen.getByTitle("chat:task.condenseContext")).toBeInTheDocument()
// Find the button that contains the FoldVertical icon
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))
expect(condenseButton).toBeDefined()
expect(condenseButton?.querySelector("svg")).toBeInTheDocument()
})
it("should call handleCondenseContext when condense context button is clicked", () => {
const handleCondenseContext = vi.fn()
renderTaskHeader({ handleCondenseContext })
const condenseButton = screen.getByTitle("chat:task.condenseContext")
fireEvent.click(condenseButton)
// Find the button that contains the FoldVertical icon
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))
expect(condenseButton).toBeDefined()
fireEvent.click(condenseButton!)
expect(handleCondenseContext).toHaveBeenCalledWith("test-task-id")
})
it("should disable the condense context button when buttonsDisabled is true", () => {
const handleCondenseContext = vi.fn()
renderTaskHeader({ buttonsDisabled: true, handleCondenseContext })
const condenseButton = screen.getByTitle("chat:task.condenseContext")
fireEvent.click(condenseButton)
// Find the button that contains the FoldVertical icon
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))
expect(condenseButton).toBeDefined()
expect(condenseButton).toBeDisabled()
fireEvent.click(condenseButton!)
expect(handleCondenseContext).not.toHaveBeenCalled()
})
})

View file

@ -2,7 +2,7 @@ import { useState, useCallback } from "react"
import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
import { useTranslation } from "react-i18next"
import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
import { Button, Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
import { useRooPortal } from "@/components/ui/hooks"
import { vscode } from "@src/utils/vscode"
@ -48,13 +48,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
return (
<div className="flex flex-row gap-1">
{isDiffAvailable && (
<Button
variant="ghost"
size="icon"
onClick={onCheckpointDiff}
title={t("chat:checkpoint.menu.viewDiff")}>
<span className="codicon codicon-diff-single" />
</Button>
<StandardTooltip content={t("chat:checkpoint.menu.viewDiff")}>
<Button variant="ghost" size="icon" onClick={onCheckpointDiff}>
<span className="codicon codicon-diff-single" />
</Button>
</StandardTooltip>
)}
{isRestoreAvailable && (
<Popover
@ -64,9 +62,11 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
setIsConfirming(false)
}}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" title={t("chat:checkpoint.menu.restore")}>
<span className="codicon codicon-history" />
</Button>
<StandardTooltip content={t("chat:checkpoint.menu.restore")}>
<Button variant="ghost" size="icon">
<span className="codicon codicon-history" />
</Button>
</StandardTooltip>
</PopoverTrigger>
<PopoverContent align="end" container={portalContainer}>
<div className="flex flex-col gap-2">

View file

@ -6,6 +6,7 @@ import { bundledLanguages } from "shiki"
import type { ShikiTransformer } from "shiki"
import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { StandardTooltip } from "@/components/ui"
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
export const WRAPPER_ALPHA = "cc" // 80% opacity
@ -725,48 +726,55 @@ const CodeBlock = memo(
}
</LanguageSelect>
{showCollapseButton && (
<CodeBlockButton
onClick={() => {
// Get the current code block element
const codeBlock = codeBlockRef.current // Capture ref early
// Toggle window shade state
setWindowShade(!windowShade)
<StandardTooltip
content={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}
side="top">
<CodeBlockButton
onClick={() => {
// Get the current code block element
const codeBlock = codeBlockRef.current // Capture ref early
// Toggle window shade state
setWindowShade(!windowShade)
// Clear any previous timeouts
if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current)
if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current)
// Clear any previous timeouts
if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current)
if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current)
// After UI updates, ensure code block is visible and update button position
collapseTimeout1Ref.current = setTimeout(
() => {
if (codeBlock) {
// Check if codeBlock element still exists
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
// After UI updates, ensure code block is visible and update button position
collapseTimeout1Ref.current = setTimeout(
() => {
if (codeBlock) {
// Check if codeBlock element still exists
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
// Wait for scroll to complete before updating button position
collapseTimeout2Ref.current = setTimeout(() => {
// updateCodeBlockButtonPosition itself should also check for refs if needed
updateCodeBlockButtonPosition()
collapseTimeout2Ref.current = null
}, 50)
}
collapseTimeout1Ref.current = null
},
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
)
}}
title={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}>
{windowShade ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</CodeBlockButton>
// Wait for scroll to complete before updating button position
collapseTimeout2Ref.current = setTimeout(() => {
// updateCodeBlockButtonPosition itself should also check for refs if needed
updateCodeBlockButtonPosition()
collapseTimeout2Ref.current = null
}, 50)
}
collapseTimeout1Ref.current = null
},
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
)
}}>
{windowShade ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
</CodeBlockButton>
</StandardTooltip>
)}
<CodeBlockButton
onClick={() => setWordWrap(!wordWrap)}
title={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}>
{wordWrap ? <AlignJustify size={16} /> : <WrapText size={16} />}
</CodeBlockButton>
<CodeBlockButton onClick={handleCopy} title={t("chat:codeblock.tooltips.copy_code")}>
{showCopyFeedback ? <Check size={16} /> : <Copy size={16} />}
</CodeBlockButton>
<StandardTooltip
content={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}
side="top">
<CodeBlockButton onClick={() => setWordWrap(!wordWrap)}>
{wordWrap ? <AlignJustify size={16} /> : <WrapText size={16} />}
</CodeBlockButton>
</StandardTooltip>
<StandardTooltip content={t("chat:codeblock.tooltips.copy_code")} side="top">
<CodeBlockButton onClick={handleCopy}>
{showCopyFeedback ? <Check size={16} /> : <Copy size={16} />}
</CodeBlockButton>
</StandardTooltip>
</CodeBlockButtonWrapper>
)}
</CodeBlockContainer>

View file

@ -1,3 +1,5 @@
import { StandardTooltip } from "@/components/ui"
interface IconButtonProps {
icon: string
onClick?: (e: React.MouseEvent) => void
@ -31,15 +33,21 @@ export function IconButton({
const handleClick = onClick || ((_event: React.MouseEvent) => {})
return (
const button = (
<button
className={`${sizeClasses[size]} flex items-center justify-center border-none text-vscode-editor-foreground cursor-pointer rounded-[3px] ${variantClasses[variant]}`}
aria-label={title}
onClick={handleClick}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
onMouseLeave={onMouseLeave}
title={title}>
onMouseLeave={onMouseLeave}>
<span className={`codicon codicon-${icon}`}></span>
</button>
)
if (title) {
return <StandardTooltip content={title}>{button}</StandardTooltip>
}
return button
}

View file

@ -2,6 +2,7 @@ import React from "react"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { IconButton } from "./IconButton"
import { ZoomControls } from "./ZoomControls"
import { StandardTooltip } from "@/components/ui"
interface MermaidActionButtonsProps {
onZoom?: (e: React.MouseEvent) => void
@ -40,41 +41,51 @@ export const MermaidActionButtons: React.FC<MermaidActionButtonsProps> = ({
zoomInTitle={t("common:mermaid.buttons.zoomIn")}
zoomOutTitle={t("common:mermaid.buttons.zoomOut")}
/>
<IconButton
icon="code"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
onViewCode()
}}
title={t("common:mermaid.buttons.viewCode")}
/>
<IconButton
icon={copyFeedback ? "check" : "copy"}
onClick={onCopy}
title={t("common:mermaid.buttons.copy")}
/>
<StandardTooltip content={t("common:mermaid.buttons.viewCode")}>
<IconButton
icon="code"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
onViewCode()
}}
/>
</StandardTooltip>
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={onCopy} />
</StandardTooltip>
</>
)
}
return (
<>
{onZoom && <IconButton icon="zoom-in" onClick={onZoom} title={t("common:mermaid.buttons.zoom")} />}
<IconButton
icon="code"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
onViewCode()
}}
title={t("common:mermaid.buttons.viewCode")}
/>
<IconButton
icon={copyFeedback ? "check" : "copy"}
onClick={onCopy}
title={t("common:mermaid.buttons.copy")}
/>
{onSave && <IconButton icon="save" onClick={onSave} title={t("common:mermaid.buttons.save")} />}
{onClose && <IconButton icon="close" onClick={onClose} title={t("common:mermaid.buttons.close")} />}
{onZoom && (
<StandardTooltip content={t("common:mermaid.buttons.zoom")}>
<IconButton icon="zoom-in" onClick={onZoom} />
</StandardTooltip>
)}
<StandardTooltip content={t("common:mermaid.buttons.viewCode")}>
<IconButton
icon="code"
onClick={(e: React.MouseEvent) => {
e.stopPropagation()
onViewCode()
}}
/>
</StandardTooltip>
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={onCopy} />
</StandardTooltip>
{onSave && (
<StandardTooltip content={t("common:mermaid.buttons.save")}>
<IconButton icon="save" onClick={onSave} />
</StandardTooltip>
)}
{onClose && (
<StandardTooltip content={t("common:mermaid.buttons.close")}>
<IconButton icon="close" onClick={onClose} />
</StandardTooltip>
)}
</>
)
}

View file

@ -7,6 +7,7 @@ import { Modal } from "./Modal"
import { TabButton } from "./TabButton"
import { IconButton } from "./IconButton"
import { ZoomControls } from "./ZoomControls"
import { StandardTooltip } from "@/components/ui"
const MIN_ZOOM = 0.5
const MAX_ZOOM = 20
@ -160,11 +161,9 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
</div>
<div className="pr-3">
<IconButton
icon="close"
onClick={() => setShowModal(false)}
title={t("common:mermaid.buttons.close")}
/>
<StandardTooltip content={t("common:mermaid.buttons.close")}>
<IconButton icon="close" onClick={() => setShowModal(false)} />
</StandardTooltip>
</div>
</div>
<div
@ -222,22 +221,23 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
zoomInStep={0.2}
zoomOutStep={-0.2}
/>
<IconButton
icon={copyFeedback ? "check" : "copy"}
onClick={handleCopy}
title={t("common:mermaid.buttons.copy")}
/>
<IconButton icon="save" onClick={handleSave} title={t("common:mermaid.buttons.save")} />
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={handleCopy} />
</StandardTooltip>
<StandardTooltip content={t("common:mermaid.buttons.save")}>
<IconButton icon="save" onClick={handleSave} />
</StandardTooltip>
</>
) : (
<IconButton
icon={copyFeedback ? "check" : "copy"}
onClick={(e) => {
e.stopPropagation()
copyWithFeedback(code, e)
}}
title={t("common:mermaid.buttons.copy")}
/>
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
<IconButton
icon={copyFeedback ? "check" : "copy"}
onClick={(e) => {
e.stopPropagation()
copyWithFeedback(code, e)
}}
/>
</StandardTooltip>
)}
</div>
</Modal>

View file

@ -1,5 +1,6 @@
import { IconButton } from "./IconButton"
import { useRef, useEffect } from "react"
import { StandardTooltip } from "@/components/ui"
interface ZoomControlsProps {
zoomLevel: number
@ -67,25 +68,27 @@ export function ZoomControls({
return (
<div className="flex items-center gap-2">
<IconButton
icon="zoom-out"
title={zoomOutTitle}
onClick={!useContinuousZoom ? onZoomOut || (() => adjustZoom?.(zoomOutStep)) : undefined}
onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined}
onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
/>
<StandardTooltip content={zoomOutTitle}>
<IconButton
icon="zoom-out"
onClick={!useContinuousZoom ? onZoomOut || (() => adjustZoom?.(zoomOutStep)) : undefined}
onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomOutStep) : undefined}
onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
/>
</StandardTooltip>
<div className="text-sm text-vscode-editor-foreground min-w-[50px] text-center">
{Math.round(zoomLevel * 100)}%
</div>
<IconButton
icon="zoom-in"
title={zoomInTitle}
onClick={!useContinuousZoom ? onZoomIn || (() => adjustZoom?.(zoomInStep)) : undefined}
onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined}
onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
/>
<StandardTooltip content={zoomInTitle}>
<IconButton
icon="zoom-in"
onClick={!useContinuousZoom ? onZoomIn || (() => adjustZoom?.(zoomInStep)) : undefined}
onMouseDown={useContinuousZoom && adjustZoom ? () => startContinuousZoom(zoomInStep) : undefined}
onMouseUp={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
onMouseLeave={useContinuousZoom && adjustZoom ? stopContinuousZoom : undefined}
/>
</StandardTooltip>
</div>
)
}

View file

@ -1,6 +1,6 @@
// npx vitest run src/components/common/__tests__/CodeBlock.spec.tsx
import { render, screen, fireEvent, act } from "@testing-library/react"
import { render, screen, fireEvent, act } from "@/utils/test-utils"
import CodeBlock from "../CodeBlock"
@ -170,9 +170,15 @@ describe("CodeBlock", () => {
codeBlock.setAttribute("data-partially-visible", "true")
}
const copyButton = screen.getByTitle("Copy code")
await act(async () => {
fireEvent.click(copyButton)
})
// Find the copy button by looking for the button containing the Copy icon
const buttons = screen.getAllByRole("button")
const copyButton = buttons.find((btn) => btn.querySelector("svg.lucide-copy"))
expect(copyButton).toBeTruthy()
if (copyButton) {
await act(async () => {
fireEvent.click(copyButton)
})
}
})
})

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import MarkdownBlock from "../MarkdownBlock"
import { vi } from "vitest"

View file

@ -1,7 +1,7 @@
import { useCallback } from "react"
import { useClipboard } from "@/components/ui/hooks"
import { Button } from "@/components/ui"
import { Button, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { cn } from "@/lib/utils"
@ -25,14 +25,15 @@ export const CopyButton = ({ itemTask }: CopyButtonProps) => {
)
return (
<Button
variant="ghost"
size="icon"
title={t("history:copyPrompt")}
onClick={onCopy}
className="group-hover:opacity-100 opacity-50 transition-opacity"
data-testid="copy-prompt-button">
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
</Button>
<StandardTooltip content={t("history:copyPrompt")}>
<Button
variant="ghost"
size="icon"
onClick={onCopy}
className="group-hover:opacity-100 opacity-50 transition-opacity"
data-testid="copy-prompt-button">
<span className={cn("codicon scale-80", { "codicon-check": isCopied, "codicon-copy": !isCopied })} />
</Button>
</StandardTooltip>
)
}

View file

@ -1,6 +1,6 @@
import { useCallback } from "react"
import { Button } from "@/components/ui"
import { Button, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { vscode } from "@/utils/vscode"
@ -25,14 +25,15 @@ export const DeleteButton = ({ itemId, onDelete }: DeleteButtonProps) => {
)
return (
<Button
variant="ghost"
size="icon"
title={t("history:deleteTaskTitle")}
data-testid="delete-task-button"
onClick={handleDeleteClick}
className="group-hover:opacity-100 opacity-50 transition-opacity">
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
</Button>
<StandardTooltip content={t("history:deleteTaskTitle")}>
<Button
variant="ghost"
size="icon"
data-testid="delete-task-button"
onClick={handleDeleteClick}
className="group-hover:opacity-100 opacity-50 transition-opacity">
<span className="codicon codicon-trash size-4 align-middle text-vscode-descriptionForeground" />
</Button>
</StandardTooltip>
)
}

View file

@ -1,5 +1,5 @@
import { vscode } from "@/utils/vscode"
import { Button } from "@/components/ui"
import { Button, StandardTooltip } from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { useCallback } from "react"
@ -15,14 +15,15 @@ export const ExportButton = ({ itemId }: { itemId: string }) => {
)
return (
<Button
data-testid="export"
variant="ghost"
size="icon"
title={t("history:exportTask")}
className="group-hover:opacity-100 opacity-50 transition-opacity"
onClick={handleExportClick}>
<span className="codicon codicon-desktop-download scale-80" />
</Button>
<StandardTooltip content={t("history:exportTask")}>
<Button
data-testid="export"
variant="ghost"
size="icon"
className="group-hover:opacity-100 opacity-50 transition-opacity"
onClick={handleExportClick}>
<span className="codicon codicon-desktop-download scale-80" />
</Button>
</StandardTooltip>
)
}

View file

@ -5,7 +5,16 @@ import { Virtuoso } from "react-virtuoso"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import { Button, Checkbox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui"
import {
Button,
Checkbox,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
StandardTooltip,
} from "@/components/ui"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { Tab, TabContent, TabHeader } from "../common/Tab"
@ -75,20 +84,22 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
<div className="flex justify-between items-center">
<h3 className="text-vscode-foreground m-0">{t("history:history")}</h3>
<div className="flex gap-2">
<Button
variant={isSelectionMode ? "default" : "secondary"}
onClick={toggleSelectionMode}
data-testid="toggle-selection-mode-button"
title={
<StandardTooltip
content={
isSelectionMode
? `${t("history:exitSelectionMode")}`
: `${t("history:enterSelectionMode")}`
}>
<span
className={`codicon ${isSelectionMode ? "codicon-check-all" : "codicon-checklist"} mr-1`}
/>
{isSelectionMode ? t("history:exitSelection") : t("history:selectionMode")}
</Button>
<Button
variant={isSelectionMode ? "default" : "secondary"}
onClick={toggleSelectionMode}
data-testid="toggle-selection-mode-button">
<span
className={`codicon ${isSelectionMode ? "codicon-check-all" : "codicon-checklist"} mr-1`}
/>
{isSelectionMode ? t("history:exitSelection") : t("history:selectionMode")}
</Button>
</StandardTooltip>
<Button onClick={onDone}>{t("history:done")}</Button>
</div>
</div>

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { vscode } from "@/utils/vscode"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { useClipboard } from "@/components/ui/hooks"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { DeleteButton } from "../DeleteButton"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { vscode } from "@/utils/vscode"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { vscode } from "@src/utils/vscode"

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import type { HistoryItem } from "@roo-code/types"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { useExtensionState } from "@src/context/ExtensionStateContext"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import TaskItem from "../TaskItem"

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import TaskItemFooter from "../TaskItemFooter"

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import TaskItemHeader from "../TaskItemHeader"

View file

@ -1,4 +1,4 @@
import { renderHook, act } from "@testing-library/react"
import { renderHook, act } from "@/utils/test-utils"
import type { HistoryItem } from "@roo-code/types"

View file

@ -81,7 +81,7 @@ export function MarketplaceView({ stateManager, onDone, targetTab }: Marketplace
const filteredTags = useMemo(() => allTags, [allTags])
return (
<TooltipProvider>
<TooltipProvider delayDuration={300}>
<Tab>
<TabHeader className="flex flex-col sticky top-0 z-10 px-3 py-2">
<div className="flex justify-between items-center px-2">

View file

@ -1,6 +1,6 @@
// npx vitest run src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import userEvent from "@testing-library/user-event"
import { TooltipProvider } from "@/components/ui/tooltip"
@ -49,7 +49,7 @@ describe("MarketplaceListView", () => {
const renderWithProviders = (props = {}) =>
render(
<ExtensionStateContextProvider>
<TooltipProvider>
<TooltipProvider delayDuration={300}>
<MarketplaceListView {...defaultProps} {...props} />
</TooltipProvider>
</ExtensionStateContextProvider>,

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import userEvent from "@testing-library/user-event"
import { MarketplaceView } from "../MarketplaceView"

View file

@ -7,7 +7,7 @@ import { useAppTranslation } from "@/i18n/TranslationContext"
import { isValidUrl } from "../../../utils/url"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
import { StandardTooltip } from "@/components/ui"
import { MarketplaceInstallModal } from "./MarketplaceInstallModal"
import { useExtensionState } from "@/context/ExtensionStateContext"
@ -79,37 +79,33 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
<div className="flex items-center gap-1">
{isInstalled ? (
/* Single Remove button when installed */
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-block">
<Button
size="sm"
variant="secondary"
className="text-xs h-5 py-0 px-2"
onClick={() => {
// Determine which installation to remove (prefer project over global)
const target = isInstalledInProject ? "project" : "global"
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: item,
mpInstallOptions: { target },
})
// Request fresh marketplace data to update installed status
vscode.postMessage({
type: "fetchMarketplaceData",
})
}}>
{t("marketplace:items.card.remove")}
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{isInstalledInProject
<StandardTooltip
content={
isInstalledInProject
? t("marketplace:items.card.removeProjectTooltip")
: t("marketplace:items.card.removeGlobalTooltip")}
</TooltipContent>
</Tooltip>
: t("marketplace:items.card.removeGlobalTooltip")
}>
<Button
size="sm"
variant="secondary"
className="text-xs h-5 py-0 px-2"
onClick={() => {
// Determine which installation to remove (prefer project over global)
const target = isInstalledInProject ? "project" : "global"
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: item,
mpInstallOptions: { target },
})
// Request fresh marketplace data to update installed status
vscode.postMessage({
type: "fetchMarketplaceData",
})
}}>
{t("marketplace:items.card.remove")}
</Button>
</StandardTooltip>
) : (
/* Single Install button when not installed */
<Button
@ -139,26 +135,28 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
{item.tags &&
item.tags.length > 0 &&
item.tags.map((tag) => (
<Button
<StandardTooltip
key={tag}
size="sm"
variant="secondary"
className={cn("rounded-sm capitalize text-xs px-2 h-5", {
"border-solid border-primary text-primary": filters.tags.includes(tag),
})}
onClick={() => {
const newTags = filters.tags.includes(tag)
? filters.tags.filter((t: string) => t !== tag)
: [...filters.tags, tag]
setFilters({ tags: newTags })
}}
title={
content={
filters.tags.includes(tag)
? t("marketplace:filters.tags.clear", { count: tag })
: t("marketplace:filters.tags.clickToFilter")
}>
{tag}
</Button>
<Button
size="sm"
variant="secondary"
className={cn("rounded-sm capitalize text-xs px-2 h-5", {
"border-solid border-primary text-primary": filters.tags.includes(tag),
})}
onClick={() => {
const newTags = filters.tags.includes(tag)
? filters.tags.filter((t: string) => t !== tag)
: [...filters.tags, tag]
setFilters({ tags: newTags })
}}>
{tag}
</Button>
</StandardTooltip>
))}
</div>
)}

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { MarketplaceItem } from "@roo-code/types"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { MarketplaceItem } from "@roo-code/types"

View file

@ -1,4 +1,4 @@
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import userEvent from "@testing-library/user-event"
import { MarketplaceItem } from "@roo-code/types"
@ -57,7 +57,7 @@ vi.mock("@/i18n/TranslationContext", () => ({
}))
const renderWithProviders = (ui: React.ReactElement) => {
return render(<TooltipProvider>{ui}</TooltipProvider>)
return render(<TooltipProvider delayDuration={300}>{ui}</TooltipProvider>)
}
describe("MarketplaceItemCard", () => {

View file

@ -4,6 +4,7 @@ import { McpTool } from "@roo/mcp"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { StandardTooltip } from "@/components/ui"
type McpToolRowProps = {
tool: McpTool
@ -46,9 +47,9 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
{/* Tool name section */}
<div className="flex items-center min-w-0 flex-1">
<span className="codicon codicon-symbol-method mr-2 flex-shrink-0 text-vscode-symbolIcon-methodForeground"></span>
<span className="font-medium truncate text-vscode-foreground" title={tool.name}>
{tool.name}
</span>
<StandardTooltip content={tool.name}>
<span className="font-medium truncate text-vscode-foreground">{tool.name}</span>
</StandardTooltip>
</div>
{/* Controls section */}
@ -69,24 +70,25 @@ const McpToolRow = ({ tool, serverName, serverSource, alwaysAllowMcp, isInChatCo
{/* Enabled eye button - only show in settings context */}
{!isInChatContext && (
<button
role="button"
aria-pressed={tool.enabledForPrompt}
aria-label={t("mcp:tool.togglePromptInclusion")}
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${
tool.enabledForPrompt
? "text-vscode-foreground"
: "text-vscode-descriptionForeground opacity-60"
}`}
onClick={handleEnabledForPromptChange}
data-tool-prompt-toggle={tool.name}
title={t("mcp:tool.togglePromptInclusion")}>
<span
className={`codicon ${
tool.enabledForPrompt ? "codicon-eye-closed" : "codicon-eye"
} text-base`}
/>
</button>
<StandardTooltip content={t("mcp:tool.togglePromptInclusion")}>
<button
role="button"
aria-pressed={tool.enabledForPrompt}
aria-label={t("mcp:tool.togglePromptInclusion")}
className={`p-1 rounded hover:bg-vscode-toolbar-hoverBackground transition-colors ${
tool.enabledForPrompt
? "text-vscode-foreground"
: "text-vscode-descriptionForeground opacity-60"
}`}
onClick={handleEnabledForPromptChange}
data-tool-prompt-toggle={tool.name}>
<span
className={`codicon ${
tool.enabledForPrompt ? "codicon-eye-closed" : "codicon-eye"
} text-base`}
/>
</button>
</StandardTooltip>
)}
</div>
)}

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, fireEvent, screen } from "@testing-library/react"
import { render, fireEvent, screen } from "@/utils/test-utils"
import { vscode } from "@src/utils/vscode"

View file

@ -45,6 +45,7 @@ import {
CommandItem,
CommandGroup,
Input,
StandardTooltip,
} from "@src/components/ui"
// Get all available groups that should show in prompts view
@ -431,30 +432,29 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
<h3 className="text-vscode-foreground m-0">{t("prompts:modes.title")}</h3>
<div className="flex gap-2">
<Button
variant="ghost"
size="icon"
onClick={openCreateModeDialog}
title={t("prompts:modes.createNewMode")}>
<span className="codicon codicon-add"></span>
</Button>
<div className="relative inline-block">
<Button
variant="ghost"
size="icon"
title={t("prompts:modes.editModesConfig")}
className="flex"
onClick={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setShowConfigMenu((prev) => !prev)
}}
onBlur={() => {
// Add slight delay to allow menu item clicks to register
setTimeout(() => setShowConfigMenu(false), 200)
}}>
<span className="codicon codicon-json"></span>
<StandardTooltip content={t("prompts:modes.createNewMode")}>
<Button variant="ghost" size="icon" onClick={openCreateModeDialog}>
<span className="codicon codicon-add"></span>
</Button>
</StandardTooltip>
<div className="relative inline-block">
<StandardTooltip content={t("prompts:modes.editModesConfig")}>
<Button
variant="ghost"
size="icon"
className="flex"
onClick={(e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
setShowConfigMenu((prev) => !prev)
}}
onBlur={() => {
// Add slight delay to allow menu item clicks to register
setTimeout(() => setShowConfigMenu(false), 200)
}}>
<span className="codicon codicon-json"></span>
</Button>
</StandardTooltip>
{showConfigMenu && (
<div
onClick={(e) => e.stopPropagation()}
@ -652,18 +652,19 @@ const ModesView = ({ onDone }: ModesViewProps) => {
}}
className="w-full"
/>
<Button
variant="ghost"
size="icon"
title={t("prompts:createModeDialog.deleteMode")}
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: visualMode,
})
}}>
<span className="codicon codicon-trash"></span>
</Button>
<StandardTooltip content={t("prompts:createModeDialog.deleteMode")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
vscode.postMessage({
type: "deleteCustomMode",
slug: visualMode,
})
}}>
<span className="codicon codicon-trash"></span>
</Button>
</StandardTooltip>
</div>
</div>
</div>
@ -674,19 +675,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:roleDefinition.title")}</div>
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "roleDefinition")
}
}}
title={t("prompts:roleDefinition.resetToDefault")}
data-testid="role-definition-reset">
<span className="codicon codicon-discard"></span>
</Button>
<StandardTooltip content={t("prompts:roleDefinition.resetToDefault")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "roleDefinition")
}
}}
data-testid="role-definition-reset">
<span className="codicon codicon-discard"></span>
</Button>
</StandardTooltip>
)}
</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
@ -733,19 +735,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:description.title")}</div>
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "description")
}
}}
title={t("prompts:description.resetToDefault")}
data-testid="description-reset">
<span className="codicon codicon-discard"></span>
</Button>
<StandardTooltip content={t("prompts:description.resetToDefault")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "description")
}
}}
data-testid="description-reset">
<span className="codicon codicon-discard"></span>
</Button>
</StandardTooltip>
)}
</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
@ -786,19 +789,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:whenToUse.title")}</div>
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "whenToUse")
}
}}
title={t("prompts:whenToUse.resetToDefault")}
data-testid="when-to-use-reset">
<span className="codicon codicon-discard"></span>
</Button>
<StandardTooltip content={t("prompts:whenToUse.resetToDefault")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "whenToUse")
}
}}
data-testid="when-to-use-reset">
<span className="codicon codicon-discard"></span>
</Button>
</StandardTooltip>
)}
</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
@ -843,18 +847,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:tools.title")}</div>
{findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
onClick={() => setIsToolsEditMode(!isToolsEditMode)}
title={
<StandardTooltip
content={
isToolsEditMode
? t("prompts:tools.doneEditing")
: t("prompts:tools.editTools")
}>
<span
className={`codicon codicon-${isToolsEditMode ? "check" : "edit"}`}></span>
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => setIsToolsEditMode(!isToolsEditMode)}>
<span
className={`codicon codicon-${isToolsEditMode ? "check" : "edit"}`}></span>
</Button>
</StandardTooltip>
)}
</div>
{!findModeBySlug(visualMode, customModes) && (
@ -936,19 +942,20 @@ const ModesView = ({ onDone }: ModesViewProps) => {
<div className="flex justify-between items-center mb-1">
<div className="font-bold">{t("prompts:customInstructions.title")}</div>
{!findModeBySlug(visualMode, customModes) && (
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "customInstructions")
}
}}
title={t("prompts:customInstructions.resetToDefault")}
data-testid="custom-instructions-reset">
<span className="codicon codicon-discard"></span>
</Button>
<StandardTooltip content={t("prompts:customInstructions.resetToDefault")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode?.slug) {
handleAgentReset(currentMode.slug, "customInstructions")
}
}}
data-testid="custom-instructions-reset">
<span className="codicon codicon-discard"></span>
</Button>
</StandardTooltip>
)}
</div>
<div className="text-[13px] text-vscode-descriptionForeground mb-2">
@ -1041,22 +1048,23 @@ const ModesView = ({ onDone }: ModesViewProps) => {
data-testid="preview-prompt-button">
{t("prompts:systemPrompt.preview")}
</Button>
<Button
variant="ghost"
size="icon"
title={t("prompts:systemPrompt.copy")}
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode) {
vscode.postMessage({
type: "copySystemPrompt",
mode: currentMode.slug,
})
}
}}
data-testid="copy-prompt-button">
<span className="codicon codicon-copy"></span>
</Button>
<StandardTooltip content={t("prompts:systemPrompt.copy")}>
<Button
variant="ghost"
size="icon"
onClick={() => {
const currentMode = getCurrentMode()
if (currentMode) {
vscode.postMessage({
type: "copySystemPrompt",
mode: currentMode.slug,
})
}
}}
data-testid="copy-prompt-button">
<span className="codicon codicon-copy"></span>
</Button>
</StandardTooltip>
</div>
{/* Custom System Prompt Disclosure */}

View file

@ -1,6 +1,6 @@
// npx vitest src/components/modes/__tests__/ModesView.spec.tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import ModesView from "../ModesView"
import { ExtensionStateContext } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"

View file

@ -21,6 +21,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
StandardTooltip,
} from "@/components/ui"
interface ApiConfigManagerProps {
@ -248,23 +249,25 @@ const ApiConfigManager = ({
}}
className="grow"
/>
<Button
variant="ghost"
size="icon"
disabled={!inputValue.trim()}
onClick={handleSave}
title={t("settings:common.save")}
data-testid="save-rename-button">
<span className="codicon codicon-check" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleCancel}
title={t("settings:common.cancel")}
data-testid="cancel-rename-button">
<span className="codicon codicon-close" />
</Button>
<StandardTooltip content={t("settings:common.save")}>
<Button
variant="ghost"
size="icon"
disabled={!inputValue.trim()}
onClick={handleSave}
data-testid="save-rename-button">
<span className="codicon codicon-check" />
</Button>
</StandardTooltip>
<StandardTooltip content={t("settings:common.cancel")}>
<Button
variant="ghost"
size="icon"
onClick={handleCancel}
data-testid="cancel-rename-button">
<span className="codicon codicon-close" />
</Button>
</StandardTooltip>
</div>
{error && (
<div className="text-vscode-descriptionForeground text-sm mt-1" data-testid="error-message">
@ -334,13 +337,17 @@ const ApiConfigManager = ({
className={!valid ? "text-vscode-errorForeground" : ""}>
<div className="flex items-center">
{!valid && (
<span
title={t("settings:validation.profileInvalid")}>
<AlertTriangle
size={16}
className="mr-2 text-vscode-errorForeground"
/>
</span>
<StandardTooltip
content={t(
"settings:validation.profileInvalid",
)}>
<span>
<AlertTriangle
size={16}
className="mr-2 text-vscode-errorForeground"
/>
</span>
</StandardTooltip>
)}
{config.name}
</div>
@ -360,37 +367,37 @@ const ApiConfigManager = ({
</Command>
</PopoverContent>
</Popover>
<Button
variant="ghost"
size="icon"
onClick={handleAdd}
title={t("settings:providers.addProfile")}
data-testid="add-profile-button">
<span className="codicon codicon-add" />
</Button>
<StandardTooltip content={t("settings:providers.addProfile")}>
<Button variant="ghost" size="icon" onClick={handleAdd} data-testid="add-profile-button">
<span className="codicon codicon-add" />
</Button>
</StandardTooltip>
{currentApiConfigName && (
<>
<Button
variant="ghost"
size="icon"
onClick={handleStartRename}
title={t("settings:providers.renameProfile")}
data-testid="rename-profile-button">
<span className="codicon codicon-edit" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={handleDelete}
title={
<StandardTooltip content={t("settings:providers.renameProfile")}>
<Button
variant="ghost"
size="icon"
onClick={handleStartRename}
data-testid="rename-profile-button">
<span className="codicon codicon-edit" />
</Button>
</StandardTooltip>
<StandardTooltip
content={
isOnlyProfile
? t("settings:providers.cannotDeleteOnlyProfile")
: t("settings:providers.deleteProfile")
}
data-testid="delete-profile-button"
disabled={isOnlyProfile}>
<span className="codicon codicon-trash" />
</Button>
}>
<Button
variant="ghost"
size="icon"
onClick={handleDelete}
data-testid="delete-profile-button"
disabled={isOnlyProfile}>
<span className="codicon codicon-trash" />
</Button>
</StandardTooltip>
</>
)}
</div>

View file

@ -2,7 +2,7 @@ import type { GlobalSettings } from "@roo-code/types"
import { useAppTranslation } from "@/i18n/TranslationContext"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui"
import { Button, StandardTooltip } from "@/components/ui"
type AutoApproveToggles = Pick<
GlobalSettings,
@ -100,20 +100,20 @@ export const AutoApproveToggle = ({ onToggle, ...props }: AutoApproveToggleProps
"[@media(min-width:800px)]:max-w-[800px]",
)}>
{Object.values(autoApproveSettingsConfig).map(({ key, descriptionKey, labelKey, icon, testId }) => (
<Button
key={key}
variant={props[key] ? "default" : "outline"}
onClick={() => onToggle(key, !props[key])}
title={t(descriptionKey || "")}
aria-label={t(labelKey)}
aria-pressed={!!props[key]}
data-testid={testId}
className={cn(" aspect-square h-[80px]", !props[key] && "opacity-50")}>
<span className={cn("flex flex-col items-center gap-1")}>
<span className={`codicon codicon-${icon}`} />
<span className="text-sm text-center">{t(labelKey)}</span>
</span>
</Button>
<StandardTooltip key={key} content={t(descriptionKey || "")}>
<Button
variant={props[key] ? "default" : "outline"}
onClick={() => onToggle(key, !props[key])}
aria-label={t(labelKey)}
aria-pressed={!!props[key]}
data-testid={testId}
className={cn(" aspect-square h-[80px]", !props[key] && "opacity-50")}>
<span className={cn("flex flex-col items-center gap-1")}>
<span className={`codicon codicon-${icon}`} />
<span className="text-sm text-center">{t(labelKey)}</span>
</span>
</Button>
</StandardTooltip>
))}
</div>
)

View file

@ -6,7 +6,15 @@ import { supportPrompt, SupportPromptType } from "@roo/support-prompt"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { Button, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import {
Button,
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
StandardTooltip,
} from "@src/components/ui"
import { SectionHeader } from "./SectionHeader"
import { Section } from "./Section"
import { MessageSquare } from "lucide-react"
@ -97,15 +105,14 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom
<div key={activeSupportOption} className="mt-4">
<div className="flex justify-between items-center mb-1">
<label className="block font-medium">{t("prompts:supportPrompts.prompt")}</label>
<Button
variant="ghost"
size="icon"
onClick={() => handleSupportReset(activeSupportOption)}
title={t("prompts:supportPrompts.resetPrompt", {
<StandardTooltip
content={t("prompts:supportPrompts.resetPrompt", {
promptType: activeSupportOption,
})}>
<span className="codicon codicon-discard"></span>
</Button>
<Button variant="ghost" size="icon" onClick={() => handleSupportReset(activeSupportOption)}>
<span className="codicon codicon-discard"></span>
</Button>
</StandardTooltip>
</div>
<VSCodeTextArea

View file

@ -46,6 +46,7 @@ import {
TooltipContent,
TooltipProvider,
TooltipTrigger,
StandardTooltip,
} from "@src/components/ui"
import { Tab, TabContent, TabHeader, TabList, TabTrigger } from "../common/Tab"
@ -448,27 +449,28 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<h3 className="text-vscode-foreground m-0">{t("settings:header.title")}</h3>
</div>
<div className="flex gap-2">
<Button
variant={isSettingValid ? "default" : "secondary"}
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
title={
<StandardTooltip
content={
!isSettingValid
? errorMessage
: isChangeDetected
? t("settings:header.saveButtonTooltip")
: t("settings:header.nothingChangedTooltip")
}
onClick={handleSubmit}
disabled={!isChangeDetected || !isSettingValid}
data-testid="save-button">
{t("settings:common.save")}
</Button>
<Button
variant="secondary"
title={t("settings:header.doneButtonTooltip")}
onClick={() => checkUnsaveChanges(onDone)}>
{t("settings:common.done")}
</Button>
}>
<Button
variant={isSettingValid ? "default" : "secondary"}
className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
onClick={handleSubmit}
disabled={!isChangeDetected || !isSettingValid}
data-testid="save-button">
{t("settings:common.save")}
</Button>
</StandardTooltip>
<StandardTooltip content={t("settings:header.doneButtonTooltip")}>
<Button variant="secondary" onClick={() => checkUnsaveChanges(onDone)}>
{t("settings:common.done")}
</Button>
</StandardTooltip>
</div>
</TabHeader>
@ -510,7 +512,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
if (isCompactMode) {
// Wrap in Tooltip and manually add onClick to the trigger
return (
<TooltipProvider key={id} delayDuration={0}>
<TooltipProvider key={id} delayDuration={300}>
<Tooltip>
<TooltipTrigger asChild onClick={onSelect}>
{/* Clone to avoid ref issues if triggerComponent itself had a key */}

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/ApiConfigManager.spec.tsx
import { render, screen, fireEvent, within } from "@testing-library/react"
import { render, screen, fireEvent, within } from "@/utils/test-utils"
import ApiConfigManager from "../ApiConfigManager"
@ -41,6 +41,7 @@ vitest.mock("@/components/ui", () => ({
data-testid={dataTestId}
/>
),
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
// New components for searchable dropdown
Popover: ({ children, open }: any) => (
<div className="popover" style={{ position: "relative" }}>

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/ApiOptions.spec.tsx
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { type ModelInfo, type ProviderSettings, openAiModelInfoSaneDefaults } from "@roo-code/types"
@ -61,6 +61,7 @@ vi.mock("@/components/ui", () => ({
{children}
</button>
),
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
// Add missing components used by ModelPicker
Command: ({ children }: any) => <div className="command-mock">{children}</div>,
CommandEmpty: ({ children }: any) => <div className="command-empty-mock">{children}</div>,

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { TranslationProvider } from "@/i18n/__mocks__/TranslationContext"

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/CodeIndexSettings.spec.tsx
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import userEvent from "@testing-library/user-event"
import { CodeIndexSettings } from "../CodeIndexSettings"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { ContextManagementSettings } from "@src/components/settings/ContextManagementSettings"

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/ModelPicker.spec.tsx
import { screen, fireEvent, render } from "@testing-library/react"
import { screen, fireEvent, render } from "@/utils/test-utils"
import { act } from "react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { vi } from "vitest"

View file

@ -1,4 +1,4 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { vscode } from "@/utils/vscode"
@ -115,6 +115,7 @@ vi.mock("@/components/ui", () => ({
{children}
</button>
),
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
Input: ({ value, onChange, placeholder, "data-testid": dataTestId }: any) => (
<input type="text" value={value} onChange={onChange} placeholder={placeholder} data-testid={dataTestId} />
),

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/TemperatureControl.spec.tsx
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { render, screen, fireEvent, waitFor } from "@/utils/test-utils"
import { TemperatureControl } from "../TemperatureControl"

View file

@ -1,6 +1,6 @@
// npx vitest src/components/settings/__tests__/ThinkingBudget.spec.tsx
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import type { ModelInfo } from "@roo-code/types"

View file

@ -5,7 +5,7 @@ import { VSCodeTextField, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-
import { type ProviderSettings, type ModelInfo, BEDROCK_REGIONS } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StandardTooltip } from "@src/components/ui"
import { inputEventTransform, noTransform } from "../transforms"
@ -114,11 +114,12 @@ export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedMo
onChange={handleInputChange("awsUsePromptCache", noTransform)}>
<div className="flex items-center gap-1">
<span>{t("settings:providers.enablePromptCaching")}</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.enablePromptCachingTitle")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.enablePromptCachingTitle")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
</Checkbox>
<div className="text-sm text-vscode-descriptionForeground ml-6 mt-1">

View file

@ -15,7 +15,7 @@ import {
import { ExtensionMessage } from "@roo/ExtensionMessage"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Button } from "@src/components/ui"
import { Button, StandardTooltip } from "@src/components/ui"
import { convertHeadersToObject } from "../utils/headers"
import { inputEventTransform, noTransform } from "../transforms"
@ -208,9 +208,11 @@ export const OpenAICompatible = ({
<div className="mb-4">
<div className="flex justify-between items-center mb-2">
<label className="block font-medium">{t("settings:providers.customHeaders")}</label>
<VSCodeButton appearance="icon" title={t("settings:common.add")} onClick={handleAddCustomHeader}>
<span className="codicon codicon-add"></span>
</VSCodeButton>
<StandardTooltip content={t("settings:common.add")}>
<VSCodeButton appearance="icon" onClick={handleAddCustomHeader}>
<span className="codicon codicon-add"></span>
</VSCodeButton>
</StandardTooltip>
</div>
{!customHeaders.length ? (
<div className="text-sm text-vscode-descriptionForeground">
@ -231,12 +233,11 @@ export const OpenAICompatible = ({
placeholder={t("settings:providers.headerValue")}
onInput={(e: any) => handleUpdateHeaderValue(index, e.target.value)}
/>
<VSCodeButton
appearance="icon"
title={t("settings:common.remove")}
onClick={() => handleRemoveCustomHeader(index)}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
<StandardTooltip content={t("settings:common.remove")}>
<VSCodeButton appearance="icon" onClick={() => handleRemoveCustomHeader(index)}>
<span className="codicon codicon-trash"></span>
</VSCodeButton>
</StandardTooltip>
</div>
))
)}
@ -305,7 +306,6 @@ export const OpenAICompatible = ({
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
title={t("settings:providers.customModel.maxTokens.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = parseInt((e.target as HTMLInputElement).value)
@ -344,7 +344,6 @@ export const OpenAICompatible = ({
return value > 0 ? "var(--vscode-charts-green)" : "var(--vscode-errorForeground)"
})(),
}}
title={t("settings:providers.customModel.contextWindow.description")}
onInput={handleInputChange("openAiCustomModelInfo", (e) => {
const value = (e.target as HTMLInputElement).value
const parsed = parseInt(value)
@ -382,11 +381,12 @@ export const OpenAICompatible = ({
{t("settings:providers.customModel.imageSupport.label")}
</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.imageSupport.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.customModel.imageSupport.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.imageSupport.description")}
@ -405,11 +405,12 @@ export const OpenAICompatible = ({
})}>
<span className="font-medium">{t("settings:providers.customModel.computerUse.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.computerUse.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.customModel.computerUse.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.computerUse.description")}
@ -428,11 +429,12 @@ export const OpenAICompatible = ({
})}>
<span className="font-medium">{t("settings:providers.customModel.promptCache.label")}</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.promptCache.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.customModel.promptCache.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
{t("settings:providers.customModel.promptCache.description")}
@ -473,11 +475,12 @@ export const OpenAICompatible = ({
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.input.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.input.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.customModel.pricing.input.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
</VSCodeTextField>
</div>
@ -516,11 +519,12 @@ export const OpenAICompatible = ({
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.output.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.output.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip content={t("settings:providers.customModel.pricing.output.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
</VSCodeTextField>
</div>
@ -559,11 +563,13 @@ export const OpenAICompatible = ({
<span className="font-medium">
{t("settings:providers.customModel.pricing.cacheReads.label")}
</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheReads.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip
content={t("settings:providers.customModel.pricing.cacheReads.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
</VSCodeTextField>
</div>
@ -599,11 +605,13 @@ export const OpenAICompatible = ({
<label className="block font-medium mb-1">
{t("settings:providers.customModel.pricing.cacheWrites.label")}
</label>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title={t("settings:providers.customModel.pricing.cacheWrites.description")}
style={{ fontSize: "12px" }}
/>
<StandardTooltip
content={t("settings:providers.customModel.pricing.cacheWrites.description")}>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
style={{ fontSize: "12px" }}
/>
</StandardTooltip>
</div>
</VSCodeTextField>
</div>

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { Bedrock } from "../Bedrock"
import { ProviderSettings } from "@roo-code/types"

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { OpenAICompatible } from "../OpenAICompatible"
import { ProviderSettings } from "@roo-code/types"
@ -64,6 +64,7 @@ vi.mock("@src/i18n/TranslationContext", () => ({
// Mock the UI components
vi.mock("@src/components/ui", () => ({
Button: ({ children, onClick }: any) => <button onClick={onClick}>{children}</button>,
StandardTooltip: ({ children, content }: any) => <div title={content}>{children}</div>,
}))
// Mock other components

View file

@ -1,7 +1,7 @@
// npx vitest run src/components/ui/__tests__/select-dropdown.spec.tsx
import { ReactNode } from "react"
import { render, screen, fireEvent } from "@testing-library/react"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { SelectDropdown, DropdownOptionType } from "../select-dropdown"

View file

@ -0,0 +1,182 @@
import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event"
import { describe, it, expect } from "vitest"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip"
import { StandardTooltip } from "../standard-tooltip"
describe("Tooltip", () => {
it("should render tooltip content on hover", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Tooltip text</TooltipContent>
</Tooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText("Tooltip text")
expect(tooltips.length).toBeGreaterThan(0)
},
{ timeout: 1000 },
)
})
it("should apply text wrapping classes", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>
This is a very long tooltip text that should wrap when it reaches the maximum width
</TooltipContent>
</Tooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText(/This is a very long tooltip text/)
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words")
},
{ timeout: 1000 },
)
})
it("should not have overflow-hidden class", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>Hover me</TooltipTrigger>
<TooltipContent>Tooltip text</TooltipContent>
</Tooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText("Tooltip text")
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
expect(visibleTooltip).not.toHaveClass("overflow-hidden")
},
{ timeout: 1000 },
)
})
})
describe("StandardTooltip", () => {
it("should render with default delay", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={300}>
<StandardTooltip content="Tooltip text">
<button>Hover me</button>
</StandardTooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText("Tooltip text")
expect(tooltips.length).toBeGreaterThan(0)
},
{ timeout: 1000 },
)
})
it("should apply custom maxWidth", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={0}>
<StandardTooltip content="Long tooltip text" maxWidth={200}>
<button>Hover me</button>
</StandardTooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText("Long tooltip text")
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
expect(visibleTooltip).toHaveStyle({ maxWidth: "200px" })
},
{ timeout: 1000 },
)
})
it("should apply custom maxWidth as string", async () => {
const user = userEvent.setup()
render(
<TooltipProvider delayDuration={0}>
<StandardTooltip content="Long tooltip text" maxWidth="15rem">
<button>Hover me</button>
</StandardTooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText("Long tooltip text")
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
expect(visibleTooltip).toHaveStyle({ maxWidth: "15rem" })
},
{ timeout: 1000 },
)
})
it("should handle long content with text wrapping", async () => {
const user = userEvent.setup()
const longContent =
"This is a very long tooltip content that should definitely wrap when displayed because it exceeds the maximum width constraint"
render(
<TooltipProvider delayDuration={0}>
<StandardTooltip content={longContent}>
<button>Hover me</button>
</StandardTooltip>
</TooltipProvider>,
)
const trigger = screen.getByText("Hover me")
await user.hover(trigger)
await waitFor(
() => {
const tooltips = screen.getAllByText(longContent)
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words")
},
{ timeout: 1000 },
)
})
})

View file

@ -16,3 +16,4 @@ export * from "./select-dropdown"
export * from "./select"
export * from "./textarea"
export * from "./tooltip"
export * from "./standard-tooltip"

View file

@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next"
import { cn } from "@/lib/utils"
import { useRooPortal } from "./hooks/useRooPortal"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
import { StandardTooltip } from "@/components/ui"
export enum DropdownOptionType {
ITEM = "item",
@ -185,25 +186,28 @@ export const SelectDropdown = React.memo(
[onChange, options],
)
const triggerContent = (
<PopoverTrigger
ref={ref}
disabled={disabled}
data-testid="dropdown-trigger"
className={cn(
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground w-auto",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
)}>
<CaretUpIcon className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
<span className="truncate">{displayText}</span>
</PopoverTrigger>
)
return (
<Popover open={open} onOpenChange={onOpenChange} data-testid="dropdown-root">
<PopoverTrigger
ref={ref}
disabled={disabled}
title={title}
data-testid="dropdown-trigger"
className={cn(
"w-full min-w-0 max-w-full inline-flex items-center gap-1.5 relative whitespace-nowrap px-1.5 py-1 text-xs",
"bg-transparent border border-[rgba(255,255,255,0.08)] rounded-md text-vscode-foreground w-auto",
"transition-all duration-150 focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder focus-visible:ring-inset",
disabled
? "opacity-50 cursor-not-allowed"
: "opacity-90 hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)] cursor-pointer",
triggerClassName,
)}>
<CaretUpIcon className="pointer-events-none opacity-80 flex-shrink-0 size-3" />
<span className="truncate">{displayText}</span>
</PopoverTrigger>
{title ? <StandardTooltip content={title}>{triggerContent}</StandardTooltip> : triggerContent}
<PopoverContent
align={align}
sideOffset={sideOffset}

View file

@ -0,0 +1,69 @@
import * as React from "react"
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
export const STANDARD_TOOLTIP_DELAY = 300
interface StandardTooltipProps {
/** The element(s) that trigger the tooltip */
children: React.ReactNode
/** The content to display in the tooltip */
content: React.ReactNode
/** The preferred side of the trigger to render the tooltip */
side?: "top" | "right" | "bottom" | "left"
/** The preferred alignment against the trigger */
align?: "start" | "center" | "end"
/** Distance in pixels from the trigger */
sideOffset?: number
/** Additional CSS classes for the tooltip content */
className?: string
/** Whether the trigger should be rendered as a child */
asChild?: boolean
/** Maximum width of the tooltip content */
maxWidth?: number | string
}
/**
* StandardTooltip component that enforces consistent 300ms delay across the application.
* This component wraps the Radix UI tooltip with a standardized delay duration.
*
* @example
* // Basic usage
* <StandardTooltip content="Delete item">
* <Button>Delete</Button>
* </StandardTooltip>
*
* // With custom positioning
* <StandardTooltip content="Long tooltip text" side="right" sideOffset={8}>
* <IconButton icon="info" />
* </StandardTooltip>
*
* @note This replaces native HTML title attributes for consistent timing.
* @note Requires a TooltipProvider to be present in the component tree (typically at the app root).
* @note Do not nest StandardTooltip components as this can cause UI issues.
*/
export function StandardTooltip({
children,
content,
side = "top",
align = "center",
sideOffset = 4,
className,
asChild = true,
maxWidth,
}: StandardTooltipProps) {
// Don't render tooltip if content is empty or only whitespace
if (!content || (typeof content === "string" && !content.trim())) {
return <>{children}</>
}
const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined
return (
<Tooltip>
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
<TooltipContent side={side} align={align} sideOffset={sideOffset} className={className} style={style}>
{content}
</TooltipContent>
</Tooltip>
)
}

View file

@ -17,8 +17,11 @@ const TooltipContent = React.forwardRef<
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
collisionPadding={10}
avoidCollisions={true}
className={cn(
"z-50 overflow-hidden rounded-xs bg-vscode-notifications-background border border-vscode-notifications-border px-3 py-1.5 text-xs text-vscode-notifications-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"z-50 rounded-xs bg-vscode-notifications-background border border-vscode-notifications-border px-3 py-1.5 text-xs text-vscode-notifications-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
"max-w-[300px] break-words",
className,
)}
{...props}

View file

@ -1,5 +1,5 @@
import React from "react"
import { render, screen } from "@testing-library/react"
import { render, screen } from "@/utils/test-utils"
import RooTips from "../RooTips"

View file

@ -1,4 +1,4 @@
import { render, screen, act } from "@testing-library/react"
import { render, screen, act } from "@/utils/test-utils"
import { ProviderSettings, ExperimentId } from "@roo-code/types"

View file

@ -1,4 +1,4 @@
import { render } from "@testing-library/react"
import { render } from "@/utils/test-utils"
import TranslationProvider, { useAppTranslation } from "../TranslationContext"

View file

@ -0,0 +1,21 @@
import React from "react"
import { render, RenderOptions } from "@testing-library/react"
import { TooltipProvider } from "@/components/ui/tooltip"
import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip"
interface AllTheProvidersProps {
children: React.ReactNode
}
const AllTheProviders = ({ children }: AllTheProvidersProps) => {
return <TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>{children}</TooltipProvider>
}
const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, "wrapper">) =>
render(ui, { wrapper: AllTheProviders, ...options })
// re-export everything
export * from "@testing-library/react"
// override render method
export { customRender as render }