Skip to content

Commit 469a70f

Browse files
brunobergherdaniel-lxsItsOnlyBinaryroomote[bot]roomote
authored
feat: In-extension dismissible upsells for Roo Code Cloud (#7850)
* First pass at separate upsell dialog * Revert PR #7188 - Restore temperature parameter to fix TabbyApi/ExLlamaV2 crashes (#7594) * fix: reduce CodeBlock button z-index to prevent overlap with popovers (#7783) Fixes #7703 - CodeBlock language dropdown and copy button were appearing above popovers due to z-index: 100. Reduced to z-index: 40 to maintain proper layering hierarchy while keeping buttons functional. * Make ollama models info transport work like lmstudio (#7679) * feat: add click-to-edit, ESC-to-cancel, and fix padding consistency for chat messages (#7790) * feat: add click-to-edit, ESC-to-cancel, and fix padding consistency - Enable click-to-edit for past messages by making message text clickable - Add ESC key handler to cancel edit mode in ChatTextArea - Fix padding consistency between past and queued message editors - Adjust right padding for edit mode to accommodate cancel button Fixes #7788 * fix: adjust padding and layout for ChatTextArea in edit mode * refactor: replace hardcoded pr-[72px] with standard Tailwind pr-20 class --------- Co-authored-by: Roo Code <[email protected]> Co-authored-by: Hannes Rudolph <[email protected]> Co-authored-by: daniel-lxs <[email protected]> * Let people paste in the auth redirect url (#7805) Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Bruno Bergher <[email protected]> * fix: resolve chat message edit/delete duplication issues (#7793) * fix: add GIT_EDITOR env var to merge-resolver mode for non-interactive rebase (#7819) * UI: Render reasoning as plain italic (match <thinking>) (#7752) Co-authored-by: Roo Code <[email protected]> Co-authored-by: Hannes Rudolph <[email protected]> Co-authored-by: daniel-lxs <[email protected]> * Add taskSyncEnabled to userSettingsConfigSchema (#7827) feat: add taskSyncEnabled to userSettingsConfigSchema Co-authored-by: Roo Code <[email protected]> * Release: v1.75.0 (#7829) chore: bump version to v1.75.0 * fix: prevent negative cost values and improve label visibility in evals chart (#7830) Co-authored-by: Roo Code <[email protected]> * Fix Groq context window display (#7839) * feat: add DismissibleUpsell component for dismissible messages - Created DismissibleUpsell component with variant support (banner/default) - Added dismissedUpsells to GlobalState for persistence - Implemented message handlers for dismissing and retrieving dismissed upsells - Added comprehensive tests for the component - Uses VSCode extension globalState for persistent storage * fix: Apply PR feedback for DismissibleUpsell component - Changed from className to separate 'id' and 'className' props for better semantics - Added i18n support for accessibility labels (aria-label and title) - Fixed memory leak by adding mounted flag to prevent state updates after unmount - Fixed race condition by sending dismiss message before hiding component - Fixed inefficient array operations in webviewMessageHandler - Added comprehensive test coverage for edge cases including: - Multiple rapid dismissals - Component unmounting during async operations - Invalid/malformed message handling - Proper message sending before unmount - Added null checks for message data to handle edge cases gracefully * New Cloud upsell dialog in task share and cloud view, shared component * Properly working DismissibleUpsell * Working upsell for long-running tasks * CTA in AutoApproveMenu * Home page CTA * Fixes the autoapprove upsell and some tests * Visual and copy fixes * Test fix * Translations * Stray className attribute * Cloud view fixes in a left-aligned layout * Removes unnecessary test * Less flaky tests * Fixes sharebutton behavior and updates associated tests * Update webview-ui/src/i18n/locales/it/cloud.json Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * Fix dismissed flicker * Fix long task upsell --------- Co-authored-by: Daniel <[email protected]> Co-authored-by: ItsOnlyBinary <[email protected]> Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com> Co-authored-by: Roo Code <[email protected]> Co-authored-by: Hannes Rudolph <[email protected]> Co-authored-by: daniel-lxs <[email protected]> Co-authored-by: Matt Rubens <[email protected]> Co-authored-by: John Richmond <[email protected]> Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent 80af39d commit 469a70f

37 files changed

+1490
-240
lines changed

packages/types/src/global-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const globalSettingsSchema = z.object({
4141
lastShownAnnouncementId: z.string().optional(),
4242
customInstructions: z.string().optional(),
4343
taskHistory: z.array(historyItemSchema).optional(),
44+
dismissedUpsells: z.array(z.string()).optional(),
4445

4546
// Image generation settings (experimental) - flattened for simplicity
4647
openRouterImageApiKey: z.string().optional(),

src/core/webview/webviewMessageHandler.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3004,5 +3004,39 @@ export const webviewMessageHandler = async (
30043004

30053005
break
30063006
}
3007+
case "dismissUpsell": {
3008+
if (message.upsellId) {
3009+
try {
3010+
// Get current list of dismissed upsells
3011+
const dismissedUpsells = getGlobalState("dismissedUpsells") || []
3012+
3013+
// Add the new upsell ID if not already present
3014+
let updatedList = dismissedUpsells
3015+
if (!dismissedUpsells.includes(message.upsellId)) {
3016+
updatedList = [...dismissedUpsells, message.upsellId]
3017+
await updateGlobalState("dismissedUpsells", updatedList)
3018+
}
3019+
3020+
// Send updated list back to webview (use the already computed updatedList)
3021+
await provider.postMessageToWebview({
3022+
type: "dismissedUpsells",
3023+
list: updatedList,
3024+
})
3025+
} catch (error) {
3026+
// Fail silently as per Bruno's comment - it's OK to fail silently in this case
3027+
provider.log(`Failed to dismiss upsell: ${error instanceof Error ? error.message : String(error)}`)
3028+
}
3029+
}
3030+
break
3031+
}
3032+
case "getDismissedUpsells": {
3033+
// Send the current list of dismissed upsells to the webview
3034+
const dismissedUpsells = getGlobalState("dismissedUpsells") || []
3035+
await provider.postMessageToWebview({
3036+
type: "dismissedUpsells",
3037+
list: dismissedUpsells,
3038+
})
3039+
break
3040+
}
30073041
}
30083042
}

src/shared/ExtensionMessage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ export interface ExtensionMessage {
123123
| "showEditMessageDialog"
124124
| "commands"
125125
| "insertTextIntoTextarea"
126+
| "dismissedUpsells"
126127
text?: string
127128
payload?: any // Add a generic payload for now, can refine later
128129
action?:
@@ -199,6 +200,7 @@ export interface ExtensionMessage {
199200
context?: string
200201
commands?: Command[]
201202
queuedMessages?: QueuedMessage[]
203+
list?: string[] // For dismissedUpsells
202204
}
203205

204206
export type ExtensionState = Pick<
@@ -209,6 +211,7 @@ export type ExtensionState = Pick<
209211
// | "lastShownAnnouncementId"
210212
| "customInstructions"
211213
// | "taskHistory" // Optional in GlobalSettings, required here.
214+
| "dismissedUpsells"
212215
| "autoApprovalEnabled"
213216
| "alwaysAllowReadOnly"
214217
| "alwaysAllowReadOnlyOutsideWorkspace"

src/shared/WebviewMessage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ export interface WebviewMessage {
223223
| "queueMessage"
224224
| "removeQueuedMessage"
225225
| "editQueuedMessage"
226+
| "dismissUpsell"
227+
| "getDismissedUpsells"
226228
text?: string
227229
editedMessageContent?: string
228230
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
@@ -268,6 +270,8 @@ export interface WebviewMessage {
268270
visibility?: ShareVisibility // For share visibility
269271
hasContent?: boolean // For checkRulesDirectoryResult
270272
checkOnly?: boolean // For deleteCustomMode check
273+
upsellId?: string // For dismissUpsell
274+
list?: string[] // For dismissedUpsells response
271275
codeIndexSettings?: {
272276
// Global state settings
273277
codebaseIndexEnabled: boolean

webview-ui/src/components/chat/AutoApproveMenu.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import { AutoApproveToggle, AutoApproveSetting, autoApproveSettingsConfig } from
99
import { StandardTooltip } from "@src/components/ui"
1010
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
1111
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
12+
import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
13+
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
14+
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
1215

1316
interface AutoApproveMenuProps {
1417
style?: React.CSSProperties
@@ -35,7 +38,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
3538

3639
const { t } = useAppTranslation()
3740

41+
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
42+
autoOpenOnAuth: false,
43+
})
44+
3845
const baseToggles = useAutoApprovalToggles()
46+
const enabledCount = useMemo(() => Object.values(baseToggles).filter(Boolean).length, [baseToggles])
3947

4048
// AutoApproveMenu needs alwaysApproveResubmit in addition to the base toggles
4149
const toggles = useMemo(
@@ -173,6 +181,23 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
173181
</div>
174182

175183
<AutoApproveToggle {...toggles} onToggle={onAutoApproveToggle} />
184+
185+
{enabledCount > 7 && (
186+
<>
187+
<DismissibleUpsell
188+
upsellId="autoApprovePowerUserA"
189+
onClick={() => openUpsell()}
190+
dismissOnClick={false}
191+
variant="banner">
192+
<Trans
193+
i18nKey="cloud:upsell.autoApprovePowerUser"
194+
components={{
195+
learnMoreLink: <VSCodeLink href="#" />,
196+
}}
197+
/>
198+
</DismissibleUpsell>
199+
</>
200+
)}
176201
</div>
177202
)}
178203

@@ -240,6 +265,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
240265
/>
241266
</div>
242267
</div>
268+
<CloudUpsellDialog open={isOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
243269
</div>
244270
)
245271
}

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import { useDeepCompareEffect, useEvent, useMount } from "react-use"
33
import debounce from "debounce"
44
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
55
import removeMd from "remove-markdown"
6-
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
6+
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
77
import useSound from "use-sound"
88
import { LRUCache } from "lru-cache"
9-
import { useTranslation } from "react-i18next"
9+
import { Trans, useTranslation } from "react-i18next"
1010

1111
import { useDebounceEffect } from "@src/utils/useDebounceEffect"
1212
import { appendImages } from "@src/utils/imageUtils"
@@ -37,10 +37,10 @@ import { useExtensionState } from "@src/context/ExtensionStateContext"
3737
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
3838
import RooHero from "@src/components/welcome/RooHero"
3939
import RooTips from "@src/components/welcome/RooTips"
40-
import RooCloudCTA from "@src/components/welcome/RooCloudCTA"
4140
import { StandardTooltip } from "@src/components/ui"
4241
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
4342
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
43+
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
4444

4545
import TelemetryBanner from "../common/TelemetryBanner"
4646
import VersionIndicator from "../common/VersionIndicator"
@@ -56,6 +56,9 @@ import SystemPromptWarning from "./SystemPromptWarning"
5656
import ProfileViolationWarning from "./ProfileViolationWarning"
5757
import { CheckpointWarning } from "./CheckpointWarning"
5858
import { QueuedMessages } from "./QueuedMessages"
59+
import DismissibleUpsell from "../common/DismissibleUpsell"
60+
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
61+
import { Cloud } from "lucide-react"
5962

6063
export interface ChatViewProps {
6164
isHidden: boolean
@@ -208,6 +211,15 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
208211
clineAskRef.current = clineAsk
209212
}, [clineAsk])
210213

214+
const {
215+
isOpen: isUpsellOpen,
216+
openUpsell,
217+
closeUpsell,
218+
handleConnect,
219+
} = useCloudUpsell({
220+
autoOpenOnAuth: false,
221+
})
222+
211223
// Keep inputValueRef in sync with inputValue state
212224
useEffect(() => {
213225
inputValueRef.current = inputValue
@@ -1831,7 +1843,25 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
18311843
{telemetrySetting === "unset" && <TelemetryBanner />}
18321844

18331845
<div className="mb-2.5">
1834-
{cloudIsAuthenticated || taskHistory.length < 4 ? <RooTips /> : <RooCloudCTA />}
1846+
{cloudIsAuthenticated || taskHistory.length < 4 ? (
1847+
<RooTips />
1848+
) : (
1849+
<>
1850+
<DismissibleUpsell
1851+
upsellId="taskList"
1852+
icon={<Cloud className="size-4 mt-0.5 shrink-0" />}
1853+
onClick={() => openUpsell()}
1854+
dismissOnClick={false}
1855+
className="bg-vscode-editor-background p-4 !text-base">
1856+
<Trans
1857+
i18nKey="cloud:upsell.taskList"
1858+
components={{
1859+
learnMoreLink: <VSCodeLink href="#" />,
1860+
}}
1861+
/>
1862+
</DismissibleUpsell>
1863+
</>
1864+
)}
18351865
</div>
18361866
{/* Show the task history preview if expanded and tasks exist */}
18371867
{taskHistory.length > 0 && isExpanded && <HistoryPreview />}
@@ -2013,6 +2043,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
20132043
)}
20142044

20152045
<div id="roo-portal" />
2046+
<CloudUpsellDialog open={isUpsellOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
20162047
</div>
20172048
)
20182049
}

