From e76d22ec755a0fc7380c55813add92b46fa6829f Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Thu, 2 Oct 2025 18:15:31 +0100 Subject: [PATCH 01/15] First pass of cloud agents in extension, with mock data --- packages/cloud/src/CloudAPI.ts | 31 +++- packages/types/src/cloud.ts | 27 ++++ .../src/components/chat/ChatTextArea.tsx | 4 +- webview-ui/src/components/chat/ChatView.tsx | 87 ++++------ webview-ui/src/components/chat/TaskHeader.tsx | 8 +- .../src/components/cloud/CloudAgents.tsx | 129 +++++++++++++++ .../cloud/__tests__/CloudAgents.test.tsx | 150 ++++++++++++++++++ .../src/components/history/HistoryPreview.tsx | 17 +- .../src/components/history/TaskItem.tsx | 4 +- .../src/components/history/TaskItemFooter.tsx | 4 +- webview-ui/src/components/welcome/RooHero.tsx | 6 +- webview-ui/src/components/welcome/RooTips.tsx | 21 +-- .../src/context/ExtensionStateContext.tsx | 2 + webview-ui/src/i18n/locales/en/cloud.json | 2 +- webview-ui/src/i18n/locales/en/history.json | 2 +- 15 files changed, 406 insertions(+), 88 deletions(-) create mode 100644 webview-ui/src/components/cloud/CloudAgents.tsx create mode 100644 webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts index d1c3f89c2b8..629985a56b1 100644 --- a/packages/cloud/src/CloudAPI.ts +++ b/packages/cloud/src/CloudAPI.ts @@ -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" @@ -134,4 +140,27 @@ export class CloudAPI { .parse(data), }) } + + async getCloudAgents(): Promise { + 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", icon: "💻" }, + { id: "2", name: "Test Generator", type: "test", icon: "🧪" }, + { id: "3", name: "Code Reviewer", type: "review", icon: "👁️" }, + { id: "4", name: "Documentation Writer", type: "docs", icon: "📝" }, + ] + } + } } diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 903dfcb93fd..21150776494 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -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 diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 993912f240e..c7f31672c61 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -979,7 +979,7 @@ export const ChatTextArea = forwardRef( "flex-col-reverse", "min-h-0", "overflow-hidden", - "rounded", + "rounded-lg", )}>
( isEditMode ? "pr-20" : "pr-9", "z-10", "forced-color-adjust-none", - "rounded", + "rounded-lg", )} style={{ color: "transparent", diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d358c68f1cf..8499cdcce7f 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -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" @@ -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 @@ -118,10 +118,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction { - 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). @@ -1817,53 +1803,41 @@ const ChatViewComponent: React.ForwardRefRenderFunction ) : ( -
- {/* Moved Task Bar Header Here */} - {tasks.length !== 0 && ( -
-
- {tasks.length < 10 && ( - {t("history:recentTasks")} - )} - -
-
- )} -
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" /> - +
+ -
- {cloudIsAuthenticated || taskHistory.length < 4 ? ( - - ) : ( - <> - } - onClick={() => openUpsell()} - dismissOnClick={false} - className="bg-vscode-editor-background p-4 !text-base"> - , - }} - /> - - - )} + {taskHistory.length < 4 && } + + {taskHistory.length > 0 && }
- {/* Show the task history preview if expanded and tasks exist */} - {taskHistory.length > 0 && isExpanded && } + + {!cloudIsAuthenticated ? ( + } + onClick={() => openUpsell()} + dismissOnClick={false} + className="!bg-vscode-editor-background mt-6 border-border rounded-xl pl-4 pr-3 py-3 !text-base"> + , + }} + /> + + ) : ( + + )}
)} @@ -1989,6 +1963,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + { @@ -163,7 +163,9 @@ const TaskHeader = ({
{!isTaskExpanded && contextWindow > 0 && ( -
e.stopPropagation()}> +
e.stopPropagation()}> diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx new file mode 100644 index 00000000000..ea53f3b3cbf --- /dev/null +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -0,0 +1,129 @@ +import React, { useEffect, useState } from "react" +import { Button } from "@/components/ui" +import { Brain, Plus } from "lucide-react" +import type { CloudAgent } from "@roo-code/types" + +interface CloudAgentsProps { + cloudApiUrl: string + sessionToken?: string +} + +const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken }) => { + const [agents, setAgents] = useState([]) + 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", icon: "💻" }, + { id: "2", name: "Test Generator", type: "test", icon: "🧪" }, + { id: "3", name: "Code Reviewer", type: "review", icon: "👁️" }, + { id: "4", name: "Documentation Writer", type: "docs", icon: "📝" }, + ] + 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 ( +
+
+

Cloud Agents

+ +
+ + {agents.length === 0 ? ( +
+

Create your first cloud agent

+ +
+ ) : ( +
+ {agents.map((agent) => ( +
handleAgentClick(agent.id)}> + + + +
+
+ {agent.name} +
+
+ {agent.type} agents +
+
+
+ ))} +
+ )} +
+ ) +} + +export default CloudAgents diff --git a/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx b/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx new file mode 100644 index 00000000000..c479e8e81e7 --- /dev/null +++ b/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx @@ -0,0 +1,150 @@ +import React from "react" +import { render, screen, fireEvent, waitFor } from "@testing-library/react" +import "@testing-library/jest-dom" +import CloudAgents from "../CloudAgents" + +// Mock window.open +const mockWindowOpen = vi.fn() +window.open = mockWindowOpen + +// Mock fetch +global.fetch = vi.fn() + +describe("CloudAgents", () => { + beforeEach(() => { + mockWindowOpen.mockClear() + ;(global.fetch as any).mockClear() + }) + + const defaultProps = { + cloudApiUrl: "https://app.roocode.com", + sessionToken: undefined, + } + + it("should render mock data when no session token is provided", async () => { + render() + + // Wait for the component to load + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + }) + + // Check if mock agents are displayed + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + expect(screen.getByText("Test Generator")).toBeInTheDocument() + expect(screen.getByText("Code Reviewer")).toBeInTheDocument() + expect(screen.getByText("Documentation Writer")).toBeInTheDocument() + + // Check agent types + expect(screen.getByText("code")).toBeInTheDocument() + expect(screen.getByText("test")).toBeInTheDocument() + expect(screen.getByText("review")).toBeInTheDocument() + expect(screen.getByText("docs")).toBeInTheDocument() + }) + + it("should handle agent click and open correct URL", async () => { + render() + + await waitFor(() => { + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + }) + + // Click on an agent + const codeAssistant = screen.getByText("Code Assistant").closest("div.cursor-pointer") + fireEvent.click(codeAssistant!) + + expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/1", "_blank") + }) + + it("should handle create button click", async () => { + render() + + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + }) + + // Find and click the create button (Plus icon button) + const createButton = screen.getByTitle("Create new agent") + fireEvent.click(createButton) + + expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/create", "_blank") + }) + + it("should show empty state when no agents and handle create button", async () => { + // Mock fetch to return empty agents + ;(global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ agents: [] }), + }) + + render() + + await waitFor(() => { + expect(screen.getByText("Create your first cloud agent")).toBeInTheDocument() + }) + + // Check for the create agent button in empty state + const createButton = screen.getByRole("button", { name: /Create Agent/i }) + expect(createButton).toBeInTheDocument() + + fireEvent.click(createButton) + + expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/create", "_blank") + }) + + it("should use mock data when API call fails", async () => { + // Mock fetch to fail + ;(global.fetch as any).mockRejectedValueOnce(new Error("API Error")) + + render() + + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + }) + + // Should still show mock data + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + expect(screen.getByText("Test Generator")).toBeInTheDocument() + expect(screen.getByText("Code Reviewer")).toBeInTheDocument() + expect(screen.getByText("Documentation Writer")).toBeInTheDocument() + }) + + it("should not render anything while loading", () => { + const { container } = render() + + // Initially should be empty (loading state returns null) + expect(container.firstChild).toBeNull() + }) + + it("should fetch agents from API when session token is provided", async () => { + const mockAgents = [ + { id: "api-1", name: "API Agent 1", type: "api-type-1", icon: "🤖" }, + { id: "api-2", name: "API Agent 2", type: "api-type-2", icon: "🎯" }, + ] + + ;(global.fetch as any).mockResolvedValueOnce({ + ok: true, + json: async () => ({ agents: mockAgents }), + }) + + render() + + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + }) + + // Check API call was made with correct headers + expect(global.fetch).toHaveBeenCalledWith("https://app.roocode.com/api/cloud_agents", { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer test-token", + }, + }) + + // Check if API agents are displayed + expect(screen.getByText("API Agent 1")).toBeInTheDocument() + expect(screen.getByText("API Agent 2")).toBeInTheDocument() + expect(screen.getByText("api-type-1")).toBeInTheDocument() + expect(screen.getByText("api-type-2")).toBeInTheDocument() + }) +}) diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx index 753b4b84e7c..3f1362f3407 100644 --- a/webview-ui/src/components/history/HistoryPreview.tsx +++ b/webview-ui/src/components/history/HistoryPreview.tsx @@ -15,18 +15,21 @@ const HistoryPreview = () => { } return ( -
+
+
+

{t("history:recentTasks")}

