diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index dd72d72fe9..2b5e47199c 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -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(), diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 9c28120f17..7dc7ae0530 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 4dd0fee75e..9cfc0f060f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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({ diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ebdc137432..2e1327438d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -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 diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d59ccd556c..3fd39e3d15 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -52,6 +52,7 @@ export interface WebviewMessage { | "terminalOperation" | "clearTask" | "didShowAnnouncement" + | "didAcknowledgeAnnouncement" | "selectImages" | "exportCurrentTask" | "shareCurrentTask" diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 3782242707..39f4fb989f 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -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("chat") const [humanRelayDialogState, setHumanRelayDialogState] = useState({ @@ -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" }) + }} /> 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 = ( - { isHidden, showAnnouncement, hideAnnouncement }, + { isHidden, showAnnouncement, hideAnnouncement, hasNewAnnouncement = false, onVersionIndicatorClick }, ref, ) => { const isMountedRef = useRef(true) @@ -1810,6 +1814,16 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) : (
+ {/* Version indicator in top-right corner - only on welcome screen */} + { + setShowAnnouncementModal(true) + onVersionIndicatorClick?.() + }} + showBadge={hasNewAnnouncement} + className="absolute top-2 right-3 z-10" + /> + {/* Moved Task Bar Header Here */} {tasks.length !== 0 && (
@@ -1825,12 +1839,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction 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 */} - setShowAnnouncementModal(true)} - className="absolute top-2 right-3 z-10" - /> - {telemetrySetting === "unset" && } diff --git a/webview-ui/src/components/common/VersionIndicator.tsx b/webview-ui/src/components/common/VersionIndicator.tsx index 1776a2d39a..216c904bf5 100644 --- a/webview-ui/src/components/common/VersionIndicator.tsx +++ b/webview-ui/src/components/common/VersionIndicator.tsx @@ -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 = ({ onClick, className = "" }) => { +const VersionIndicator: React.FC = ({ onClick, className = "", showBadge = false }) => { const { t } = useTranslation() return ( ) }