webview-ui/src/components/chat/ShareButton.tsx

Lines changed: 31 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState, useEffect, useRef } from "react"
1+
import { useState, useEffect } from "react"
22
import { useTranslation } from "react-i18next"
33
import { SquareArrowOutUpRightIcon } from "lucide-react"
44

@@ -7,6 +7,8 @@ import { type HistoryItem, type ShareVisibility, TelemetryEventName } from "@roo
77
import { vscode } from "@/utils/vscode"
88
import { telemetryClient } from "@/utils/TelemetryClient"
99
import { useExtensionState } from "@/context/ExtensionStateContext"
10+
import { useCloudUpsell } from "@/hooks/useCloudUpsell"
11+
import { CloudUpsellDialog } from "@/components/cloud/CloudUpsellDialog"
1012
import {
1113
Button,
1214
Popover,
@@ -16,10 +18,6 @@ import {
1618
CommandList,
1719
CommandItem,
1820
CommandGroup,
19-
Dialog,
20-
DialogContent,
21-
DialogHeader,
22-
DialogTitle,
2321
StandardTooltip,
2422
} from "@/components/ui"
2523

@@ -31,29 +29,34 @@ interface ShareButtonProps {
3129

3230
export const ShareButton = ({ item, disabled = false, showLabel = false }: ShareButtonProps) => {
3331
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
34-
const [connectModalOpen, setConnectModalOpen] = useState(false)
3532
const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null)
33+
const [wasConnectInitiatedFromShare, setWasConnectInitiatedFromShare] = useState(false)
3634
const { t } = useTranslation()
37-
const { sharingEnabled, cloudIsAuthenticated, cloudUserInfo } = useExtensionState()
38-
const wasUnauthenticatedRef = useRef(false)
39-
const initiatedAuthFromThisButtonRef = useRef(false)
35+
const { cloudUserInfo } = useExtensionState()
36+
37+
// Use enhanced cloud upsell hook with auto-open on auth success
38+
const {
39+
isOpen: connectModalOpen,
40+
openUpsell,
41+
closeUpsell,
42+
handleConnect,
43+
isAuthenticated: cloudIsAuthenticated,
44+
sharingEnabled,
45+
} = useCloudUpsell({
46+
onAuthSuccess: () => {
47+
// Auto-open share dropdown after successful authentication
48+
setShareDropdownOpen(true)
49+
setWasConnectInitiatedFromShare(false)
50+
},
51+
})
4052

41-
// Track authentication state changes to auto-open popover after login
53+
// Auto-open popover when user becomes authenticated after clicking Connect from share button
4254
useEffect(() => {
43-
if (!cloudIsAuthenticated || !sharingEnabled) {
44-
wasUnauthenticatedRef.current = true
45-
} else if (wasUnauthenticatedRef.current && cloudIsAuthenticated && sharingEnabled) {
46-
// Only open dropdown if auth was initiated from this button
47-
if (initiatedAuthFromThisButtonRef.current) {
48-
// User just authenticated from this share button, send telemetry, close modal, and open the popover
49-
telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_SUCCESS)
50-
setConnectModalOpen(false)
51-
setShareDropdownOpen(true)
52-
initiatedAuthFromThisButtonRef.current = false // Reset the flag
53-
}
54-
wasUnauthenticatedRef.current = false
55+
if (wasConnectInitiatedFromShare && cloudIsAuthenticated) {
56+
setShareDropdownOpen(true)
57+
setWasConnectInitiatedFromShare(false)
5558
}
56-
}, [cloudIsAuthenticated, sharingEnabled])
59+
}, [wasConnectInitiatedFromShare, cloudIsAuthenticated])
5760

