feat: add persistent version indicator on chat screen (#5115) (#5128)

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-06-30 08:53:55 -06:00 committed by GitHub
parent 000a9e5268
commit 904e956179
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 209 additions and 1 deletions

View file

@ -33,6 +33,7 @@ import RooTips from "@src/components/welcome/RooTips"
import { StandardTooltip } from "@src/components/ui"
import TelemetryBanner from "../common/TelemetryBanner"
import VersionIndicator from "../common/VersionIndicator"
import { useTaskSearch } from "../history/useTaskSearch"
import HistoryPreview from "../history/HistoryPreview"
import Announcement from "./Announcement"
@ -151,6 +152,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const [wasStreaming, setWasStreaming] = useState<boolean>(false)
const [showCheckpointWarning, setShowCheckpointWarning] = useState<boolean>(false)
const [isCondensing, setIsCondensing] = useState<boolean>(false)
const [showAnnouncementModal, setShowAnnouncementModal] = useState(false)
const everVisibleMessagesTsRef = useRef<LRUCache<number, boolean>>(
new LRUCache({
max: 250,
@ -1365,7 +1367,21 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return (
<div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
{showAnnouncement && <Announcement hideAnnouncement={hideAnnouncement} />}
{/* Version indicator in top-right corner */}
<VersionIndicator onClick={() => setShowAnnouncementModal(true)} className="absolute top-2 right-3 z-10" />
{(showAnnouncement || showAnnouncementModal) && (
<Announcement
hideAnnouncement={() => {
if (showAnnouncementModal) {
setShowAnnouncementModal(false)
}
if (showAnnouncement) {
hideAnnouncement()
}
}}
/>
)}
{task ? (
<>
<TaskHeader

View file

@ -61,6 +61,55 @@ vi.mock("../AutoApproveMenu", () => ({
default: () => null,
}))
vi.mock("@src/components/common/VersionIndicator", () => ({
default: function MockVersionIndicator({ onClick, className }: { onClick: () => void; className?: string }) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react")
return React.createElement(
"button",
{
onClick,
className,
"aria-label": "chat:versionIndicator.ariaLabel",
"data-testid": "version-indicator",
},
"v3.21.5",
)
},
}))
vi.mock("@src/components/modals/Announcement", () => ({
default: function MockAnnouncement({ hideAnnouncement }: { hideAnnouncement: () => void }) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const React = require("react")
return React.createElement(
"div",
{ "data-testid": "announcement-modal" },
React.createElement("div", null, "What's New"),
React.createElement("button", { onClick: hideAnnouncement }, "Close"),
)
},
}))
// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string, options?: any) => {
if (key === "chat:versionIndicator.ariaLabel" && options?.version) {
return `Version ${options.version}`
}
return key
},
}),
initReactI18next: {
type: "3rdParty",
init: () => {},
},
Trans: ({ i18nKey, children }: { i18nKey: string; children?: React.ReactNode }) => {
return <>{children || i18nKey}</>
},
}))
interface ChatTextAreaProps {
onSend: (value: string) => void
inputValue?: string
@ -1068,3 +1117,69 @@ describe("ChatView - Focus Grabbing Tests", () => {
expect(mockFocus).toHaveBeenCalledTimes(FOCUS_CALLS_ON_INIT)
})
})
describe("ChatView - Version Indicator Tests", () => {
beforeEach(() => vi.clearAllMocks())
it("displays version indicator button", () => {
const { getByLabelText } = renderChatView()
// First hydrate state
mockPostMessage({
clineMessages: [],
})
// Check that version indicator is displayed
const versionButton = getByLabelText(/version/i)
expect(versionButton).toBeInTheDocument()
expect(versionButton).toHaveTextContent(/^v\d+\.\d+\.\d+/)
})
it("opens announcement modal when version indicator is clicked", () => {
const { container } = renderChatView()
// First hydrate state
mockPostMessage({
clineMessages: [],
})
// Find version indicator
const versionButton = container.querySelector('button[aria-label*="version"]') as HTMLButtonElement
expect(versionButton).toBeTruthy()
// Click should trigger modal - we'll just verify the button exists and is clickable
// The actual modal rendering is handled by the component state
expect(versionButton.onclick).toBeDefined()
})
it("version indicator has correct styling classes", () => {
const { getByTestId } = renderChatView()
// First hydrate state
mockPostMessage({
clineMessages: [],
})
// Check styling classes - the VersionIndicator component receives className prop
const versionButton = getByTestId("version-indicator")
expect(versionButton).toBeInTheDocument()
// The className is passed as a prop to VersionIndicator
expect(versionButton.className).toContain("absolute top-2 right-3 z-10")
})
it("version indicator has proper accessibility attributes", () => {
const { container } = renderChatView()
// First hydrate state
mockPostMessage({
clineMessages: [],
})
// Check accessibility - find button by its content
const versionButton = container.querySelector('button[aria-label*="version"]')
expect(versionButton).toBeTruthy()
expect(versionButton).toHaveAttribute("aria-label")
// The mock returns the key, so we check for that
expect(versionButton?.getAttribute("aria-label")).toBe("chat:versionIndicator.ariaLabel")
})
})

