Update icons in chattextarea (#5520)

* Update icons in chattextarea

* fix: update icons in ChatTextArea and IndexingStatusBadge components

* fix: update Camera icon to Image and fix alignment

- Changed Camera icon to Image icon from lucide-react
- Fixed alignment issue by adjusting gap and removing extra margin
- Updated tests to work with new Lucide icon structure

* fix: revert alignment changes for Image icon

- Keep original alignment with gap-0.5 and mr-1 to match send and enhance buttons

---------

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Matt Rubens 2025-07-09 15:03:04 -04:00 committed by GitHub
parent fed603a927
commit 2b7c266546
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 81 additions and 36 deletions

View file

@ -25,9 +25,8 @@ import Thumbnails from "../common/Thumbnails"
import ModeSelector from "./ModeSelector"
import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
import ContextMenu from "./ContextMenu"
import { VolumeX, Pin, Check } from "lucide-react"
import { IconButton } from "./IconButton"
import { IndexingStatusDot } from "./IndexingStatusBadge"
import { VolumeX, Pin, Check, Image, WandSparkles, SendHorizontal } from "lucide-react"
import { IndexingStatusBadge } from "./IndexingStatusBadge"
import { cn } from "@/lib/utils"
import { usePromptHistory } from "./hooks/usePromptHistory"
@ -962,24 +961,49 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
)}
<div className="absolute top-1 right-1 z-30">
<IconButton
iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
<button
aria-label={t("chat:enhancePrompt")}
title={t("chat:enhancePrompt")}
disabled={sendingDisabled}
isLoading={isEnhancingPrompt}
onClick={handleEnhancePrompt}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-150",
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
!sendingDisabled && "cursor-pointer",
sendingDisabled &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
)}>
<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
</button>
</div>
<div className="absolute bottom-1 right-1 z-30">
<IconButton
iconClass="codicon-send"
<button
aria-label={t("chat:sendMessage")}
title={t("chat:sendMessage")}
disabled={sendingDisabled}
onClick={onSend}
className="opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground"
/>
onClick={!sendingDisabled ? onSend : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"opacity-60 hover:opacity-100 text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-150",
"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
!sendingDisabled && "cursor-pointer",
sendingDisabled &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
)}>
<SendHorizontal className="w-4 h-4" />
</button>
</div>
{!inputValue && (
@ -1145,14 +1169,28 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</div>
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
<IndexingStatusDot />
<IconButton
iconClass="codicon-device-camera"
<IndexingStatusBadge />
<button
aria-label={t("chat:addImages")}
title={t("chat:addImages")}
disabled={shouldDisableImages}
onClick={onSelectImages}
className="mr-1"
/>
onClick={!shouldDisableImages ? onSelectImages : undefined}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-foreground opacity-85",
"transition-all duration-150",
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
!shouldDisableImages && "cursor-pointer",
shouldDisableImages &&
"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
"mr-1",
)}>
<Image className="w-4 h-4" />
</button>
</div>
</div>
</div>

View file

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo } from "react"
import { Database } from "lucide-react"
import { cn } from "@src/lib/utils"
import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@/i18n/TranslationContext"
@ -6,11 +7,11 @@ import { useTooltip } from "@/hooks/useTooltip"
import { CodeIndexPopover } from "./CodeIndexPopover"
import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage"
interface IndexingStatusDotProps {
interface IndexingStatusBadgeProps {
className?: string
}
export const IndexingStatusDot: React.FC<IndexingStatusDotProps> = ({ className }) => {
export const IndexingStatusBadge: React.FC<IndexingStatusBadgeProps> = ({ className }) => {
const { t } = useAppTranslation()
const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 })
const [isHovered, setIsHovered] = useState(false)
@ -77,23 +78,23 @@ export const IndexingStatusDot: React.FC<IndexingStatusDotProps> = ({ className
handleMouseLeave()
}
// Get status color classes based on status and hover state
// Get status color classes for the badge dot
const getStatusColorClass = () => {
const statusColors = {
Standby: {
default: "bg-vscode-descriptionForeground/40",
hover: "bg-vscode-descriptionForeground/60",
default: "bg-vscode-descriptionForeground/60",
hover: "bg-vscode-descriptionForeground/80",
},
Indexing: {
default: "bg-yellow-500/40 animate-pulse",
default: "bg-yellow-500 animate-pulse",
hover: "bg-yellow-500 animate-pulse",
},
Indexed: {
default: "bg-green-500/40",
default: "bg-green-500",
hover: "bg-green-500",
},
Error: {
default: "bg-red-500/40",
default: "bg-red-500",
hover: "bg-red-500",
},
}
@ -117,12 +118,17 @@ export const IndexingStatusDot: React.FC<IndexingStatusDotProps> = ({ className
"hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
"cursor-pointer",
className,
)}
aria-label={getTooltipText()}>
{/* File search icon */}
<Database className="w-4 h-4 text-vscode-foreground" />
{/* Status dot badge */}
<span
className={cn(
"inline-block w-2 h-2 rounded-full relative z-10 transition-colors duration-200",
"absolute top-0.5 right-0.5 w-1.5 h-1.5 rounded-full transition-colors duration-200",
getStatusColorClass(),
)}
/>

View file

@ -38,8 +38,8 @@ vi.mock("@src/context/ExtensionStateContext")
const getEnhancePromptButton = () => {
return screen.getByRole("button", {
name: (_, element) => {
// Find the button with the sparkle icon
return element.querySelector(".codicon-sparkle") !== null
// Find the button with the wand sparkles icon (Lucide React)
return element.querySelector(".lucide-wand-sparkles") !== null
},
})
}
@ -154,8 +154,9 @@ describe("ChatTextArea", () => {
const enhanceButton = getEnhancePromptButton()
fireEvent.click(enhanceButton)
const loadingSpinner = screen.getByText("", { selector: ".codicon-loading" })
expect(loadingSpinner).toBeInTheDocument()
// Check if the WandSparkles icon has the animate-spin class
const animatingIcon = enhanceButton.querySelector(".animate-spin")
expect(animatingIcon).toBeInTheDocument()
})
})

View file

@ -3,7 +3,7 @@ import { render, screen, fireEvent, waitFor, act } from "@/utils/test-utils"
import { vscode } from "@src/utils/vscode"
import { IndexingStatusDot } from "../IndexingStatusBadge"
import { IndexingStatusBadge } from "../IndexingStatusBadge"
vi.mock("@/i18n/setup", () => ({
__esModule: true,
@ -104,9 +104,9 @@ vi.mock("@/i18n/TranslationContext", () => ({
}),
}))
describe("IndexingStatusDot", () => {
describe("IndexingStatusBadge", () => {
const renderComponent = (props = {}) => {
return render(<IndexingStatusDot {...props} />)
return render(<IndexingStatusBadge {...props} />)
}
beforeEach(() => {