5861
// Listen for share success messages from the extension
5962
useEffect(() => {
@@ -95,14 +98,9 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
9598
}
9699

97100
const handleConnectToCloud = () => {
98-
// Send telemetry for connect to cloud action
99-
telemetryClient.capture(TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED)
100-
101-
// Mark that authentication was initiated from this button
102-
initiatedAuthFromThisButtonRef.current = true
103-
vscode.postMessage({ type: "rooCloudSignIn" })
101+
setWasConnectInitiatedFromShare(true)
102+
handleConnect()
104103
setShareDropdownOpen(false)
105-
setConnectModalOpen(false)
106104
}
107105

108106
const handleShareButtonClick = () => {
@@ -111,7 +109,8 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
111109

112110
if (!cloudIsAuthenticated) {
113111
// Show modal for unauthenticated users
114-
setConnectModalOpen(true)
112+
openUpsell()
113+
telemetryClient.capture(TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED)
115114
} else {
116115
// Show popover for authenticated users
117116
setShareDropdownOpen(true)
@@ -241,43 +240,7 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
241240
)}
242241

243242
{/* Connect to Cloud Modal */}
244-
<Dialog open={connectModalOpen} onOpenChange={setConnectModalOpen}>
245-
<DialogContent className="max-w-sm">
246-
<DialogHeader className="text-center">
247-
<DialogTitle className="text-lg font-medium text-vscode-foreground">
248-
{t("cloud:cloudBenefitsTitle")}
249-
</DialogTitle>
250-
</DialogHeader>
251-
252-
<div className="flex flex-col space-y-6">
253-
<div>
254-
<p className="text-md text-vscode-descriptionForeground mb-4">
255-
{t("cloud:cloudBenefitsSubtitle")}
256-
</p>
257-
<ul className="text-sm text-vscode-descriptionForeground space-y-2">
258-
<li className="flex items-start">
259-
<span className="mr-2 text-vscode-foreground"></span>
260-
{t("cloud:cloudBenefitSharing")}
261-
</li>
262-
<li className="flex items-start">
263-
<span className="mr-2 text-vscode-foreground"></span>
264-
{t("cloud:cloudBenefitHistory")}
265-
</li>
266-
<li className="flex items-start">
267-
<span className="mr-2 text-vscode-foreground"></span>
268-
{t("cloud:cloudBenefitMetrics")}
269-
</li>
270-
</ul>
271-
</div>
272-
273-
<div className="flex flex-col gap-4">
274-
<Button onClick={handleConnectToCloud} className="w-full">
275-
{t("cloud:connect")}
276-
</Button>
277-
</div>
278-
</div>
279-
</DialogContent>
280-
</Dialog>
243+
<CloudUpsellDialog open={connectModalOpen} onOpenChange={closeUpsell} onConnect={handleConnectToCloud} />
281244
</>
282245
)
283246
}

0 commit comments

Comments
 (0)