feat(webview): persistent NEW badge over version indicator with cross-session acknowledgement

This commit is contained in:
Roo Code 2025-08-21 15:18:05 +00:00
parent 6fd261d3b6
commit 0b8d41e617
8 changed files with 58 additions and 4 deletions

View file

@ -39,6 +39,8 @@ export const globalSettingsSchema = z.object({
pinnedApiConfigs: z.record(z.string(), z.boolean()).optional(),
lastShownAnnouncementId: z.string().optional(),
// Tracks when the user has manually acknowledged the announcement by clicking the version indicator
lastAcknowledgedAnnouncementId: z.string().optional(),
customInstructions: z.string().optional(),
taskHistory: z.array(historyItemSchema).optional(),

View file

@ -1670,6 +1670,7 @@ export class ClineProvider
const {
apiConfiguration,
lastShownAnnouncementId,
lastAcknowledgedAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowReadOnlyOutsideWorkspace,
@ -1800,6 +1801,8 @@ export class ClineProvider
enableCheckpoints: enableCheckpoints ?? true,
shouldShowAnnouncement:
telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
// Badge should persist across sessions until manually acknowledged by clicking version indicator
shouldShowAnnouncementBadge: lastAcknowledgedAnnouncementId !== this.latestAnnouncementId,
allowedCommands: mergedAllowedCommands,
deniedCommands: mergedDeniedCommands,
soundVolume: soundVolume ?? 0.5,
@ -1963,6 +1966,7 @@ export class ClineProvider
return {
apiConfiguration: providerSettings,
lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
lastAcknowledgedAnnouncementId: stateValues.lastAcknowledgedAnnouncementId,
customInstructions: stateValues.customInstructions,
apiModelId: stateValues.apiModelId,
alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,

View file

@ -381,6 +381,11 @@ export const webviewMessageHandler = async (
await updateGlobalState("lastShownAnnouncementId", provider.latestAnnouncementId)
await provider.postStateToWebview()
break
case "didAcknowledgeAnnouncement":
// Persist user's manual acknowledgement so the badge clears across sessions
await updateGlobalState("lastAcknowledgedAnnouncementId", provider.latestAnnouncementId)
await provider.postStateToWebview()
break
case "selectImages":
const images = await selectImages()
await provider.postMessageToWebview({

View file

@ -308,6 +308,8 @@ export type ExtensionState = Pick<
renderContext: "sidebar" | "editor"
settingsImportedAt?: number
historyPreviewCollapsed?: boolean
// Whether to show the persistent badge over the version indicator (cross-session)
shouldShowAnnouncementBadge?: boolean
cloudUserInfo: CloudUserInfo | null
cloudIsAuthenticated: boolean

View file

@ -52,6 +52,7 @@ export interface WebviewMessage {
| "terminalOperation"
| "clearTask"
| "didShowAnnouncement"
| "didAcknowledgeAnnouncement"
| "selectImages"
| "exportCurrentTask"
| "shareCurrentTask"

View file

@ -74,12 +74,16 @@ const App = () => {
cloudApiUrl,
renderContext,
mdmCompliant,
// New: persisted badge state from extension
shouldShowAnnouncementBadge,
} = useExtensionState()
// Create a persistent state manager
const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), [])
const [showAnnouncement, setShowAnnouncement] = useState(false)
// Badge indicator over version number that persists until user manually clicks version
const [hasNewAnnouncementBadge, setHasNewAnnouncementBadge] = useState(false)
const [tab, setTab] = useState<Tab>("chat")
const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
@ -179,11 +183,24 @@ const App = () => {
useEffect(() => {
if (shouldShowAnnouncement) {
// Auto-open the announcement modal when a new announcement triggers
setShowAnnouncement(true)
// Set the badge to persist until the user manually clicks the version indicator
setHasNewAnnouncementBadge(true)
// Notify extension that the announcement was shown (to prevent re-triggering)
vscode.postMessage({ type: "didShowAnnouncement" })
}
}, [shouldShowAnnouncement])
// Sync local badge state from extension state for cross-session persistence
useEffect(() => {
// Only update if the extension indicates we should show the badge
// This allows local state to clear immediately after user clicks
if (shouldShowAnnouncementBadge) {
setHasNewAnnouncementBadge(true)
}
}, [shouldShowAnnouncementBadge])
useEffect(() => {
if (didHydrateState) {
telemetryClient.updateTelemetryState(telemetrySetting, telemetryKey, machineId)
@ -259,6 +276,12 @@ const App = () => {
isHidden={tab !== "chat"}
showAnnouncement={showAnnouncement}
hideAnnouncement={() => setShowAnnouncement(false)}
hasNewAnnouncement={hasNewAnnouncementBadge}
onVersionIndicatorClick={() => {
setHasNewAnnouncementBadge(false)
// Persist acknowledgement so badge stays cleared across reloads
vscode.postMessage({ type: "didAcknowledgeAnnouncement" })
}}
/>
<MemoizedHumanRelayDialog
isOpen={humanRelayDialogState.isOpen}

View file

@ -62,6 +62,10 @@ export interface ChatViewProps {
isHidden: boolean
showAnnouncement: boolean
hideAnnouncement: () => void
// When true, shows a "NEW" badge over the version indicator until the user clicks it
hasNewAnnouncement?: boolean
// Called when the user clicks the version indicator (used to acknowledge the announcement)
onVersionIndicatorClick?: () => void
}
export interface ChatViewRef {
@ -73,7 +77,7 @@ export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewProps> = (
{ isHidden, showAnnouncement, hideAnnouncement },
{ isHidden, showAnnouncement, hideAnnouncement, hasNewAnnouncement = false, onVersionIndicatorClick },
ref,
) => {
const isMountedRef = useRef(true)
@ -1827,7 +1831,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
className={` w-full flex flex-col gap-4 m-auto ${isExpanded && tasks.length > 0 ? "mt-0" : ""} px-3.5 min-[370px]:px-10 pt-5 transition-all duration-300`}>
{/* Version indicator in top-right corner - only on welcome screen */}
<VersionIndicator
onClick={() => setShowAnnouncementModal(true)}
onClick={() => {
setShowAnnouncementModal(true)
onVersionIndicatorClick?.()
}}
showBadge={hasNewAnnouncement}
className="absolute top-2 right-3 z-10"
/>

View file

@ -5,17 +5,26 @@ import { Package } from "@roo/package"
interface VersionIndicatorProps {
onClick: () => void
className?: string
// When true, renders a small "NEW" badge over the version button
showBadge?: boolean
}
const VersionIndicator: React.FC<VersionIndicatorProps> = ({ onClick, className = "" }) => {
const VersionIndicator: React.FC<VersionIndicatorProps> = ({ onClick, className = "", showBadge = false }) => {
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 ${className}`}
className={`relative inline-flex items-center text-xs text-vscode-descriptionForeground hover:text-vscode-foreground transition-colors cursor-pointer px-2 py-1 rounded border ${className}`}
aria-label={t("chat:versionIndicator.ariaLabel", { version: Package.version })}>
v{Package.version}
{showBadge && (
<span
// The badge uses VS Code theme variables to blend with light/dark themes
className="absolute -top-1.5 -right-1.5 rounded-full bg-vscode-button-background text-vscode-button-foreground text-[10px] leading-none px-1.5 py-0.5 shadow ring-2 ring-vscode-editor-background select-none">
NEW
</span>
)}
</button>
)
}