Browse Source

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

Co-authored-by: Daniel Riccio <[email protected]>
Hannes Rudolph 6 months ago
parent
commit
3318366b66
85 changed files with 1240 additions and 796 deletions
  1. 5 1
      webview-ui/src/App.tsx
  2. 1 1
      webview-ui/src/__tests__/App.spec.tsx
  3. 16 12
      webview-ui/src/__tests__/ContextWindowProgress.spec.tsx
  4. 1 1
      webview-ui/src/components/account/__tests__/AccountView.spec.tsx
  5. 21 18
      webview-ui/src/components/chat/ChatTextArea.tsx
  6. 36 30
      webview-ui/src/components/chat/ChatView.tsx
  7. 14 12
      webview-ui/src/components/chat/CodebaseSearchResult.tsx
  8. 55 48
      webview-ui/src/components/chat/ContextWindowProgress.tsx
  9. 14 13
      webview-ui/src/components/chat/FollowUpSuggest.tsx
  10. 4 2
      webview-ui/src/components/chat/IconButton.tsx
  11. 25 23
      webview-ui/src/components/chat/Markdown.tsx
  12. 23 20
      webview-ui/src/components/chat/ModeSelector.tsx
  13. 21 18
      webview-ui/src/components/chat/ShareButton.tsx
  14. 14 16
      webview-ui/src/components/chat/TaskHeader.tsx
  15. 1 1
      webview-ui/src/components/chat/__tests__/Announcement.spec.tsx
  16. 1 1
      webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx
  17. 2 2
      webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx
  18. 1 1
      webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx
  19. 1 1
      webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
  20. 1 1
      webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx
  21. 1 1
      webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx
  22. 1 1
      webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx
  23. 58 27
      webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx
  24. 17 6
      webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
  25. 11 11
      webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx
  26. 49 41
      webview-ui/src/components/common/CodeBlock.tsx
  27. 11 3
      webview-ui/src/components/common/IconButton.tsx
  28. 39 28
      webview-ui/src/components/common/MermaidActionButtons.tsx
  29. 18 18
      webview-ui/src/components/common/MermaidButton.tsx
  30. 19 16
      webview-ui/src/components/common/ZoomControls.tsx
  31. 11 5
      webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx
  32. 1 1
      webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx
  33. 11 10
      webview-ui/src/components/history/CopyButton.tsx
  34. 11 10
      webview-ui/src/components/history/DeleteButton.tsx
  35. 11 10
      webview-ui/src/components/history/ExportButton.tsx
  36. 22 11
      webview-ui/src/components/history/HistoryView.tsx
  37. 1 1
      webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx
  38. 1 1
      webview-ui/src/components/history/__tests__/CopyButton.spec.tsx
  39. 1 1
      webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx
  40. 1 1
      webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx
  41. 1 1
      webview-ui/src/components/history/__tests__/ExportButton.spec.tsx
  42. 1 1
      webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx
  43. 1 1
      webview-ui/src/components/history/__tests__/HistoryView.spec.tsx
  44. 1 1
      webview-ui/src/components/history/__tests__/TaskItem.spec.tsx
  45. 1 1
      webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx
  46. 1 1
      webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx
  47. 1 1
      webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx
  48. 1 1
      webview-ui/src/components/marketplace/MarketplaceView.tsx
  49. 2 2
      webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx
  50. 1 1
      webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx
  51. 44 46
      webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx
  52. 1 1
      webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx
  53. 1 1
      webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx
  54. 2 2
      webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx
  55. 23 21
      webview-ui/src/components/mcp/McpToolRow.tsx
  56. 1 1
      webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx
  57. 119 111
      webview-ui/src/components/modes/ModesView.tsx
  58. 1 1
      webview-ui/src/components/modes/__tests__/ModesView.spec.tsx
  59. 57 50
      webview-ui/src/components/settings/ApiConfigManager.tsx
  60. 15 15
      webview-ui/src/components/settings/AutoApproveToggle.tsx
  61. 15 8
      webview-ui/src/components/settings/PromptsSettings.tsx
  62. 19 17
      webview-ui/src/components/settings/SettingsView.tsx
  63. 2 1
      webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx
  64. 2 1
      webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx
  65. 1 1
      webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx
  66. 1 1
      webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx
  67. 1 1
      webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx
  68. 1 1
      webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx
  69. 2 1
      webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx
  70. 1 1
      webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx
  71. 1 1
      webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx
  72. 7 6
      webview-ui/src/components/settings/providers/Bedrock.tsx
  73. 55 47
      webview-ui/src/components/settings/providers/OpenAICompatible.tsx
  74. 1 1
      webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx
  75. 2 1
      webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx
  76. 1 1
      webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx
  77. 182 0
      webview-ui/src/components/ui/__tests__/tooltip.spec.tsx
  78. 1 0
      webview-ui/src/components/ui/index.ts
  79. 21 17
      webview-ui/src/components/ui/select-dropdown.tsx
  80. 69 0
      webview-ui/src/components/ui/standard-tooltip.tsx
  81. 4 1
      webview-ui/src/components/ui/tooltip.tsx
  82. 1 1
      webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx
  83. 1 1
      webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx
  84. 1 1
      webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx
  85. 21 0
      webview-ui/src/utils/test-utils.tsx

