Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),

Expand Down
4 changes: 4 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1670,6 +1670,7 @@ export class ClineProvider
const {
apiConfiguration,
lastShownAnnouncementId,
lastAcknowledgedAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
alwaysAllowReadOnlyOutsideWorkspace,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface WebviewMessage {
| "terminalOperation"
| "clearTask"
| "didShowAnnouncement"
| "didAcknowledgeAnnouncement"
| "selectImages"
| "exportCurrentTask"
| "shareCurrentTask"
Expand Down
23 changes: 23 additions & 0 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>({
Expand Down Expand Up @@ -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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The badge state syncs from extension state but could be overridden by local state changes. The comment suggests it's intentional to allow immediate local clearing, but this could create a race condition if the extension state updates at the same time. Consider using a more explicit state management pattern or documenting this behavior more clearly.

setHasNewAnnouncementBadge(true)
}
}, [shouldShowAnnouncementBadge])

useEffect(() => {
if (didHydrateState) {
telemetryClient.updateTelemetryState(telemetrySetting, telemetryKey, machineId)
Expand Down Expand Up @@ -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}
Expand Down
22 changes: 15 additions & 7 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -1810,6 +1814,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</>
) : (
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-4 relative">
{/* Version indicator in top-right corner - only on welcome screen */}
<VersionIndicator
onClick={() => {
setShowAnnouncementModal(true)
onVersionIndicatorClick?.()
}}
showBadge={hasNewAnnouncement}
className="absolute top-2 right-3 z-10"
/>

{/* Moved Task Bar Header Here */}
{tasks.length !== 0 && (
<div className="flex text-vscode-descriptionForeground w-full mx-auto px-5 pt-3">
Expand All @@ -1825,12 +1839,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
)}
<div
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)}
className="absolute top-2 right-3 z-10"
/>

<RooHero />
{telemetrySetting === "unset" && <TelemetryBanner />}

Expand Down
13 changes: 11 additions & 2 deletions webview-ui/src/components/common/VersionIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing aria-label for the badge. Screen reader users won't know what "NEW" refers to. Consider adding:

Suggested change
<span
<span
// The badge uses VS Code theme variables to blend with light/dark themes
aria-label="New announcement available"
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">

// 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">
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider extracting these badge styles to a reusable component or at least a constant. This long className string could be:

const BADGE_STYLES = "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"

This would improve maintainability and reusability.

NEW
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "NEW" text is hardcoded. Could this be made configurable or use i18n for localization? Something like t('common:badge.new') would make it more flexible for international users.

</span>
)}
</button>
)
}
Expand Down
Loading