View file

@ -0,0 +1,23 @@
import React from "react"
import { useTranslation } from "react-i18next"
import { Package } from "@roo/package"
interface VersionIndicatorProps {
onClick: () => void
className?: string
}
const VersionIndicator: React.FC<VersionIndicatorProps> = ({ onClick, className = "" }) => {
const { t } = useTranslation()
return (
<button
onClick={onClick}
className={`text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border border-vscode-panel-border hover:border-vscode-focusBorder ${className}`}
aria-label={t("chat:versionIndicator.ariaLabel", { version: Package.version })}>
v{Package.version}
</button>
)
}
export default VersionIndicator

View file

@ -310,5 +310,8 @@
"indexed": "Indexat",
"error": "Error d'índex",
"status": "Estat de l'índex"
},
"versionIndicator": {
"ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indiziert",
"error": "Index-Fehler",
"status": "Index-Status"
},
"versionIndicator": {
"ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indexed",
"error": "Index error",
"status": "Index status"
},
"versionIndicator": {
"ariaLabel": "Version {{version}} - Click to view release notes"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indexado",
"error": "Error de índice",
"status": "Estado del índice"
},
"versionIndicator": {
"ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indexé",
"error": "Erreur d'index",
"status": "Statut de l'index"
},
"versionIndicator": {
"ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "इंडेक्स किया गया",
"error": "इंडेक्स त्रुटि",
"status": "इंडेक्स स्थिति"
},
"versionIndicator": {
"ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें"
}
}

View file

@ -316,5 +316,8 @@
"indexed": "Terindeks",
"error": "Error indeks",
"status": "Status indeks"
},
"versionIndicator": {
"ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indicizzato",
"error": "Errore indice",
"status": "Stato indice"
},
"versionIndicator": {
"ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "インデックス作成済み",
"error": "インデックスエラー",
"status": "インデックス状態"
},
"versionIndicator": {
"ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "인덱싱 완료",
"error": "인덱스 오류",
"status": "인덱스 상태"
},
"versionIndicator": {
"ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Geïndexeerd",
"error": "Index fout",
"status": "Index status"
},
"versionIndicator": {
"ariaLabel": "Versie {{version}} - Klik om release notes te bekijken"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Zaindeksowane",
"error": "Błąd indeksu",
"status": "Status indeksu"
},
"versionIndicator": {
"ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Indexado",
"error": "Erro do índice",
"status": "Status do índice"
},
"versionIndicator": {
"ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Проиндексировано",
"error": "Ошибка индекса",
"status": "Статус индекса"
},
"versionIndicator": {
"ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "İndekslendi",
"error": "İndeks hatası",
"status": "İndeks durumu"
},
"versionIndicator": {
"ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "Đã lập chỉ mục",
"error": "Lỗi chỉ mục",
"status": "Trạng thái chỉ mục"
},
"versionIndicator": {
"ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "已索引",
"error": "索引错误",
"status": "索引状态"
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 点击查看发布说明"
}
}

View file

@ -310,5 +310,8 @@
"indexed": "已索引",
"error": "索引錯誤",
"status": "索引狀態"
},
"versionIndicator": {
"ariaLabel": "版本 {{version}} - 點擊查看發布說明"
}
}