Skip to content
31 changes: 30 additions & 1 deletion packages/cloud/src/CloudAPI.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { z } from "zod"

import { type AuthService, type ShareVisibility, type ShareResponse, shareResponseSchema } from "@roo-code/types"
import {
type AuthService,
type ShareVisibility,
type ShareResponse,
shareResponseSchema,
type CloudAgent,
} from "@roo-code/types"

import { getRooCodeApiUrl } from "./config.js"
import { getUserAgent } from "./utils.js"
Expand Down Expand Up @@ -134,4 +140,27 @@ export class CloudAPI {
.parse(data),
})
}

async getCloudAgents(): Promise<CloudAgent[]> {
try {
this.log("[CloudAPI] Fetching cloud agents")

const response = await this.request<{ agents: CloudAgent[] }>("/api/cloud_agents", {
method: "GET",
})

this.log("[CloudAPI] Cloud agents response:", response)
return response.agents || []
} catch (error) {
this.log("[CloudAPI] Failed to fetch cloud agents, returning mock data:", error)

// Return mock data when API fails as requested
return [
{ id: "1", name: "Code Assistant", type: "code" },
{ id: "2", name: "Test Generator", type: "test" },
{ id: "3", name: "Code Reviewer", type: "review" },
{ id: "4", name: "Documentation Writer", type: "docs" },
]
}
}
}
27 changes: 27 additions & 0 deletions packages/types/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,3 +721,30 @@ export type LeaveResponse = {
taskId?: string
timestamp?: string
}

/**
* CloudAgent
*/

export interface CloudAgent {
id: string
name: string
type: string // e.g., "code", "review", "test", "docs"
}

/**
* CloudAgents API Response
*/

export const cloudAgentsResponseSchema = z.object({
agents: z.array(
z.object({
id: z.string(),
name: z.string(),
type: z.string(),
icon: z.string(),
}),
),
})

export type CloudAgentsResponse = z.infer<typeof cloudAgentsResponseSchema>
4 changes: 2 additions & 2 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -979,7 +979,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
"flex-col-reverse",
"min-h-0",
"overflow-hidden",
"rounded",
"rounded-lg",
)}>
<div
ref={highlightLayerRef}
Expand All @@ -1005,7 +1005,7 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
isEditMode ? "pr-20" : "pr-9",
"z-10",
"forced-color-adjust-none",
"rounded",
"rounded-lg",
)}
style={{
color: "transparent",
Expand Down
87 changes: 31 additions & 56 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"

import TelemetryBanner from "../common/TelemetryBanner"
import VersionIndicator from "../common/VersionIndicator"
import { useTaskSearch } from "../history/useTaskSearch"
import HistoryPreview from "../history/HistoryPreview"
import Announcement from "./Announcement"
import BrowserSessionRow from "./BrowserSessionRow"
Expand All @@ -58,6 +57,7 @@ import { QueuedMessages } from "./QueuedMessages"
import DismissibleUpsell from "../common/DismissibleUpsell"
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
import { Cloud } from "lucide-react"
import CloudAgents from "../cloud/CloudAgents"

export interface ChatViewProps {
isHidden: boolean
Expand Down Expand Up @@ -118,10 +118,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
customModes,
telemetrySetting,
hasSystemPromptOverride,
historyPreviewCollapsed, // Added historyPreviewCollapsed
soundEnabled,
soundVolume,
cloudIsAuthenticated,
cloudApiUrl,
messageQueue = [],
} = useExtensionState()

Expand All @@ -131,20 +131,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
messagesRef.current = messages
}, [messages])

const { tasks } = useTaskSearch()

// Initialize expanded state based on the persisted setting (default to expanded if undefined)
const [isExpanded, setIsExpanded] = useState(
historyPreviewCollapsed === undefined ? true : !historyPreviewCollapsed,
)

const toggleExpanded = useCallback(() => {
const newState = !isExpanded
setIsExpanded(newState)
// Send message to extension to persist the new collapsed state
vscode.postMessage({ type: "setHistoryPreviewCollapsed", bool: !newState })
}, [isExpanded])

