mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
parent
c797c9a1d1
commit
3318366b66
85 changed files with 1242 additions and 798 deletions
|
|
@ -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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"
|
|||
|
||||
import { formatLargeNumber } from "@/utils/format"
|
||||
import { calculateTokenDistribution } from "@/utils/model-utils"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
|
||||
interface ContextWindowProgressProps {
|
||||
contextWindow: number
|
||||
|
|
@ -26,64 +27,70 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
|
|||
const safeContextWindow = Math.max(0, contextWindow)
|
||||
const safeContextTokens = Math.max(0, contextTokens)
|
||||
|
||||
// Combine all tooltip content into a single tooltip
|
||||
const tooltipContent = (
|
||||
<div className="space-y-1">
|
||||
<div>
|
||||
{t("chat:tokenProgress.tokensUsed", {
|
||||
used: formatLargeNumber(safeContextTokens),
|
||||
total: formatLargeNumber(safeContextWindow),
|
||||
})}
|
||||
</div>
|
||||
{reservedForOutput > 0 && (
|
||||
<div>
|
||||
{t("chat:tokenProgress.reservedForResponse", {
|
||||
amount: formatLargeNumber(reservedForOutput),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{availableSize > 0 && (
|
||||
<div>
|
||||
{t("chat:tokenProgress.availableSpace", {
|
||||
amount: formatLargeNumber(availableSize),
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2 flex-1 whitespace-nowrap px-2">
|
||||
<div data-testid="context-tokens-count">{formatLargeNumber(safeContextTokens)}</div>
|
||||
<div className="flex-1 relative">
|
||||
{/* Invisible overlay for hover area */}
|
||||
<div
|
||||
className="absolute w-full h-4 -top-[7px] z-5"
|
||||
title={t("chat:tokenProgress.availableSpace", { amount: formatLargeNumber(availableSize) })}
|
||||
data-testid="context-available-space"
|
||||
/>
|
||||
|
||||
{/* Main progress bar container */}
|
||||
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
|
||||
{/* Current tokens container */}
|
||||
<div className="relative h-full" style={{ width: `${currentPercent}%` }}>
|
||||
{/* Invisible overlay for current tokens section */}
|
||||
<StandardTooltip content={tooltipContent} side="top" sideOffset={8}>
|
||||
<div className="flex-1 relative">
|
||||
{/* Main progress bar container */}
|
||||
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
|
||||
{/* Current tokens container */}
|
||||
<div
|
||||
className="absolute h-4 -top-[7px] w-full z-6"
|
||||
title={t("chat:tokenProgress.tokensUsed", {
|
||||
used: formatLargeNumber(safeContextTokens),
|
||||
total: formatLargeNumber(safeContextWindow),
|
||||
})}
|
||||
data-testid="context-tokens-used"
|
||||
/>
|
||||
{/* Current tokens used - darkest */}
|
||||
<div className="h-full w-full bg-[var(--vscode-foreground)] transition-width duration-300 ease-out" />
|
||||
</div>
|
||||
|
||||
{/* Container for reserved tokens */}
|
||||
<div className="relative h-full" style={{ width: `${reservedPercent}%` }}>
|
||||
{/* Invisible overlay for reserved section */}
|
||||
<div
|
||||
className="absolute h-4 -top-[7px] w-full z-6"
|
||||
title={t("chat:tokenProgress.reservedForResponse", {
|
||||
amount: formatLargeNumber(reservedForOutput),
|
||||
})}
|
||||
data-testid="context-reserved-tokens"
|
||||
/>
|
||||
{/* Reserved for output section - medium gray */}
|
||||
<div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
|
||||
</div>
|
||||
|
||||
{/* Empty section (if any) */}
|
||||
{availablePercent > 0 && (
|
||||
<div className="relative h-full" style={{ width: `${availablePercent}%` }}>
|
||||
{/* Invisible overlay for available space */}
|
||||
<div
|
||||
className="absolute h-4 -top-[7px] w-full z-6"
|
||||
title={t("chat:tokenProgress.availableSpace", {
|
||||
amount: formatLargeNumber(availableSize),
|
||||
})}
|
||||
data-testid="context-available-space-section"
|
||||
/>
|
||||
className="relative h-full"
|
||||
style={{ width: `${currentPercent}%` }}
|
||||
data-testid="context-tokens-used">
|
||||
{/* Current tokens used - darkest */}
|
||||
<div className="h-full w-full bg-[var(--vscode-foreground)] transition-width duration-300 ease-out" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Container for reserved tokens */}
|
||||
<div
|
||||
className="relative h-full"
|
||||
style={{ width: `${reservedPercent}%` }}
|
||||
data-testid="context-reserved-tokens">
|
||||
{/* Reserved for output section - medium gray */}
|
||||
<div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
|
||||
</div>
|
||||
|
||||
{/* Empty section (if any) */}
|
||||
{availablePercent > 0 && (
|
||||
<div
|
||||
className="relative h-full"
|
||||
style={{ width: `${availablePercent}%` }}
|
||||
data-testid="context-available-space-section">
|
||||
{/* Available space - transparent */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
<div data-testid="context-window-size">{formatLargeNumber(safeContextWindow)}</div>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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,4 +1,4 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import { render, screen } from "@/utils/test-utils"
|
||||
|
||||
import { Package } from "@roo/package"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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,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,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,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,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,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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { bundledLanguages } from "shiki"
|
|||
import type { ShikiTransformer } from "shiki"
|
||||
import { ChevronDown, ChevronUp, WrapText, AlignJustify, Copy, Check } from "lucide-react"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
|
||||
export const CODE_BLOCK_BG_COLOR = "var(--vscode-editor-background, --vscode-sideBar-background, rgb(30 30 30))"
|
||||
export const WRAPPER_ALPHA = "cc" // 80% opacity
|
||||
|
|
@ -725,48 +726,55 @@ const CodeBlock = memo(
|
|||
}
|
||||
</LanguageSelect>
|
||||
{showCollapseButton && (
|
||||
<CodeBlockButton
|
||||
onClick={() => {
|
||||
// Get the current code block element
|
||||
const codeBlock = codeBlockRef.current // Capture ref early
|
||||
// Toggle window shade state
|
||||
setWindowShade(!windowShade)
|
||||
<StandardTooltip
|
||||
content={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}
|
||||
side="top">
|
||||
<CodeBlockButton
|
||||
onClick={() => {
|
||||
// Get the current code block element
|
||||
const codeBlock = codeBlockRef.current // Capture ref early
|
||||
// Toggle window shade state
|
||||
setWindowShade(!windowShade)
|
||||
|
||||
// Clear any previous timeouts
|
||||
if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current)
|
||||
if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current)
|
||||
// Clear any previous timeouts
|
||||
if (collapseTimeout1Ref.current) clearTimeout(collapseTimeout1Ref.current)
|
||||
if (collapseTimeout2Ref.current) clearTimeout(collapseTimeout2Ref.current)
|
||||
|
||||
// After UI updates, ensure code block is visible and update button position
|
||||
collapseTimeout1Ref.current = setTimeout(
|
||||
() => {
|
||||
if (codeBlock) {
|
||||
// Check if codeBlock element still exists
|
||||
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||
// After UI updates, ensure code block is visible and update button position
|
||||
collapseTimeout1Ref.current = setTimeout(
|
||||
() => {
|
||||
if (codeBlock) {
|
||||
// Check if codeBlock element still exists
|
||||
codeBlock.scrollIntoView({ behavior: "smooth", block: "nearest" })
|
||||
|
||||
// Wait for scroll to complete before updating button position
|
||||
collapseTimeout2Ref.current = setTimeout(() => {
|
||||
// updateCodeBlockButtonPosition itself should also check for refs if needed
|
||||
updateCodeBlockButtonPosition()
|
||||
collapseTimeout2Ref.current = null
|
||||
}, 50)
|
||||
}
|
||||
collapseTimeout1Ref.current = null
|
||||
},
|
||||
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
|
||||
)
|
||||
}}
|
||||
title={t(`chat:codeblock.tooltips.${windowShade ? "expand" : "collapse"}`)}>
|
||||
{windowShade ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</CodeBlockButton>
|
||||
// Wait for scroll to complete before updating button position
|
||||
collapseTimeout2Ref.current = setTimeout(() => {
|
||||
// updateCodeBlockButtonPosition itself should also check for refs if needed
|
||||
updateCodeBlockButtonPosition()
|
||||
collapseTimeout2Ref.current = null
|
||||
}, 50)
|
||||
}
|
||||
collapseTimeout1Ref.current = null
|
||||
},
|
||||
WINDOW_SHADE_SETTINGS.transitionDelayS * 1000 + 50,
|
||||
)
|
||||
}}>
|
||||
{windowShade ? <ChevronDown size={16} /> : <ChevronUp size={16} />}
|
||||
</CodeBlockButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<CodeBlockButton
|
||||
onClick={() => setWordWrap(!wordWrap)}
|
||||
title={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}>
|
||||
{wordWrap ? <AlignJustify size={16} /> : <WrapText size={16} />}
|
||||
</CodeBlockButton>
|
||||
<CodeBlockButton onClick={handleCopy} title={t("chat:codeblock.tooltips.copy_code")}>
|
||||
{showCopyFeedback ? <Check size={16} /> : <Copy size={16} />}
|
||||
</CodeBlockButton>
|
||||
<StandardTooltip
|
||||
content={t(`chat:codeblock.tooltips.${wordWrap ? "disable_wrap" : "enable_wrap"}`)}
|
||||
side="top">
|
||||
<CodeBlockButton onClick={() => setWordWrap(!wordWrap)}>
|
||||
{wordWrap ? <AlignJustify size={16} /> : <WrapText size={16} />}
|
||||
</CodeBlockButton>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("chat:codeblock.tooltips.copy_code")} side="top">
|
||||
<CodeBlockButton onClick={handleCopy}>
|
||||
{showCopyFeedback ? <Check size={16} /> : <Copy size={16} />}
|
||||
</CodeBlockButton>
|
||||
</StandardTooltip>
|
||||
</CodeBlockButtonWrapper>
|
||||
)}
|
||||
</CodeBlockContainer>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React from "react"
|
|||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { IconButton } from "./IconButton"
|
||||
import { ZoomControls } from "./ZoomControls"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
|
||||
interface MermaidActionButtonsProps {
|
||||
onZoom?: (e: React.MouseEvent) => void
|
||||
|
|
@ -40,41 +41,51 @@ export const MermaidActionButtons: React.FC<MermaidActionButtonsProps> = ({
|
|||
zoomInTitle={t("common:mermaid.buttons.zoomIn")}
|
||||
zoomOutTitle={t("common:mermaid.buttons.zoomOut")}
|
||||
/>
|
||||
<IconButton
|
||||
icon="code"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onViewCode()
|
||||
}}
|
||||
title={t("common:mermaid.buttons.viewCode")}
|
||||
/>
|
||||
<IconButton
|
||||
icon={copyFeedback ? "check" : "copy"}
|
||||
onClick={onCopy}
|
||||
title={t("common:mermaid.buttons.copy")}
|
||||
/>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.viewCode")}>
|
||||
<IconButton
|
||||
icon="code"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onViewCode()
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
|
||||
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={onCopy} />
|
||||
</StandardTooltip>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{onZoom && <IconButton icon="zoom-in" onClick={onZoom} title={t("common:mermaid.buttons.zoom")} />}
|
||||
<IconButton
|
||||
icon="code"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onViewCode()
|
||||
}}
|
||||
title={t("common:mermaid.buttons.viewCode")}
|
||||
/>
|
||||
<IconButton
|
||||
icon={copyFeedback ? "check" : "copy"}
|
||||
onClick={onCopy}
|
||||
title={t("common:mermaid.buttons.copy")}
|
||||
/>
|
||||
{onSave && <IconButton icon="save" onClick={onSave} title={t("common:mermaid.buttons.save")} />}
|
||||
{onClose && <IconButton icon="close" onClick={onClose} title={t("common:mermaid.buttons.close")} />}
|
||||
{onZoom && (
|
||||
<StandardTooltip content={t("common:mermaid.buttons.zoom")}>
|
||||
<IconButton icon="zoom-in" onClick={onZoom} />
|
||||
</StandardTooltip>
|
||||
)}
|
||||
<StandardTooltip content={t("common:mermaid.buttons.viewCode")}>
|
||||
<IconButton
|
||||
icon="code"
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onViewCode()
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
|
||||
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={onCopy} />
|
||||
</StandardTooltip>
|
||||
{onSave && (
|
||||
<StandardTooltip content={t("common:mermaid.buttons.save")}>
|
||||
<IconButton icon="save" onClick={onSave} />
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{onClose && (
|
||||
<StandardTooltip content={t("common:mermaid.buttons.close")}>
|
||||
<IconButton icon="close" onClick={onClose} />
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Modal } from "./Modal"
|
|||
import { TabButton } from "./TabButton"
|
||||
import { IconButton } from "./IconButton"
|
||||
import { ZoomControls } from "./ZoomControls"
|
||||
import { StandardTooltip } from "@/components/ui"
|
||||
|
||||
const MIN_ZOOM = 0.5
|
||||
const MAX_ZOOM = 20
|
||||
|
|
@ -160,11 +161,9 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
|
|||
</div>
|
||||
|
||||
<div className="pr-3">
|
||||
<IconButton
|
||||
icon="close"
|
||||
onClick={() => setShowModal(false)}
|
||||
title={t("common:mermaid.buttons.close")}
|
||||
/>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.close")}>
|
||||
<IconButton icon="close" onClick={() => setShowModal(false)} />
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -222,22 +221,23 @@ export function MermaidButton({ containerRef, code, isLoading, svgToPng, childre
|
|||
zoomInStep={0.2}
|
||||
zoomOutStep={-0.2}
|
||||
/>
|
||||
<IconButton
|
||||
icon={copyFeedback ? "check" : "copy"}
|
||||
onClick={handleCopy}
|
||||
title={t("common:mermaid.buttons.copy")}
|
||||
/>
|
||||
<IconButton icon="save" onClick={handleSave} title={t("common:mermaid.buttons.save")} />
|
||||
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
|
||||
<IconButton icon={copyFeedback ? "check" : "copy"} onClick={handleCopy} />
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.save")}>
|
||||
<IconButton icon="save" onClick={handleSave} />
|
||||
</StandardTooltip>
|
||||
</>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={copyFeedback ? "check" : "copy"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copyWithFeedback(code, e)
|
||||
}}
|
||||
title={t("common:mermaid.buttons.copy")}
|
||||
/>
|
||||
<StandardTooltip content={t("common:mermaid.buttons.copy")}>
|
||||
<IconButton
|
||||
icon={copyFeedback ? "check" : "copy"}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
copyWithFeedback(code, e)
|
||||
}}
|
||||
/>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,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,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,4 +1,4 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { render, screen, fireEvent } from "@/utils/test-utils"
|
||||
|
||||
import { DeleteButton } from "../DeleteButton"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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,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,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,4 +1,4 @@
|
|||
import { render, screen, fireEvent } from "@testing-library/react"
|
||||
import { render, screen, fireEvent } from "@/utils/test-utils"
|
||||
|
||||
import TaskItem from "../TaskItem"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import { render, screen } from "@/utils/test-utils"
|
||||
|
||||
import TaskItemFooter from "../TaskItemFooter"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { render, screen } from "@testing-library/react"
|
||||
import { render, screen } from "@/utils/test-utils"
|
||||
|
||||
import TaskItemHeader from "../TaskItemHeader"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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,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,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,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", () => {
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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" }}>
|
||||
|
|
|
|||
|
|
@ -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,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,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,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,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"
|
||||
|
|
|
|||
|
|
@ -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,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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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
webview-ui/src/components/ui/__tests__/tooltip.spec.tsx
Normal file
182
webview-ui/src/components/ui/__tests__/tooltip.spec.tsx
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
import { render, screen, waitFor } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../tooltip"
|
||||
import { StandardTooltip } from "../standard-tooltip"
|
||||
|
||||
describe("Tooltip", () => {
|
||||
it("should render tooltip content on hover", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText("Tooltip text")
|
||||
expect(tooltips.length).toBeGreaterThan(0)
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
|
||||
it("should apply text wrapping classes", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
This is a very long tooltip text that should wrap when it reaches the maximum width
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText(/This is a very long tooltip text/)
|
||||
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
|
||||
expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words")
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
|
||||
it("should not have overflow-hidden class", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||
<TooltipContent>Tooltip text</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText("Tooltip text")
|
||||
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
|
||||
expect(visibleTooltip).not.toHaveClass("overflow-hidden")
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("StandardTooltip", () => {
|
||||
it("should render with default delay", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<StandardTooltip content="Tooltip text">
|
||||
<button>Hover me</button>
|
||||
</StandardTooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText("Tooltip text")
|
||||
expect(tooltips.length).toBeGreaterThan(0)
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
|
||||
it("should apply custom maxWidth", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<StandardTooltip content="Long tooltip text" maxWidth={200}>
|
||||
<button>Hover me</button>
|
||||
</StandardTooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText("Long tooltip text")
|
||||
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
|
||||
expect(visibleTooltip).toHaveStyle({ maxWidth: "200px" })
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
|
||||
it("should apply custom maxWidth as string", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<StandardTooltip content="Long tooltip text" maxWidth="15rem">
|
||||
<button>Hover me</button>
|
||||
</StandardTooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText("Long tooltip text")
|
||||
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
|
||||
expect(visibleTooltip).toHaveStyle({ maxWidth: "15rem" })
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
|
||||
it("should handle long content with text wrapping", async () => {
|
||||
const user = userEvent.setup()
|
||||
const longContent =
|
||||
"This is a very long tooltip content that should definitely wrap when displayed because it exceeds the maximum width constraint"
|
||||
|
||||
render(
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<StandardTooltip content={longContent}>
|
||||
<button>Hover me</button>
|
||||
</StandardTooltip>
|
||||
</TooltipProvider>,
|
||||
)
|
||||
|
||||
const trigger = screen.getByText("Hover me")
|
||||
await user.hover(trigger)
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
const tooltips = screen.getAllByText(longContent)
|
||||
const visibleTooltip = tooltips.find((el) => el.getAttribute("role") !== "tooltip")
|
||||
expect(visibleTooltip).toHaveClass("max-w-[300px]", "break-words")
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -16,3 +16,4 @@ export * from "./select-dropdown"
|
|||
export * from "./select"
|
||||
export * from "./textarea"
|
||||
export * from "./tooltip"
|
||||
export * from "./standard-tooltip"
|
||||
|
|
|
|||
|
|
@ -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
webview-ui/src/components/ui/standard-tooltip.tsx
Normal file
69
webview-ui/src/components/ui/standard-tooltip.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import * as React from "react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip"
|
||||
|
||||
export const STANDARD_TOOLTIP_DELAY = 300
|
||||
|
||||
interface StandardTooltipProps {
|
||||
/** The element(s) that trigger the tooltip */
|
||||
children: React.ReactNode
|
||||
/** The content to display in the tooltip */
|
||||
content: React.ReactNode
|
||||
/** The preferred side of the trigger to render the tooltip */
|
||||
side?: "top" | "right" | "bottom" | "left"
|
||||
/** The preferred alignment against the trigger */
|
||||
align?: "start" | "center" | "end"
|
||||
/** Distance in pixels from the trigger */
|
||||
sideOffset?: number
|
||||
/** Additional CSS classes for the tooltip content */
|
||||
className?: string
|
||||
/** Whether the trigger should be rendered as a child */
|
||||
asChild?: boolean
|
||||
/** Maximum width of the tooltip content */
|
||||
maxWidth?: number | string
|
||||
}
|
||||
|
||||
/**
|
||||
* StandardTooltip component that enforces consistent 300ms delay across the application.
|
||||
* This component wraps the Radix UI tooltip with a standardized delay duration.
|
||||
*
|
||||
* @example
|
||||
* // Basic usage
|
||||
* <StandardTooltip content="Delete item">
|
||||
* <Button>Delete</Button>
|
||||
* </StandardTooltip>
|
||||
*
|
||||
* // With custom positioning
|
||||
* <StandardTooltip content="Long tooltip text" side="right" sideOffset={8}>
|
||||
* <IconButton icon="info" />
|
||||
* </StandardTooltip>
|
||||
*
|
||||
* @note This replaces native HTML title attributes for consistent timing.
|
||||
* @note Requires a TooltipProvider to be present in the component tree (typically at the app root).
|
||||
* @note Do not nest StandardTooltip components as this can cause UI issues.
|
||||
*/
|
||||
export function StandardTooltip({
|
||||
children,
|
||||
content,
|
||||
side = "top",
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
className,
|
||||
asChild = true,
|
||||
maxWidth,
|
||||
}: StandardTooltipProps) {
|
||||
// Don't render tooltip if content is empty or only whitespace
|
||||
if (!content || (typeof content === "string" && !content.trim())) {
|
||||
return <>{children}</>
|
||||
}
|
||||
|
||||
const style = maxWidth ? { maxWidth: typeof maxWidth === "number" ? `${maxWidth}px` : maxWidth } : undefined
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={asChild}>{children}</TooltipTrigger>
|
||||
<TooltipContent side={side} align={align} sideOffset={sideOffset} className={className} style={style}>
|
||||
{content}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,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,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,4 +1,4 @@
|
|||
import { render } from "@testing-library/react"
|
||||
import { render } from "@/utils/test-utils"
|
||||
|
||||
import TranslationProvider, { useAppTranslation } from "../TranslationContext"
|
||||
|
||||
|
|
|
|||
21
webview-ui/src/utils/test-utils.tsx
Normal file
21
webview-ui/src/utils/test-utils.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import React from "react"
|
||||
import { render, RenderOptions } from "@testing-library/react"
|
||||
import { TooltipProvider } from "@/components/ui/tooltip"
|
||||
import { STANDARD_TOOLTIP_DELAY } from "@/components/ui/standard-tooltip"
|
||||
|
||||
interface AllTheProvidersProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
const AllTheProviders = ({ children }: AllTheProvidersProps) => {
|
||||
return <TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>{children}</TooltipProvider>
|
||||
}
|
||||
|
||||
const customRender = (ui: React.ReactElement, options?: Omit<RenderOptions, "wrapper">) =>
|
||||
render(ui, { wrapper: AllTheProviders, ...options })
|
||||
|
||||
// re-export everything
|
||||
export * from "@testing-library/react"
|
||||
|
||||
// override render method
|
||||
export { customRender as render }
|
||||
Loading…
Add table
Reference in a new issue