Skip to content
Merged
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
6 changes: 6 additions & 0 deletions apps/web-roo-code/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const nextConfig: NextConfig = {
destination: "https://roocode.com/:path*",
permanent: true,
},
// Redirect cloud waitlist to Notion page
{
source: "/cloud-waitlist",
destination: "https://shard-dogwood-daf.notion.site/238fd1401b0a8087b858e1ad431507cf?pvs=105",
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can I help set up a better domain for this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes please, it's configured in Notion Settings -> Public Pages

permanent: false,
},
]
},
}
Expand Down
37 changes: 28 additions & 9 deletions apps/web-roo-code/src/components/chromes/nav-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,25 +61,31 @@ export function NavBar({ stars, downloads }: NavBarProps) {
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Enterprise
</Link>
<a
href={EXTERNAL_LINKS.SECURITY}
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Security
</a>
<a
href={EXTERNAL_LINKS.DOCUMENTATION}
target="_blank"
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Documentation
Docs
</a>
<a
href={EXTERNAL_LINKS.CAREERS}
target="_blank"
className="text-muted-foreground transition-transform duration-200 hover:scale-105 hover:text-foreground">
Careers
</a>
<div className="flex items-center rounded-full bg-gradient-to-r from-blue-400 to-cyan-400 p-0.5 text-xs">
<div className="rounded-full bg-background px-2 py-1.5">
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-1.5">
Roo Code Cloud is coming
</span>
<a
href="/cloud-waitlist"
rel="noopener noreferrer"
className="font-medium text-primary hover:underline pl-1.5">
Sign up
</a>
</div>
</div>
</nav>

<div className="hidden md:flex md:items-center md:space-x-4">
Expand Down Expand Up @@ -119,6 +125,19 @@ export function NavBar({ stars, downloads }: NavBarProps) {
<div
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"}`}>
<nav className="flex flex-col py-2">
<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">
<div className="flex-grow text-center rounded-full bg-background px-2 py-1.5">
<span className="text-muted-foreground border-r-2 border-foreground/50 pr-3">
Roo Code Cloud is coming
</span>
<a
href="/cloud-waitlist"
rel="noopener noreferrer"
className="font-medium text-primary hover:underline pl-3">
Sign up
</a>
</div>
</div>
<ScrollButton
targetId="features"
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
Expand Down Expand Up @@ -162,7 +181,7 @@ export function NavBar({ stars, downloads }: NavBarProps) {
target="_blank"
className="w-full px-8 py-3 text-left text-sm font-medium text-foreground/80 transition-colors hover:bg-accent hover:text-foreground"
onClick={() => setIsMenuOpen(false)}>
Documentation
Docs
</a>
<a
href={EXTERNAL_LINKS.CAREERS}
Expand Down
19 changes: 4 additions & 15 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { useDeepCompareEffect, useEvent, useMount } from "react-use"
import debounce from "debounce"
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
import removeMd from "remove-markdown"
import { Trans } from "react-i18next"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import useSound from "use-sound"
import { LRUCache } from "lru-cache"
Expand Down Expand Up @@ -32,12 +31,12 @@ import {
parseCommand,
} from "@src/utils/command-validation"
import { useTranslation } from "react-i18next"
import { buildDocLink } from "@src/utils/docLinks"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
import RooHero from "@src/components/welcome/RooHero"
import RooTips from "@src/components/welcome/RooTips"
import RooCloudCTA from "@src/components/welcome/RooCloudCTA"
import { StandardTooltip } from "@src/components/ui"
import { useAutoApprovalState } from "@src/hooks/useAutoApprovalState"
import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
Expand Down Expand Up @@ -115,6 +114,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
historyPreviewCollapsed, // Added historyPreviewCollapsed
soundEnabled,
soundVolume,
cloudIsAuthenticated,
} = useExtensionState()

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

<RooHero />
{telemetrySetting === "unset" && <TelemetryBanner />}
<p className="text-vscode-editor-foreground leading-tight font-vscode-font-family text-center text-balance max-w-[380px] mx-auto my-0">
<Trans
i18nKey="chat:about"
components={{
DocsLink: (
<a href={buildDocLink("", "welcome")} target="_blank" rel="noopener noreferrer">
the docs
</a>
),
}}
/>
</p>

<div className="mb-2.5">
<RooTips cycle={false} />
{cloudIsAuthenticated || taskHistory.length < 4 ? <RooTips /> : <RooCloudCTA />}
</div>
{/* Show the task history preview if expanded and tasks exist */}
{taskHistory.length > 0 && isExpanded && <HistoryPreview />}
Expand Down
197 changes: 197 additions & 0 deletions webview-ui/src/components/chat/__tests__/ChatView.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,40 @@ vi.mock("@src/components/modals/Announcement", () => ({
},
}))

// Mock RooCloudCTA component
vi.mock("@src/components/welcome/RooCloudCTA", () => ({
default: function MockRooCloudCTA() {
return (
<div data-testid="roo-cloud-cta">
<div>rooCloudCTA.title</div>
<div>rooCloudCTA.description</div>
<div>rooCloudCTA.joinWaitlist</div>
</div>
)
},
}))

// Mock RooTips component
vi.mock("@src/components/welcome/RooTips", () => ({
default: function MockRooTips() {
return <div data-testid="roo-tips">Tips content</div>
},
}))

// Mock RooHero component
vi.mock("@src/components/welcome/RooHero", () => ({
default: function MockRooHero() {
return <div data-testid="roo-hero">Hero content</div>
},
}))

// Mock TelemetryBanner component
vi.mock("../common/TelemetryBanner", () => ({
default: function MockTelemetryBanner() {
return null // Don't render anything to avoid interference
},
}))

// Mock i18n
vi.mock("react-i18next", () => ({
useTranslation: () => ({
Expand Down Expand Up @@ -191,6 +225,8 @@ const mockPostMessage = (state: Partial<ExtensionState>) => {
shouldShowAnnouncement: false,
allowedCommands: [],
alwaysAllowExecute: false,
cloudIsAuthenticated: false,
telemetrySetting: "enabled",
...state,
},
},
Expand Down Expand Up @@ -1310,3 +1346,164 @@ describe("ChatView - Version Indicator Tests", () => {
expect(versionButton).not.toBeInTheDocument()
})
})

describe("ChatView - RooCloudCTA Display Tests", () => {
beforeEach(() => vi.clearAllMocks())

it("does not show RooCloudCTA when user is authenticated to Cloud", () => {
const { queryByTestId, getByTestId } = renderChatView()

// Hydrate state with user authenticated to cloud and some task history
mockPostMessage({
cloudIsAuthenticated: true,
taskHistory: [
{ id: "1", ts: Date.now() - 4000 },
{ id: "2", ts: Date.now() - 3000 },
{ id: "3", ts: Date.now() - 2000 },
{ id: "4", ts: Date.now() - 1000 },
{ id: "5", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should not show RooCloudCTA but should show RooTips
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
expect(getByTestId("roo-tips")).toBeInTheDocument()
})

it("does not show RooCloudCTA when user has only run 3 tasks in their history", () => {
const { queryByTestId, getByTestId } = renderChatView()

// Hydrate state with user not authenticated and only 3 tasks in history
mockPostMessage({
cloudIsAuthenticated: false,
taskHistory: [
{ id: "1", ts: Date.now() - 2000 },
{ id: "2", ts: Date.now() - 1000 },
{ id: "3", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should not show RooCloudCTA but should show RooTips
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
expect(getByTestId("roo-tips")).toBeInTheDocument()
})

it("shows RooCloudCTA when user is not authenticated and has run 4 or more tasks", async () => {
const { getByTestId, queryByTestId } = renderChatView()

// Hydrate state with user not authenticated and 4+ tasks in history
mockPostMessage({
cloudIsAuthenticated: false,
taskHistory: [
{ id: "1", ts: Date.now() - 3000 },
{ id: "2", ts: Date.now() - 2000 },
{ id: "3", ts: Date.now() - 1000 },
{ id: "4", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should show RooCloudCTA and not RooTips
await waitFor(() => {
expect(getByTestId("roo-cloud-cta")).toBeInTheDocument()
})
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
})

it("shows RooCloudCTA when user is not authenticated and has run 5 tasks", async () => {
const { getByTestId, queryByTestId } = renderChatView()

// Hydrate state with user not authenticated and 5 tasks in history
mockPostMessage({
cloudIsAuthenticated: false,
taskHistory: [
{ id: "1", ts: Date.now() - 4000 },
{ id: "2", ts: Date.now() - 3000 },
{ id: "3", ts: Date.now() - 2000 },
{ id: "4", ts: Date.now() - 1000 },
{ id: "5", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should show RooCloudCTA and not RooTips
await waitFor(() => {
expect(getByTestId("roo-cloud-cta")).toBeInTheDocument()
})
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
})

it("does not show RooCloudCTA when there is an active task (regardless of auth status)", async () => {
const { queryByTestId } = renderChatView()

// Hydrate state with user not authenticated, 4+ tasks, but with an active task
mockPostMessage({
cloudIsAuthenticated: false,
taskHistory: [
{ id: "1", ts: Date.now() - 3000 },
{ id: "2", ts: Date.now() - 2000 },
{ id: "3", ts: Date.now() - 1000 },
{ id: "4", ts: Date.now() },
],
clineMessages: [
{
type: "say",
say: "task",
ts: Date.now(),
text: "Active task in progress",
},
],
})

// Wait for the state to be updated and the task view to be shown
await waitFor(() => {
// Should not show RooCloudCTA when there's an active task
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
// Should not show RooTips either since the entire welcome screen is hidden during active tasks
expect(queryByTestId("roo-tips")).not.toBeInTheDocument()
// Should not show RooHero either since the entire welcome screen is hidden during active tasks
expect(queryByTestId("roo-hero")).not.toBeInTheDocument()
})
})

it("shows RooTips when user is authenticated (instead of RooCloudCTA)", () => {
const { queryByTestId, getByTestId } = renderChatView()

// Hydrate state with user authenticated to cloud
mockPostMessage({
cloudIsAuthenticated: true,
taskHistory: [
{ id: "1", ts: Date.now() - 3000 },
{ id: "2", ts: Date.now() - 2000 },
{ id: "3", ts: Date.now() - 1000 },
{ id: "4", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should not show RooCloudCTA but should show RooTips
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
expect(getByTestId("roo-tips")).toBeInTheDocument()
})

it("shows RooTips when user has fewer than 4 tasks (instead of RooCloudCTA)", () => {
const { queryByTestId, getByTestId } = renderChatView()

// Hydrate state with user not authenticated but fewer than 4 tasks
mockPostMessage({
cloudIsAuthenticated: false,
taskHistory: [
{ id: "1", ts: Date.now() - 2000 },
{ id: "2", ts: Date.now() - 1000 },
{ id: "3", ts: Date.now() },
],
clineMessages: [], // No active task
})

// Should not show RooCloudCTA but should show RooTips
expect(queryByTestId("roo-cloud-cta")).not.toBeInTheDocument()
expect(getByTestId("roo-tips")).toBeInTheDocument()
})
})
23 changes: 23 additions & 0 deletions webview-ui/src/components/welcome/RooCloudCTA.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useTranslation } from "react-i18next"

export function RooCloudCTA() {
const { t } = useTranslation("chat")

return (
<div className="border border-muted/20 px-4 py-1 text-center flex items-start gap-2">
<i className="mr-1 codicon codicon-cloud text-xl! mt-2 text-vscode-descriptionForeground" />
<div className="text-left">
<p>
<strong>{t("rooCloudCTA.title")}</strong>
<br />
<span>{t("rooCloudCTA.description")}</span>
</p>
<p>
<a href="https://roocode.com/cloud-waitlist">{t("rooCloudCTA.joinWaitlist")}</a>
</p>
</div>
</div>
)
}

export default RooCloudCTA
Loading