// Leaving this less safe version here since if the first message is not a
// task, then the extension is in a bad state and needs to be debugged (see
// Cline.abort).
Expand Down Expand Up @@ -1817,53 +1803,41 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
)}
</>
) : (
<div className="flex-1 min-h-0 overflow-y-auto flex flex-col gap-4 relative">
{/* Moved Task Bar Header Here */}
{tasks.length !== 0 && (
<div className="flex text-vscode-descriptionForeground w-full mx-auto px-5 pt-3">
<div className="flex items-center gap-1 cursor-pointer" onClick={toggleExpanded}>
{tasks.length < 10 && (
<span className={`font-medium text-xs `}>{t("history:recentTasks")}</span>
)}
<span
className={`codicon ${isExpanded ? "codicon-eye" : "codicon-eye-closed"} scale-90`}
/>
</div>
</div>
)}
<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 */}
<div className="flex flex-col h-full justify-center p-6 min-h-0 overflow-y-auto gap-4 relative">
<div className="flex flex-col items-start gap-2 justify-center max-w-md h-full">
<VersionIndicator
onClick={() => setShowAnnouncementModal(true)}
className="absolute top-2 right-3 z-10"
/>

<RooHero />
<div className="flex flex-col gap-4">
<RooHero />

<div className="mb-2.5">
{cloudIsAuthenticated || taskHistory.length < 4 ? (
<RooTips />
) : (
<>
<DismissibleUpsell
upsellId="taskList"
icon={<Cloud className="size-4 mt-0.5 shrink-0" />}
onClick={() => openUpsell()}
dismissOnClick={false}
className="bg-vscode-editor-background p-4 !text-base">
<Trans
i18nKey="cloud:upsell.taskList"
components={{
learnMoreLink: <VSCodeLink href="#" />,
}}
/>
</DismissibleUpsell>
</>
)}
{taskHistory.length < 4 && <RooTips />}

{taskHistory.length > 0 && <HistoryPreview />}
</div>
{/* Show the task history preview if expanded and tasks exist */}
{taskHistory.length > 0 && isExpanded && <HistoryPreview />}

{!cloudIsAuthenticated ? (
<DismissibleUpsell
upsellId="taskList"
icon={<Cloud className="size-5 mt-0.5 shrink-0" />}
onClick={() => openUpsell()}
dismissOnClick={false}
className="!bg-vscode-editor-background mt-6 border-border rounded-xl pl-4 pr-3 py-3 !text-base">
<Trans
i18nKey="cloud:upsell.taskList"
components={{
learnMoreLink: <VSCodeLink href="#" />,
}}
/>
</DismissibleUpsell>
) : (
<CloudAgents
cloudApiUrl={cloudApiUrl}
sessionToken={undefined} // Will use mock data for now
/>
)}
</div>
</div>
)}
Expand Down Expand Up @@ -1989,6 +1963,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
}
}}
/>