+ +
{tasks.length !== 0 && ( <> {tasks.slice(0, 3).map((item) => ( ))} - )}
diff --git a/webview-ui/src/components/history/TaskItem.tsx b/webview-ui/src/components/history/TaskItem.tsx index d661d999300..949306fa2ab 100644 --- a/webview-ui/src/components/history/TaskItem.tsx +++ b/webview-ui/src/components/history/TaskItem.tsx @@ -47,7 +47,7 @@ const TaskItem = ({ key={item.id} data-testid={`task-item-${item.id}`} className={cn( - "cursor-pointer group bg-vscode-editor-background rounded relative overflow-hidden border border-transparent hover:bg-vscode-list-hoverBackground transition-colors", + "cursor-pointer group bg-vscode-editor-background rounded-xl relative overflow-hidden border border-transparent hover:bg-vscode-editor-foreground/10 transition-colors", className, )} onClick={handleClick}> @@ -70,7 +70,7 @@ const TaskItem = ({
= ({ item, variant, isSelectionMode = false, onDelete }) => { return (
-
+
{/* Datetime with time-ago format */} {formatTimeAgo(item.ts)} @@ -32,7 +32,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {/* Action Buttons for non-compact view */} {!isSelectionMode && ( -
+
{variant === "full" && } {onDelete && } diff --git a/webview-ui/src/components/welcome/RooHero.tsx b/webview-ui/src/components/welcome/RooHero.tsx index 3d25ea87b55..fe8a0f9da1f 100644 --- a/webview-ui/src/components/welcome/RooHero.tsx +++ b/webview-ui/src/components/welcome/RooHero.tsx @@ -7,7 +7,7 @@ const RooHero = () => { }) return ( -
+
{ maskRepeat: "no-repeat", maskSize: "contain", }} - className="mx-auto"> - Roo logo + className="mx-auto hover:animate-bounce"> + Roo logo
) diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx index c0d6682d567..7deef969fb9 100644 --- a/webview-ui/src/components/welcome/RooTips.tsx +++ b/webview-ui/src/components/welcome/RooTips.tsx @@ -3,16 +3,17 @@ import { useTranslation } from "react-i18next" import { Trans } from "react-i18next" import { buildDocLink } from "@src/utils/docLinks" +import { ListTree, Users } from "lucide-react" const tips = [ { - icon: "codicon-account", + icon: , href: buildDocLink("basic-usage/using-modes", "tips"), titleKey: "rooTips.customizableModes.title", descriptionKey: "rooTips.customizableModes.description", }, { - icon: "codicon-list-tree", + icon: , href: buildDocLink("features/boomerang-tasks", "tips"), titleKey: "rooTips.boomerangTasks.title", descriptionKey: "rooTips.boomerangTasks.description", @@ -23,8 +24,8 @@ const RooTips = () => { const { t } = useTranslation("chat") return ( -
-

+

+

{ }} />

-
+
{tips.map((tip) => ( -
- +
+ {tip.icon} - + {t(tip.titleKey)} : {t(tip.descriptionKey)} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 542b2385c02..55b94b60367 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -41,6 +41,7 @@ export interface ExtensionStateContextType extends ExtensionState { organizationSettingsVersion: number cloudIsAuthenticated: boolean cloudOrganizations?: CloudOrganizationMembership[] + cloudApiUrl: string sharingEnabled: boolean maxConcurrentFileReads?: number mdmCompliant?: boolean @@ -435,6 +436,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode routerModels: extensionRouterModels, cloudIsAuthenticated: state.cloudIsAuthenticated ?? false, cloudOrganizations: state.cloudOrganizations ?? [], + cloudApiUrl: "https://app.roocode.com", organizationSettingsVersion: state.organizationSettingsVersion ?? -1, marketplaceItems, marketplaceInstalledMetadata, diff --git a/webview-ui/src/i18n/locales/en/cloud.json b/webview-ui/src/i18n/locales/en/cloud.json index 05220f83336..991b8117bd4 100644 --- a/webview-ui/src/i18n/locales/en/cloud.json +++ b/webview-ui/src/i18n/locales/en/cloud.json @@ -29,6 +29,6 @@ "upsell": { "autoApprovePowerUser": "Giving Roo some independence? Control it from anywhere with Roo Code Cloud. Learn more.", "longRunningTask": "This might take a while. Continue from anywhere with Cloud.", - "taskList": "Roo Code Cloud is here: follow and control your tasks from anywhere. Learn more." + "taskList": "Get token usage stats, continue coding from anywhere, run agents in the cloud and more: check out Roo Code Cloud." } } diff --git a/webview-ui/src/i18n/locales/en/history.json b/webview-ui/src/i18n/locales/en/history.json index 8d004331709..608be93140c 100644 --- a/webview-ui/src/i18n/locales/en/history.json +++ b/webview-ui/src/i18n/locales/en/history.json @@ -41,5 +41,5 @@ "mostTokens": "Most Tokens", "mostRelevant": "Most Relevant" }, - "viewAllHistory": "View all tasks" + "viewAllHistory": "View all" } From 3d7debba8e22354a189e66b476c789002c5c4202 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Thu, 2 Oct 2025 18:16:31 +0100 Subject: [PATCH 02/15] Mock data fix --- packages/cloud/src/CloudAPI.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts index 629985a56b1..3ef6fbb5f38 100644 --- a/packages/cloud/src/CloudAPI.ts +++ b/packages/cloud/src/CloudAPI.ts @@ -156,10 +156,10 @@ export class CloudAPI { // Return mock data when API fails as requested return [ - { id: "1", name: "Code Assistant", type: "code", icon: "💻" }, - { id: "2", name: "Test Generator", type: "test", icon: "🧪" }, - { id: "3", name: "Code Reviewer", type: "review", icon: "👁️" }, - { id: "4", name: "Documentation Writer", type: "docs", icon: "📝" }, + { 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" }, ] } } From cffea36911eb32b1359a51f432706e8f6da6024f Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Thu, 2 Oct 2025 18:22:42 +0100 Subject: [PATCH 03/15] Empty state for no cloud agents --- .../src/components/cloud/CloudAgents.tsx | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index ea53f3b3cbf..439eeb5078e 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react" -import { Button } from "@/components/ui" import { Brain, Plus } from "lucide-react" import type { CloudAgent } from "@roo-code/types" @@ -81,24 +80,29 @@ const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken }) return (
-
+

Cloud Agents

- + {agents.length > 0 && ( + + )}
{agents.length === 0 ? ( -
-

Create your first cloud agent

- +
+

+ No Cloud agents yes? + +

) : (
From bb2937bad2f7ebf9583ef74f6bc463320662fa07 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Thu, 2 Oct 2025 18:24:09 +0100 Subject: [PATCH 04/15] Mock data fiz --- webview-ui/src/components/cloud/CloudAgents.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index 439eeb5078e..3750f01dcba 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -46,10 +46,10 @@ const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken }) 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", icon: "💻" }, - { id: "2", name: "Test Generator", type: "test", icon: "🧪" }, - { id: "3", name: "Code Reviewer", type: "review", icon: "👁️" }, - { id: "4", name: "Documentation Writer", type: "docs", icon: "📝" }, + { 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 { From 85771014caa39cbcfb9b499ceba24c5c938eb468 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 15:31:18 +0100 Subject: [PATCH 05/15] It works --- packages/cloud/src/CloudAPI.ts | 24 +- packages/cloud/src/__tests__/CloudAPI.test.ts | 81 ++++++ packages/types/src/cloud.ts | 3 +- src/core/webview/ClineProvider.ts | 3 +- .../webviewMessageHandler.cloudAgents.test.ts | 266 ++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 60 ++++ src/shared/ExtensionMessage.ts | 2 + src/shared/WebviewMessage.ts | 1 + webview-ui/src/components/chat/ChatView.tsx | 16 +- .../src/components/cloud/CloudAgents.tsx | 115 ++++---- .../cloud/__tests__/CloudAgents.spec.tsx | 212 ++++++++++++++ .../cloud/__tests__/CloudAgents.test.tsx | 150 ---------- .../src/components/history/HistoryPreview.tsx | 2 +- webview-ui/src/components/welcome/RooTips.tsx | 38 +-- .../src/context/ExtensionStateContext.tsx | 2 +- webview-ui/src/i18n/locales/en/chat.json | 24 +- 16 files changed, 728 insertions(+), 271 deletions(-) create mode 100644 packages/cloud/src/__tests__/CloudAPI.test.ts create mode 100644 src/core/webview/__tests__/webviewMessageHandler.cloudAgents.test.ts create mode 100644 webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx delete mode 100644 webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts index 3ef6fbb5f38..38342179206 100644 --- a/packages/cloud/src/CloudAPI.ts +++ b/packages/cloud/src/CloudAPI.ts @@ -142,25 +142,13 @@ export class CloudAPI { } async getCloudAgents(): Promise { - try { - this.log("[CloudAPI] Fetching cloud agents") + this.log("[CloudAPI] Fetching cloud agents") - const response = await this.request<{ agents: CloudAgent[] }>("/api/cloud_agents", { - method: "GET", - }) + const response = await this.request<{ success: boolean; data: 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" }, - ] - } + this.log("[CloudAPI] Cloud agents response:", response) + return response.data || [] } } diff --git a/packages/cloud/src/__tests__/CloudAPI.test.ts b/packages/cloud/src/__tests__/CloudAPI.test.ts new file mode 100644 index 00000000000..a0267d0f114 --- /dev/null +++ b/packages/cloud/src/__tests__/CloudAPI.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, beforeEach } from "vitest" +import { CloudAPI } from "../CloudAPI.js" +import { AuthenticationError } from "../errors.js" +import type { AuthService } from "@roo-code/types" + +// Mock fetch globally +global.fetch = vi.fn() + +describe("CloudAPI", () => { + let mockAuthService: Partial + let cloudAPI: CloudAPI + + beforeEach(() => { + // Mock only the methods we need for testing + mockAuthService = { + getSessionToken: vi.fn().mockReturnValue("test-token"), + } + + cloudAPI = new CloudAPI(mockAuthService as AuthService) + vi.clearAllMocks() + }) + + describe("getCloudAgents", () => { + it("should return cloud agents on success", async () => { + const mockAgents = [ + { id: "1", name: "Agent 1", type: "code", icon: "code" }, + { id: "2", name: "Agent 2", type: "chat", icon: "chat" }, + ] + + // Mock successful response + ;(global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true, data: mockAgents }), + }) + + const agents = await cloudAPI.getCloudAgents() + + expect(agents).toEqual(mockAgents) + expect(global.fetch).toHaveBeenCalledWith( + expect.stringContaining("/api/cloud-agents"), + expect.objectContaining({ + method: "GET", + headers: expect.objectContaining({ + Authorization: "Bearer test-token", + }), + }), + ) + }) + + it("should throw AuthenticationError on 401 response", async () => { + // Mock 401 response + ;(global.fetch as ReturnType).mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: "Unauthorized", + json: async () => ({ error: "Authentication required" }), + }) + + await expect(cloudAPI.getCloudAgents()).rejects.toThrow(AuthenticationError) + }) + + it("should throw AuthenticationError when no session token", async () => { + // Mock no session token + mockAuthService.getSessionToken = vi.fn().mockReturnValue(null) + + await expect(cloudAPI.getCloudAgents()).rejects.toThrow(AuthenticationError) + }) + + it("should return empty array when data is missing", async () => { + // Mock response with no data + ;(global.fetch as ReturnType).mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }), + }) + + const agents = await cloudAPI.getCloudAgents() + + expect(agents).toEqual([]) + }) + }) +}) diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 059dd0b1bc4..465bb9b77fb 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -730,7 +730,8 @@ export type LeaveResponse = { export interface CloudAgent { id: string name: string - type: string // e.g., "code", "review", "test", "docs" + type: string // e.g., "PR Reviewer", "Documentation Writer" + icon?: string // e.g., "pr-reviewer", "documentation-writer" } /** diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 91b86879668..b6df04249f1 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1034,12 +1034,11 @@ export class ClineProvider window.__vite_plugin_react_preamble_installed__ = true ` - const csp = [ "default-src 'none'", `font-src ${webview.cspSource} data:`, `style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`, - `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`, + `img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com ${getRooCodeApiUrl()} data:`, `media-src ${webview.cspSource}`, `script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`, `connect-src ${webview.cspSource} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`, diff --git a/src/core/webview/__tests__/webviewMessageHandler.cloudAgents.test.ts b/src/core/webview/__tests__/webviewMessageHandler.cloudAgents.test.ts new file mode 100644 index 00000000000..d6eb9f52d16 --- /dev/null +++ b/src/core/webview/__tests__/webviewMessageHandler.cloudAgents.test.ts @@ -0,0 +1,266 @@ +import { describe, it, expect, vi, beforeEach, type Mock } from "vitest" +import * as vscode from "vscode" +import { webviewMessageHandler } from "../webviewMessageHandler" +import type { ClineProvider } from "../ClineProvider" + +// Mock vscode with all required exports +vi.mock("vscode", () => ({ + window: { + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), + showWarningMessage: vi.fn(), + }, + workspace: { + workspaceFolders: [], + getConfiguration: vi.fn(() => ({ + get: vi.fn(), + update: vi.fn(), + })), + }, + Uri: { + parse: vi.fn((str: string) => ({ fsPath: str })), + file: vi.fn((str: string) => ({ fsPath: str })), + }, + commands: { + executeCommand: vi.fn(), + }, + env: { + openExternal: vi.fn(), + clipboard: { + writeText: vi.fn(), + }, + }, +})) + +// Create AuthenticationError class for testing +class AuthenticationError extends Error { + constructor(message = "Authentication required") { + super(message) + this.name = "AuthenticationError" + } +} + +// Mock CloudService with a factory function to allow resetting +const createMockCloudService = () => { + let instance: any = null + return { + hasInstance: vi.fn(() => !!instance), + instance: null as any, + setInstance: (newInstance: any) => { + instance = newInstance + return instance + }, + getInstance: () => instance, + } +} + +let mockCloudService = createMockCloudService() + +// Mock CloudService +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + get hasInstance() { + return mockCloudService.hasInstance + }, + get instance() { + return mockCloudService.getInstance() + }, + }, +})) + +// Mock i18n +vi.mock("../../../i18n", () => ({ + t: (key: string) => key, + changeLanguage: vi.fn(), +})) + +// Mock other dependencies that might be imported +vi.mock("../../task-persistence", () => ({ + saveTaskMessages: vi.fn(), +})) + +vi.mock("../../../utils/safeWriteJson", () => ({ + safeWriteJson: vi.fn(), +})) + +vi.mock("../../../shared/package", () => ({ + Package: { + name: "test-package", + }, +})) + +vi.mock("../../../shared/api", () => ({ + toRouterName: vi.fn((name: string) => name), +})) + +vi.mock("../../../shared/experiments", () => ({ + experimentDefault: {}, +})) + +vi.mock("../../../shared/modes", () => ({ + defaultModeSlug: "code", +})) + +vi.mock("../generateSystemPrompt", () => ({ + generateSystemPrompt: vi.fn(), +})) + +vi.mock("../messageEnhancer", () => ({ + MessageEnhancer: { + enhanceMessage: vi.fn(), + captureTelemetry: vi.fn(), + }, +})) + +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + hasInstance: vi.fn(() => false), + instance: null, + }, + TelemetryEventName: {}, +})) + +describe("webviewMessageHandler - getCloudAgents", () => { + let mockProvider: Partial + let postMessageToWebview: Mock + let log: Mock + + beforeEach(() => { + vi.clearAllMocks() + // Reset CloudService mock for each test + mockCloudService = createMockCloudService() + + postMessageToWebview = vi.fn() + log = vi.fn() + + mockProvider = { + postMessageToWebview, + log, + // Add other required provider properties as needed + } + }) + + it("should handle CloudService not initialized", async () => { + // CloudService.hasInstance will return false because instance is null + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(log).toHaveBeenCalledWith("[getCloudAgents] CloudService not initialized") + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: [], + error: "CloudService not initialized", + }) + }) + + it("should handle CloudAPI not available", async () => { + // Set instance with null cloudAPI + mockCloudService.setInstance({ + cloudAPI: null, + }) + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(log).toHaveBeenCalledWith("[getCloudAgents] CloudAPI not available") + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: [], + error: "CloudAPI not available", + }) + }) + + it("should successfully fetch cloud agents", async () => { + const mockAgents = [ + { id: "1", name: "Agent 1", type: "code", icon: "code" }, + { id: "2", name: "Agent 2", type: "chat", icon: "chat" }, + ] + + const mockCloudAPI = { + getCloudAgents: vi.fn().mockResolvedValue(mockAgents), + } + + // Set instance with mock cloudAPI + mockCloudService.setInstance({ + cloudAPI: mockCloudAPI, + }) + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(log).toHaveBeenCalledWith("[getCloudAgents] Fetching cloud agents") + expect(log).toHaveBeenCalledWith("[getCloudAgents] Fetched 2 cloud agents") + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: mockAgents, + }) + }) + + it("should handle authentication errors and show user message", async () => { + const authError = new AuthenticationError("Authentication required") + + const mockCloudAPI = { + getCloudAgents: vi.fn().mockRejectedValue(authError), + } + + // Set instance with mock cloudAPI + mockCloudService.setInstance({ + cloudAPI: mockCloudAPI, + }) + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(log).toHaveBeenCalledWith("[getCloudAgents] Error fetching cloud agents: Authentication required") + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: [], + error: "Authentication required", + }) + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.auth_required_for_cloud_agents") + }) + + it("should handle general errors without showing auth message", async () => { + const genericError = new Error("Network error") + + const mockCloudAPI = { + getCloudAgents: vi.fn().mockRejectedValue(genericError), + } + + // Set instance with mock cloudAPI + mockCloudService.setInstance({ + cloudAPI: mockCloudAPI, + }) + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(log).toHaveBeenCalledWith("[getCloudAgents] Error fetching cloud agents: Network error") + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: [], + error: "Network error", + }) + // Should NOT show auth error message for general errors + expect(vscode.window.showErrorMessage).not.toHaveBeenCalled() + }) + + it("should detect 401 errors in error message", async () => { + const error401 = new Error("HTTP 401: Unauthorized") + + const mockCloudAPI = { + getCloudAgents: vi.fn().mockRejectedValue(error401), + } + + // Set instance with mock cloudAPI + mockCloudService.setInstance({ + cloudAPI: mockCloudAPI, + }) + + await webviewMessageHandler(mockProvider as ClineProvider, { type: "getCloudAgents" }) + + expect(postMessageToWebview).toHaveBeenCalledWith({ + type: "cloudAgents", + agents: [], + error: "HTTP 401: Unauthorized", + }) + // Should show auth error message for 401 errors + expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("common:errors.auth_required_for_cloud_agents") + }) +}) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index af5f9925c35..1358d23d2c7 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2431,6 +2431,66 @@ export const webviewMessageHandler = async ( } break } + case "getCloudAgents": { + try { + // Ensure CloudService is initialized + if (!CloudService.hasInstance()) { + provider.log("[getCloudAgents] CloudService not initialized") + await provider.postMessageToWebview({ + type: "cloudAgents", + agents: [], + error: "CloudService not initialized", + }) + break + } + + // Fetch cloud agents using the CloudAPI instance + const cloudAPI = CloudService.instance.cloudAPI + if (!cloudAPI) { + provider.log("[getCloudAgents] CloudAPI not available") + await provider.postMessageToWebview({ + type: "cloudAgents", + agents: [], + error: "CloudAPI not available", + }) + break + } + + provider.log("[getCloudAgents] Fetching cloud agents") + const agents = await cloudAPI.getCloudAgents() + provider.log(`[getCloudAgents] Fetched ${agents.length} cloud agents`) + + // Send the agents back to the webview + await provider.postMessageToWebview({ + type: "cloudAgents", + agents: agents, + }) + } catch (error) { + provider.log( + `[getCloudAgents] Error fetching cloud agents: ${error instanceof Error ? error.message : String(error)}`, + ) + + // Check if it's an authentication error + const errorMessage = error instanceof Error ? error.message : String(error) + const isAuthError = errorMessage.includes("Authentication") || errorMessage.includes("401") + + // Send empty array with error information + await provider.postMessageToWebview({ + type: "cloudAgents", + agents: [], + error: errorMessage, + }) + + // If it's an authentication error, show a user-friendly message + if (isAuthError) { + vscode.window.showErrorMessage( + t("common:errors.auth_required_for_cloud_agents") || + "Authentication required to access cloud agents", + ) + } + } + break + } case "saveCodeIndexSettingsAtomic": { if (!message.codeIndexSettings) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 66f389f81c1..eea8f805dc5 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -126,6 +126,7 @@ export interface ExtensionMessage { | "insertTextIntoTextarea" | "dismissedUpsells" | "organizationSwitchResult" + | "cloudAgents" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -205,6 +206,7 @@ export interface ExtensionMessage { queuedMessages?: QueuedMessage[] list?: string[] // For dismissedUpsells organizationId?: string | null // For organizationSwitchResult + agents?: any[] // For cloudAgents } export type ExtensionState = Pick< diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d43a2fce043..21a1c1c5ea4 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -185,6 +185,7 @@ export interface WebviewMessage { | "rooCloudSignOut" | "rooCloudManualUrl" | "switchOrganization" + | "getCloudAgents" | "condenseTaskContextRequest" | "requestIndexingStatus" | "startIndexing" diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 8499cdcce7f..0248abb6eed 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -121,7 +121,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction -
+
+ {/* New users should see tips */} {taskHistory.length < 4 && } + {/* Everyone should see their task history if any */} {taskHistory.length > 0 && }
- {!cloudIsAuthenticated ? ( + {cloudIsAuthenticated ? ( + // Logged in users should always see their agents (or be upsold) + + ) : ( + // Logged out users should be upsold at least once on Cloud } @@ -1832,11 +1837,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction - ) : ( - )}
diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index 3750f01dcba..ab10e2833c6 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -1,67 +1,65 @@ import React, { useEffect, useState } from "react" -import { Brain, Plus } from "lucide-react" +import { Cloud, Plus, SquarePen } from "lucide-react" import type { CloudAgent } from "@roo-code/types" +import { vscode } from "@/utils/vscode" +import { useExtensionState } from "@/context/ExtensionStateContext" -interface CloudAgentsProps { - cloudApiUrl: string - sessionToken?: string -} - -const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken }) => { +const CloudAgents: React.FC = () => { + const { cloudIsAuthenticated, cloudUserInfo, cloudApiUrl } = useExtensionState() const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) useEffect(() => { - const fetchAgents = async () => { - try { - setLoading(true) + const getCloudAgents = () => { + // Only fetch agents if user is authenticated + if (!cloudIsAuthenticated) { + console.log("[CloudAgents] User not authenticated, skipping fetch") + setAgents([]) + setLoading(false) setError(false) + return + } - 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 - } + setLoading(true) + setError(false) - const response = await fetch(`${cloudApiUrl}/api/cloud_agents`, { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${sessionToken}`, - }, - }) + console.log("[CloudAgents] Requesting cloud agents from extension") - if (!response.ok) { - throw new Error("Failed to fetch agents") + // Request cloud agents from the extension + vscode.postMessage({ type: "getCloudAgents" }) + } + + // Set up message listener for the response + const handleMessage = (event: MessageEvent) => { + const message = event.data + if (message.type === "cloudAgents") { + console.log("[CloudAgents] Received cloud agents response:", message) + + if (message.error) { + console.log("[CloudAgents] Error fetching cloud agents:", message.error) + setError(true) + // Don't use mock data on error - just show empty state + setAgents([]) + } else { + setAgents(message.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]) + window.addEventListener("message", handleMessage) + getCloudAgents() - // If there's an error, show nothing as requested - if (error) { + // Cleanup listener on unmount + return () => { + window.removeEventListener("message", handleMessage) + } + }, [cloudIsAuthenticated, cloudUserInfo?.organizationId]) // agents is excluded intentionally as it's set by the response + + // If not authenticated or there's an error, show nothing as requested + if (!cloudIsAuthenticated || error) { return null } @@ -71,11 +69,11 @@ const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken }) } const handleAgentClick = (agentId: string) => { - window.open(`${cloudApiUrl}/cloud-agents/${agentId}`, "_blank") + vscode.postMessage({ type: "openExternal", url: `${cloudApiUrl}/cloud-agents/${agentId}/run` }) } const handleCreateClick = () => { - window.open(`${cloudApiUrl}/cloud-agents/create`, "_blank") + vscode.postMessage({ type: "openExternal", url: `${cloudApiUrl}/cloud-agents/create` }) } return ( @@ -94,9 +92,10 @@ const CloudAgents: React.FC = ({ cloudApiUrl, sessionToken })
{agents.length === 0 ? ( -
+
+

- No Cloud agents yes? + Code away from your IDE with Roo's Cloud Agents.

diff --git a/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx new file mode 100644 index 00000000000..f2114df4f70 --- /dev/null +++ b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx @@ -0,0 +1,212 @@ +import React from "react" +import { render, screen, waitFor, fireEvent } from "@testing-library/react" +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import CloudAgents from "../CloudAgents" +import type { CloudAgent } from "@roo-code/types" + +const MOCK_AGENTS: CloudAgent[] = [ + { + id: "agent1", + name: "Code Assistant", + type: "assistant", + icon: "assistant", + }, + { + id: "agent2", + name: "Test Agent", + type: "test", + icon: "test", + }, +] + +// Mock vscode postMessage +const mockPostMessage = vi.fn() +vi.mock("@/utils/vscode", () => ({ + vscode: { + postMessage: (message: any) => mockPostMessage(message), + }, +})) + +// Mock useExtensionState +const mockCloudIsAuthenticated = vi.fn() +const mockCloudUserInfo = vi.fn() +const mockCloudApiUrl = vi.fn() + +vi.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + cloudIsAuthenticated: mockCloudIsAuthenticated(), + cloudUserInfo: mockCloudUserInfo(), + cloudApiUrl: mockCloudApiUrl(), + }), +})) + +// Helper function to simulate message from extension +const simulateExtensionMessage = (data: any) => { + const event = new MessageEvent("message", { data }) + window.dispatchEvent(event) +} + +describe("CloudAgents", () => { + beforeEach(() => { + mockPostMessage.mockClear() + // Set default mocked values + mockCloudIsAuthenticated.mockReturnValue(true) + mockCloudUserInfo.mockReturnValue({ organizationId: "org123" }) + mockCloudApiUrl.mockReturnValue("https://api.test.com") + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + it("should not render when not authenticated", () => { + mockCloudIsAuthenticated.mockReturnValue(false) + const { container } = render() + + // Component should render nothing when not authenticated + expect(container.firstChild).toBeNull() + expect(mockPostMessage).not.toHaveBeenCalled() + }) + + it("should request cloud agents on mount when authenticated", async () => { + render() + + // Should request cloud agents from extension + expect(mockPostMessage).toHaveBeenCalledWith({ type: "getCloudAgents" }) + + // Simulate response from extension + simulateExtensionMessage({ + type: "cloudAgents", + agents: MOCK_AGENTS, + }) + + // Wait for the component to render agents + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + expect(screen.getByText("Test Agent")).toBeInTheDocument() + }) + }) + + it("should handle agent click and open correct URL", async () => { + render() + + // Simulate response from extension + simulateExtensionMessage({ + type: "cloudAgents", + agents: MOCK_AGENTS, + }) + + await waitFor(() => { + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + }) + + const agentElement = screen.getByText("Code Assistant").closest("div.cursor-pointer") + fireEvent.click(agentElement!) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "openExternal", + url: "https://api.test.com/cloud-agents/agent1/run", + }) + }) + + it("should handle create button click", async () => { + render() + + // Simulate response from extension with agents + simulateExtensionMessage({ + type: "cloudAgents", + agents: MOCK_AGENTS, + }) + + await waitFor(() => { + expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + }) + + const createButton = screen.getByTitle("Create new agent") + fireEvent.click(createButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "openExternal", + url: "https://api.test.com/cloud-agents/create", + }) + }) + + it("should show empty state when no agents and handle create button", async () => { + render() + + // Simulate response from extension with empty agents + simulateExtensionMessage({ + type: "cloudAgents", + agents: [], + }) + + await waitFor(() => { + expect(screen.getByText(/Code away from your IDE/)).toBeInTheDocument() + }) + + // Find and click the "Create your first" button in the empty state + const createFirstButton = screen.getByText("Create your first.") + fireEvent.click(createFirstButton) + + expect(mockPostMessage).toHaveBeenCalledWith({ + type: "openExternal", + url: "https://api.test.com/cloud-agents/create", + }) + }) + + it("should handle error gracefully and show nothing", async () => { + render() + + // Simulate error response from extension + simulateExtensionMessage({ + type: "cloudAgents", + error: "Failed to fetch agents", + agents: [], + }) + + // Wait for the component to process the error + await waitFor(() => { + // Component should render nothing on error + expect(screen.queryByText("Cloud Agents")).not.toBeInTheDocument() + }) + }) + + it("should not render anything while loading", () => { + const { container } = render() + + // Before receiving the message response, component should render nothing + expect(container.firstChild).toBeNull() + expect(screen.queryByText("Cloud Agents")).not.toBeInTheDocument() + }) + + it("should re-fetch agents when organization changes", async () => { + const { rerender } = render() + + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "getCloudAgents" }) + + // Clear previous calls + mockPostMessage.mockClear() + + // Change organization + mockCloudUserInfo.mockReturnValue({ organizationId: "org456" }) + rerender() + + // Should request agents again with new org + expect(mockPostMessage).toHaveBeenCalledTimes(1) + expect(mockPostMessage).toHaveBeenCalledWith({ type: "getCloudAgents" }) + }) + + it("should properly clean up message listener on unmount", () => { + const removeEventListenerSpy = vi.spyOn(window, "removeEventListener") + + const { unmount } = render() + + unmount() + + expect(removeEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + + removeEventListenerSpy.mockRestore() + }) +}) diff --git a/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx b/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx deleted file mode 100644 index c479e8e81e7..00000000000 --- a/webview-ui/src/components/cloud/__tests__/CloudAgents.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from "react" -import { render, screen, fireEvent, waitFor } from "@testing-library/react" -import "@testing-library/jest-dom" -import CloudAgents from "../CloudAgents" - -// Mock window.open -const mockWindowOpen = vi.fn() -window.open = mockWindowOpen - -// Mock fetch -global.fetch = vi.fn() - -describe("CloudAgents", () => { - beforeEach(() => { - mockWindowOpen.mockClear() - ;(global.fetch as any).mockClear() - }) - - const defaultProps = { - cloudApiUrl: "https://app.roocode.com", - sessionToken: undefined, - } - - it("should render mock data when no session token is provided", async () => { - render() - - // Wait for the component to load - await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() - }) - - // Check if mock agents are displayed - expect(screen.getByText("Code Assistant")).toBeInTheDocument() - expect(screen.getByText("Test Generator")).toBeInTheDocument() - expect(screen.getByText("Code Reviewer")).toBeInTheDocument() - expect(screen.getByText("Documentation Writer")).toBeInTheDocument() - - // Check agent types - expect(screen.getByText("code")).toBeInTheDocument() - expect(screen.getByText("test")).toBeInTheDocument() - expect(screen.getByText("review")).toBeInTheDocument() - expect(screen.getByText("docs")).toBeInTheDocument() - }) - - it("should handle agent click and open correct URL", async () => { - render() - - await waitFor(() => { - expect(screen.getByText("Code Assistant")).toBeInTheDocument() - }) - - // Click on an agent - const codeAssistant = screen.getByText("Code Assistant").closest("div.cursor-pointer") - fireEvent.click(codeAssistant!) - - expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/1", "_blank") - }) - - it("should handle create button click", async () => { - render() - - await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() - }) - - // Find and click the create button (Plus icon button) - const createButton = screen.getByTitle("Create new agent") - fireEvent.click(createButton) - - expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/create", "_blank") - }) - - it("should show empty state when no agents and handle create button", async () => { - // Mock fetch to return empty agents - ;(global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ agents: [] }), - }) - - render() - - await waitFor(() => { - expect(screen.getByText("Create your first cloud agent")).toBeInTheDocument() - }) - - // Check for the create agent button in empty state - const createButton = screen.getByRole("button", { name: /Create Agent/i }) - expect(createButton).toBeInTheDocument() - - fireEvent.click(createButton) - - expect(mockWindowOpen).toHaveBeenCalledWith("https://app.roocode.com/cloud-agents/create", "_blank") - }) - - it("should use mock data when API call fails", async () => { - // Mock fetch to fail - ;(global.fetch as any).mockRejectedValueOnce(new Error("API Error")) - - render() - - await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() - }) - - // Should still show mock data - expect(screen.getByText("Code Assistant")).toBeInTheDocument() - expect(screen.getByText("Test Generator")).toBeInTheDocument() - expect(screen.getByText("Code Reviewer")).toBeInTheDocument() - expect(screen.getByText("Documentation Writer")).toBeInTheDocument() - }) - - it("should not render anything while loading", () => { - const { container } = render() - - // Initially should be empty (loading state returns null) - expect(container.firstChild).toBeNull() - }) - - it("should fetch agents from API when session token is provided", async () => { - const mockAgents = [ - { id: "api-1", name: "API Agent 1", type: "api-type-1", icon: "🤖" }, - { id: "api-2", name: "API Agent 2", type: "api-type-2", icon: "🎯" }, - ] - - ;(global.fetch as any).mockResolvedValueOnce({ - ok: true, - json: async () => ({ agents: mockAgents }), - }) - - render() - - await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() - }) - - // Check API call was made with correct headers - expect(global.fetch).toHaveBeenCalledWith("https://app.roocode.com/api/cloud_agents", { - headers: { - "Content-Type": "application/json", - Authorization: "Bearer test-token", - }, - }) - - // Check if API agents are displayed - expect(screen.getByText("API Agent 1")).toBeInTheDocument() - expect(screen.getByText("API Agent 2")).toBeInTheDocument() - expect(screen.getByText("api-type-1")).toBeInTheDocument() - expect(screen.getByText("api-type-2")).toBeInTheDocument() - }) -}) diff --git a/webview-ui/src/components/history/HistoryPreview.tsx b/webview-ui/src/components/history/HistoryPreview.tsx index 3f1362f3407..8c0ce0ad87c 100644 --- a/webview-ui/src/components/history/HistoryPreview.tsx +++ b/webview-ui/src/components/history/HistoryPreview.tsx @@ -17,7 +17,7 @@ const HistoryPreview = () => { return (
-

{t("history:recentTasks")}

+

{t("history:recentTasks")}

)}
@@ -95,11 +97,11 @@ const CloudAgents: React.FC = () => {

- Code away from your IDE with Roo's Cloud Agents. + {t("chat:cloudAgents.description")}

@@ -109,7 +111,8 @@ const CloudAgents: React.FC = () => {
handleAgentClick(agent.id)}> + onClick={() => handleAgentClick(agent.id)} + aria-label={t("chat:cloudAgents.clickToRun", { name: agent.name })}> documentació per obtenir més informació.", - "onboarding": " La vostra llista de tasques en aquest espai de treball està buida. Comença escrivint una tasca a continuació. \nNo esteu segur per on començar? \nMés informació sobre què pot fer Roo als documents.", + "about": "Roo Code és un equip de desenvolupament d'IA sencer al teu editor.", + "docs": "Consulta la nostra documentació per aprendre més.", "rooTips": { - "boomerangTasks": { - "title": "Orquestració de Tasques", - "description": "Divideix les tasques en parts més petites i manejables." - }, - "stickyModels": { - "title": "Modes persistents", - "description": "Cada mode recorda el vostre darrer model utilitzat" - }, - "tools": { - "title": "Eines", - "description": "Permet que la IA resolgui problemes navegant per la web, executant ordres i molt més." - }, "customizableModes": { "title": "Modes personalitzables", - "description": "Personalitats especialitzades amb comportaments propis i models assignats" + "description": "Personatges especialitzats que es mantenen a la tasca i compleixen." + }, + "modelAgnostic": { + "title": "Porta el teu propi model", + "description": "Fes servir la teva pròpia clau de proveïdor o fins i tot executa inferència local — sense sobrecost, sense bloqueig, sense restriccions" } }, "selectMode": "Selecciona el mode d'interacció", @@ -412,5 +404,12 @@ "problems": "Problemes", "terminal": "Terminal", "url": "Enganxa la URL per obtenir-ne el contingut" + }, + "cloudAgents": { + "create": "Crear", + "title": "Agents al núvol", + "description": "Encara no has creat cap Agent al núvol.", + "createFirst": "Crea el teu primer", + "clickToRun": "Fes clic per executar {{name}}" } } diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 65934ab5a29..6fa4e08ba5f 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -85,24 +85,16 @@ "tooltip": "Aktuelle Operation abbrechen" }, "scrollToBottom": "Zum Chat-Ende scrollen", - "about": "Generiere, überarbeite und debugge Code mit KI-Unterstützung. Weitere Informationen findest du in unserer Dokumentation.", - "onboarding": "Deine Aufgabenliste in diesem Arbeitsbereich ist leer. Beginne mit der Eingabe einer Aufgabe unten. Du bist dir nicht sicher, wie du anfangen sollst? Lies mehr darüber, was Roo für dich tun kann, in den Dokumenten.", + "about": "Roo Code ist ein ganzes KI-Entwicklerteam in deinem Editor.", + "docs": "Schau in unsere Dokumentation, um mehr zu erfahren.", "rooTips": { - "boomerangTasks": { - "title": "Aufgaben-Orchestrierung", - "description": "Teile Aufgaben in kleinere, überschaubare Teile auf." - }, - "stickyModels": { - "title": "Sticky Modi", - "description": "Jeder Modus merkt sich dein zuletzt verwendetes Modell" - }, - "tools": { - "title": "Tools", - "description": "Erlaube der KI, Probleme durch Surfen im Web, Ausführen von Befehlen und mehr zu lösen." - }, "customizableModes": { "title": "Anpassbare Modi", - "description": "Spezialisierte Personas mit eigenem Verhalten und zugewiesenen Modellen" + "description": "Spezialisierte Personas, die bei der Sache bleiben und Ergebnisse liefern." + }, + "modelAgnostic": { + "title": "Bring dein eigenes Modell mit", + "description": "Verwende deinen eigenen Anbieter-Schlüssel oder führe sogar lokale Inferenz aus — kein Aufpreis, keine Bindung, keine Einschränkungen" } }, "selectMode": "Interaktionsmodus auswählen", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo möchte einen Slash-Befehl ausführen", "didRun": "Roo hat einen Slash-Befehl ausgeführt" + }, + "cloudAgents": { + "create": "Erstellen", + "title": "Cloud-Agenten", + "description": "Du hast noch keine Cloud-Agenten erstellt.", + "createFirst": "Erstelle deinen ersten", + "clickToRun": "Klicke, um {{name}} auszuführen" } } diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index abe106aeee5..1db0b04d175 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -97,7 +97,7 @@ }, "scrollToBottom": "Scroll to bottom of chat", "about": "Roo Code is a whole AI dev team in your editor.", - "docs": "Check out our documentation to learn more.", + "docs": "Check our docs to learn more.", "rooTips": { "customizableModes": { "title": "Customizable modes", @@ -398,5 +398,12 @@ "problems": "Problems", "terminal": "Terminal", "url": "Paste URL to fetch contents" + }, + "cloudAgents": { + "create": "Create", + "title": "Cloud Agents", + "description": "You haven’t created any Cloud Agents yet.", + "createFirst": "Create your first", + "clickToRun": "Click to run {{name}}" } } diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 30693285db2..92b56fe124b 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -85,24 +85,16 @@ "tooltip": "Cancelar la operación actual" }, "scrollToBottom": "Desplazarse al final del chat", - "about": "Genera, refactoriza y depura código con asistencia de IA. Consulta nuestra documentación para obtener más información.", - "onboarding": "Tu lista de tareas en este espacio de trabajo está vacía. Comienza escribiendo una tarea abajo. ¿No estás seguro cómo empezar? Lee más sobre lo que Roo puede hacer por ti en la documentación.", + "about": "Roo Code es un equipo de desarrollo de IA completo en tu editor.", + "docs": "Consulta nuestros documentos para obtener más información.", "rooTips": { - "boomerangTasks": { - "title": "Orquestación de Tareas", - "description": "Divide las tareas en partes más pequeñas y manejables." - }, - "stickyModels": { - "title": "Modos persistentes", - "description": "Cada modo recuerda tu último modelo utilizado" - }, - "tools": { - "title": "Herramientas", - "description": "Permite que la IA resuelva problemas navegando por la web, ejecutando comandos y mucho más." - }, "customizableModes": { "title": "Modos personalizables", - "description": "Personalidades especializadas con sus propios comportamientos y modelos asignados" + "description": "Personas especializadas que se mantienen en la tarea y cumplen." + }, + "modelAgnostic": { + "title": "Trae tu propio modelo", + "description": "Usa tu propia clave de proveedor o incluso ejecuta inferencia local — sin sobrecostos, sin ataduras, sin restricciones" } }, "selectMode": "Seleccionar modo de interacción", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo quiere ejecutar un comando slash", "didRun": "Roo ejecutó un comando slash" + }, + "cloudAgents": { + "create": "Crear", + "title": "Agentes en la nube", + "description": "Aún no has creado ningún Agente en la nube.", + "createFirst": "Crea tu primero", + "clickToRun": "Haz clic para ejecutar {{name}}" } } diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index d5dae24a791..949ff0b3d25 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -85,24 +85,16 @@ "tooltip": "Annuler l'opération actuelle" }, "scrollToBottom": "Défiler jusqu'au bas du chat", - "about": "Générer, refactoriser et déboguer du code avec l'assistance de l'IA. Consultez notre documentation pour en savoir plus.", - "onboarding": "Grâce aux dernières avancées en matière de capacités de codage agent, je peux gérer des tâches complexes de développement logiciel étape par étape. Avec des outils qui me permettent de créer et d'éditer des fichiers, d'explorer des projets complexes, d'utiliser le navigateur et d'exécuter des commandes de terminal (après votre autorisation), je peux vous aider de manières qui vont au-delà de la complétion de code ou du support technique. Je peux même utiliser MCP pour créer de nouveaux outils et étendre mes propres capacités.", + "about": "Roo Code est une équipe de développement IA complète dans votre éditeur.", + "docs": "Consultez notre documentation pour en savoir plus.", "rooTips": { - "boomerangTasks": { - "title": "Orchestration de Tâches", - "description": "Divisez les tâches en parties plus petites et gérables." - }, - "stickyModels": { - "title": "Modes persistants", - "description": "Chaque mode se souvient de votre dernier modèle utilisé" - }, - "tools": { - "title": "Outils", - "description": "Permettez à l'IA de résoudre des problèmes en naviguant sur le Web, en exécutant des commandes, et plus encore." - }, "customizableModes": { "title": "Modes personnalisables", - "description": "Des personas spécialisés avec leurs propres comportements et modèles assignés" + "description": "Des personas spécialisés qui restent concentrés sur la tâche et livrent des résultats." + }, + "modelAgnostic": { + "title": "Apportez votre propre modèle", + "description": "Utilisez votre propre clé de fournisseur ou exécutez même une inférence locale — sans majoration, sans verrouillage, sans restrictions" } }, "selectMode": "Sélectionner le mode d'interaction", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo veut exécuter une commande slash", "didRun": "Roo a exécuté une commande slash" + }, + "cloudAgents": { + "create": "Créer", + "title": "Agents Cloud", + "description": "Tu n'as pas encore créé d'Agents Cloud.", + "createFirst": "Crée ton premier", + "clickToRun": "Clique pour exécuter {{name}}" } } diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 0ec9cd7c7e9..47cd22806b5 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -85,24 +85,16 @@ "tooltip": "वर्तमान ऑपरेशन रद्द करें" }, "scrollToBottom": "चैट के निचले हिस्से तक स्क्रॉल करें", - "about": "एआई सहायता से कोड जेनरेट करें, रिफैक्टर करें और डिबग करें। अधिक जानने के लिए हमारे दस्तावेज़ देखें।", - "onboarding": "एजेंटिक कोडिंग क्षमताओं में नवीनतम प्रगति के कारण, मैं जटिल सॉफ्टवेयर विकास कार्यों को चरण-दर-चरण संभाल सकता हूं। ऐसे उपकरणों के साथ जो मुझे फ़ाइलें बनाने और संपादित करने, जटिल प्रोजेक्ट का अन्वेषण करने, ब्राउज़र का उपयोग करने और टर्मिनल कमांड (आपकी अनुमति के बाद) निष्पादित करने की अनुमति देते हैं, मैं आपकी मदद कोड पूर्णता या तकनीकी समर्थन से परे तरीकों से कर सकता हूं। मैं अपनी क्षमताओं का विस्तार करने और नए उपकरण बनाने के लिए MCP का भी उपयोग कर सकता हूं।", + "about": "रू कोड आपके संपादक में एक पूरी एआई देव टीम है।", + "docs": "अधिक जानने के लिए हमारे दस्तावेज़ देखें।", "rooTips": { - "boomerangTasks": { - "title": "कार्य संयोजन", - "description": "कार्यों को छोटे, प्रबंधनीय भागों में विभाजित करें।" - }, - "stickyModels": { - "title": "स्टिकी मोड", - "description": "प्रत्येक मोड आपके अंतिम उपयोग किए गए मॉडल को याद रखता है" - }, - "tools": { - "title": "उपकरण", - "description": "एआई को वेब ब्राउज़ करके, कमांड चलाकर और अधिक समस्याओं को हल करने की अनुमति दें।" - }, "customizableModes": { "title": "अनुकूलन योग्य मोड", - "description": "विशिष्ट प्रोफाइल अपने व्यवहार और निर्धारित मॉडल के साथ" + "description": "विशेषज्ञ व्यक्ति जो कार्य पर बने रहते हैं और वितरित करते हैं।" + }, + "modelAgnostic": { + "title": "अपना खुद का मॉडल लाओ", + "description": "अपने स्वयं के प्रदाता कुंजी का उपयोग करें या यहां तक ​​कि स्थानीय अनुमान चलाएं - कोई मार्कअप नहीं, कोई लॉक-इन नहीं, कोई प्रतिबंध नहीं" } }, "selectMode": "इंटरैक्शन मोड चुनें", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo एक स्लैश कमांड चलाना चाहता है", "didRun": "Roo ने एक स्लैश कमांड चलाया" + }, + "cloudAgents": { + "create": "बनाएं", + "title": "क्लाउड एजेंट", + "description": "आपने अभी तक कोई क्लाउड एजेंट नहीं बनाया है।", + "createFirst": "अपना पहला बनाएं", + "clickToRun": "{{name}} चलाने के लिए क्लिक करें" } } diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index a2c5377691e..55b3cbf6094 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -99,24 +99,16 @@ "tooltip": "Batalkan operasi saat ini" }, "scrollToBottom": "Gulir ke bawah chat", - "about": "Buat, refaktor, dan debug kode dengan bantuan AI.
Lihat dokumentasi kami untuk mempelajari lebih lanjut.", - "onboarding": "Daftar tugas di workspace ini kosong.", + "about": "Roo Code adalah seluruh tim pengembang AI di editor Anda.", + "docs": "Lihat dokumentasi kami untuk mempelajari lebih lanjut.", "rooTips": { - "boomerangTasks": { - "title": "Orkestrasi Tugas", - "description": "Bagi tugas menjadi bagian-bagian kecil yang dapat dikelola" - }, - "stickyModels": { - "title": "Model Sticky", - "description": "Setiap mode mengingat model terakhir yang kamu gunakan" - }, - "tools": { - "title": "Tools", - "description": "Izinkan AI menyelesaikan masalah dengan browsing web, menjalankan perintah, dan lainnya" - }, "customizableModes": { - "title": "Mode yang Dapat Disesuaikan", - "description": "Persona khusus dengan perilaku dan model yang ditugaskan sendiri" + "title": "Mode yang dapat disesuaikan", + "description": "Persona khusus yang tetap fokus pada tugas dan memberikan hasil." + }, + "modelAgnostic": { + "title": "Bawa model Anda sendiri", + "description": "Gunakan kunci penyedia Anda sendiri atau bahkan jalankan inferensi lokal — tanpa markup, tanpa kunci, tanpa batasan" } }, "selectMode": "Pilih mode untuk interaksi", @@ -418,5 +410,12 @@ "slashCommand": { "wantsToRun": "Roo ingin menjalankan perintah slash", "didRun": "Roo telah menjalankan perintah slash" + }, + "cloudAgents": { + "create": "Buat", + "title": "Agen Cloud", + "description": "Anda belum membuat Agen Cloud apa pun.", + "createFirst": "Buat yang pertama", + "clickToRun": "Klik untuk menjalankan {{name}}" } } diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index a176be1f2fb..d546daae10c 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -85,24 +85,16 @@ "tooltip": "Annulla l'operazione corrente" }, "scrollToBottom": "Scorri fino alla fine della chat", - "about": "Genera, refactor e debug del codice con l'assistenza dell'IA. Consulta la nostra documentazione per saperne di più.", - "onboarding": "Grazie alle più recenti innovazioni nelle capacità di codifica agentica, posso gestire complesse attività di sviluppo software passo dopo passo. Con strumenti che mi permettono di creare e modificare file, esplorare progetti complessi, utilizzare il browser ed eseguire comandi da terminale (dopo la tua autorizzazione), posso aiutarti in modi che vanno oltre il completamento del codice o il supporto tecnico. Posso persino usare MCP per creare nuovi strumenti ed estendere le mie capacità.", + "about": "Roo Code è un intero team di sviluppo AI nel tuo editor.", + "docs": "Consulta la nostra documentazione per saperne di più.", "rooTips": { - "boomerangTasks": { - "title": "Orchestrazione di Attività", - "description": "Dividi le attività in parti più piccole e gestibili." - }, - "stickyModels": { - "title": "Modalità persistenti", - "description": "Ogni modalità ricorda il tuo ultimo modello utilizzato" - }, - "tools": { - "title": "Strumenti", - "description": "Consenti all'IA di risolvere i problemi navigando sul Web, eseguendo comandi e altro ancora." - }, "customizableModes": { "title": "Modalità personalizzabili", - "description": "Personalità specializzate con comportamenti propri e modelli assegnati" + "description": "Personaggi specializzati che rimangono concentrati sull'attività e producono risultati." + }, + "modelAgnostic": { + "title": "Porta il tuo modello", + "description": "Usa la tua chiave provider o esegui persino l'inferenza locale — senza markup, senza vincoli, senza restrizioni" } }, "selectMode": "Seleziona modalità di interazione", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo vuole eseguire un comando slash", "didRun": "Roo ha eseguito un comando slash" + }, + "cloudAgents": { + "create": "Crea", + "title": "Agenti Cloud", + "description": "Non hai ancora creato alcun Agente Cloud.", + "createFirst": "Crea il tuo primo", + "clickToRun": "Clicca per eseguire {{name}}" } } diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 0e940b17eca..b2a45f469a3 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -85,24 +85,16 @@ "tooltip": "現在の操作をキャンセル" }, "scrollToBottom": "チャットの最下部にスクロール", - "about": "AI支援でコードを生成、リファクタリング、デバッグします。詳細については、ドキュメントをご覧ください。", - "onboarding": "最新のエージェント型コーディング能力の進歩により、複雑なソフトウェア開発タスクをステップバイステップで処理できます。ファイルの作成や編集、複雑なプロジェクトの探索、ブラウザの使用、ターミナルコマンドの実行(許可後)を可能にするツールにより、コード補完や技術サポート以上の方法であなたをサポートできます。MCPを使用して新しいツールを作成し、自分の能力を拡張することもできます。", + "about": "Roo Codeはエディターの中にいるAI開発チーム全体です。", + "docs": "詳細については、ドキュメントを確認してください。", "rooTips": { - "boomerangTasks": { - "title": "タスクオーケストレーション", - "description": "タスクをより小さく、管理しやすい部分に分割します。" - }, - "stickyModels": { - "title": "スティッキーモード", - "description": "各モードは、最後に使用したモデルを記憶しています" - }, - "tools": { - "title": "ツール", - "description": "AIがWebの閲覧、コマンドの実行などによって問題を解決できるようにします。" - }, "customizableModes": { "title": "カスタマイズ可能なモード", - "description": "独自の動作と割り当てられたモデルを持つ専門的なペルソナ" + "description": "タスクに集中し、成果を出す専門のペルソナ。" + }, + "modelAgnostic": { + "title": "独自のモデルを使用", + "description": "独自のプロバイダーキーを使用したり、ローカル推論を実行したりできます — マークアップ、ロックイン、制限なし" } }, "selectMode": "対話モードを選択", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Rooはスラッシュコマンドを実行したい", "didRun": "Rooはスラッシュコマンドを実行しました" + }, + "cloudAgents": { + "create": "作成", + "title": "クラウドエージェント", + "description": "まだクラウドエージェントを作成していません。", + "createFirst": "最初に作成する", + "clickToRun": "クリックして{{name}}を実行" } } diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 6fa7c5692ac..cbfc52e865e 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -85,24 +85,16 @@ "tooltip": "현재 작업 취소" }, "scrollToBottom": "채팅 하단으로 스크롤", - "about": "AI 지원으로 코드를 생성, 리팩터링 및 디버깅합니다. 자세한 내용은 문서를 확인하세요.", - "onboarding": "이 작업 공간의 작업 목록이 비어 있습니다. 아래에 작업을 입력하여 시작하세요. 어떻게 시작해야 할지 모르겠나요? Roo가 무엇을 할 수 있는지 문서에서 자세히 알아보세요.", + "about": "Roo Code는 편집기 안에 있는 전체 AI 개발팀입니다.", + "docs": "더 자세히 알아보려면 문서를 확인하세요.", "rooTips": { - "boomerangTasks": { - "title": "작업 오케스트레이션", - "description": "작업을 더 작고 관리하기 쉬운 부분으로 나눕니다." - }, - "stickyModels": { - "title": "스티키 모드", - "description": "각 모드는 마지막으로 사용한 모델을 기억합니다." - }, - "tools": { - "title": "도구", - "description": "AI가 웹 탐색, 명령 실행 등으로 문제를 해결하도록 허용합니다." - }, "customizableModes": { "title": "사용자 정의 모드", - "description": "고유한 동작과 할당된 모델을 가진 전문 페르소나" + "description": "작업에 집중하고 결과물을 전달하는 전문화된 페르소나." + }, + "modelAgnostic": { + "title": "자신만의 모델 가져오기", + "description": "자신만의 공급자 키를 사용하거나 로컬 추론을 실행하세요 — 마크업, 종속, 제한 없음" } }, "selectMode": "상호작용 모드 선택", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo가 슬래시 명령어를 실행하려고 합니다", "didRun": "Roo가 슬래시 명령어를 실행했습니다" + }, + "cloudAgents": { + "create": "만들기", + "title": "클라우드 에이전트", + "description": "아직 클라우드 에이전트를 만들지 않았습니다.", + "createFirst": "첫 번째 만들기", + "clickToRun": "클릭하여 {{name}} 실행" } } diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 2be312228d2..ca6ebfb673f 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -85,24 +85,16 @@ "tooltip": "Annuleer de huidige bewerking" }, "scrollToBottom": "Scroll naar onderaan de chat", - "about": "Genereer, refactor en debug code met AI-assistentie. Bekijk onze documentatie voor meer informatie.", - "onboarding": "Je takenlijst in deze werkruimte is leeg.", + "about": "Roo Code is een heel AI-ontwikkelteam in je editor.", + "docs": "Bekijk onze documentatie voor meer informatie.", "rooTips": { - "boomerangTasks": { - "title": "Taak Orchestratie", - "description": "Splits taken op in kleinere, beheersbare delen" - }, - "stickyModels": { - "title": "Vastgezette modellen", - "description": "Elke modus onthoudt je laatst gebruikte model" - }, - "tools": { - "title": "Tools", - "description": "Laat de AI problemen oplossen door te browsen, commando's uit te voeren en meer" - }, "customizableModes": { "title": "Aanpasbare modi", - "description": "Gespecialiseerde persona's met hun eigen gedrag en toegewezen modellen" + "description": "Gespecialiseerde persona's die bij de taak blijven en leveren." + }, + "modelAgnostic": { + "title": "Gebruik je eigen model", + "description": "Gebruik je eigen provider key of voer zelfs lokale inferentie uit — geen markup, geen lock-in, geen beperkingen" } }, "selectMode": "Selecteer modus voor interactie", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo wil een slash commando uitvoeren", "didRun": "Roo heeft een slash commando uitgevoerd" + }, + "cloudAgents": { + "create": "Aanmaken", + "title": "Cloud Agents", + "description": "Je hebt nog geen Cloud Agents aangemaakt.", + "createFirst": "Maak je eerste aan", + "clickToRun": "Klik om {{name}} uit te voeren" } } diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index e60ec330db2..6762ef3fdca 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -85,24 +85,16 @@ "tooltip": "Anuluj bieżącą operację" }, "scrollToBottom": "Przewiń do dołu czatu", - "about": "Generuj, refaktoryzuj i debuguj kod z pomocą sztucznej inteligencji. Sprawdź naszą dokumentację, aby dowiedzieć się więcej.", - "onboarding": "Twoja lista zadań w tym obszarze roboczym jest pusta. Zacznij od wpisania zadania poniżej. Nie wiesz, jak zacząć? Przeczytaj więcej o tym, co Roo może dla Ciebie zrobić w dokumentacji.", + "about": "Roo Code to cały zespół deweloperów AI w Twoim edytorze.", + "docs": "Sprawdź naszą dokumentację, aby dowiedzieć się więcej.", "rooTips": { - "boomerangTasks": { - "title": "Orkiestracja Zadań", - "description": "Podziel zadania na mniejsze, łatwiejsze do zarządzania części." - }, - "stickyModels": { - "title": "Tryby trwałe", - "description": "Każdy tryb zapamiętuje ostatnio używany model" - }, - "tools": { - "title": "Narzędzia", - "description": "Pozwól sztucznej inteligencji rozwiązywać problemy, przeglądając sieć, uruchamiając polecenia i nie tylko." - }, "customizableModes": { "title": "Konfigurowalne tryby", - "description": "Wyspecjalizowane persona z własnymi zachowaniami i przypisanymi modelami" + "description": "Specjalistyczne persony, które trzymają się zadania i dostarczają wyniki." + }, + "modelAgnostic": { + "title": "Przynieś własny model", + "description": "Użyj własnego klucza dostawcy lub uruchom lokalną inferencję — bez narzutów, bez blokad, bez ograniczeń" } }, "selectMode": "Wybierz tryb interakcji", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo chce uruchomić komendę slash", "didRun": "Roo uruchomił komendę slash" + }, + "cloudAgents": { + "create": "Utwórz", + "title": "Agenci w chmurze", + "description": "Nie utworzyłeś jeszcze żadnych Agentów w chmurze.", + "createFirst": "Utwórz swojego pierwszego", + "clickToRun": "Kliknij, aby uruchomić {{name}}" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 3408f3a1aa9..bede3e3df49 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -85,24 +85,16 @@ "tooltip": "Cancelar a operação atual" }, "scrollToBottom": "Rolar para o final do chat", - "about": "Gere, refatore e depure código com assistência de IA. Confira nossa documentação para saber mais.", - "onboarding": "Sua lista de tarefas neste espaço de trabalho está vazia. Comece digitando uma tarefa abaixo. Não sabe como começar? Leia mais sobre o que o Roo pode fazer por você nos documentos.", + "about": "O Roo Code é uma equipe inteira de desenvolvimento de IA em seu editor.", + "docs": "Confira nossa documentação para saber mais.", "rooTips": { - "boomerangTasks": { - "title": "Orquestração de Tarefas", - "description": "Divida as tarefas em partes menores e gerenciáveis." - }, - "stickyModels": { - "title": "Modos Fixos", - "description": "Cada modo lembra o seu último modelo usado" - }, - "tools": { - "title": "Ferramentas", - "description": "Permita que a IA resolva problemas navegando na web, executando comandos e muito mais." - }, "customizableModes": { "title": "Modos personalizáveis", - "description": "Personas especializadas com comportamentos próprios e modelos atribuídos" + "description": "Personas especializadas que se mantêm na tarefa e entregam resultados." + }, + "modelAgnostic": { + "title": "Traga seu próprio modelo", + "description": "Use sua própria chave de provedor ou até mesmo execute inferência local — sem taxas extras, sem aprisionamento, sem restrições" } }, "selectMode": "Selecionar modo de interação", @@ -412,5 +404,12 @@ "slashCommand": { "wantsToRun": "Roo quer executar um comando slash", "didRun": "Roo executou um comando slash" + }, + "cloudAgents": { + "create": "Criar", + "title": "Agentes da Nuvem", + "description": "Você ainda não criou nenhum Agente da Nuvem.", + "createFirst": "Crie o seu primeiro", + "clickToRun": "Clique para executar {{name}}" } } diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 5018f817aff..48768a50603 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -85,23 +85,16 @@ "tooltip": "Отменить текущую операцию" }, "scrollToBottom": "Прокрутить чат вниз", - "about": "Создавайте, рефакторите и отлаживайте код с помощью ИИ. Подробнее см. в нашей документации.", + "about": "Roo Code — это целая команда разработчиков ИИ в вашем редакторе.", + "docs": "Ознакомьтесь с нашей документацией, чтобы узнать больше.", "rooTips": { - "boomerangTasks": { - "title": "Оркестрация задач", - "description": "Разделяйте задачи на более мелкие, управляемые части" - }, - "stickyModels": { - "title": "Липкие режимы", - "description": "Каждый режим запоминает вашу последнюю использованную модель" - }, - "tools": { - "title": "Инструменты", - "description": "Разрешите ИИ решать проблемы, просматривая веб-страницы, выполняя команды и т. д." - }, "customizableModes": { "title": "Настраиваемые режимы", - "description": "Специализированные персонажи с собственным поведением и назначенными моделями" + "description": "Специализированные персонажи, которые следят за выполнением задачи и добиваются результата." + }, + "modelAgnostic": { + "title": "Используйте свою модель", + "description": "Используйте собственный ключ провайдера или даже запускайте локальный инференс — без наценки, без привязки, без ограничений" } }, "onboarding": "Ваш список задач в этом рабочем пространстве пуст. Начните с ввода задачи ниже. Не знаете, с чего начать? Подробнее о возможностях Roo читайте в документации.", @@ -413,5 +406,12 @@ "slashCommand": { "wantsToRun": "Roo хочет выполнить слеш-команду", "didRun": "Roo выполнил слеш-команду" + }, + "cloudAgents": { + "create": "Создать", + "title": "Облачные агенты", + "description": "Вы еще не создали ни одного облачного агента.", + "createFirst": "Создайте своего первого", + "clickToRun": "Нажмите, чтобы запустить {{name}}" } } diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 2290a85f6df..2b44001b0fa 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -85,24 +85,16 @@ "tooltip": "Mevcut işlemi iptal et" }, "scrollToBottom": "Sohbetin altına kaydır", - "about": "AI yardımıyla kod oluşturun, yeniden düzenleyin ve hatalarını ayıklayın. Daha fazla bilgi edinmek için belgelerimize göz atın.", - "onboarding": "Bu çalışma alanındaki görev listeniz boş. Aşağıya bir görev yazarak başlayın. Nasıl başlayacağınızdan emin değil misiniz? Roo'nun sizin için neler yapabileceği hakkında daha fazla bilgiyi belgelerde okuyun.", + "about": "Roo Code, düzenleyicinizde bütün bir AI geliştirme ekibidir.", + "docs": "Daha fazla bilgi edinmek için belgelerimize göz atın.", "rooTips": { - "boomerangTasks": { - "title": "Görev Orkestrasyonu", - "description": "Görevleri daha küçük, yönetilebilir parçalara ayırın." - }, - "stickyModels": { - "title": "Yapışkan Modlar", - "description": "Her mod, en son kullandığınız modeli hatırlar" - }, - "tools": { - "title": "Araçlar", - "description": "AI'nın web'e göz atarak, komutlar çalıştırarak ve daha fazlasını yaparak sorunları çözmesine izin verin." - }, "customizableModes": { - "title": "Özelleştirilebilir Modlar", - "description": "Kendi davranışları ve atanmış modelleri ile özelleştirilmiş kişilikler" + "title": "Özelleştirilebilir modlar", + "description": "Göreve sadık kalan ve sonuç veren özel kişilikler." + }, + "modelAgnostic": { + "title": "Kendi modelini getir", + "description": "Kendi sağlayıcı anahtarınızı kullanın veya hatta yerel çıkarım çalıştırın — ek ücret yok, kilitlenme yok, kısıtlama yok" } }, "selectMode": "Etkileşim modunu seçin", @@ -413,5 +405,12 @@ "slashCommand": { "wantsToRun": "Roo bir slash komutu çalıştırmak istiyor", "didRun": "Roo bir slash komutu çalıştırdı" + }, + "cloudAgents": { + "create": "Oluştur", + "title": "Bulut Aracıları", + "description": "Henüz herhangi bir Bulut Aracısı oluşturmadınız.", + "createFirst": "İlkini oluştur", + "clickToRun": "{{name}}'i çalıştırmak için tıklayın" } } diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index d9362ea39dd..e692fdb8a53 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -85,24 +85,16 @@ "tooltip": "Hủy thao tác hiện tại" }, "scrollToBottom": "Cuộn xuống cuối cuộc trò chuyện", - "about": "Tạo, tái cấu trúc và gỡ lỗi mã bằng sự hỗ trợ của AI. Kiểm tra tài liệu của chúng tôi để tìm hiểu thêm.", - "onboarding": "Danh sách nhiệm vụ của bạn trong không gian làm việc này trống. Bắt đầu bằng cách nhập nhiệm vụ bên dưới. Bạn không chắc chắn nên bắt đầu như thế nào? Đọc thêm về những gì Roo có thể làm cho bạn trong tài liệu.", + "about": "Roo Code là một đội ngũ phát triển AI toàn diện ngay trong trình soạn thảo của bạn.", + "docs": "Kiểm tra tài liệu của chúng tôi để tìm hiểu thêm.", "rooTips": { - "boomerangTasks": { - "title": "Điều phối Nhiệm vụ", - "description": "Chia nhỏ các nhiệm vụ thành các phần nhỏ hơn, dễ quản lý hơn." - }, - "stickyModels": { - "title": "Chế độ dính", - "description": "Mỗi chế độ ghi nhớ mô hình đã sử dụng cuối cùng của bạn" - }, - "tools": { - "title": "Công cụ", - "description": "Cho phép AI giải quyết vấn đề bằng cách duyệt web, chạy lệnh, v.v." - }, "customizableModes": { - "title": "Chế độ tùy chỉnh", - "description": "Các nhân vật chuyên biệt với hành vi riêng và mô hình được chỉ định" + "title": "Chế độ có thể tùy chỉnh", + "description": "Các nhân vật chuyên biệt luôn tập trung vào nhiệm vụ và hoàn thành công việc." + }, + "modelAgnostic": { + "title": "Mang theo mô hình của riêng bạn", + "description": "Sử dụng khóa nhà cung cấp của riêng bạn hoặc thậm chí chạy suy luận cục bộ — không phụ phí, không ràng buộc, không giới hạn" } }, "selectMode": "Chọn chế độ tương tác", @@ -413,5 +405,12 @@ "slashCommand": { "wantsToRun": "Roo muốn chạy lệnh slash", "didRun": "Roo đã chạy lệnh slash" + }, + "cloudAgents": { + "create": "Tạo", + "title": "Agents trên Cloud", + "description": "Bạn chưa tạo Agents trên Cloud nào.", + "createFirst": "Tạo agent đầu tiên của bạn", + "clickToRun": "Nhấp để chạy {{name}}" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 850f79e6303..9cc758fad61 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -85,24 +85,16 @@ "tooltip": "取消当前操作" }, "scrollToBottom": "滚动到聊天底部", - "about": "通过 AI 辅助生成、重构和调试代码。查看我们的 文档 了解更多信息。", - "onboarding": "此工作区中的任务列表为空。 请在下方输入任务开始。 不确定如何开始? 在 文档 中阅读更多关于 Roo 可以为您做什么的信息。", + "about": "Roo Code 是您编辑器中的整个 AI 开发团队。", + "docs": "查看我们的 文档 了解更多信息。", "rooTips": { - "boomerangTasks": { - "title": "任务编排", - "description": "将任务拆分为更小、更易于管理的部分。" - }, - "stickyModels": { - "title": "粘性模式", - "description": "每个模式 都会记住 您上次使用的模型" - }, - "tools": { - "title": "工具", - "description": "允许 AI 通过浏览网络、运行命令等方式解决问题。" - }, "customizableModes": { - "title": "自定义模式", - "description": "具有专属行为和指定模型的特定角色" + "title": "可自定义模式", + "description": "专注于任务并交付成果的专业角色。" + }, + "modelAgnostic": { + "title": "自带模型", + "description": "使用您自己的提供商密钥,甚至可以运行本地推理——无加价、无锁定、无限制" } }, "selectMode": "选择交互模式", @@ -413,5 +405,12 @@ "slashCommand": { "wantsToRun": "Roo 想要运行斜杠命令", "didRun": "Roo 运行了斜杠命令" + }, + "cloudAgents": { + "create": "创建", + "title": "云代理", + "description": "您还没有创建任何云代理。", + "createFirst": "创建您的第一个", + "clickToRun": "点击运行 {{name}}" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 715452977af..cd1614f9956 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -96,24 +96,16 @@ "placeholder": "編輯您的訊息..." }, "scrollToBottom": "捲動至聊天室底部", - "about": "利用 AI 輔助產生、重構和偵錯程式碼。請參閱我們的說明文件以瞭解更多資訊。", - "onboarding": "此工作區的工作清單是空的。", + "about": "Roo Code 是您編輯器中的整個 AI 開發團隊。", + "docs": "查看我們的文件以了解更多信息。", "rooTips": { - "boomerangTasks": { - "title": "工作編排", - "description": "將工作拆分為更小、更易於管理的部分。" - }, - "stickyModels": { - "title": "記憶模型", - "description": "每個模式都會記住您上次使用的模型" - }, - "tools": { - "title": "工具", - "description": "允許 AI 透過瀏覽網路、執行命令等方式解決問題。" - }, "customizableModes": { "title": "可自訂模式", - "description": "具有專屬行為和指定模型的特定角色" + "description": "專注於任務並交付成果的專業角色。" + }, + "modelAgnostic": { + "title": "自備模型", + "description": "使用您自己的提供商密鑰,甚至可以運行本地推理——無加價、無鎖定、無限制" } }, "selectMode": "選擇互動模式", @@ -413,5 +405,12 @@ "slashCommand": { "wantsToRun": "Roo 想要執行斜線指令", "didRun": "Roo 執行了斜線指令" + }, + "cloudAgents": { + "create": "創建", + "title": "雲端代理", + "description": "您尚未建立任何雲端代理程式。", + "createFirst": "建立您的第一個", + "clickToRun": "點擊以執行 {{name}}" } } From 0e8048d7681b5de186ed3eafdcade00d946067de Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 16:29:00 +0100 Subject: [PATCH 07/15] Removes debug console.log calls --- webview-ui/src/components/cloud/CloudAgents.tsx | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index a299a2045d3..65c58c27241 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -16,7 +16,6 @@ const CloudAgents: React.FC = () => { const getCloudAgents = () => { // Only fetch agents if user is authenticated if (!cloudIsAuthenticated) { - console.log("[CloudAgents] User not authenticated, skipping fetch") setAgents([]) setLoading(false) setError(false) @@ -25,10 +24,6 @@ const CloudAgents: React.FC = () => { setLoading(true) setError(false) - - console.log("[CloudAgents] Requesting cloud agents from extension") - - // Request cloud agents from the extension vscode.postMessage({ type: "getCloudAgents" }) } @@ -36,12 +31,8 @@ const CloudAgents: React.FC = () => { const handleMessage = (event: MessageEvent) => { const message = event.data if (message.type === "cloudAgents") { - console.log("[CloudAgents] Received cloud agents response:", message) - if (message.error) { - console.log("[CloudAgents] Error fetching cloud agents:", message.error) setError(true) - // Don't use mock data on error - just show empty state setAgents([]) } else { setAgents(message.agents || []) @@ -60,7 +51,6 @@ const CloudAgents: React.FC = () => { } }, [cloudIsAuthenticated, cloudUserInfo?.organizationId]) // agents is excluded intentionally as it's set by the response - // If not authenticated or there's an error, show nothing as requested if (!cloudIsAuthenticated || error) { return null } From dd4b2a9857ef8faffbcc152b81053b42e76b0770 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 16:55:13 +0100 Subject: [PATCH 08/15] PR Feedback --- src/shared/ExtensionMessage.ts | 3 ++- webview-ui/src/components/chat/ChatView.tsx | 2 +- .../src/components/cloud/CloudAgents.tsx | 22 +++++++++++-------- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index eea8f805dc5..f87d7f25be9 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -14,6 +14,7 @@ import type { OrganizationAllowList, ShareVisibility, QueuedMessage, + CloudAgent, } from "@roo-code/types" import { GitCommit } from "../utils/git" @@ -206,7 +207,7 @@ export interface ExtensionMessage { queuedMessages?: QueuedMessage[] list?: string[] // For dismissedUpsells organizationId?: string | null // For organizationSwitchResult - agents?: any[] // For cloudAgents + agents?: CloudAgent[] // For cloudAgents } export type ExtensionState = Pick< diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 0248abb6eed..17f99c42c97 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1813,7 +1813,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction {/* New users should see tips */} - {taskHistory.length < 4 && } + {taskHistory.length < 400000 && } {/* Everyone should see their task history if any */} {taskHistory.length > 0 && } diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index 65c58c27241..ab5ccfd3440 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from "react" -import { Cloud, Plus, SquarePen } from "lucide-react" +import { Cloud, Hammer, Plus, SquarePen } from "lucide-react" import type { CloudAgent } from "@roo-code/types" import { useTranslation } from "react-i18next" import { vscode } from "@/utils/vscode" @@ -103,14 +103,18 @@ const CloudAgents: React.FC = () => { className="flex items-center relative group gap-2 px-4 py-2 rounded-xl bg-vscode-editor-background hover:bg-vscode-list-hoverBackground cursor-pointer transition-colors" onClick={() => handleAgentClick(agent.id)} aria-label={t("chat:cloudAgents.clickToRun", { name: agent.name })}> - + {agent.icon ? ( + + ) : ( + + )}
{agent.name} From a255a141e26b6f93600d712a10f2d0ba97ee521f Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 17:50:26 +0100 Subject: [PATCH 09/15] Fixes tests --- webview-ui/src/components/chat/ChatView.tsx | 2 +- .../chat/__tests__/ChatView.spec.tsx | 89 +++++++------------ .../cloud/__tests__/CloudAgents.spec.tsx | 2 +- .../history/__tests__/HistoryPreview.spec.tsx | 21 ----- .../history/__tests__/TaskItem.spec.tsx | 15 ---- 5 files changed, 32 insertions(+), 97 deletions(-) diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 17f99c42c97..0248abb6eed 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -1813,7 +1813,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction {/* New users should see tips */} - {taskHistory.length < 400000 && } + {taskHistory.length < 4 && } {/* Everyone should see their task history if any */} {taskHistory.length > 0 && } diff --git a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx index f7ba2732fd1..c710c6aa7c6 100644 --- a/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatView.spec.tsx @@ -61,6 +61,13 @@ vi.mock("../AutoApproveMenu", () => ({ default: () => null, })) +// Mock CloudAgents component +vi.mock("../../cloud/CloudAgents", () => ({ + default: function MockCloudAgents() { + return
Cloud Agents
+ }, +})) + // Mock VersionIndicator - returns null by default to prevent rendering in tests vi.mock("../../common/VersionIndicator", () => ({ default: vi.fn(() => null), @@ -1268,10 +1275,10 @@ describe("ChatView - Version Indicator Tests", () => { }) }) -describe("ChatView - DismissibleUpsell Display Tests", () => { +describe("ChatView - DismissibleUpsell and RooTips Display Tests", () => { beforeEach(() => vi.clearAllMocks()) - it("does not show DismissibleUpsell when user is authenticated to Cloud", () => { + it("does not show DismissibleUpsell when user is authenticated to Cloud", async () => { const { queryByTestId } = renderChatView() // Hydrate state with user authenticated to cloud @@ -1286,29 +1293,16 @@ describe("ChatView - DismissibleUpsell Display Tests", () => { clineMessages: [], // No active task }) - // Should not show DismissibleUpsell when authenticated - expect(queryByTestId("dismissible-upsell")).not.toBeInTheDocument() - }) - - it("does not show DismissibleUpsell when user has only run 3 tasks in their history", () => { - const { queryByTestId } = renderChatView() - - // Hydrate state with user not authenticated but only 3 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 + // Wait for the state to be updated and component to re-render + await waitFor(() => { + // CloudAgents should be shown instead of DismissibleUpsell when authenticated + expect(queryByTestId("cloud-agents")).toBeInTheDocument() + // Should not show DismissibleUpsell when authenticated + expect(queryByTestId("dismissible-upsell")).not.toBeInTheDocument() }) - - // Should not show DismissibleUpsell with less than 4 tasks - expect(queryByTestId("dismissible-upsell")).not.toBeInTheDocument() }) - it("shows DismissibleUpsell when user is not authenticated and has run 4 or more tasks", async () => { + it("shows DismissibleUpsell when user is not authenticated", async () => { const { getByTestId } = renderChatView() // Hydrate state with user not authenticated and 4 tasks @@ -1329,29 +1323,7 @@ describe("ChatView - DismissibleUpsell Display Tests", () => { }) }) - it("shows DismissibleUpsell when user is not authenticated and has run 5 tasks", async () => { - const { getByTestId } = renderChatView() - - // Hydrate state with user not authenticated and 5 tasks - 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 - }) - - // Wait for component to render and show DismissibleUpsell - await waitFor(() => { - expect(getByTestId("dismissible-upsell")).toBeInTheDocument() - }) - }) - - it("does not show DismissibleUpsell when there is an active task (regardless of auth status)", async () => { + it("does not show any home screen elements when there is an active task (regardless of auth status)", async () => { const { queryByTestId } = renderChatView() // Hydrate state with active task @@ -1384,14 +1356,13 @@ describe("ChatView - DismissibleUpsell Display Tests", () => { }) }) - it("shows RooTips when user is authenticated (instead of DismissibleUpsell)", () => { - const { queryByTestId, getByTestId } = renderChatView() + it("shows RooTips when the user has less than 4 tasks", () => { + const { queryByTestId } = 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() }, @@ -1400,27 +1371,27 @@ describe("ChatView - DismissibleUpsell Display Tests", () => { }) // Should not show DismissibleUpsell but should show RooTips - expect(queryByTestId("dismissible-upsell")).not.toBeInTheDocument() - expect(getByTestId("roo-tips")).toBeInTheDocument() + expect(queryByTestId("roo-tips")).toBeInTheDocument() }) - it("shows RooTips when user has fewer than 4 tasks (instead of DismissibleUpsell)", () => { - const { queryByTestId, getByTestId } = renderChatView() + it("does not show RooTips when the user has more than 4 tasks", () => { + const { queryByTestId } = renderChatView() - // Hydrate state with user not authenticated but fewer than 4 tasks + // Hydrate state with user authenticated to cloud mockPostMessage({ - cloudIsAuthenticated: false, + cloudIsAuthenticated: true, taskHistory: [ - { id: "1", ts: Date.now() - 2000 }, - { id: "2", ts: Date.now() - 1000 }, - { id: "3", ts: Date.now() }, + { id: "0", ts: Date.now() - 4000 }, + { 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 DismissibleUpsell but should show RooTips - expect(queryByTestId("dismissible-upsell")).not.toBeInTheDocument() - expect(getByTestId("roo-tips")).toBeInTheDocument() + expect(queryByTestId("roo-tips")).toBeInTheDocument() }) }) diff --git a/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx index f2114df4f70..9ab1f91aaa9 100644 --- a/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx +++ b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx @@ -142,7 +142,7 @@ describe("CloudAgents", () => { }) await waitFor(() => { - expect(screen.getByText(/Code away from your IDE/)).toBeInTheDocument() + expect(screen.getByText(/any Cloud Agents yet/)).toBeInTheDocument() }) // Find and click the "Create your first" button in the empty state diff --git a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx index 20e7fcbdf3c..26da6811e27 100644 --- a/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx +++ b/webview-ui/src/components/history/__tests__/HistoryPreview.spec.tsx @@ -97,10 +97,7 @@ describe("HistoryPreview", () => { setShowAllWorkspaces: vi.fn(), }) - const { container } = render() - // Should render the container but no task items - expect(container.firstChild).toHaveClass("flex", "flex-col", "gap-3") expect(screen.queryByTestId(/task-item-/)).not.toBeInTheDocument() }) @@ -210,22 +207,4 @@ describe("HistoryPreview", () => { expect.anything(), ) }) - - it("renders with correct container classes", () => { - mockUseTaskSearch.mockReturnValue({ - tasks: mockTasks.slice(0, 1), - searchQuery: "", - setSearchQuery: vi.fn(), - sortOption: "newest", - setSortOption: vi.fn(), - lastNonRelevantSort: null, - setLastNonRelevantSort: vi.fn(), - showAllWorkspaces: false, - setShowAllWorkspaces: vi.fn(), - }) - - const { container } = render() - - expect(container.firstChild).toHaveClass("flex", "flex-col", "gap-3") - }) }) diff --git a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx index 6995d5840c0..de9a4ba02af 100644 --- a/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx +++ b/webview-ui/src/components/history/__tests__/TaskItem.spec.tsx @@ -94,19 +94,4 @@ describe("TaskItem", () => { // Should display time ago format expect(screen.getByText(/ago/)).toBeInTheDocument() }) - - it("applies hover effect class", () => { - render( - , - ) - - const taskItem = screen.getByTestId("task-item-1") - expect(taskItem).toHaveClass("hover:bg-vscode-list-hoverBackground") - }) }) From 6ce9ce5a0638e02b7ffa327cc723ace7f3e7c3f7 Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 18:07:46 +0100 Subject: [PATCH 10/15] Test fixes --- webview-ui/src/components/cloud/CloudAgents.tsx | 2 +- .../cloud/__tests__/CloudAgents.spec.tsx | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index ab5ccfd3440..a77886ec7bb 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -76,7 +76,7 @@ const CloudAgents: React.FC = () => { diff --git a/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx index 9ab1f91aaa9..f975934a47d 100644 --- a/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx +++ b/webview-ui/src/components/cloud/__tests__/CloudAgents.spec.tsx @@ -82,7 +82,7 @@ describe("CloudAgents", () => { // Wait for the component to render agents await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + expect(screen.getByText("chat:cloudAgents.title")).toBeInTheDocument() expect(screen.getByText("Code Assistant")).toBeInTheDocument() expect(screen.getByText("Test Agent")).toBeInTheDocument() }) @@ -120,10 +120,12 @@ describe("CloudAgents", () => { }) await waitFor(() => { - expect(screen.getByText("Cloud Agents")).toBeInTheDocument() + expect(screen.getByText("chat:cloudAgents.title")).toBeInTheDocument() + expect(screen.getByText("Code Assistant")).toBeInTheDocument() + expect(screen.getByText("Test Agent")).toBeInTheDocument() }) - const createButton = screen.getByTitle("Create new agent") + const createButton = screen.getByTitle("chat:cloudAgents.create") fireEvent.click(createButton) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -142,11 +144,11 @@ describe("CloudAgents", () => { }) await waitFor(() => { - expect(screen.getByText(/any Cloud Agents yet/)).toBeInTheDocument() + expect(screen.getByText("chat:cloudAgents.createFirst")).toBeInTheDocument() }) // Find and click the "Create your first" button in the empty state - const createFirstButton = screen.getByText("Create your first.") + const createFirstButton = screen.getByText("chat:cloudAgents.createFirst") fireEvent.click(createFirstButton) expect(mockPostMessage).toHaveBeenCalledWith({ @@ -168,7 +170,7 @@ describe("CloudAgents", () => { // Wait for the component to process the error await waitFor(() => { // Component should render nothing on error - expect(screen.queryByText("Cloud Agents")).not.toBeInTheDocument() + expect(screen.queryByText("chat:cloudAgents.title")).not.toBeInTheDocument() }) }) From a1f98f90b6d2e3c7e2c7c7d5bfcb892ce6b2b9ce Mon Sep 17 00:00:00 2001 From: Bruno Bergher Date: Tue, 14 Oct 2025 18:17:32 +0100 Subject: [PATCH 11/15] PR Review --- packages/types/src/cloud.ts | 2 +- webview-ui/src/components/welcome/RooTips.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 465bb9b77fb..07cc5912bb7 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -744,7 +744,7 @@ export const cloudAgentsResponseSchema = z.object({ id: z.string(), name: z.string(), type: z.string(), - icon: z.string(), + icon: z.string().optional(), }), ), }) diff --git a/webview-ui/src/components/welcome/RooTips.tsx b/webview-ui/src/components/welcome/RooTips.tsx index 3ffcd820ff8..273a2df1e90 100644 --- a/webview-ui/src/components/welcome/RooTips.tsx +++ b/webview-ui/src/components/welcome/RooTips.tsx @@ -24,13 +24,13 @@ const RooTips = () => { const { t } = useTranslation("chat") return ( -
+

{tips.map((tip) => ( -
+
{tip.icon} From 14d5eba6a61df979bf48d6670a332a95c203fd34 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 16 Oct 2025 23:39:48 -0400 Subject: [PATCH 12/15] PR feedback --- packages/cloud/src/CloudAPI.ts | 8 +++++--- packages/cloud/src/__tests__/CloudAPI.test.ts | 8 ++++---- packages/types/src/cloud.ts | 3 ++- src/core/webview/webviewMessageHandler.ts | 17 +++-------------- 4 files changed, 14 insertions(+), 22 deletions(-) diff --git a/packages/cloud/src/CloudAPI.ts b/packages/cloud/src/CloudAPI.ts index 38342179206..9fb51b553fe 100644 --- a/packages/cloud/src/CloudAPI.ts +++ b/packages/cloud/src/CloudAPI.ts @@ -6,6 +6,7 @@ import { type ShareResponse, shareResponseSchema, type CloudAgent, + cloudAgentsResponseSchema, } from "@roo-code/types" import { getRooCodeApiUrl } from "./config.js" @@ -144,11 +145,12 @@ export class CloudAPI { async getCloudAgents(): Promise { this.log("[CloudAPI] Fetching cloud agents") - const response = await this.request<{ success: boolean; data: CloudAgent[] }>("/api/cloud-agents", { + const agents = await this.request("/api/cloud-agents", { method: "GET", + parseResponse: (data) => cloudAgentsResponseSchema.parse(data).data, }) - this.log("[CloudAPI] Cloud agents response:", response) - return response.data || [] + this.log("[CloudAPI] Cloud agents response:", agents) + return agents } } diff --git a/packages/cloud/src/__tests__/CloudAPI.test.ts b/packages/cloud/src/__tests__/CloudAPI.test.ts index a0267d0f114..2390878dacf 100644 --- a/packages/cloud/src/__tests__/CloudAPI.test.ts +++ b/packages/cloud/src/__tests__/CloudAPI.test.ts @@ -27,7 +27,7 @@ describe("CloudAPI", () => { { id: "2", name: "Agent 2", type: "chat", icon: "chat" }, ] - // Mock successful response + // Mock successful response with schema-compliant format ;(global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: async () => ({ success: true, data: mockAgents }), @@ -66,11 +66,11 @@ describe("CloudAPI", () => { await expect(cloudAPI.getCloudAgents()).rejects.toThrow(AuthenticationError) }) - it("should return empty array when data is missing", async () => { - // Mock response with no data + it("should return empty array when agents array is empty", async () => { + // Mock response with empty agents array ;(global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, - json: async () => ({ success: true }), + json: async () => ({ success: true, data: [] }), }) const agents = await cloudAPI.getCloudAgents() diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 07cc5912bb7..b636a8c4449 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -739,7 +739,8 @@ export interface CloudAgent { */ export const cloudAgentsResponseSchema = z.object({ - agents: z.array( + success: z.boolean(), + data: z.array( z.object({ id: z.string(), name: z.string(), diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1358d23d2c7..5edd2fd7f2f 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2470,24 +2470,13 @@ export const webviewMessageHandler = async ( `[getCloudAgents] Error fetching cloud agents: ${error instanceof Error ? error.message : String(error)}`, ) - // Check if it's an authentication error - const errorMessage = error instanceof Error ? error.message : String(error) - const isAuthError = errorMessage.includes("Authentication") || errorMessage.includes("401") - - // Send empty array with error information + // Send response to stop loading state in UI + // The CloudAgents component will handle this gracefully by returning null await provider.postMessageToWebview({ type: "cloudAgents", agents: [], - error: errorMessage, + error: error instanceof Error ? error.message : String(error), }) - - // If it's an authentication error, show a user-friendly message - if (isAuthError) { - vscode.window.showErrorMessage( - t("common:errors.auth_required_for_cloud_agents") || - "Authentication required to access cloud agents", - ) - } } break } From 13677922ed9a2c138840838c6be5d640dce0b59f Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Thu, 16 Oct 2025 23:49:33 -0400 Subject: [PATCH 13/15] Layout tweaks --- webview-ui/src/components/cloud/CloudAgents.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webview-ui/src/components/cloud/CloudAgents.tsx b/webview-ui/src/components/cloud/CloudAgents.tsx index a77886ec7bb..7d9eb3aa715 100644 --- a/webview-ui/src/components/cloud/CloudAgents.tsx +++ b/webview-ui/src/components/cloud/CloudAgents.tsx @@ -84,9 +84,9 @@ const CloudAgents: React.FC = () => {
{agents.length === 0 ? ( -
- -

+

+ +

{t("chat:cloudAgents.description")}