+ 5 - 1
webview-ui/src/App.tsx

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

+ 1 - 1
webview-ui/src/__tests__/App.spec.tsx

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

+ 16 - 12
webview-ui/src/__tests__/ContextWindowProgress.spec.tsx

@@ -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()
 	})
 })

+ 1 - 1
webview-ui/src/components/account/__tests__/AccountView.spec.tsx

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

+ 21 - 18
webview-ui/src/components/chat/ChatTextArea.tsx

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

+ 36 - 30
webview-ui/src/components/chat/ChatView.tsx

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

+ 14 - 12
webview-ui/src/components/chat/CodebaseSearchResult.tsx

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

+ 55 - 48
webview-ui/src/components/chat/ContextWindowProgress.tsx

@@ -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>
+								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}%` }}>
-							{/* Invisible overlay for reserved section */}
+							{/* Container for reserved tokens */}
 							<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>
+								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}%` }}>
-								{/* Invisible overlay for available space */}
+							{/* Empty section (if any) */}
+							{availablePercent > 0 && (
 								<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"
-								/>
-							</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>
 		</>

+ 14 - 13
webview-ui/src/components/chat/FollowUpSuggest.tsx

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

+ 4 - 2
webview-ui/src/components/chat/IconButton.tsx

@@ -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>
 }

+ 25 - 23
webview-ui/src/components/chat/Markdown.tsx

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

+ 23 - 20
webview-ui/src/components/chat/ModeSelector.tsx

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

+ 21 - 18
webview-ui/src/components/chat/ShareButton.tsx

@@ -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 */}

+ 14 - 16
webview-ui/src/components/chat/TaskHeader.tsx

@@ -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 && (

+ 1 - 1
webview-ui/src/components/chat/__tests__/Announcement.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/BatchFilePermission.spec.tsx

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

+ 2 - 2
webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/ChatView.auto-approve.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/ChatView.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/IndexingStatusBadge.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/ModeSelector.spec.tsx

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

+ 1 - 1
webview-ui/src/components/chat/__tests__/ShareButton.spec.tsx

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

+ 58 - 27
webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx

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

+ 17 - 6
webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx

@@ -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()
 	})
 })

+ 11 - 11
webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx

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

+ 49 - 41
webview-ui/src/components/common/CodeBlock.tsx

@@ -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)
-
-									// 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" })
-
-												// 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>
+							<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)
+
+										// 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,
+										)
+									}}>
+									{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>

+ 11 - 3
webview-ui/src/components/common/IconButton.tsx

@@ -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
 }

+ 39 - 28
webview-ui/src/components/common/MermaidActionButtons.tsx

@@ -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")}
 				/>
+				<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 && (
+				<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()
 					}}
-					title={t("common:mermaid.buttons.viewCode")}
-				/>
-				<IconButton
-					icon={copyFeedback ? "check" : "copy"}
-					onClick={onCopy}
-					title={t("common:mermaid.buttons.copy")}
 				/>
-			</>
-		)
-	}
-
-	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")} />}
+			</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>
+			)}
 		</>
 	)
 }

+ 18 - 18
webview-ui/src/components/common/MermaidButton.tsx

@@ -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}
 							/>
+							<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>
+						</>
+					) : (
+						<StandardTooltip content={t("common:mermaid.buttons.copy")}>
 							<IconButton
 								icon={copyFeedback ? "check" : "copy"}
-								onClick={handleCopy}
-								title={t("common:mermaid.buttons.copy")}
+								onClick={(e) => {
+									e.stopPropagation()
+									copyWithFeedback(code, e)
+								}}
 							/>
-							<IconButton icon="save" onClick={handleSave} title={t("common:mermaid.buttons.save")} />
-						</>
-					) : (
-						<IconButton
-							icon={copyFeedback ? "check" : "copy"}
-							onClick={(e) => {
-								e.stopPropagation()
-								copyWithFeedback(code, e)
-							}}
-							title={t("common:mermaid.buttons.copy")}
-						/>
+						</StandardTooltip>
 					)}
 				</div>
 			</Modal>

+ 19 - 16
webview-ui/src/components/common/ZoomControls.tsx

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

