mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Cloud account switcher (#8223)
* Cloud account switcher * Bare metal evals fixes (#8224) Co-authored-by: Roo Code <roomote@roocode.com> * Rounded icons in chat account switcher * Visual tweaks to CloudView * Remove hardcoded timeout * Safer check * PR feedback * Fix test * Check for org mismatches in handleCredentialsChange * Cloud: use the existing auth event flow to handle org switching * Cleanup: broadcast() might be confusingly named. --------- Co-authored-by: Chris Estreich <cestreich@gmail.com> Co-authored-by: Roo Code <roomote@roocode.com> Co-authored-by: Bruno Bergher <bruno@roocode.com> Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
This commit is contained in:
parent
382ab63ea1
commit
0682629ac8
33 changed files with 529 additions and 45 deletions
|
|
@ -8,6 +8,7 @@ import type {
|
|||
AuthService,
|
||||
SettingsService,
|
||||
CloudUserInfo,
|
||||
CloudOrganizationMembership,
|
||||
OrganizationAllowList,
|
||||
OrganizationSettings,
|
||||
ShareVisibility,
|
||||
|
|
@ -242,6 +243,21 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
|
|||
return this.authService!.handleCallback(code, state, organizationId)
|
||||
}
|
||||
|
||||
public async switchOrganization(organizationId: string | null): Promise<void> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Perform the organization switch
|
||||
// StaticTokenAuthService will throw an error if organization switching is not supported
|
||||
await this.authService!.switchOrganization(organizationId)
|
||||
}
|
||||
|
||||
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// StaticTokenAuthService will throw an error if organization memberships are not supported
|
||||
return await this.authService!.getOrganizationMemberships()
|
||||
}
|
||||
|
||||
// SettingsService
|
||||
|
||||
public getAllowList(): OrganizationAllowList {
|
||||
|
|
|
|||
|
|
@ -63,6 +63,14 @@ export class StaticTokenAuthService extends EventEmitter<AuthServiceEvents> impl
|
|||
throw new Error("Authentication methods are disabled in StaticTokenAuthService")
|
||||
}
|
||||
|
||||
public async switchOrganization(_organizationId: string | null): Promise<void> {
|
||||
throw new Error("Authentication methods are disabled in StaticTokenAuthService")
|
||||
}
|
||||
|
||||
public async getOrganizationMemberships(): Promise<import("@roo-code/types").CloudOrganizationMembership[]> {
|
||||
throw new Error("Authentication methods are disabled in StaticTokenAuthService")
|
||||
}
|
||||
|
||||
public getState(): AuthState {
|
||||
return this.state
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,7 +141,8 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
if (
|
||||
this.credentials === null ||
|
||||
this.credentials.clientToken !== credentials.clientToken ||
|
||||
this.credentials.sessionId !== credentials.sessionId
|
||||
this.credentials.sessionId !== credentials.sessionId ||
|
||||
this.credentials.organizationId !== credentials.organizationId
|
||||
) {
|
||||
this.transitionToAttemptingSession(credentials)
|
||||
}
|
||||
|
|
@ -174,6 +175,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
|
||||
this.changeState("attempting-session")
|
||||
|
||||
this.timer.stop()
|
||||
this.timer.start()
|
||||
}
|
||||
|
||||
|
|
@ -469,6 +471,42 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
return this.credentials?.organizationId || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to a different organization context
|
||||
* @param organizationId The organization ID to switch to, or null for personal account
|
||||
*/
|
||||
public async switchOrganization(organizationId: string | null): Promise<void> {
|
||||
if (!this.credentials) {
|
||||
throw new Error("Cannot switch organization: not authenticated")
|
||||
}
|
||||
|
||||
// Update the stored credentials with the new organization ID
|
||||
const updatedCredentials: AuthCredentials = {
|
||||
...this.credentials,
|
||||
organizationId: organizationId,
|
||||
}
|
||||
|
||||
// Store the updated credentials, handleCredentialsChange will handle the update
|
||||
await this.storeCredentials(updatedCredentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all organization memberships for the current user
|
||||
* @returns Array of organization memberships
|
||||
*/
|
||||
public async getOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
|
||||
if (!this.credentials) {
|
||||
return []
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.clerkGetOrganizationMemberships()
|
||||
} catch (error) {
|
||||
this.log(`[auth] Failed to get organization memberships: ${error}`)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
|
||||
const formData = new URLSearchParams()
|
||||
formData.append("strategy", "ticket")
|
||||
|
|
@ -653,9 +691,14 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
|
|||
}
|
||||
|
||||
private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
|
||||
if (!this.credentials) {
|
||||
this.log("[auth] Cannot get organization memberships: missing credentials")
|
||||
return []
|
||||
}
|
||||
|
||||
const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.credentials!.clientToken}`,
|
||||
Authorization: `Bearer ${this.credentials.clientToken}`,
|
||||
"User-Agent": this.userAgent(),
|
||||
},
|
||||
signal: AbortSignal.timeout(10000),
|
||||
|
|
|
|||
|
|
@ -242,6 +242,7 @@ export interface AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
login(landingPageSlug?: string): Promise<void>
|
||||
logout(): Promise<void>
|
||||
handleCallback(code: string | null, state: string | null, organizationId?: string | null): Promise<void>
|
||||
switchOrganization(organizationId: string | null): Promise<void>
|
||||
|
||||
// State methods
|
||||
getState(): AuthState
|
||||
|
|
@ -253,6 +254,9 @@ export interface AuthService extends EventEmitter<AuthServiceEvents> {
|
|||
getSessionToken(): string | undefined
|
||||
getUserInfo(): CloudUserInfo | null
|
||||
getStoredOrganizationId(): string | null
|
||||
|
||||
// Organization management
|
||||
getOrganizationMemberships(): Promise<CloudOrganizationMembership[]>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import {
|
|||
type TerminalActionPromptType,
|
||||
type HistoryItem,
|
||||
type CloudUserInfo,
|
||||
type CloudOrganizationMembership,
|
||||
type CreateTaskOptions,
|
||||
type TokenUsage,
|
||||
RooCodeEventName,
|
||||
|
|
@ -1815,6 +1816,16 @@ export class ClineProvider
|
|||
featureRoomoteControlEnabled,
|
||||
} = await this.getState()
|
||||
|
||||
let cloudOrganizations: CloudOrganizationMembership[] = []
|
||||
|
||||
try {
|
||||
cloudOrganizations = await CloudService.instance.getOrganizationMemberships()
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[getStateToPostToWebview] failed to get cloud organizations: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
|
||||
const telemetryKey = process.env.POSTHOG_API_KEY
|
||||
const machineId = vscode.env.machineId
|
||||
const mergedAllowedCommands = this.mergeAllowedCommands(allowedCommands)
|
||||
|
|
@ -1916,6 +1927,7 @@ export class ClineProvider
|
|||
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
|
||||
cloudUserInfo,
|
||||
cloudIsAuthenticated: cloudIsAuthenticated ?? false,
|
||||
cloudOrganizations,
|
||||
sharingEnabled: sharingEnabled ?? false,
|
||||
organizationAllowList,
|
||||
organizationSettingsVersion,
|
||||
|
|
|
|||
|
|
@ -2379,6 +2379,38 @@ export const webviewMessageHandler = async (
|
|||
|
||||
break
|
||||
}
|
||||
case "switchOrganization": {
|
||||
try {
|
||||
const organizationId = message.organizationId ?? null
|
||||
|
||||
// Switch to the new organization context
|
||||
await CloudService.instance.switchOrganization(organizationId)
|
||||
|
||||
// Refresh the state to update UI
|
||||
await provider.postStateToWebview()
|
||||
|
||||
// Send success response back to webview
|
||||
await provider.postMessageToWebview({
|
||||
type: "organizationSwitchResult",
|
||||
success: true,
|
||||
organizationId: organizationId,
|
||||
})
|
||||
} catch (error) {
|
||||
provider.log(`Organization switch failed: ${error}`)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Send error response back to webview
|
||||
await provider.postMessageToWebview({
|
||||
type: "organizationSwitchResult",
|
||||
success: false,
|
||||
error: errorMessage,
|
||||
organizationId: message.organizationId ?? null,
|
||||
})
|
||||
|
||||
vscode.window.showErrorMessage(`Failed to switch organization: ${errorMessage}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case "saveCodeIndexSettingsAtomic": {
|
||||
if (!message.codeIndexSettings) {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import type {
|
|||
MarketplaceItem,
|
||||
TodoItem,
|
||||
CloudUserInfo,
|
||||
CloudOrganizationMembership,
|
||||
OrganizationAllowList,
|
||||
ShareVisibility,
|
||||
QueuedMessage,
|
||||
|
|
@ -124,6 +125,7 @@ export interface ExtensionMessage {
|
|||
| "commands"
|
||||
| "insertTextIntoTextarea"
|
||||
| "dismissedUpsells"
|
||||
| "organizationSwitchResult"
|
||||
text?: string
|
||||
payload?: any // Add a generic payload for now, can refine later
|
||||
action?:
|
||||
|
|
@ -202,6 +204,7 @@ export interface ExtensionMessage {
|
|||
commands?: Command[]
|
||||
queuedMessages?: QueuedMessage[]
|
||||
list?: string[] // For dismissedUpsells
|
||||
organizationId?: string | null // For organizationSwitchResult
|
||||
}
|
||||
|
||||
export type ExtensionState = Pick<
|
||||
|
|
@ -327,6 +330,7 @@ export type ExtensionState = Pick<
|
|||
cloudUserInfo: CloudUserInfo | null
|
||||
cloudIsAuthenticated: boolean
|
||||
cloudApiUrl?: string
|
||||
cloudOrganizations?: CloudOrganizationMembership[]
|
||||
sharingEnabled: boolean
|
||||
organizationAllowList: OrganizationAllowList
|
||||
organizationSettingsVersion?: number
|
||||
|
|
|
|||
|
|
@ -184,6 +184,7 @@ export interface WebviewMessage {
|
|||
| "cloudLandingPageSignIn"
|
||||
| "rooCloudSignOut"
|
||||
| "rooCloudManualUrl"
|
||||
| "switchOrganization"
|
||||
| "condenseTaskContextRequest"
|
||||
| "requestIndexingStatus"
|
||||
| "startIndexing"
|
||||
|
|
@ -274,6 +275,7 @@ export interface WebviewMessage {
|
|||
checkOnly?: boolean // For deleteCustomMode check
|
||||
upsellId?: string // For dismissUpsell
|
||||
list?: string[] // For dismissedUpsells response
|
||||
organizationId?: string | null // For organization switching
|
||||
codeIndexSettings?: {
|
||||
// Global state settings
|
||||
codebaseIndexEnabled: boolean
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ const App = () => {
|
|||
cloudUserInfo,
|
||||
cloudIsAuthenticated,
|
||||
cloudApiUrl,
|
||||
cloudOrganizations,
|
||||
renderContext,
|
||||
mdmCompliant,
|
||||
} = useExtensionState()
|
||||
|
|
@ -267,6 +268,7 @@ const App = () => {
|
|||
userInfo={cloudUserInfo}
|
||||
isAuthenticated={cloudIsAuthenticated}
|
||||
cloudApiUrl={cloudApiUrl}
|
||||
organizations={cloudOrganizations}
|
||||
onDone={() => switchTab("chat")}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
|
|||
import ContextMenu from "./ContextMenu"
|
||||
import { IndexingStatusBadge } from "./IndexingStatusBadge"
|
||||
import { usePromptHistory } from "./hooks/usePromptHistory"
|
||||
import { CloudAccountSwitcher } from "../cloud/CloudAccountSwitcher"
|
||||
|
||||
interface ChatTextAreaProps {
|
||||
inputValue: string
|
||||
|
|
@ -87,6 +88,8 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
taskHistory,
|
||||
clineMessages,
|
||||
commands,
|
||||
cloudUserInfo,
|
||||
cloudOrganizations,
|
||||
} = useExtensionState()
|
||||
|
||||
// Find the ID and display text for the currently selected API configuration.
|
||||
|
|
@ -1232,7 +1235,13 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
/>
|
||||
<AutoApproveDropdown triggerClassName="min-w-[28px] text-ellipsis overflow-hidden flex-shrink" />
|
||||
</div>
|
||||
<div className="flex flex-shrink-0 items-center gap-0.5 pr-2">
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-shrink-0 items-center gap-0.5",
|
||||
!isEditMode && cloudOrganizations && cloudOrganizations.length > 0 && cloudUserInfo
|
||||
? ""
|
||||
: "pr-2",
|
||||
)}>
|
||||
{isTtsPlaying && (
|
||||
<StandardTooltip content={t("chat:stopTts")}>
|
||||
<button
|
||||
|
|
@ -1254,6 +1263,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
|
|||
</StandardTooltip>
|
||||
)}
|
||||
{!isEditMode ? <IndexingStatusBadge /> : null}
|
||||
{!isEditMode && cloudOrganizations && cloudOrganizations.length > 0 && cloudUserInfo && (
|
||||
<CloudAccountSwitcher />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
149
webview-ui/src/components/cloud/CloudAccountSwitcher.tsx
Normal file
149
webview-ui/src/components/cloud/CloudAccountSwitcher.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { useState, useEffect } from "react"
|
||||
import { Building2 } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectSeparator } from "@/components/ui/select"
|
||||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { cn } from "@src/lib/utils"
|
||||
|
||||
export const CloudAccountSwitcher = () => {
|
||||
const { t } = useAppTranslation()
|
||||
const { cloudUserInfo, cloudOrganizations = [] } = useExtensionState()
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(cloudUserInfo?.organizationId || null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Update selected org when userInfo changes
|
||||
useEffect(() => {
|
||||
setSelectedOrgId(cloudUserInfo?.organizationId || null)
|
||||
}, [cloudUserInfo?.organizationId])
|
||||
|
||||
// Don't show the switcher if user has no organizations
|
||||
if (!cloudOrganizations || cloudOrganizations.length === 0 || !cloudUserInfo) {
|
||||
return null
|
||||
}
|
||||
|
||||
const handleOrganizationChange = async (value: string) => {
|
||||
const newOrgId = value === "personal" ? null : value
|
||||
|
||||
// Don't do anything if selecting the same organization
|
||||
if (newOrgId === selectedOrgId) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// Send message to switch organization
|
||||
vscode.postMessage({
|
||||
type: "switchOrganization",
|
||||
organizationId: newOrgId,
|
||||
})
|
||||
|
||||
// Update local state optimistically
|
||||
setSelectedOrgId(newOrgId)
|
||||
|
||||
// Reset loading state after a delay
|
||||
setTimeout(() => {
|
||||
setIsLoading(false)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const currentValue = selectedOrgId || "personal"
|
||||
const currentOrg = cloudOrganizations.find((org) => org.organization.id === selectedOrgId)
|
||||
|
||||
// Render the account icon based on current context
|
||||
const renderAccountIcon = () => {
|
||||
if (selectedOrgId && currentOrg?.organization.image_url) {
|
||||
// Organization with logo
|
||||
return (
|
||||
<img
|
||||
src={currentOrg.organization.image_url}
|
||||
alt={currentOrg.organization.name}
|
||||
className="w-5 h-5 rounded object-cover"
|
||||
/>
|
||||
)
|
||||
} else if (selectedOrgId) {
|
||||
// Organization without logo
|
||||
return <Building2 className="w-4.5 h-4.5" />
|
||||
} else if (cloudUserInfo.picture) {
|
||||
// Personal account with avatar
|
||||
return (
|
||||
<img
|
||||
src={cloudUserInfo.picture}
|
||||
alt={cloudUserInfo.name || cloudUserInfo.email}
|
||||
className="w-5 h-5 rounded-full object-cover"
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
// Personal account without avatar - show initials
|
||||
const initial = cloudUserInfo.name?.charAt(0) || cloudUserInfo.email?.charAt(0) || "?"
|
||||
return (
|
||||
<div className="w-5 h-5 rounded-full flex items-center justify-center bg-vscode-button-background text-vscode-button-foreground text-xs">
|
||||
{initial}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StandardTooltip content={t("cloud:switchAccount")}>
|
||||
<div className="inline-block ml-1">
|
||||
<Select value={currentValue} onValueChange={handleOrganizationChange} disabled={isLoading}>
|
||||
<SelectTrigger
|
||||
className={cn(
|
||||
"h-4.5 w-4.5 p-0 gap-0",
|
||||
"bg-transparent opacity-90 hover:opacity-50",
|
||||
"flex items-center justify-center",
|
||||
"rounded-lg overflow-clip",
|
||||
"border border-vscode-dropdown-border",
|
||||
"[&>svg]:hidden", // Hide the default chevron/caret
|
||||
isLoading && "opacity-50",
|
||||
)}
|
||||
aria-label={selectedOrgId ? currentOrg?.organization.name : t("cloud:personalAccount")}>
|
||||
{renderAccountIcon()}
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{/* Personal Account Option */}
|
||||
<SelectItem value="personal">
|
||||
<div className="flex items-center gap-2">
|
||||
{cloudUserInfo.picture ? (
|
||||
<img
|
||||
src={cloudUserInfo.picture}
|
||||
alt={cloudUserInfo.name || cloudUserInfo.email}
|
||||
className="w-4.5 h-4.5 rounded-full object-cover overflow-clip"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-4.5 h-4.5 rounded-full flex items-center justify-center bg-vscode-button-background text-vscode-button-foreground text-xs">
|
||||
{cloudUserInfo.name?.charAt(0) || cloudUserInfo.email?.charAt(0) || "?"}
|
||||
</div>
|
||||
)}
|
||||
<span>{t("cloud:personalAccount")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
|
||||
{cloudOrganizations.length > 0 && <SelectSeparator />}
|
||||
|
||||
{/* Organization Options */}
|
||||
{cloudOrganizations.map((org) => (
|
||||
<SelectItem key={org.organization.id} value={org.organization.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{org.organization.image_url ? (
|
||||
<img
|
||||
src={org.organization.image_url}
|
||||
alt=""
|
||||
className="w-4.5 h-4.5 rounded-full object-cover overflow-clip"
|
||||
/>
|
||||
) : (
|
||||
<Building2 className="w-4.5 h-4.5" />
|
||||
)}
|
||||
<span className="truncate">{org.organization.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useRef, useState } from "react"
|
||||
import { VSCodeButton, VSCodeProgressRing, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { type CloudUserInfo, TelemetryEventName } from "@roo-code/types"
|
||||
import { type CloudUserInfo, type CloudOrganizationMembership, TelemetryEventName } from "@roo-code/types"
|
||||
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
|
@ -9,10 +9,12 @@ import { vscode } from "@src/utils/vscode"
|
|||
import { telemetryClient } from "@src/utils/TelemetryClient"
|
||||
import { ToggleSwitch } from "@/components/ui/toggle-switch"
|
||||
import { renderCloudBenefitsContent } from "./CloudUpsellDialog"
|
||||
import { TriangleAlert } from "lucide-react"
|
||||
import { CircleAlert, Info, Lock, TriangleAlert } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Tab, TabContent, TabHeader } from "../common/Tab"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { OrganizationSwitcher } from "./OrganizationSwitcher"
|
||||
import { StandardTooltip } from "../ui"
|
||||
|
||||
// Define the production URL constant locally to avoid importing from cloud package in tests
|
||||
const PRODUCTION_ROO_CODE_API_URL = "https://app.roocode.com"
|
||||
|
|
@ -22,9 +24,10 @@ type CloudViewProps = {
|
|||
isAuthenticated: boolean
|
||||
cloudApiUrl?: string
|
||||
onDone: () => void
|
||||
organizations?: CloudOrganizationMembership[]
|
||||
}
|
||||
|
||||
export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: CloudViewProps) => {
|
||||
export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone, organizations = [] }: CloudViewProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const {
|
||||
remoteControlEnabled,
|
||||
|
|
@ -161,11 +164,11 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
|
|||
<Button onClick={onDone}>{t("settings:common.done")}</Button>
|
||||
</TabHeader>
|
||||
|
||||
<TabContent>
|
||||
<TabContent className="pt-10">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
{userInfo && (
|
||||
<div className="flex flex-col items-center mb-6">
|
||||
<div className="flex flex-col items-start ml-4 mb-6">
|
||||
<div className="w-16 h-16 mb-3 rounded-full overflow-hidden">
|
||||
{userInfo?.picture ? (
|
||||
<img
|
||||
|
|
@ -185,23 +188,18 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
|
|||
{userInfo?.email && (
|
||||
<p className="text-sm text-vscode-descriptionForeground my-0">{userInfo?.email}</p>
|
||||
)}
|
||||
{userInfo?.organizationName && (
|
||||
<div className="flex items-center gap-2 text-sm text-vscode-descriptionForeground mt-2">
|
||||
{userInfo.organizationImageUrl && (
|
||||
<img
|
||||
src={userInfo.organizationImageUrl}
|
||||
alt={userInfo.organizationName}
|
||||
className="w-4 h-4 rounded object-cover"
|
||||
/>
|
||||
)}
|
||||
<span>{userInfo.organizationName}</span>
|
||||
|
||||
{/* Organization Switcher - moved below email */}
|
||||
{organizations && organizations.length > 0 && (
|
||||
<div className="w-full max-w-60 mt-4">
|
||||
<OrganizationSwitcher userInfo={userInfo} organizations={organizations} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Task Sync Toggle - Always shown when authenticated */}
|
||||
<div className="border-t border-vscode-widget-border pt-4 mt-4">
|
||||
<div className="mt-4 p-4 border-b border-t border-vscode-widget-border pl-4 max-w-140">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<ToggleSwitch
|
||||
checked={taskSyncEnabled}
|
||||
|
|
@ -211,21 +209,25 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
|
|||
data-testid="task-sync-toggle"
|
||||
disabled={!!userInfo?.organizationId}
|
||||
/>
|
||||
<span className="font-medium text-vscode-foreground">{t("cloud:taskSync")}</span>
|
||||
<span className="font-medium text-vscode-foreground flex items-center">
|
||||
{t("cloud:taskSync")}
|
||||
{userInfo?.organizationId && (
|
||||
<StandardTooltip content={t("cloud:taskSyncManagedByOrganization")}>
|
||||
<div className="bg-vscode-badge-background text-vscode-badge-foreground/80 p-1.5 ml-2 -mb-2 relative -top-1 rounded-full inline-block cursor-help">
|
||||
<Lock className="size-3 block" />
|
||||
</div>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-4 ml-8">
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 ml-8">
|
||||
{t("cloud:taskSyncDescription")}
|
||||
</div>
|
||||
{userInfo?.organizationId && (
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-4 ml-8 italic">
|
||||
{t("cloud:taskSyncManagedByOrganization")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Remote Control Toggle - Only shown when both extensionBridgeEnabled and featureRoomoteControlEnabled are true */}
|
||||
{userInfo?.extensionBridgeEnabled && featureRoomoteControlEnabled && (
|
||||
<>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="flex items-center gap-3 mt-4 mb-2">
|
||||
<ToggleSwitch
|
||||
checked={remoteControlEnabled}
|
||||
onChange={handleRemoteControlToggle}
|
||||
|
|
@ -238,37 +240,42 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
|
|||
{t("cloud:remoteControl")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-4 ml-8">
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1 mb-2 ml-8">
|
||||
{t("cloud:remoteControlDescription")}
|
||||
{!taskSyncEnabled && (
|
||||
<div className="text-vscode-errorForeground mt-2">
|
||||
<div className="text-vscode-editorWarning-foreground mt-2">
|
||||
<CircleAlert className="inline size-3 mr-1 mb-0.5 text-vscode-editorWarning-foreground" />
|
||||
{t("cloud:remoteControlRequiresTaskSync")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Info text about usage metrics */}
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-4 mb-4 ml-8 italic">
|
||||
{t("cloud:usageMetricsAlwaysReported")}
|
||||
</div>
|
||||
|
||||
<hr className="border-vscode-widget-border mb-4" />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<VSCodeButton appearance="secondary" onClick={handleVisitCloudWebsite} className="w-full">
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-4 mb-8 pl-4">
|
||||
<Info className="inline size-3 mr-1 mb-0.5 text-vscode-descriptionForeground" />
|
||||
{t("cloud:usageMetricsAlwaysReported")}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4 pl-4">
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleVisitCloudWebsite}
|
||||
className="w-full max-w-80">
|
||||
{t("cloud:visitCloudWebsite")}
|
||||
</VSCodeButton>
|
||||
<VSCodeButton appearance="secondary" onClick={handleLogoutClick} className="w-full">
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
onClick={handleLogoutClick}
|
||||
className="w-full max-w-80">
|
||||
{t("cloud:logOut")}
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col items-start gap-4 px-8">
|
||||
<div className="flex flex-col items-start gap-4 px-8 max-w-100">
|
||||
<div className={cn(authInProgress && "opacity-50")}>{renderCloudBenefitsContent(t)}</div>
|
||||
|
||||
{!authInProgress && (
|
||||
|
|
@ -323,10 +330,10 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: Cl
|
|||
</>
|
||||
)}
|
||||
{cloudApiUrl && cloudApiUrl !== PRODUCTION_ROO_CODE_API_URL && (
|
||||
<div className="ml-8 mt-6 flex justify-start">
|
||||
<div className="ml-4 mt-6 flex">
|
||||
<div className="inline-flex items-center gap-2 text-xs">
|
||||
<TriangleAlert className="size-4 text-vscode-descriptionForeground" />
|
||||
<span className="text-vscode-foreground/75">{t("cloud:cloudUrlPillLabel")}: </span>
|
||||
<TriangleAlert className="size-3 text-vscode-descriptionForeground" />
|
||||
<span className="text-vscode-foreground/75">{t("cloud:cloudUrlPillLabel")} </span>
|
||||
<button
|
||||
onClick={handleOpenCloudUrl}
|
||||
className="text-vscode-textLink-foreground hover:text-vscode-textLink-activeForeground underline cursor-pointer bg-transparent border-none p-0">
|
||||
|
|
|
|||
146
webview-ui/src/components/cloud/OrganizationSwitcher.tsx
Normal file
146
webview-ui/src/components/cloud/OrganizationSwitcher.tsx
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { useState, useEffect } from "react"
|
||||
import { Building2, User } from "lucide-react"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, SelectSeparator } from "@/components/ui/select"
|
||||
import { type CloudUserInfo, type CloudOrganizationMembership } from "@roo-code/types"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { type ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
|
||||
type OrganizationSwitcherProps = {
|
||||
userInfo: CloudUserInfo
|
||||
organizations: CloudOrganizationMembership[]
|
||||
onOrganizationChange?: (organizationId: string | null) => void
|
||||
}
|
||||
|
||||
export const OrganizationSwitcher = ({ userInfo, organizations, onOrganizationChange }: OrganizationSwitcherProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const [selectedOrgId, setSelectedOrgId] = useState<string | null>(userInfo.organizationId || null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// Update selected org when userInfo changes
|
||||
useEffect(() => {
|
||||
setSelectedOrgId(userInfo.organizationId || null)
|
||||
}, [userInfo.organizationId])
|
||||
|
||||
// Listen for organization switch results
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
const message = event.data as ExtensionMessage
|
||||
if (message.type === "organizationSwitchResult") {
|
||||
// Reset loading state when we receive the result
|
||||
setIsLoading(false)
|
||||
|
||||
if (message.success) {
|
||||
// Update selected org based on the result
|
||||
setSelectedOrgId(message.organizationId ?? null)
|
||||
} else {
|
||||
// Revert to the previous organization on error
|
||||
setSelectedOrgId(userInfo.organizationId || null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => window.removeEventListener("message", handleMessage)
|
||||
}, [userInfo.organizationId])
|
||||
|
||||
const handleOrganizationChange = async (value: string) => {
|
||||
const newOrgId = value === "personal" ? null : value
|
||||
|
||||
// Don't do anything if selecting the same organization
|
||||
if (newOrgId === selectedOrgId) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
// Send message to switch organization
|
||||
vscode.postMessage({
|
||||
type: "switchOrganization",
|
||||
organizationId: newOrgId,
|
||||
})
|
||||
|
||||
// Update local state optimistically
|
||||
setSelectedOrgId(newOrgId)
|
||||
|
||||
// Call the callback if provided
|
||||
if (onOrganizationChange) {
|
||||
onOrganizationChange(newOrgId)
|
||||
}
|
||||
}
|
||||
|
||||
// If user has no organizations, don't show the switcher
|
||||
if (!organizations || organizations.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const currentValue = selectedOrgId || "personal"
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Select value={currentValue} onValueChange={handleOrganizationChange} disabled={isLoading}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedOrgId ? (
|
||||
<>
|
||||
{organizations.find((org) => org.organization.id === selectedOrgId)?.organization
|
||||
.image_url ? (
|
||||
<img
|
||||
src={
|
||||
organizations.find((org) => org.organization.id === selectedOrgId)
|
||||
?.organization.image_url
|
||||
}
|
||||
alt=""
|
||||
className="w-4.5 h-4.5 rounded-full object-cover overflow-clip"
|
||||
/>
|
||||
) : (
|
||||
<Building2 className="w-4.5 h-4.5" />
|
||||
)}
|
||||
<span className="truncate">
|
||||
{
|
||||
organizations.find((org) => org.organization.id === selectedOrgId)
|
||||
?.organization.name
|
||||
}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="p-0.5 bg-vscode-button-background rounded-full flex items-center justify-center text-vscode-button-foreground text-xs">
|
||||
<User className="w-4 h-4 text-vscode-button-foreground" />
|
||||
</div>
|
||||
<span>{t("cloud:personalAccount")}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="personal">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4.5 h-4.5" />
|
||||
<span>{t("cloud:personalAccount")}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
{organizations.length > 0 && <SelectSeparator />}
|
||||
{organizations.map((org) => (
|
||||
<SelectItem key={org.organization.id} value={org.organization.id}>
|
||||
<div className="flex items-center gap-2">
|
||||
{org.organization.image_url ? (
|
||||
<img
|
||||
src={org.organization.image_url}
|
||||
alt=""
|
||||
className="w-4.5 h-4.5 rounded-full object-cover overflow-clip"
|
||||
/>
|
||||
) : (
|
||||
<Building2 className="w-4.5 h-4.5" />
|
||||
)}
|
||||
<span className="truncate">{org.organization.name}</span>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -308,8 +308,15 @@ describe("CloudView", () => {
|
|||
expect(taskSyncToggle).toBeInTheDocument()
|
||||
expect(taskSyncToggle).toHaveAttribute("tabindex", "-1")
|
||||
|
||||
// Check that the organization message is displayed
|
||||
expect(screen.getByText("Task sync is managed by your organization")).toBeInTheDocument()
|
||||
// Check that the lock icon is displayed (indicating organization control)
|
||||
const lockIcon = screen.getByTestId("task-sync-toggle").parentElement?.querySelector(".lucide-lock")
|
||||
expect(lockIcon).toBeInTheDocument()
|
||||
|
||||
// Check that the tooltip trigger is present (which contains the organization message)
|
||||
const tooltipTrigger = screen
|
||||
.getByTestId("task-sync-toggle")
|
||||
.parentElement?.querySelector('[data-slot="tooltip-trigger"]')
|
||||
expect(tooltipTrigger).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should enable task sync toggle for non-organization users", () => {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
type TodoItem,
|
||||
type TelemetrySetting,
|
||||
type OrganizationAllowList,
|
||||
type CloudOrganizationMembership,
|
||||
ORGANIZATION_ALLOW_ALL,
|
||||
} from "@roo-code/types"
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
organizationAllowList: OrganizationAllowList
|
||||
organizationSettingsVersion: number
|
||||
cloudIsAuthenticated: boolean
|
||||
cloudOrganizations?: CloudOrganizationMembership[]
|
||||
sharingEnabled: boolean
|
||||
maxConcurrentFileReads?: number
|
||||
mdmCompliant?: boolean
|
||||
|
|
@ -240,6 +242,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
|
||||
cloudUserInfo: null,
|
||||
cloudIsAuthenticated: false,
|
||||
cloudOrganizations: [],
|
||||
sharingEnabled: false,
|
||||
organizationAllowList: ORGANIZATION_ALLOW_ALL,
|
||||
organizationSettingsVersion: -1,
|
||||
|
|
@ -428,6 +431,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
screenshotQuality: state.screenshotQuality,
|
||||
routerModels: extensionRouterModels,
|
||||
cloudIsAuthenticated: state.cloudIsAuthenticated ?? false,
|
||||
cloudOrganizations: state.cloudOrganizations ?? [],
|
||||
organizationSettingsVersion: state.organizationSettingsVersion ?? -1,
|
||||
marketplaceItems,
|
||||
marketplaceInstalledMetadata,
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ca/cloud.json
generated
2
webview-ui/src/i18n/locales/ca/cloud.json
generated
|
|
@ -23,6 +23,8 @@
|
|||
"havingTrouble": "Tens problemes?",
|
||||
"pasteCallbackUrl": "Copia l'URL de redirect del teu navegador i enganxa-la aquí:",
|
||||
"startOver": "Torna a començar",
|
||||
"personalAccount": "Compte Personal",
|
||||
"switchAccount": "Canviar Compte de Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Donant-li una mica d'independència a Roo? Controla'l des de qualsevol lloc amb Roo Code Cloud. <learnMoreLink>Més informació</learnMoreLink>.",
|
||||
"longRunningTask": "Això pot trigar una estona. Continua des de qualsevol lloc amb Cloud.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/de/cloud.json
generated
2
webview-ui/src/i18n/locales/de/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Probleme?",
|
||||
"pasteCallbackUrl": "Kopiere die Redirect-URL aus deinem Browser und füge sie hier ein:",
|
||||
"startOver": "Von vorne beginnen",
|
||||
"personalAccount": "Persönliches Konto",
|
||||
"switchAccount": "Roo Code Cloud Konto wechseln",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Roo etwas Unabhängigkeit geben? Kontrolliere es von überall mit Roo Code Cloud. <learnMoreLink>Mehr erfahren</learnMoreLink>.",
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@
|
|||
"havingTrouble": "Having trouble?",
|
||||
"pasteCallbackUrl": "Copy the redirect URL from your browser and paste it here:",
|
||||
"startOver": "Start over",
|
||||
"personalAccount": "Personal Account",
|
||||
"switchAccount": "Switch Roo Code Cloud Account",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Giving Roo some independence? Control it from anywhere with Roo Code Cloud. <learnMoreLink>Learn more</learnMoreLink>.",
|
||||
"longRunningTask": "This might take a while. Continue from anywhere with Cloud.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/es/cloud.json
generated
2
webview-ui/src/i18n/locales/es/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "¿Tienes problemas?",
|
||||
"pasteCallbackUrl": "Copia la URL de redirect desde tu navegador y pégala aquí:",
|
||||
"startOver": "Empezar de nuevo",
|
||||
"personalAccount": "Cuenta Personal",
|
||||
"switchAccount": "Cambiar Cuenta de Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL de Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "¿Dándole a Roo un poco de independencia? Contrólalo desde cualquier lugar con Roo Code Cloud. <learnMoreLink>Saber más</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/fr/cloud.json
generated
2
webview-ui/src/i18n/locales/fr/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Des difficultés ?",
|
||||
"pasteCallbackUrl": "Copie l'URL de redirect depuis ton navigateur et colle-la ici :",
|
||||
"startOver": "Recommencer",
|
||||
"personalAccount": "Compte Personnel",
|
||||
"switchAccount": "Changer de Compte Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL de Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Donner à Roo un peu d'indépendance ? Contrôlez-le de n'importe où avec Roo Code Cloud. <learnMoreLink>En savoir plus</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/hi/cloud.json
generated
2
webview-ui/src/i18n/locales/hi/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "समस्या हो रही है?",
|
||||
"pasteCallbackUrl": "अपने ब्राउज़र से redirect URL कॉपी करें और यहाँ पेस्ट करें:",
|
||||
"startOver": "फिर से शुरू करें",
|
||||
"personalAccount": "व्यक्तिगत खाता",
|
||||
"switchAccount": "Roo Code Cloud खाता बदलें",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "रू को थोड़ी स्वतंत्रता दे रहे हैं? रू कोड क्लाउड के साथ इसे कहीं से भी नियंत्रित करें। <learnMoreLink>और जानें</learnMoreLink>।",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/id/cloud.json
generated
2
webview-ui/src/i18n/locales/id/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Ada masalah?",
|
||||
"pasteCallbackUrl": "Salin URL redirect dari browser dan tempel di sini:",
|
||||
"startOver": "Mulai dari awal",
|
||||
"personalAccount": "Akun Pribadi",
|
||||
"switchAccount": "Ganti Akun Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Memberi Roo sedikit kebebasan? Kendalikan dari mana saja dengan Roo Code Cloud. <learnMoreLink>Pelajari lebih lanjut</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/it/cloud.json
generated
2
webview-ui/src/i18n/locales/it/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Hai problemi?",
|
||||
"pasteCallbackUrl": "Copia l'URL di redirect dal tuo browser e incollalo qui:",
|
||||
"startOver": "Ricomincia",
|
||||
"personalAccount": "Account Personale",
|
||||
"switchAccount": "Cambia Account Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL di Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Vuoi dare un po' di indipendenza a Roo? Controllalo da qualsiasi luogo con Roo Code Cloud. <learnMoreLink>Scopri di più</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ja/cloud.json
generated
2
webview-ui/src/i18n/locales/ja/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "問題が発生していますか?",
|
||||
"pasteCallbackUrl": "ブラウザからリダイレクトURLをコピーし、ここに貼り付けてください:",
|
||||
"startOver": "最初からやり直す",
|
||||
"personalAccount": "個人アカウント",
|
||||
"switchAccount": "Roo Code Cloud アカウントを切り替え",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Rooに少し独立性を与えませんか?Roo Code Cloudでどこからでもコントロールできます。<learnMoreLink>詳細</learnMoreLink>。",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ko/cloud.json
generated
2
webview-ui/src/i18n/locales/ko/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "문제가 있나요?",
|
||||
"pasteCallbackUrl": "브라우저에서 리다이렉트 URL을 복사하여 여기에 붙여넣으세요:",
|
||||
"startOver": "다시 시작",
|
||||
"personalAccount": "개인 계정",
|
||||
"switchAccount": "Roo Code Cloud 계정 전환",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Roo에게 약간의 독립성을 부여하시겠습니까? Roo Code Cloud로 어디서든 제어하세요. <learnMoreLink>더 알아보기</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/nl/cloud.json
generated
2
webview-ui/src/i18n/locales/nl/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Problemen?",
|
||||
"pasteCallbackUrl": "Kopieer de redirect-URL uit je browser en plak hem hier:",
|
||||
"startOver": "Opnieuw beginnen",
|
||||
"personalAccount": "Persoonlijk Account",
|
||||
"switchAccount": "Wissel van Roo Code Cloud Account",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Roo wat onafhankelijkheid geven? Bedien het overal met Roo Code Cloud. <learnMoreLink>Meer informatie</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pl/cloud.json
generated
2
webview-ui/src/i18n/locales/pl/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Masz problemy?",
|
||||
"pasteCallbackUrl": "Skopiuj URL redirect z przeglądarki i wklej tutaj:",
|
||||
"startOver": "Zacznij od nowa",
|
||||
"personalAccount": "Konto Osobiste",
|
||||
"switchAccount": "Przełącz Konto Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Dać Roo trochę niezależności? Kontroluj go z dowolnego miejsca dzięki Roo Code Cloud. <learnMoreLink>Dowiedz się więcej</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/pt-BR/cloud.json
generated
2
webview-ui/src/i18n/locales/pt-BR/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Tendo problemas?",
|
||||
"pasteCallbackUrl": "Copie a URL de redirect do seu navegador e cole aqui:",
|
||||
"startOver": "Recomeçar",
|
||||
"personalAccount": "Conta Pessoal",
|
||||
"switchAccount": "Alternar Conta do Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL do Roo Code Cloud ",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Dando um pouco de independência ao Roo? Controle-o de qualquer lugar com o Roo Code Cloud. <learnMoreLink>Saiba mais</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/ru/cloud.json
generated
2
webview-ui/src/i18n/locales/ru/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Проблемы?",
|
||||
"pasteCallbackUrl": "Скопируй URL перенаправления из браузера и вставь его сюда:",
|
||||
"startOver": "Начать заново",
|
||||
"personalAccount": "Личный аккаунт",
|
||||
"switchAccount": "Переключить аккаунт Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Предоставить Roo немного независимости? Управляйте им из любого места с помощью Roo Code Cloud. <learnMoreLink>Узнать больше</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/tr/cloud.json
generated
2
webview-ui/src/i18n/locales/tr/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Sorun yaşıyor musun?",
|
||||
"pasteCallbackUrl": "Tarayıcından redirect URL'sini kopyala ve buraya yapıştır:",
|
||||
"startOver": "Baştan başla",
|
||||
"personalAccount": "Kişisel Hesap",
|
||||
"switchAccount": "Roo Code Cloud Hesabını Değiştir",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL'si",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Roo'ya biraz bağımsızlık mı veriyorsunuz? Roo Code Cloud ile onu her yerden kontrol edin. <learnMoreLink>Daha fazla bilgi edinin</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/vi/cloud.json
generated
2
webview-ui/src/i18n/locales/vi/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "Gặp vấn đề?",
|
||||
"pasteCallbackUrl": "Sao chép URL redirect từ trình duyệt và dán vào đây:",
|
||||
"startOver": "Bắt đầu lại",
|
||||
"personalAccount": "Tài Khoản Cá Nhân",
|
||||
"switchAccount": "Chuyển Tài Khoản Roo Code Cloud",
|
||||
"cloudUrlPillLabel": "URL Roo Code Cloud",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "Trao cho Roo một chút độc lập? Kiểm soát nó từ mọi nơi với Roo Code Cloud. <learnMoreLink>Tìm hiểu thêm</learnMoreLink>.",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-CN/cloud.json
generated
2
webview-ui/src/i18n/locales/zh-CN/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "遇到问题?",
|
||||
"pasteCallbackUrl": "从浏览器复制重定向 URL 并粘贴到这里:",
|
||||
"startOver": "重新开始",
|
||||
"personalAccount": "个人账户",
|
||||
"switchAccount": "切换 Roo Code Cloud 账户",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "给 Roo 一些独立性?使用 Roo Code Cloud 从任何地方控制它。 <learnMoreLink>了解更多</learnMoreLink>。",
|
||||
|
|
|
|||
2
webview-ui/src/i18n/locales/zh-TW/cloud.json
generated
2
webview-ui/src/i18n/locales/zh-TW/cloud.json
generated
|
|
@ -22,6 +22,8 @@
|
|||
"havingTrouble": "遇到問題?",
|
||||
"pasteCallbackUrl": "從瀏覽器複製重新導向 URL 並貼上到這裡:",
|
||||
"startOver": "重新開始",
|
||||
"personalAccount": "個人帳戶",
|
||||
"switchAccount": "切換 Roo Code Cloud 帳戶",
|
||||
"cloudUrlPillLabel": "Roo Code Cloud URL",
|
||||
"upsell": {
|
||||
"autoApprovePowerUser": "給 Roo 一點獨立性?使用 Roo Code Cloud 隨時隨地控制它。<learnMoreLink>了解更多</learnMoreLink>。",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue