Skip to content

Commit 4f8c968

Browse files
brunobergherroomoteBruno Bergher
authored
Roo Code Cloud Waitlist CTAs (#6104)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: Bruno Bergher <[email protected]>
1 parent 4042fb0 commit 4f8c968

File tree

25 files changed

+380
-85
lines changed

25 files changed

+380
-85
lines changed

apps/web-roo-code/next.config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ const nextConfig: NextConfig = {
2121
destination: "https://roocode.com/:path*",
2222
permanent: true,
2323
},
24+
// Redirect cloud waitlist to Notion page
25+
{
26+
source: "/cloud-waitlist",
27+
destination: "https://shard-dogwood-daf.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",
28+
permanent: false,
29+
},
2430
]
2531
},
2632
}

apps/web-roo-code/src/components/chromes/nav-bar.tsx

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -61,25 +61,31 @@ export function NavBar({ stars, downloads }: NavBarProps) {
6161
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
6262
Enterprise
6363
</Link>
64-
<a
65-
href={EXTERNAL_LINKS.SECURITY}
66-
target="_blank"
67-
rel="noopener noreferrer"
68-
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
69-
Security
70-
</a>
7164
<a
7265
href={EXTERNAL_LINKS.DOCUMENTATION}
7366
target="_blank"
7467
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
75-
Documentation
68+
Docs
7669
</a>
7770
<a
7871
href={EXTERNAL_LINKS.CAREERS}
7972
target="_blank"
8073
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
8174
Careers
8275
</a>
76+
<div className="flex items-center rounded-full bg-gradient-to-r from-blue-400 to-cyan-400 p-0.5 text-xs">
77+
<div className="rounded-full bg-background px-2 py-1.5">
78+
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-1.5">
79+
Roo Code Cloud is coming
80+
</span>
81+
<a
82+
href="/cloud-waitlist"
83+
rel="noopener noreferrer"
84+
className="font-medium text-primary hover:underline pl-1.5">
85+
Sign up
86+
</a>
87+
</div>
88+
</div>
8389
</nav>
8490

8591
<div className="hidden md:flex md:items-center md:space-x-4">
@@ -119,6 +125,19 @@ export function NavBar({ stars, downloads }: NavBarProps) {
119125
<div
120126
className={`absolute left-0 right-0 top-16 z-50 transform border-b border-border bg-background shadow-lg backdrop-blur-none transition-all duration-200 md:hidden ${isMenuOpen ? "translate-y-0 opacity-100" : "pointer-events-none -translate-y-2 opacity-0"}`}>
121127
<nav className="flex flex-col py-2">
128+
<div className="mx-5 mb-2 flex items-center rounded-full bg-gradient-to-r from-blue-400 to-cyan-400 p-0.5 text-xs">
129+
<div className="flex-grow text-center rounded-full bg-background px-2 py-1.5">
130+
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-3">
131+
Roo Code Cloud is coming
132+
</span>
133+
<a
134+
href="/cloud-waitlist"
135+
rel="noopener noreferrer"
136+
className="font-medium text-primary hover:underline pl-3">
137+
Sign up
138+
</a>
139+
</div>
140+
</div>
122141
<ScrollButton
123142
targetId="features"
124143
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
@@ -162,7 +181,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
162181
target="_blank"
163182
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
164183
onClick={() => setIsMenuOpen(false)}>
165-
Documentation
184+
Docs
166185
</a>
167186
<a
168187
href={EXTERNAL_LINKS.CAREERS}

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

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ 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 { Trans } from "react-i18next"
76
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
87
import useSound from "use-sound"
98
import { LRUCache } from "lru-cache"
@@ -32,12 +31,12 @@ import {
3231
parseCommand,
3332
} from "@src/utils/command-validation"
3433
import { useTranslation } from "react-i18next"
35-
import { buildDocLink } from "@src/utils/docLinks"
3634
import { useAppTranslation } from "@src/i18n/TranslationContext"
3735
import { useExtensionState } from "@src/context/ExtensionStateContext"
3836
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
3937
import RooHero from "@src/components/welcome/RooHero"
4038
import RooTips from "@src/components/welcome/RooTips"
39+
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"
@@ -115,6 +114,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
115114
historyPreviewCollapsed, // Added historyPreviewCollapsed
116115
soundEnabled,
117116
soundVolume,
117+
cloudIsAuthenticated,
118118
} = useExtensionState()
119119

120120
const messagesRef = useRef(messages)
@@ -1696,20 +1696,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
16961696

16971697
<RooHero />
16981698
{telemetrySetting === "unset" && <TelemetryBanner />}
1699-
<p className="text-vscode-editor-foreground leading-tight font-vscode-font-family text-center text-balance max-w-[380px] mx-auto my-0">
1700-
<Trans
1701-
i18nKey="chat:about"
1702-
components={{
1703-
DocsLink: (
1704-
<a href={buildDocLink("", "welcome")} target="_blank" rel="noopener noreferrer">
1705-
the docs
1706-
</a>
1707-
),
1708-
}}
1709-
/>
1710-
</p>
1699+
17111700
<div className="mb-2.5">
1712-
<RooTips cycle={false} />
1701+
{cloudIsAuthenticated || taskHistory.length < 4 ? <RooTips /> : <RooCloudCTA />}
17131702
</div>
17141703
{/* Show the task history preview if expanded and tasks exist */}
17151704
{taskHistory.length > 0 && isExpanded && <HistoryPreview />}

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

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,40 @@ vi.mock("@src/components/modals/Announcement", () => ({
8585
},
8686
}))
8787

88+
// Mock RooCloudCTA component
89+
vi.mock("@src/components/welcome/RooCloudCTA", () => ({
90+
default: function MockRooCloudCTA() {
91+
return (
92+
<div data-testid="roo-cloud-cta">
93+
<div>rooCloudCTA.title</div>
94+
<div>rooCloudCTA.description</div>
95+
<div>rooCloudCTA.joinWaitlist</div>
96+
</div>
97+
)
98+
},
99+
}))
100+
101+
// Mock RooTips component
102+
vi.mock("@src/components/welcome/RooTips", () => ({
103+
default: function MockRooTips() {
104+
return <div data-testid="roo-tips">Tips content</div>
105+
},
106+
}))
107+
108+
// Mock RooHero component
109+
vi.mock("@src/components/welcome/RooHero", () => ({
110+
default: function MockRooHero() {
111+
return <div data-testid="roo-hero">Hero content</div>
112+
},
113+
}))
114+
115+
// Mock TelemetryBanner component
116+
vi.mock("../common/TelemetryBanner", () => ({
117+
default: function MockTelemetryBanner() {
118+
return null // Don't render anything to avoid interference
119+
},
120+
}))
121+
88122
// Mock i18n
89123
vi.mock("react-i18next", () => ({
90124
useTranslation: () => ({
@@ -191,6 +225,8 @@ const mockPostMessage = (state: Partial<ExtensionState>) => {
191225
shouldShowAnnouncement: false,
192226
allowedCommands: [],
193227
alwaysAllowExecute: false,
228+
cloudIsAuthenticated: false,
229+
telemetrySetting: "enabled",
194230
...state,
195231
},
196232
},
@@ -1310,3 +1346,164 @@ describe("ChatView - Version Indicator Tests", () => {
13101346
expect(versionButton).not.toBeInTheDocument()
13111347
})
13121348
})
1349+
1350+
describe("ChatView - RooCloudCTA Display Tests", () => {
1351+
beforeEach(() => vi.clearAllMocks())
1352+
1353+
it("does not show RooCloudCTA when user is authenticated to Cloud", () => {
1354+
const { queryByTestId, getByTestId } = renderChatView()
1355+
1356+
// Hydrate state with user authenticated to cloud and some task history
1357+
mockPostMessage({
1358+
cloudIsAuthenticated: true,
1359+
taskHistory: [
1360+
{ id: "1", ts: Date.now() - 4000 },
1361+
{ id: "2", ts: Date.now() - 3000 },
1362+
{ id: "3", ts: Date.now() - 2000 },
1363+
{ id: "4", ts: Date.now() - 1000 },
1364+
{ id: "5", ts: Date.now() },
1365+
],
1366+
clineMessages: [], // No active task
1367+
})
1368+
1369+
// Should not show RooCloudCTA but should show RooTips
1370+
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
1371+
expect(getByTestId("roo-tips")).toBeInTheDocument()
1372+
})
1373+
1374+
it("does not show RooCloudCTA when user has only run 3 tasks in their history", () => {
1375+
const { queryByTestId, getByTestId } = renderChatView()
1376+
1377+
// Hydrate state with user not authenticated and only 3 tasks in history
1378+
mockPostMessage({
1379+
cloudIsAuthenticated: false,
1380+
taskHistory: [
1381+
{ id: "1", ts: Date.now() - 2000 },
1382+
{ id: "2", ts: Date.now() - 1000 },
1383+
{ id: "3", ts: Date.now() },
1384+
],
1385+
clineMessages: [], // No active task
1386+
})
1387+
1388+
// Should not show RooCloudCTA but should show RooTips
1389+
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
1390+
expect(getByTestId("roo-tips")).toBeInTheDocument()
1391+
})
1392+
1393+
it("shows RooCloudCTA when user is not authenticated and has run 4 or more tasks", async () => {
1394+
const { getByTestId, queryByTestId } = renderChatView()
1395+
1396+
// Hydrate state with user not authenticated and 4+ tasks in history
1397+
mockPostMessage({
1398+
cloudIsAuthenticated: false,
1399+
taskHistory: [
1400+
{ id: "1", ts: Date.now() - 3000 },
1401+
{ id: "2", ts: Date.now() - 2000 },
1402+
{ id: "3", ts: Date.now() - 1000 },
1403+
{ id: "4", ts: Date.now() },
1404+
],
1405+
clineMessages: [], // No active task
1406+
})
1407+
1408+
// Should show RooCloudCTA and not RooTips
1409+
await waitFor(() => {
1410+
expect(getByTestId("roo-cloud-cta")).toBeInTheDocument()
1411+
})
1412+
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
1413+
})
1414+
1415+
it("shows RooCloudCTA when user is not authenticated and has run 5 tasks", async () => {
1416+
const { getByTestId, queryByTestId } = renderChatView()
1417+
1418+
// Hydrate state with user not authenticated and 5 tasks in history
1419+
mockPostMessage({
1420+
cloudIsAuthenticated: false,
1421+
taskHistory: [
1422+
{ id: "1", ts: Date.now() - 4000 },
1423+
{ id: "2", ts: Date.now() - 3000 },
1424+
{ id: "3", ts: Date.now() - 2000 },
1425+
{ id: "4", ts: Date.now() - 1000 },
1426+
{ id: "5", ts: Date.now() },
1427+
],
1428+
clineMessages: [], // No active task
1429+
})
1430+
1431+
// Should show RooCloudCTA and not RooTips
1432+
await waitFor(() => {
1433+
expect(getByTestId("roo-cloud-cta")).toBeInTheDocument()
1434+
})
1435+
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
1436+
})
1437+
1438+
it("does not show RooCloudCTA when there is an active task (regardless of auth status)", async () => {
1439+
const { queryByTestId } = renderChatView()
1440+
1441+
// Hydrate state with user not authenticated, 4+ tasks, but with an active task
1442+
mockPostMessage({
1443+
cloudIsAuthenticated: false,
1444+
taskHistory: [
1445+
{ id: "1", ts: Date.now() - 3000 },
1446+
{ id: "2", ts: Date.now() - 2000 },
1447+
{ id: "3", ts: Date.now() - 1000 },
1448+
{ id: "4", ts: Date.now() },
1449+
],
1450+
clineMessages: [
1451+
{
1452+
type: "say",
1453+
say: "task",
1454+
ts: Date.now(),
1455+
text: "Active task in progress",
1456+
},
1457+
],
1458+
})
1459+
1460+
// Wait for the state to be updated and the task view to be shown
1461+
await waitFor(() => {
1462+
// Should not show RooCloudCTA when there's an active task
1463+
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
1464+
// Should not show RooTips either since the entire welcome screen is hidden during active tasks
1465+
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
1466+
// Should not show RooHero either since the entire welcome screen is hidden during active tasks
1467+
expect(queryByTestId("roo-hero")).not.toBeInTheDocument()
1468+
})
1469+
})
1470+
1471+
it("shows RooTips when user is authenticated (instead of RooCloudCTA)", () => {
1472+
const { queryByTestId, getByTestId } = renderChatView()
1473+
1474+
// Hydrate state with user authenticated to cloud
1475+
mockPostMessage({
1476+
cloudIsAuthenticated: true,
1477+
taskHistory: [
1478+
{ id: "1", ts: Date.now() - 3000 },
1479+
{ id: "2", ts: Date.now() - 2000 },
1480+
{ id: "3", ts: Date.now() - 1000 },
1481+
{ id: "4", ts: Date.now() },
1482+
],
1483+
clineMessages: [], // No active task
1484+
})
1485+
1486+
// Should not show RooCloudCTA but should show RooTips
1487+
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
1488+
expect(getByTestId("roo-tips")).toBeInTheDocument()
1489+
})
1490+
1491+
it("shows RooTips when user has fewer than 4 tasks (instead of RooCloudCTA)", () => {
1492+
const { queryByTestId, getByTestId } = renderChatView()
1493+
1494+
// Hydrate state with user not authenticated but fewer than 4 tasks
1495+
mockPostMessage({
1496+
cloudIsAuthenticated: false,
1497+
taskHistory: [
1498+
{ id: "1", ts: Date.now() - 2000 },
1499+
{ id: "2", ts: Date.now() - 1000 },
1500+
{ id: "3", ts: Date.now() },
1501+
],
1502+
clineMessages: [], // No active task
1503+
})
1504+
1505+
// Should not show RooCloudCTA but should show RooTips
1506+
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
1507+
expect(getByTestId("roo-tips")).toBeInTheDocument()
1508+
})
1509+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { useTranslation } from "react-i18next"
2+
3+
export function RooCloudCTA() {
4+
const { t } = useTranslation("chat")
5+
6+
return (
7+
<div className="border border-muted/20 px-4 py-1 text-center flex items-start gap-2">
8+
<i className="mr-1 codicon codicon-cloud text-xl! mt-2 text-vscode-descriptionForeground" />
9+
<div className="text-left">
10+
<p>
11+
<strong>{t("rooCloudCTA.title")}</strong>
12+
<br />
13+
<span>{t("rooCloudCTA.description")}</span>
14+
</p>
15+
<p>
16+
<a href="https://roocode.com/cloud-waitlist">{t("rooCloudCTA.joinWaitlist")}</a>
17+
</p>
18+
</div>
19+
</div>
20+
)
21+
}
22+
23+
export default RooCloudCTA

0 commit comments

Comments
 (0)