<ChatTextArea
ref={textAreaRef}
inputValue={inputValue}
Expand Down
8 changes: 5 additions & 3 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ const TaskHeader = ({
)}
<div
className={cn(
"px-2.5 pt-2.5 pb-2 flex flex-col gap-1.5 relative z-1 cursor-pointer",
"px-3 pt-2.5 pb-2 flex flex-col gap-1.5 relative z-1 cursor-pointer",
"bg-vscode-input-background hover:bg-vscode-input-background/90",
"text-vscode-foreground/80 hover:text-vscode-foreground",
"shadow-sm shadow-black/30 rounded-md",
"shadow-sm shadow-black/30 rounded-xl",
hasTodos && "border-b-0",
)}
onClick={(e) => {
Expand Down Expand Up @@ -163,7 +163,9 @@ const TaskHeader = ({
</div>
</div>
{!isTaskExpanded && contextWindow > 0 && (
<div className="flex items-center gap-2 text-sm" onClick={(e) => e.stopPropagation()}>
<div
className="flex items-center gap-2 text-sm text-muted-foreground/70"
onClick={(e) => e.stopPropagation()}>
<StandardTooltip
content={
<div className="space-y-1">
Expand Down
133 changes: 133 additions & 0 deletions webview-ui/src/components/cloud/CloudAgents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useEffect, useState } from "react"
import { Brain, Plus } from "lucide-react"
import type { CloudAgent } from "@roo-code/types"

interface CloudAgentsProps {
cloudApiUrl: string
sessionToken?: string
}

const CloudAgents: React.FC<CloudAgentsProps> = ({ cloudApiUrl, sessionToken }) => {
const [agents, setAgents] = useState<CloudAgent[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(false)

useEffect(() => {
const fetchAgents = async () => {
try {
setLoading(true)
setError(false)

if (!sessionToken) {
// Use mock data if no session token
const mockAgents: CloudAgent[] = [
{ id: "1", name: "Rooviewer", type: "PR Review" },
{ id: "2", name: "Gertroode", type: "Documentation Writer" },
{ id: "3", name: "Polyglot", type: "String Translator" },
]
setAgents(mockAgents)
return
}

const response = await fetch(`${cloudApiUrl}/api/cloud_agents`, {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
},
})

if (!response.ok) {
throw new Error("Failed to fetch agents")
}

const data = await response.json()
setAgents(data.agents || [])
} catch (err) {
console.error("Failed to fetch cloud agents, using mock data:", err)
// Use mock data on error
const mockAgents: CloudAgent[] = [
{ id: "1", name: "Code Assistant", type: "code" },
{ id: "2", name: "Test Generator", type: "test" },
{ id: "3", name: "Code Reviewer", type: "review" },
{ id: "4", name: "Documentation Writer", type: "docs" },
]
setAgents(mockAgents)
} finally {
setLoading(false)
}
}

fetchAgents()
}, [cloudApiUrl, sessionToken])

// If there's an error, show nothing as requested
if (error) {
return null
}

// Don't show loading state, just render nothing until data is ready
if (loading) {
return null
}

const handleAgentClick = (agentId: string) => {
window.open(`${cloudApiUrl}/cloud-agents/${agentId}`, "_blank")
}

const handleCreateClick = () => {
window.open(`${cloudApiUrl}/cloud-agents/create`, "_blank")
}

return (
<div className="flex flex-col gap-3 mt-6 w-full">
<div className="flex flex-wrap items-center justify-between mt-4 mb-1">
<h2 className="font-semibold text-lg shrink-0 m-0">Cloud Agents</h2>
{agents.length > 0 && (
<button
onClick={handleCreateClick}
className="text-base flex items-center gap-1 text-vscode-descriptionForeground hover:text-vscode-textLink-foreground transition-colors cursor-pointer"
title="Create new agent">
<Plus className="h-4 w-4" />
Create
</button>
)}
</div>

{agents.length === 0 ? (
<div className="items-center gap-3 px-4 py-1 rounded-xl bg-vscode-editor-background">
<p className="text-base text-vscode-descriptionForeground mb-4">
No Cloud agents yes?
<button
className="inline-flex ml-1 cursor-pointer text-vscode-textLink-foreground hover:underline"
onClick={handleCreateClick}>
Create your first.
</button>
</p>
</div>
) : (
<div className="flex flex-col gap-1">
{agents.map((agent) => (
<div
key={agent.id}
className="flex items-center gap-3 px-4 py-2 rounded-xl bg-vscode-editor-background hover:bg-vscode-list-hoverBackground cursor-pointer transition-colors"
onClick={() => handleAgentClick(agent.id)}>
<span className="text-xl" role="img" aria-label={agent.type}>
<Brain className="size-6" />
</span>
<div className="flex-1 min-w-0">
<div className="text-base font-medium text-vscode-foreground truncate">
{agent.name}
</div>
<div className="text-sm font-light text-vscode-descriptionForeground">
{agent.type} agents
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}

export default CloudAgents
Loading