+ 11 - 5
webview-ui/src/components/common/__tests__/CodeBlock.spec.tsx

@@ -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)
+			})
+		}
 	})
 })

+ 1 - 1
webview-ui/src/components/common/__tests__/MarkdownBlock.spec.tsx

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

+ 11 - 10
webview-ui/src/components/history/CopyButton.tsx

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

+ 11 - 10
webview-ui/src/components/history/DeleteButton.tsx

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

+ 11 - 10
webview-ui/src/components/history/ExportButton.tsx

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

+ 22 - 11
webview-ui/src/components/history/HistoryView.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/BatchDeleteTaskDialog.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/CopyButton.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/DeleteButton.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/DeleteTaskDialog.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/ExportButton.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/HistoryView.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/TaskItem.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/TaskItemFooter.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/TaskItemHeader.spec.tsx

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

+ 1 - 1
webview-ui/src/components/history/__tests__/useTaskSearch.spec.tsx

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

+ 1 - 1
webview-ui/src/components/marketplace/MarketplaceView.tsx

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

+ 2 - 2
webview-ui/src/components/marketplace/__tests__/MarketplaceListView.spec.tsx

@@ -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>,

+ 1 - 1
webview-ui/src/components/marketplace/__tests__/MarketplaceView.spec.tsx

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

+ 44 - 46
webview-ui/src/components/marketplace/components/MarketplaceItemCard.tsx

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

+ 1 - 1
webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal-optional-params.spec.tsx

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

+ 1 - 1
webview-ui/src/components/marketplace/components/__tests__/MarketplaceInstallModal.spec.tsx

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

+ 2 - 2
webview-ui/src/components/marketplace/components/__tests__/MarketplaceItemCard.spec.tsx

@@ -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", () => {

+ 23 - 21
webview-ui/src/components/mcp/McpToolRow.tsx

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

+ 1 - 1
webview-ui/src/components/mcp/__tests__/McpToolRow.spec.tsx

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

+ 119 - 111
webview-ui/src/components/modes/ModesView.tsx

@@ -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 */}

+ 1 - 1
webview-ui/src/components/modes/__tests__/ModesView.spec.tsx

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

+ 57 - 50
webview-ui/src/components/settings/ApiConfigManager.tsx

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

+ 15 - 15
webview-ui/src/components/settings/AutoApproveToggle.tsx

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

+ 15 - 8
webview-ui/src/components/settings/PromptsSettings.tsx

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

+ 19 - 17
webview-ui/src/components/settings/SettingsView.tsx

@@ -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 */}

+ 2 - 1
webview-ui/src/components/settings/__tests__/ApiConfigManager.spec.tsx

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

+ 2 - 1
webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx

@@ -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>,

+ 1 - 1
webview-ui/src/components/settings/__tests__/AutoApproveToggle.spec.tsx

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

+ 1 - 1
webview-ui/src/components/settings/__tests__/CodeIndexSettings.spec.tsx

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

+ 1 - 1
webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx

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

+ 1 - 1
webview-ui/src/components/settings/__tests__/ModelPicker.spec.tsx

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

+ 2 - 1
webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx

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

+ 1 - 1
webview-ui/src/components/settings/__tests__/TemperatureControl.spec.tsx

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

+ 1 - 1
webview-ui/src/components/settings/__tests__/ThinkingBudget.spec.tsx

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

+ 7 - 6
webview-ui/src/components/settings/providers/Bedrock.tsx

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

+ 55 - 47
webview-ui/src/components/settings/providers/OpenAICompatible.tsx

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

+ 1 - 1
webview-ui/src/components/settings/providers/__tests__/Bedrock.spec.tsx

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

+ 2 - 1
webview-ui/src/components/settings/providers/__tests__/OpenAICompatible.spec.tsx

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

+ 1 - 1
webview-ui/src/components/ui/__tests__/select-dropdown.spec.tsx

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

+ 182 - 0
webview-ui/src/components/ui/__tests__/tooltip.spec.tsx

@@ -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 },
+		)
+	})
+})

+ 1 - 0
webview-ui/src/components/ui/index.ts

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

+ 21 - 17
webview-ui/src/components/ui/select-dropdown.tsx

@@ -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}

+ 69 - 0
webview-ui/src/components/ui/standard-tooltip.tsx

@@ -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>
+	)
+}

+ 4 - 1
webview-ui/src/components/ui/tooltip.tsx

@@ -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}

+ 1 - 1
webview-ui/src/components/welcome/__tests__/RooTips.spec.tsx

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

+ 1 - 1
webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx

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

+ 1 - 1
webview-ui/src/i18n/__tests__/TranslationContext.spec.tsx

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

+ 21 - 0
webview-ui/src/utils/test-utils.tsx

@@ -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 }