Sharing improvements (#5082)

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
Co-authored-by: cte <cestreich@gmail.com>
This commit is contained in:
Matt Rubens 2025-06-24 16:36:32 -04:00 committed by GitHub
parent ba5033e02e
commit da8b09e655
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
89 changed files with 2206 additions and 362 deletions

View file

@ -70,7 +70,7 @@ describe("generatePackageJson", () => {
{
command: "roo-cline.accountButtonClicked",
group: "navigation@6",
when: "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled",
when: "activeWebviewPanelId == roo-cline.TabPanelProvider",
},
],
},
@ -183,7 +183,7 @@ describe("generatePackageJson", () => {
{
command: "roo-code-nightly.accountButtonClicked",
group: "navigation@6",
when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider && config.roo-code-nightly.rooCodeCloudEnabled",
when: "activeWebviewPanelId == roo-code-nightly.TabPanelProvider",
},
],
},

View file

@ -13,7 +13,6 @@
"dependencies": {
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"axios": "^1.7.4",
"zod": "^3.25.61"
},
"devDependencies": {

View file

@ -42,8 +42,8 @@ const clerkCreateSessionTokenResponseSchema = z.object({
const clerkMeResponseSchema = z.object({
response: z.object({
first_name: z.string().optional(),
last_name: z.string().optional(),
first_name: z.string().optional().nullable(),
last_name: z.string().optional().nullable(),
image_url: z.string().optional(),
primary_email_address_id: z.string().optional(),
email_addresses: z
@ -531,7 +531,8 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
const userInfo: CloudUserInfo = {}
userInfo.name = `${userData.first_name} ${userData.last_name}`
const names = [userData.first_name, userData.last_name].filter((name) => !!name)
userInfo.name = names.length > 0 ? names.join(" ") : undefined
const primaryEmailAddressId = userData.primary_email_address_id
const emailAddresses = userData.email_addresses

View file

@ -1,13 +1,19 @@
import * as vscode from "vscode"
import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList } from "@roo-code/types"
import type {
CloudUserInfo,
TelemetryEvent,
OrganizationAllowList,
ClineMessage,
ShareVisibility,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudServiceCallbacks } from "./types"
import { AuthService } from "./AuthService"
import { SettingsService } from "./SettingsService"
import { TelemetryClient } from "./TelemetryClient"
import { ShareService } from "./ShareService"
import { ShareService, TaskNotFoundError } from "./ShareService"
export class CloudService {
private static _instance: CloudService | null = null
@ -161,9 +167,23 @@ export class CloudService {
// ShareService
public async shareTask(taskId: string, visibility: "organization" | "public" = "organization") {
public async shareTask(
taskId: string,
visibility: ShareVisibility = "organization",
clineMessages?: ClineMessage[],
) {
this.ensureInitialized()
return this.shareService!.shareTask(taskId, visibility)
try {
return await this.shareService!.shareTask(taskId, visibility)
} catch (error) {
if (error instanceof TaskNotFoundError && clineMessages) {
// Backfill messages and retry
await this.telemetryClient!.backfillMessages(clineMessages, taskId)
return await this.shareService!.shareTask(taskId, visibility)
}
throw error
}
}
public async canShareTask(): Promise<boolean> {

View file

@ -1,4 +1,3 @@
import axios from "axios"
import * as vscode from "vscode"
import { shareResponseSchema } from "@roo-code/types"
@ -9,6 +8,13 @@ import { getUserAgent } from "./utils"
export type ShareVisibility = "organization" | "public"
export class TaskNotFoundError extends Error {
constructor(taskId?: string) {
super(taskId ? `Task '${taskId}' not found` : "Task not found")
Object.setPrototypeOf(this, TaskNotFoundError.prototype)
}
}
export class ShareService {
private authService: AuthService
private settingsService: SettingsService
@ -31,19 +37,25 @@ export class ShareService {
throw new Error("Authentication required")
}
const response = await axios.post(
`${getRooCodeApiUrl()}/api/extension/share`,
{ taskId, visibility },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
"User-Agent": getUserAgent(),
},
const response = await fetch(`${getRooCodeApiUrl()}/api/extension/share`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
"User-Agent": getUserAgent(),
},
)
body: JSON.stringify({ taskId, visibility }),
signal: AbortSignal.timeout(10000),
})
const data = shareResponseSchema.parse(response.data)
if (!response.ok) {
if (response.status === 404) {
throw new TaskNotFoundError(taskId)
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data = shareResponseSchema.parse(await response.json())
this.log("[share] Share link created successfully:", data)
if (data.success && data.shareUrl) {

View file

@ -1,4 +1,9 @@
import { TelemetryEventName, type TelemetryEvent, rooCodeTelemetryEventSchema } from "@roo-code/types"
import {
TelemetryEventName,
type TelemetryEvent,
rooCodeTelemetryEventSchema,
type ClineMessage,
} from "@roo-code/types"
import { BaseTelemetryClient } from "@roo-code/telemetry"
import { getRooCodeApiUrl } from "./Config"
@ -79,6 +84,66 @@ export class TelemetryClient extends BaseTelemetryClient {
}
}
public async backfillMessages(messages: ClineMessage[], taskId: string): Promise<void> {
if (!this.authService.isAuthenticated()) {
if (this.debug) {
console.info(`[TelemetryClient#backfillMessages] Skipping: Not authenticated`)
}
return
}
const token = this.authService.getSessionToken()
if (!token) {
console.error(`[TelemetryClient#backfillMessages] Unauthorized: No session token available.`)
return
}
try {
const mergedProperties = await this.getEventProperties({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId },
})
const formData = new FormData()
formData.append("taskId", taskId)
formData.append("properties", JSON.stringify(mergedProperties))
formData.append(
"file",
new File([JSON.stringify(messages)], "task.json", {
type: "application/json",
}),
)
if (this.debug) {
console.info(
`[TelemetryClient#backfillMessages] Uploading ${messages.length} messages for task ${taskId}`,
)
}
// Custom fetch for multipart - don't set Content-Type header (let browser set it)
const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
// Note: No Content-Type header - browser will set multipart/form-data with boundary
},
body: formData,
})
if (!response.ok) {
console.error(
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
)
} else if (this.debug) {
console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`)
}
} catch (error) {
console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)
}
}
public override updateTelemetryState(_didUserOptIn: boolean) {}
public override isTelemetryEnabled(): boolean {

View file

@ -1,10 +1,13 @@
// npx vitest run src/__tests__/CloudService.test.ts
import * as vscode from "vscode"
import type { ClineMessage } from "@roo-code/types"
import { CloudService } from "../CloudService"
import { AuthService } from "../AuthService"
import { SettingsService } from "../SettingsService"
import { ShareService, TaskNotFoundError } from "../ShareService"
import { TelemetryClient } from "../TelemetryClient"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudServiceCallbacks } from "../types"
@ -28,6 +31,10 @@ vi.mock("../AuthService")
vi.mock("../SettingsService")
vi.mock("../ShareService")
vi.mock("../TelemetryClient")
describe("CloudService", () => {
let mockContext: vscode.ExtensionContext
let mockAuthService: {
@ -36,6 +43,7 @@ describe("CloudService", () => {
logout: ReturnType<typeof vi.fn>
isAuthenticated: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
hasOrIsAcquiringActiveSession: ReturnType<typeof vi.fn>
getUserInfo: ReturnType<typeof vi.fn>
getState: ReturnType<typeof vi.fn>
getSessionToken: ReturnType<typeof vi.fn>
@ -52,6 +60,13 @@ describe("CloudService", () => {
getAllowList: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
}
let mockShareService: {
shareTask: ReturnType<typeof vi.fn>
canShareTask: ReturnType<typeof vi.fn>
}
let mockTelemetryClient: {
backfillMessages: ReturnType<typeof vi.fn>
}
let mockTelemetryService: {
hasInstance: ReturnType<typeof vi.fn>
instance: {
@ -63,15 +78,29 @@ describe("CloudService", () => {
CloudService.resetInstance()
mockContext = {
subscriptions: [],
workspaceState: {
get: vi.fn(),
update: vi.fn(),
keys: vi.fn().mockReturnValue([]),
},
secrets: {
get: vi.fn(),
store: vi.fn(),
delete: vi.fn(),
onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
},
globalState: {
get: vi.fn(),
update: vi.fn(),
setKeysForSync: vi.fn(),
keys: vi.fn().mockReturnValue([]),
},
extensionUri: { scheme: "file", path: "/mock/path" },
extensionPath: "/mock/path",
extensionMode: 1,
asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`),
storageUri: { scheme: "file", path: "/mock/storage" },
extension: {
packageJSON: {
version: "1.0.0",
@ -85,6 +114,7 @@ describe("CloudService", () => {
logout: vi.fn(),
isAuthenticated: vi.fn().mockReturnValue(false),
hasActiveSession: vi.fn().mockReturnValue(false),
hasOrIsAcquiringActiveSession: vi.fn().mockReturnValue(false),
getUserInfo: vi.fn(),
getState: vi.fn().mockReturnValue("logged-out"),
getSessionToken: vi.fn(),
@ -103,6 +133,15 @@ describe("CloudService", () => {
dispose: vi.fn(),
}
mockShareService = {
shareTask: vi.fn(),
canShareTask: vi.fn().mockResolvedValue(true),
}
mockTelemetryClient = {
backfillMessages: vi.fn().mockResolvedValue(undefined),
}
mockTelemetryService = {
hasInstance: vi.fn().mockReturnValue(true),
instance: {
@ -112,6 +151,8 @@ describe("CloudService", () => {
vi.mocked(AuthService).mockImplementation(() => mockAuthService as unknown as AuthService)
vi.mocked(SettingsService).mockImplementation(() => mockSettingsService as unknown as SettingsService)
vi.mocked(ShareService).mockImplementation(() => mockShareService as unknown as ShareService)
vi.mocked(TelemetryClient).mockImplementation(() => mockTelemetryClient as unknown as TelemetryClient)
vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
Object.defineProperty(TelemetryService, "instance", {
@ -342,4 +383,119 @@ describe("CloudService", () => {
expect(mockSettingsService.dispose).toHaveBeenCalled()
})
})
describe("shareTask with ClineMessage retry logic", () => {
let cloudService: CloudService
beforeEach(async () => {
// Reset mocks for shareTask tests
vi.clearAllMocks()
// Reset authentication state for shareTask tests
mockAuthService.isAuthenticated.mockReturnValue(true)
mockAuthService.hasActiveSession.mockReturnValue(true)
mockAuthService.hasOrIsAcquiringActiveSession.mockReturnValue(true)
mockAuthService.getState.mockReturnValue("active")
cloudService = await CloudService.createInstance(mockContext, {})
})
it("should call shareTask without retry when successful", async () => {
const taskId = "test-task-id"
const visibility = "organization"
const clineMessages: ClineMessage[] = [
{
ts: Date.now(),
type: "say",
say: "text",
text: "Hello world",
},
]
const expectedResult = { success: true, shareUrl: "https://example.com/share/123" }
mockShareService.shareTask.mockResolvedValue(expectedResult)
const result = await cloudService.shareTask(taskId, visibility, clineMessages)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, visibility)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
expect(result).toEqual(expectedResult)
})
it("should retry with backfill when TaskNotFoundError occurs", async () => {
const taskId = "test-task-id"
const visibility = "organization"
const clineMessages: ClineMessage[] = [
{
ts: Date.now(),
type: "say",
say: "text",
text: "Hello world",
},
]
const expectedResult = { success: true, shareUrl: "https://example.com/share/123" }
// First call throws TaskNotFoundError, second call succeeds
mockShareService.shareTask
.mockRejectedValueOnce(new TaskNotFoundError(taskId))
.mockResolvedValueOnce(expectedResult)
const result = await cloudService.shareTask(taskId, visibility, clineMessages)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(2)
expect(mockShareService.shareTask).toHaveBeenNthCalledWith(1, taskId, visibility)
expect(mockShareService.shareTask).toHaveBeenNthCalledWith(2, taskId, visibility)
expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).toHaveBeenCalledWith(clineMessages, taskId)
expect(result).toEqual(expectedResult)
})
it("should not retry when TaskNotFoundError occurs but no clineMessages provided", async () => {
const taskId = "test-task-id"
const visibility = "organization"
const taskNotFoundError = new TaskNotFoundError(taskId)
mockShareService.shareTask.mockRejectedValue(taskNotFoundError)
await expect(cloudService.shareTask(taskId, visibility)).rejects.toThrow(TaskNotFoundError)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
})
it("should not retry when non-TaskNotFoundError occurs", async () => {
const taskId = "test-task-id"
const visibility = "organization"
const clineMessages: ClineMessage[] = [
{
ts: Date.now(),
type: "say",
say: "text",
text: "Hello world",
},
]
const genericError = new Error("Some other error")
mockShareService.shareTask.mockRejectedValue(genericError)
await expect(cloudService.shareTask(taskId, visibility, clineMessages)).rejects.toThrow(genericError)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockTelemetryClient.backfillMessages).not.toHaveBeenCalled()
})
it("should work with default parameters", async () => {
const taskId = "test-task-id"
const expectedResult = { success: true, shareUrl: "https://example.com/share/123" }
mockShareService.shareTask.mockResolvedValue(expectedResult)
const result = await cloudService.shareTask(taskId)
expect(mockShareService.shareTask).toHaveBeenCalledTimes(1)
expect(mockShareService.shareTask).toHaveBeenCalledWith(taskId, "organization")
expect(result).toEqual(expectedResult)
})
})
})

View file

@ -1,16 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MockedFunction } from "vitest"
import axios from "axios"
import * as vscode from "vscode"
import { ShareService } from "../ShareService"
import { ShareService, TaskNotFoundError } from "../ShareService"
import type { AuthService } from "../AuthService"
import type { SettingsService } from "../SettingsService"
// Mock axios
vi.mock("axios")
const mockedAxios = axios as any
// Mock fetch
const mockFetch = vi.fn()
global.fetch = mockFetch as any
// Mock vscode
vi.mock("vscode", () => ({
@ -53,6 +52,7 @@ describe("ShareService", () => {
beforeEach(() => {
vi.clearAllMocks()
mockFetch.mockClear()
mockLog = vi.fn()
mockAuthService = {
@ -70,86 +70,99 @@ describe("ShareService", () => {
describe("shareTask", () => {
it("should share task with organization visibility and copy to clipboard", async () => {
const mockResponse = {
data: {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
},
const mockResponseData = {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
}
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponseData),
})
const result = await shareService.shareTask("task-123", "organization")
expect(result.success).toBe(true)
expect(result.shareUrl).toBe("https://app.roocode.com/share/abc123")
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123", visibility: "organization" },
{
headers: {
"Content-Type": "application/json",
Authorization: "Bearer session-token",
"User-Agent": "Roo-Code 1.0.0",
},
expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer session-token",
"User-Agent": "Roo-Code 1.0.0",
},
)
body: JSON.stringify({ taskId: "task-123", visibility: "organization" }),
signal: expect.any(AbortSignal),
})
expect(vscode.env.clipboard.writeText).toHaveBeenCalledWith("https://app.roocode.com/share/abc123")
})
it("should share task with public visibility", async () => {
const mockResponse = {
data: {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
},
const mockResponseData = {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
}
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponseData),
})
const result = await shareService.shareTask("task-123", "public")
expect(result.success).toBe(true)
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123", visibility: "public" },
expect.any(Object),
)
expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer session-token",
"User-Agent": "Roo-Code 1.0.0",
},
body: JSON.stringify({ taskId: "task-123", visibility: "public" }),
signal: expect.any(AbortSignal),
})
})
it("should default to organization visibility when not specified", async () => {
const mockResponse = {
data: {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
},
const mockResponseData = {
success: true,
shareUrl: "https://app.roocode.com/share/abc123",
}
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponseData),
})
const result = await shareService.shareTask("task-123")
expect(result.success).toBe(true)
expect(mockedAxios.post).toHaveBeenCalledWith(
"https://app.roocode.com/api/extension/share",
{ taskId: "task-123", visibility: "organization" },
expect.any(Object),
)
expect(mockFetch).toHaveBeenCalledWith("https://app.roocode.com/api/extension/share", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer session-token",
"User-Agent": "Roo-Code 1.0.0",
},
body: JSON.stringify({ taskId: "task-123", visibility: "organization" }),
signal: expect.any(AbortSignal),
})
})
it("should handle API error response", async () => {
const mockResponse = {
data: {
success: false,
error: "Task not found",
},
const mockResponseData = {
success: false,
error: "Task not found",
}
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockResolvedValue(mockResponse)
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue(mockResponseData),
})
const result = await shareService.shareTask("task-123", "organization")
@ -165,10 +178,56 @@ describe("ShareService", () => {
it("should handle unexpected errors", async () => {
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockedAxios.post.mockRejectedValue(new Error("Network error"))
mockFetch.mockRejectedValue(new Error("Network error"))
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow("Network error")
})
it("should throw TaskNotFoundError for 404 responses", async () => {
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockFetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
})
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(TaskNotFoundError)
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(
"Task 'task-123' not found",
)
})
it("should throw generic Error for non-404 HTTP errors", async () => {
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockFetch.mockResolvedValue({
ok: false,
status: 500,
statusText: "Internal Server Error",
})
await expect(shareService.shareTask("task-123", "organization")).rejects.toThrow(
"HTTP 500: Internal Server Error",
)
await expect(shareService.shareTask("task-123", "organization")).rejects.not.toThrow(TaskNotFoundError)
})
it("should create TaskNotFoundError with correct properties", async () => {
;(mockAuthService.getSessionToken as any).mockReturnValue("session-token")
mockFetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
})
try {
await shareService.shareTask("task-123", "organization")
expect.fail("Expected TaskNotFoundError to be thrown")
} catch (error) {
expect(error).toBeInstanceOf(TaskNotFoundError)
expect(error).toBeInstanceOf(Error)
expect((error as TaskNotFoundError).message).toBe("Task 'task-123' not found")
}
})
})
describe("canShareTask", () => {

View file

@ -424,4 +424,315 @@ describe("TelemetryClient", () => {
await client.shutdown()
})
})
describe("backfillMessages", () => {
it("should not send request when not authenticated", async () => {
mockAuthService.isAuthenticated.mockReturnValue(false)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not send request when no session token available", async () => {
mockAuthService.getSessionToken.mockReturnValue(null)
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Unauthorized: No session token available.",
)
})
it("should send FormData request with correct structure when authenticated", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}
client.setProvider(mockProvider)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message 1",
},
{
ts: 2,
type: "ask" as const,
ask: "followup" as const,
text: "test question",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents
const call = mockFetch.mock.calls[0]
const formData = call[1].body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
// Parse and compare properties as objects since JSON.stringify order can vary
const propertiesJson = formData.get("properties") as string
const parsedProperties = JSON.parse(propertiesJson)
expect(parsedProperties).toEqual({
taskId: "test-task-id",
...providerProperties,
})
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should handle provider errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
client.setProvider(mockProvider)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents - should still work with just taskId
const call = mockFetch.mock.calls[0]
const formData = call[1].body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
expect(formData.get("properties")).toBe(
JSON.stringify({
taskId: "test-task-id",
}),
)
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should work without provider set", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents - should work with just taskId
const call = mockFetch.mock.calls[0]
const formData = call[1].body as FormData
expect(formData.get("taskId")).toBe("test-task-id")
expect(formData.get("properties")).toBe(
JSON.stringify({
taskId: "test-task-id",
}),
)
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the messages
const fileContent = await fileField.text()
expect(fileContent).toBe(JSON.stringify(messages))
})
it("should handle fetch errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockRejectedValue(new Error("Network error"))
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await expect(client.backfillMessages(messages, "test-task-id")).resolves.not.toThrow()
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining(
"[TelemetryClient#backfillMessages] Error uploading messages: Error: Network error",
),
)
})
it("should handle HTTP error responses", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
mockFetch.mockResolvedValue({
ok: false,
status: 404,
statusText: "Not Found",
})
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(console.error).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] POST events/backfill -> 404 Not Found",
)
})
it("should log debug information when debug is enabled", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService, true)
const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]
await client.backfillMessages(messages, "test-task-id")
expect(console.info).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
)
expect(console.info).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
)
})
it("should handle empty messages array", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
await client.backfillMessages([], "test-task-id")
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events/backfill",
expect.objectContaining({
method: "POST",
headers: {
Authorization: "Bearer mock-token",
},
body: expect.any(FormData),
}),
)
// Verify FormData contents
const call = mockFetch.mock.calls[0]
const formData = call[1].body as FormData
// The messages are stored as a File object under the "file" key
const fileField = formData.get("file") as File
expect(fileField).toBeInstanceOf(File)
expect(fileField.name).toBe("task.json")
expect(fileField.type).toBe("application/json")
// Read the file content to verify the empty messages array
const fileContent = await fileField.text()
expect(fileContent).toBe("[]")
})
})
})

View file

@ -13,6 +13,7 @@ export * from "./message.js"
export * from "./mode.js"
export * from "./model.js"
export * from "./provider-settings.js"
export * from "./sharing.js"
export * from "./telemetry.js"
export * from "./terminal.js"
export * from "./tool.js"

View file

@ -0,0 +1,8 @@
/**
* Types related to task sharing functionality
*/
/**
* Visibility options for sharing tasks
*/
export type ShareVisibility = "organization" | "public"

View file

@ -50,6 +50,16 @@ export enum TelemetryEventName {
MARKETPLACE_TAB_VIEWED = "Marketplace Tab Viewed",
MARKETPLACE_INSTALL_BUTTON_CLICKED = "Marketplace Install Button Clicked",
SHARE_BUTTON_CLICKED = "Share Button Clicked",
SHARE_ORGANIZATION_CLICKED = "Share Organization Clicked",
SHARE_PUBLIC_CLICKED = "Share Public Clicked",
SHARE_CONNECT_TO_CLOUD_CLICKED = "Share Connect To Cloud Clicked",
ACCOUNT_CONNECT_CLICKED = "Account Connect Clicked",
ACCOUNT_CONNECT_SUCCESS = "Account Connect Success",
ACCOUNT_LOGOUT_CLICKED = "Account Logout Clicked",
ACCOUNT_LOGOUT_SUCCESS = "Account Logout Success",
SCHEMA_VALIDATION_ERROR = "Schema Validation Error",
DIFF_APPLICATION_ERROR = "Diff Application Error",
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
@ -68,6 +78,7 @@ export const appPropertiesSchema = z.object({
editorName: z.string(),
language: z.string(),
mode: z.string(),
cloudIsAuthenticated: z.boolean().optional(),
})
export const taskPropertiesSchema = z.object({
@ -117,6 +128,16 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.AUTHENTICATION_INITIATED,
TelemetryEventName.MARKETPLACE_ITEM_INSTALLED,
TelemetryEventName.MARKETPLACE_ITEM_REMOVED,
TelemetryEventName.MARKETPLACE_TAB_VIEWED,
TelemetryEventName.MARKETPLACE_INSTALL_BUTTON_CLICKED,
TelemetryEventName.SHARE_BUTTON_CLICKED,
TelemetryEventName.SHARE_ORGANIZATION_CLICKED,
TelemetryEventName.SHARE_PUBLIC_CLICKED,
TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED,
TelemetryEventName.ACCOUNT_CONNECT_CLICKED,
TelemetryEventName.ACCOUNT_CONNECT_SUCCESS,
TelemetryEventName.ACCOUNT_LOGOUT_CLICKED,
TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS,
TelemetryEventName.SCHEMA_VALIDATION_ERROR,
TelemetryEventName.DIFF_APPLICATION_ERROR,
TelemetryEventName.SHELL_INTEGRATION_ERROR,

3
pnpm-lock.yaml generated
View file

@ -359,9 +359,6 @@ importers:
'@roo-code/types':
specifier: workspace:^
version: link:../types
axios:
specifier: ^1.7.4
version: 1.9.0
zod:
specifier: ^3.25.61
version: 3.25.61

View file

@ -30,7 +30,7 @@ import {
ORGANIZATION_ALLOW_ALL,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService } from "@roo-code/cloud"
import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud"
import { t } from "../../i18n"
import { setPanel } from "../../activate/registerCommands"
@ -1485,6 +1485,7 @@ export class ClineProvider
},
mdmCompliant: this.checkMdmCompliance(),
profileThresholds: profileThresholds ?? {},
cloudApiUrl: getRooCodeApiUrl(),
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
}
}
@ -1759,6 +1760,18 @@ export class ClineProvider
const packageJSON = this.context.extension?.packageJSON
// Get Roo Code Cloud authentication state
let cloudIsAuthenticated: boolean | undefined
try {
if (CloudService.hasInstance()) {
cloudIsAuthenticated = CloudService.instance.isAuthenticated()
}
} catch (error) {
// Silently handle errors to avoid breaking telemetry collection
this.log(`[getTelemetryProperties] Failed to get cloud auth state: ${error}`)
}
return {
appName: packageJSON?.name ?? Package.name,
appVersion: packageJSON?.version ?? Package.version,
@ -1771,6 +1784,7 @@ export class ClineProvider
modelId: task?.api?.getModel().id,
diffStrategy: task?.diffStrategy?.getName(),
isSubtask: task ? !!task.parentTask : undefined,
cloudIsAuthenticated,
}
}
}

View file

@ -308,6 +308,18 @@ vi.mock("../diff/strategies/multi-search-replace", () => ({
})),
}))
vi.mock("@roo-code/cloud", () => ({
CloudService: {
hasInstance: vi.fn().mockReturnValue(true),
get instance() {
return {
isAuthenticated: vi.fn().mockReturnValue(false),
}
},
},
getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"),
}))
afterAll(() => {
vi.restoreAllMocks()
})
@ -2090,6 +2102,11 @@ describe("getTelemetryProperties", () => {
// Reset mocks
vi.clearAllMocks()
// Initialize TelemetryService if not already initialized
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
// Setup basic mocks
mockContext = {
globalState: {
@ -2143,6 +2160,96 @@ describe("getTelemetryProperties", () => {
expect(properties).toHaveProperty("modelId", "claude-sonnet-4-20250514")
})
describe("cloud authentication telemetry", () => {
beforeEach(() => {
// Reset all mocks before each test
vi.clearAllMocks()
})
test("includes cloud authentication property when user is authenticated", async () => {
// Import the CloudService mock and update it
const { CloudService } = await import("@roo-code/cloud")
const mockCloudService = {
isAuthenticated: vi.fn().mockReturnValue(true),
}
// Update the existing mock
Object.defineProperty(CloudService, "instance", {
get: vi.fn().mockReturnValue(mockCloudService),
configurable: true,
})
const properties = await provider.getTelemetryProperties()
expect(properties).toHaveProperty("cloudIsAuthenticated", true)
})
test("includes cloud authentication property when user is not authenticated", async () => {
// Import the CloudService mock and update it
const { CloudService } = await import("@roo-code/cloud")
const mockCloudService = {
isAuthenticated: vi.fn().mockReturnValue(false),
}
// Update the existing mock
Object.defineProperty(CloudService, "instance", {
get: vi.fn().mockReturnValue(mockCloudService),
configurable: true,
})
const properties = await provider.getTelemetryProperties()
expect(properties).toHaveProperty("cloudIsAuthenticated", false)
})
test("handles CloudService errors gracefully", async () => {
// Import the CloudService mock and update it to throw an error
const { CloudService } = await import("@roo-code/cloud")
Object.defineProperty(CloudService, "instance", {
get: vi.fn().mockImplementation(() => {
throw new Error("CloudService not available")
}),
configurable: true,
})
const properties = await provider.getTelemetryProperties()
// Should still include basic telemetry properties
expect(properties).toHaveProperty("vscodeVersion")
expect(properties).toHaveProperty("platform")
expect(properties).toHaveProperty("appVersion", "1.0.0")
// Cloud property should be undefined when CloudService is not available
expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
})
test("handles CloudService method errors gracefully", async () => {
// Import the CloudService mock and update it
const { CloudService } = await import("@roo-code/cloud")
const mockCloudService = {
isAuthenticated: vi.fn().mockImplementation(() => {
throw new Error("Authentication check error")
}),
}
// Update the existing mock
Object.defineProperty(CloudService, "instance", {
get: vi.fn().mockReturnValue(mockCloudService),
configurable: true,
})
const properties = await provider.getTelemetryProperties()
// Should still include basic telemetry properties
expect(properties).toHaveProperty("vscodeVersion")
expect(properties).toHaveProperty("platform")
expect(properties).toHaveProperty("appVersion", "1.0.0")
// Property that errored should be undefined
expect(properties).toHaveProperty("cloudIsAuthenticated", undefined)
})
})
})
describe("ClineProvider - Router Models", () => {

View file

@ -225,6 +225,7 @@ export const webviewMessageHandler = async (
break
case "shareCurrentTask":
const shareTaskId = provider.getCurrentCline()?.taskId
const clineMessages = provider.getCurrentCline()?.clineMessages
if (!shareTaskId) {
vscode.window.showErrorMessage(t("common:errors.share_no_active_task"))
break
@ -232,7 +233,7 @@ export const webviewMessageHandler = async (
try {
const visibility = message.visibility || "organization"
const result = await CloudService.instance.shareTask(shareTaskId, visibility)
const result = await CloudService.instance.shareTask(shareTaskId, visibility, clineMessages)
if (result.success && result.shareUrl) {
// Show success notification
@ -241,6 +242,13 @@ export const webviewMessageHandler = async (
? "common:info.public_share_link_copied"
: "common:info.organization_share_link_copied"
vscode.window.showInformationMessage(t(messageKey))
// Send success feedback to webview for inline display
await provider.postMessageToWebview({
type: "shareTaskSuccess",
visibility,
text: result.shareUrl,
})
} else {
// Handle error
const errorMessage = result.error || "Failed to create share link"

View file

@ -98,8 +98,7 @@
{
"command": "roo-cline.accountButtonClicked",
"title": "Account",
"icon": "$(account)",
"when": "config.roo-cline.rooCodeCloudEnabled"
"icon": "$(account)"
},
{
"command": "roo-cline.settingsButtonClicked",
@ -237,7 +236,7 @@
{
"command": "roo-cline.accountButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider && config.roo-cline.rooCodeCloudEnabled"
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
@ -269,7 +268,7 @@
{
"command": "roo-cline.accountButtonClicked",
"group": "navigation@5",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled"
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
@ -325,11 +324,6 @@
"default": "",
"description": "%settings.customStoragePath.description%"
},
"roo-cline.rooCodeCloudEnabled": {
"type": "boolean",
"default": false,
"description": "%settings.rooCodeCloudEnabled.description%"
},
"roo-cline.enableCodeActions": {
"type": "boolean",
"default": true,

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "El proveïdor del model de llenguatge (p. ex. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La família del model de llenguatge (p. ex. gpt-4)",
"settings.customStoragePath.description": "Ruta d'emmagatzematge personalitzada. Deixeu-la buida per utilitzar la ubicació predeterminada. Admet rutes absolutes (p. ex. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud.",
"settings.enableCodeActions.description": "Habilitar correccions ràpides de Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Der Anbieter des Sprachmodells (z.B. copilot)",
"settings.vsCodeLmModelSelector.family.description": "Die Familie des Sprachmodells (z.B. gpt-4)",
"settings.customStoragePath.description": "Benutzerdefinierter Speicherpfad. Leer lassen, um den Standardspeicherort zu verwenden. Unterstützt absolute Pfade (z.B. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Aktiviere Roo Code Cloud.",
"settings.enableCodeActions.description": "Roo Code Schnelle Problembehebung aktivieren."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "El proveedor del modelo de lenguaje (ej. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La familia del modelo de lenguaje (ej. gpt-4)",
"settings.customStoragePath.description": "Ruta de almacenamiento personalizada. Dejar vacío para usar la ubicación predeterminada. Admite rutas absolutas (ej. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud.",
"settings.enableCodeActions.description": "Habilitar correcciones rápidas de Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Le fournisseur du modèle de langage (ex: copilot)",
"settings.vsCodeLmModelSelector.family.description": "La famille du modèle de langage (ex: gpt-4)",
"settings.customStoragePath.description": "Chemin de stockage personnalisé. Laisser vide pour utiliser l'emplacement par défaut. Prend en charge les chemins absolus (ex: 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Activer Roo Code Cloud.",
"settings.enableCodeActions.description": "Activer les correctifs rapides de Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "भाषा मॉडल का विक्रेता (उदा. copilot)",
"settings.vsCodeLmModelSelector.family.description": "भाषा मॉडल का परिवार (उदा. gpt-4)",
"settings.customStoragePath.description": "कस्टम स्टोरेज पाथ। डिफ़ॉल्ट स्थान का उपयोग करने के लिए खाली छोड़ें। पूर्ण पथ का समर्थन करता है (उदा. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud सक्षम करें।",
"settings.enableCodeActions.description": "Roo Code त्वरित सुधार सक्षम करें"
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Vendor dari model bahasa (misalnya copilot)",
"settings.vsCodeLmModelSelector.family.description": "Keluarga dari model bahasa (misalnya gpt-4)",
"settings.customStoragePath.description": "Path penyimpanan kustom. Biarkan kosong untuk menggunakan lokasi default. Mendukung path absolut (misalnya 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Aktifkan Roo Code Cloud.",
"settings.enableCodeActions.description": "Aktifkan perbaikan cepat Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Il fornitore del modello linguistico (es. copilot)",
"settings.vsCodeLmModelSelector.family.description": "La famiglia del modello linguistico (es. gpt-4)",
"settings.customStoragePath.description": "Percorso di archiviazione personalizzato. Lasciare vuoto per utilizzare la posizione predefinita. Supporta percorsi assoluti (es. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Abilita Roo Code Cloud.",
"settings.enableCodeActions.description": "Abilita correzioni rapide di Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "言語モデルのベンダーcopilot",
"settings.vsCodeLmModelSelector.family.description": "言語モデルのファミリーgpt-4",
"settings.customStoragePath.description": "カスタムストレージパス。デフォルトの場所を使用する場合は空のままにします。絶対パスをサポートします(例:'D:\\RooCodeStorage'",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud を有効にする。",
"settings.enableCodeActions.description": "Roo Codeのクイック修正を有効にする。"
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "The vendor of the language model (e.g. copilot)",
"settings.vsCodeLmModelSelector.family.description": "The family of the language model (e.g. gpt-4)",
"settings.customStoragePath.description": "Custom storage path. Leave empty to use the default location. Supports absolute paths (e.g. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Enable Roo Code Cloud.",
"settings.enableCodeActions.description": "Enable Roo Code quick fixes"
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "언어 모델 공급자 (예: copilot)",
"settings.vsCodeLmModelSelector.family.description": "언어 모델 계열 (예: gpt-4)",
"settings.customStoragePath.description": "사용자 지정 저장소 경로. 기본 위치를 사용하려면 비워두세요. 절대 경로를 지원합니다 (예: 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud 사용 설정",
"settings.enableCodeActions.description": "Roo Code 빠른 수정 사용 설정"
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "De leverancier van het taalmodel (bijv. copilot)",
"settings.vsCodeLmModelSelector.family.description": "De familie van het taalmodel (bijv. gpt-4)",
"settings.customStoragePath.description": "Aangepast opslagpad. Laat leeg om de standaardlocatie te gebruiken. Ondersteunt absolute paden (bijv. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud inschakelen.",
"settings.enableCodeActions.description": "Snelle correcties van Roo Code inschakelen."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Dostawca modelu językowego (np. copilot)",
"settings.vsCodeLmModelSelector.family.description": "Rodzina modelu językowego (np. gpt-4)",
"settings.customStoragePath.description": "Niestandardowa ścieżka przechowywania. Pozostaw puste, aby użyć domyślnej lokalizacji. Obsługuje ścieżki bezwzględne (np. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Włącz Roo Code Cloud.",
"settings.enableCodeActions.description": "Włącz szybkie poprawki Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "O fornecedor do modelo de linguagem (ex: copilot)",
"settings.vsCodeLmModelSelector.family.description": "A família do modelo de linguagem (ex: gpt-4)",
"settings.customStoragePath.description": "Caminho de armazenamento personalizado. Deixe vazio para usar o local padrão. Suporta caminhos absolutos (ex: 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Habilitar Roo Code Cloud.",
"settings.enableCodeActions.description": "Habilitar correções rápidas do Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Поставщик языковой модели (например, copilot)",
"settings.vsCodeLmModelSelector.family.description": "Семейство языковой модели (например, gpt-4)",
"settings.customStoragePath.description": "Пользовательский путь хранения. Оставьте пустым для использования пути по умолчанию. Поддерживает абсолютные пути (например, 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Включить Roo Code Cloud.",
"settings.enableCodeActions.description": "Включить быстрые исправления Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Dil modelinin sağlayıcısı (örn: copilot)",
"settings.vsCodeLmModelSelector.family.description": "Dil modelinin ailesi (örn: gpt-4)",
"settings.customStoragePath.description": "Özel depolama yolu. Varsayılan konumu kullanmak için boş bırakın. Mutlak yolları destekler (örn: 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud'u Etkinleştir.",
"settings.enableCodeActions.description": "Roo Code hızlı düzeltmeleri etkinleştir."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "Nhà cung cấp mô hình ngôn ngữ (ví dụ: copilot)",
"settings.vsCodeLmModelSelector.family.description": "Họ mô hình ngôn ngữ (ví dụ: gpt-4)",
"settings.customStoragePath.description": "Đường dẫn lưu trữ tùy chỉnh. Để trống để sử dụng vị trí mặc định. Hỗ trợ đường dẫn tuyệt đối (ví dụ: 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Bật Roo Code Cloud.",
"settings.enableCodeActions.description": "Bật sửa lỗi nhanh Roo Code."
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "语言模型的供应商例如copilot",
"settings.vsCodeLmModelSelector.family.description": "语言模型的系列例如gpt-4",
"settings.customStoragePath.description": "自定义存储路径。留空以使用默认位置。支持绝对路径(例如:'D:\\RooCodeStorage'",
"settings.rooCodeCloudEnabled.description": "启用 Roo Code Cloud。",
"settings.enableCodeActions.description": "启用 Roo Code 快速修复"
}

View file

@ -30,6 +30,5 @@
"settings.vsCodeLmModelSelector.vendor.description": "語言模型供應商例如copilot",
"settings.vsCodeLmModelSelector.family.description": "語言模型系列例如gpt-4",
"settings.customStoragePath.description": "自訂儲存路徑。留空以使用預設位置。支援絕對路徑(例如:'D:\\RooCodeStorage'",
"settings.rooCodeCloudEnabled.description": "啟用 Roo Code Cloud。",
"settings.enableCodeActions.description": "啟用 Roo Code 快速修復。"
}

View file

@ -35,8 +35,6 @@ export class MdmService {
this.mdmConfig = await this.loadMdmConfig()
if (this.mdmConfig) {
this.log("[MDM] Loaded MDM configuration:", this.mdmConfig)
// Automatically enable Roo Code Cloud when MDM config is present
await this.ensureCloudEnabled()
} else {
this.log("[MDM] No MDM configuration found")
}
@ -60,23 +58,6 @@ export class MdmService {
return this.mdmConfig?.organizationId
}
/**
* Ensure Roo Code Cloud is enabled when MDM config is present
*/
private async ensureCloudEnabled(): Promise<void> {
try {
const config = vscode.workspace.getConfiguration(Package.name)
const currentValue = config.get<boolean>("rooCodeCloudEnabled", false)
if (!currentValue) {
this.log("[MDM] Enabling Roo Code Cloud due to MDM policy")
await config.update("rooCodeCloudEnabled", true, vscode.ConfigurationTarget.Global)
}
} catch (error) {
this.log("[MDM] Error enabling Roo Code Cloud:", error)
}
}
/**
* Check if the current state is compliant with MDM policy
*/

View file

@ -340,102 +340,6 @@ describe("MdmService", () => {
})
})
describe("cloud enablement", () => {
it("should enable Roo Code Cloud when MDM config is present and setting is disabled", async () => {
const mockConfig = {
requireCloudAuth: true,
organizationId: "test-org-123",
}
mockFs.existsSync.mockReturnValue(true)
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig))
const mockVsCodeConfig = {
get: vi.fn().mockReturnValue(false), // rooCodeCloudEnabled is false
update: vi.fn().mockResolvedValue(undefined),
}
mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig)
await MdmService.createInstance()
expect(mockVscode.workspace.getConfiguration).toHaveBeenCalledWith("roo-cline")
expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false)
expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1) // ConfigurationTarget.Global
})
it("should not update setting when Roo Code Cloud is already enabled", async () => {
const mockConfig = {
requireCloudAuth: true,
organizationId: "test-org-123",
}
mockFs.existsSync.mockReturnValue(true)
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig))
const mockVsCodeConfig = {
get: vi.fn().mockReturnValue(true), // rooCodeCloudEnabled is already true
update: vi.fn().mockResolvedValue(undefined),
}
mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig)
await MdmService.createInstance()
expect(mockVsCodeConfig.get).toHaveBeenCalledWith("rooCodeCloudEnabled", false)
expect(mockVsCodeConfig.update).not.toHaveBeenCalled()
})
it("should enable cloud even when requireCloudAuth is false", async () => {
const mockConfig = {
requireCloudAuth: false, // Cloud auth not required, but config file exists
}
mockFs.existsSync.mockReturnValue(true)
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig))
const mockVsCodeConfig = {
get: vi.fn().mockReturnValue(false),
update: vi.fn().mockResolvedValue(undefined),
}
mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig)
await MdmService.createInstance()
expect(mockVsCodeConfig.update).toHaveBeenCalledWith("rooCodeCloudEnabled", true, 1)
})
it("should not enable cloud when no MDM config exists", async () => {
mockFs.existsSync.mockReturnValue(false)
const mockVsCodeConfig = {
get: vi.fn().mockReturnValue(false),
update: vi.fn().mockResolvedValue(undefined),
}
mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig)
await MdmService.createInstance()
expect(mockVsCodeConfig.update).not.toHaveBeenCalled()
})
it("should handle VSCode configuration errors gracefully", async () => {
const mockConfig = {
requireCloudAuth: true,
}
mockFs.existsSync.mockReturnValue(true)
mockFs.readFileSync.mockReturnValue(JSON.stringify(mockConfig))
const mockVsCodeConfig = {
get: vi.fn().mockReturnValue(false),
update: vi.fn().mockRejectedValue(new Error("Configuration update failed")),
}
mockVscode.workspace.getConfiguration.mockReturnValue(mockVsCodeConfig)
// Should not throw
await expect(MdmService.createInstance()).resolves.toBeInstanceOf(MdmService)
})
})
describe("singleton pattern", () => {
it("should throw error when accessing instance before creation", () => {
expect(() => MdmService.getInstance()).toThrow("MdmService not initialized")

View file

@ -9,6 +9,7 @@ import type {
ClineMessage,
OrganizationAllowList,
CloudUserInfo,
ShareVisibility,
} from "@roo-code/types"
import { GitCommit } from "../utils/git"
@ -97,6 +98,7 @@ export interface ExtensionMessage {
| "codebaseIndexConfig"
| "marketplaceInstallResult"
| "marketplaceData"
| "shareTaskSuccess"
text?: string
payload?: any // Add a generic payload for now, can refine later
action?:
@ -145,6 +147,7 @@ export interface ExtensionMessage {
tab?: string
marketplaceItems?: MarketplaceItem[]
marketplaceInstalledMetadata?: MarketplaceInstalledMetadata
visibility?: ShareVisibility
}
export type ExtensionState = Pick<
@ -253,6 +256,7 @@ export type ExtensionState = Pick<
cloudUserInfo: CloudUserInfo | null
cloudIsAuthenticated: boolean
cloudApiUrl?: string
sharingEnabled: boolean
organizationAllowList: OrganizationAllowList

View file

@ -6,6 +6,7 @@ import type {
ModeConfig,
InstallMarketplaceItemOptions,
MarketplaceItem,
ShareVisibility,
} from "@roo-code/types"
import { marketplaceItemSchema } from "@roo-code/types"
@ -173,6 +174,7 @@ export interface WebviewMessage {
| "fetchMarketplaceData"
| "switchTab"
| "profileThresholds"
| "shareTaskSuccess"
text?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
disabled?: boolean
@ -210,7 +212,7 @@ export interface WebviewMessage {
mpItem?: MarketplaceItem
mpInstallOptions?: InstallMarketplaceItemOptions
config?: Record<string, any> // Add config to the payload
visibility?: "organization" | "public" // For share visibility
visibility?: ShareVisibility // For share visibility
}
export const checkoutDiffPayloadSchema = z.object({

View file

@ -43,6 +43,7 @@ const App = () => {
machineId,
cloudUserInfo,
cloudIsAuthenticated,
cloudApiUrl,
renderContext,
mdmCompliant,
} = useExtensionState()
@ -186,6 +187,7 @@ const App = () => {
<AccountView
userInfo={cloudUserInfo}
isAuthenticated={cloudIsAuthenticated}
cloudApiUrl={cloudApiUrl}
onDone={() => switchTab("chat")}
/>
)}

View file

@ -1,21 +1,56 @@
import { useEffect, useRef } from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import type { CloudUserInfo } from "@roo-code/types"
import { TelemetryEventName } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { vscode } from "@src/utils/vscode"
import { telemetryClient } from "@src/utils/TelemetryClient"
type AccountViewProps = {
userInfo: CloudUserInfo | null
isAuthenticated: boolean
cloudApiUrl?: string
onDone: () => void
}
export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewProps) => {
export const AccountView = ({ userInfo, isAuthenticated, cloudApiUrl, onDone }: AccountViewProps) => {
const { t } = useAppTranslation()
const wasAuthenticatedRef = useRef(false)
const rooLogoUri = (window as any).IMAGES_BASE_URI + "/roo-logo.svg"
// Track authentication state changes to detect successful logout
useEffect(() => {
if (isAuthenticated) {
wasAuthenticatedRef.current = true
} else if (wasAuthenticatedRef.current && !isAuthenticated) {
// User just logged out successfully
telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_SUCCESS)
wasAuthenticatedRef.current = false
}
}, [isAuthenticated])
const handleConnectClick = () => {
// Send telemetry for account connect action
telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED)
vscode.postMessage({ type: "rooCloudSignIn" })
}
const handleLogoutClick = () => {
// Send telemetry for account logout action
telemetryClient.capture(TelemetryEventName.ACCOUNT_LOGOUT_CLICKED)
vscode.postMessage({ type: "rooCloudSignOut" })
}
const handleVisitCloudWebsite = () => {
// Send telemetry for cloud website visit
telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_CLICKED)
const cloudUrl = cloudApiUrl || "https://app.roocode.com"
vscode.postMessage({ type: "openExternal", url: cloudUrl })
}
return (
<div className="flex flex-col h-full p-4 bg-vscode-editor-background">
<div className="flex justify-between items-center mb-6">
@ -41,9 +76,9 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
</div>
)}
</div>
<h2 className="text-lg font-medium text-vscode-foreground mb-0">
{userInfo?.name || t("account:unknownUser")}
</h2>
{userInfo.name && (
<h2 className="text-lg font-medium text-vscode-foreground mb-0">{userInfo.name}</h2>
)}
{userInfo?.email && (
<p className="text-sm text-vscode-descriptionForeground">{userInfo?.email}</p>
)}
@ -62,18 +97,18 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
</div>
)}
<div className="flex flex-col gap-2 mt-4">
<VSCodeButton
appearance="secondary"
onClick={() => vscode.postMessage({ type: "rooCloudSignOut" })}
className="w-full">
<VSCodeButton appearance="secondary" onClick={handleVisitCloudWebsite} className="w-full">
{t("account:visitCloudWebsite")}
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleLogoutClick} className="w-full">
{t("account:logOut")}
</VSCodeButton>
</div>
</>
) : (
<>
<div className="flex flex-col items-center mb-4 text-center">
<div className="w-16 h-16 mb-4 flex items-center justify-center">
<div className="flex flex-col items-center mb-1 text-center">
<div className="w-16 h-16 mb-1 flex items-center justify-center">
<div
className="w-12 h-12 bg-vscode-foreground"
style={{
@ -88,12 +123,33 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
</div>
</div>
</div>
<div className="flex flex-col mb-6 text-center">
<h2 className="text-lg font-medium text-vscode-foreground mb-2">
{t("account:cloudBenefitsTitle")}
</h2>
<p className="text-md text-vscode-descriptionForeground mb-4">
{t("account:cloudBenefitsSubtitle")}
</p>
<ul className="text-sm text-vscode-descriptionForeground space-y-2 max-w-xs mx-auto">
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitHistory")}
</li>
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitSharing")}
</li>
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitMetrics")}
</li>
</ul>
</div>
<div className="flex flex-col gap-4">
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "rooCloudSignIn" })}
className="w-full">
{t("account:signIn")}
<VSCodeButton appearance="primary" onClick={handleConnectClick} className="w-full">
{t("account:connect")}
</VSCodeButton>
</div>
</>

View file

@ -0,0 +1,92 @@
import { render, screen } from "@testing-library/react"
import { describe, it, expect, vi } from "vitest"
import { AccountView } from "../AccountView"
// Mock the translation context
vi.mock("@src/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"account:title": "Account",
"settings:common.done": "Done",
"account:signIn": "Connect to Roo Code Cloud",
"account:cloudBenefitsTitle": "Connect to Roo Code Cloud",
"account:cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:",
"account:cloudBenefitHistory": "Online task history",
"account:cloudBenefitSharing": "Sharing and collaboration features",
"account:cloudBenefitMetrics": "Task, token, and cost-based usage metrics",
"account:logOut": "Log out",
}
return translations[key] || key
},
}),
}))
// Mock vscode utilities
vi.mock("@src/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
// Mock telemetry client
vi.mock("@src/utils/TelemetryClient", () => ({
telemetryClient: {
capture: vi.fn(),
},
}))
// Mock window global for images
Object.defineProperty(window, "IMAGES_BASE_URI", {
value: "/images",
writable: true,
})
describe("AccountView", () => {
it("should display benefits when user is not authenticated", () => {
render(
<AccountView
userInfo={null}
isAuthenticated={false}
cloudApiUrl="https://app.roocode.com"
onDone={() => {}}
/>,
)
// Check that the benefits section is displayed
expect(screen.getByRole("heading", { name: "Connect to Roo Code Cloud" })).toBeInTheDocument()
expect(screen.getByText("Sync your prompts and telemetry to enable:")).toBeInTheDocument()
expect(screen.getByText("Online task history")).toBeInTheDocument()
expect(screen.getByText("Sharing and collaboration features")).toBeInTheDocument()
expect(screen.getByText("Task, token, and cost-based usage metrics")).toBeInTheDocument()
// Check that the connect button is also present
expect(screen.getByText("account:connect")).toBeInTheDocument()
})
it("should not display benefits when user is authenticated", () => {
const mockUserInfo = {
name: "Test User",
email: "test@example.com",
}
render(
<AccountView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://app.roocode.com"
onDone={() => {}}
/>,
)
// Check that the benefits section is NOT displayed
expect(screen.queryByText("Sync your prompts and telemetry to enable:")).not.toBeInTheDocument()
expect(screen.queryByText("Online task history")).not.toBeInTheDocument()
expect(screen.queryByText("Sharing and collaboration features")).not.toBeInTheDocument()
expect(screen.queryByText("Task, token, and cost-based usage metrics")).not.toBeInTheDocument()
// Check that user info is displayed instead
expect(screen.getByText("Test User")).toBeInTheDocument()
expect(screen.getByText("test@example.com")).toBeInTheDocument()
})
})

View file

@ -40,6 +40,7 @@ export const IconButton: React.FC<IconButtonProps> = ({
aria-label={title}
title={title}
className={buttonClasses}
disabled={disabled}
onClick={!disabled ? onClick : undefined}
style={{ fontSize: 16.5, ...style }}
{...props}>

View file

@ -0,0 +1,259 @@
import { useState, useEffect, useRef } from "react"
import { useTranslation } from "react-i18next"
import type { HistoryItem, ShareVisibility } from "@roo-code/types"
import { TelemetryEventName } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { telemetryClient } from "@/utils/TelemetryClient"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
Command,
CommandList,
CommandItem,
CommandGroup,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui"
interface ShareButtonProps {
item?: HistoryItem
disabled?: boolean
}
export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
const [connectModalOpen, setConnectModalOpen] = useState(false)
const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null)
const { t } = useTranslation()
const { sharingEnabled, cloudIsAuthenticated, cloudUserInfo } = useExtensionState()
const wasUnauthenticatedRef = useRef(false)
// Track authentication state changes to auto-open popover after login
useEffect(() => {
if (!cloudIsAuthenticated || !sharingEnabled) {
wasUnauthenticatedRef.current = true
} else if (wasUnauthenticatedRef.current && cloudIsAuthenticated && sharingEnabled) {
// User just authenticated, send telemetry, close modal, and open the popover
telemetryClient.capture(TelemetryEventName.ACCOUNT_CONNECT_SUCCESS)
setConnectModalOpen(false)
setShareDropdownOpen(true)
wasUnauthenticatedRef.current = false
}
}, [cloudIsAuthenticated, sharingEnabled])
// Listen for share success messages from the extension
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "shareTaskSuccess") {
setShareSuccess({
visibility: message.visibility,
url: message.text,
})
// Auto-hide success message and close popover after 5 seconds
setTimeout(() => {
setShareSuccess(null)
setShareDropdownOpen(false)
}, 5000)
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [])
const handleShare = (visibility: ShareVisibility) => {
// Clear any previous success state
setShareSuccess(null)
// Send telemetry for share action
if (visibility === "organization") {
telemetryClient.capture(TelemetryEventName.SHARE_ORGANIZATION_CLICKED)
} else {
telemetryClient.capture(TelemetryEventName.SHARE_PUBLIC_CLICKED)
}
vscode.postMessage({
type: "shareCurrentTask",
visibility,
})
// Don't close the dropdown immediately - let success message show first
}
const handleConnectToCloud = () => {
// Send telemetry for connect to cloud action
telemetryClient.capture(TelemetryEventName.SHARE_CONNECT_TO_CLOUD_CLICKED)
vscode.postMessage({ type: "rooCloudSignIn" })
setShareDropdownOpen(false)
setConnectModalOpen(false)
}
const handleShareButtonClick = () => {
// Send telemetry for share button click
telemetryClient.capture(TelemetryEventName.SHARE_BUTTON_CLICKED)
if (!cloudIsAuthenticated) {
// Show modal for unauthenticated users
setConnectModalOpen(true)
} else {
// Show popover for authenticated users
setShareDropdownOpen(true)
}
}
// Determine share button state
const getShareButtonState = () => {
if (!cloudIsAuthenticated) {
return {
disabled: false,
title: t("chat:task.share"),
showPopover: false, // We'll show modal instead
}
} else if (!sharingEnabled) {
return {
disabled: true,
title: t("chat:task.sharingDisabledByOrganization"),
showPopover: false,
}
} else {
return {
disabled: false,
title: t("chat:task.share"),
showPopover: true,
}
}
}
const shareButtonState = getShareButtonState()
// Don't render if no item ID
if (!item?.id) {
return null
}
return (
<>
{shareButtonState.showPopover ? (
<Popover open={shareDropdownOpen} onOpenChange={setShareDropdownOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
title={shareButtonState.title}
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-56 p-0" align="start">
{shareSuccess ? (
<div className="p-3">
<div className="flex items-center gap-2 text-sm text-green-600 dark:text-green-400">
<span className="codicon codicon-check"></span>
<span>
{shareSuccess.visibility === "public"
? t("chat:task.shareSuccessPublic")
: t("chat:task.shareSuccessOrganization")}
</span>
</div>
</div>
) : (
<Command>
<CommandList>
<CommandGroup>
{cloudUserInfo?.organizationName && (
<CommandItem
onSelect={() => handleShare("organization")}
className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-organization text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">
{t("chat:task.shareWithOrganization")}
</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.shareWithOrganizationDescription")}
</span>
</div>
</div>
</CommandItem>
)}
<CommandItem onSelect={() => handleShare("public")} className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-globe text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">{t("chat:task.sharePublicly")}</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.sharePubliclyDescription")}
</span>
</div>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
)}
</PopoverContent>
</Popover>
) : (
<Button
variant="ghost"
size="icon"
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
title={shareButtonState.title}
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
</Button>
)}
{/* Connect to Cloud Modal */}
<Dialog open={connectModalOpen} onOpenChange={setConnectModalOpen}>
<DialogContent className="max-w-sm">
<DialogHeader className="text-center">
<DialogTitle className="text-lg font-medium text-vscode-foreground">
{t("account:cloudBenefitsTitle")}
</DialogTitle>
</DialogHeader>
<div className="flex flex-col space-y-6">
<div>
<p className="text-md text-vscode-descriptionForeground mb-4">
{t("account:cloudBenefitsSubtitle")}
</p>
<ul className="text-sm text-vscode-descriptionForeground space-y-2">
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitSharing")}
</li>
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitHistory")}
</li>
<li className="flex items-start">
<span className="mr-2 text-vscode-foreground"></span>
{t("account:cloudBenefitMetrics")}
</li>
</ul>
</div>
<div className="flex flex-col gap-4">
<Button onClick={handleConnectToCloud} className="w-full">
{t("account:connect")}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</>
)
}

View file

@ -5,20 +5,10 @@ import { useTranslation } from "react-i18next"
import type { HistoryItem } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
Command,
CommandList,
CommandItem,
CommandGroup,
} from "@/components/ui"
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
import { IconButton } from "./IconButton"
import { ShareButton } from "./ShareButton"
interface TaskActionsProps {
item?: HistoryItem
@ -27,66 +17,11 @@ interface TaskActionsProps {
export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
const { t } = useTranslation()
const { sharingEnabled } = useExtensionState()
const handleShare = (visibility: "organization" | "public") => {
vscode.postMessage({
type: "shareCurrentTask",
visibility,
})
setShareDropdownOpen(false)
}
return (
<div className="flex flex-row gap-1">
{item?.id && sharingEnabled && (
<Popover open={shareDropdownOpen} onOpenChange={setShareDropdownOpen}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
disabled={buttonsDisabled}
className="h-6 w-6 p-0 hover:bg-vscode-toolbar-hoverBackground"
title={t("chat:task.share")}>
<span className="codicon codicon-link text-xs"></span>
</Button>
</PopoverTrigger>
<PopoverContent className="w-56 p-0" align="start">
<Command>
<CommandList>
<CommandGroup>
<CommandItem
onSelect={() => handleShare("organization")}
className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-organization text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">{t("chat:task.shareWithOrganization")}</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.shareWithOrganizationDescription")}
</span>
</div>
</div>
</CommandItem>
<CommandItem onSelect={() => handleShare("public")} className="cursor-pointer">
<div className="flex items-center gap-2">
<span className="codicon codicon-globe text-sm"></span>
<div className="flex flex-col">
<span className="text-sm">{t("chat:task.sharePublicly")}</span>
<span className="text-xs text-vscode-descriptionForeground">
{t("chat:task.sharePubliclyDescription")}
</span>
</div>
</div>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)}
<ShareButton item={item} disabled={buttonsDisabled} />
<IconButton
iconClass="codicon-desktop-download"
title={t("chat:task.export")}

View file

@ -17,6 +17,7 @@ import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
import Thumbnails from "../common/Thumbnails"
import { TaskActions } from "./TaskActions"
import { ShareButton } from "./ShareButton"
import { ContextWindowProgress } from "./ContextWindowProgress"
import { Mention } from "./Mention"
@ -118,6 +119,7 @@ const TaskHeader = ({
}
/>
{condenseButton}
<ShareButton item={currentTaskItem} disabled={buttonsDisabled} />
{!!totalCost && <VSCodeBadge>${totalCost.toFixed(2)}</VSCodeBadge>}
</div>
)}

View file

@ -0,0 +1,325 @@
import { describe, test, expect, vi, beforeEach } from "vitest"
import { render, screen, fireEvent, waitFor } from "@testing-library/react"
import { ShareButton } from "../ShareButton"
import { useTranslation } from "react-i18next"
import { vscode } from "@/utils/vscode"
// Mock the vscode utility
vi.mock("@/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
// Mock react-i18next
vi.mock("react-i18next")
// Mock the extension state context
vi.mock("@/context/ExtensionStateContext", () => ({
ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => children,
useExtensionState: () => ({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: {
id: "test-user",
email: "test@example.com",
organizationName: "Test Organization",
},
}),
}))
// Mock telemetry client
vi.mock("@/utils/TelemetryClient", () => ({
telemetryClient: {
capture: vi.fn(),
},
}))
const mockUseTranslation = vi.mocked(useTranslation)
const mockVscode = vi.mocked(vscode)
describe("ShareButton", () => {
const mockT = vi.fn((key: string) => key)
const mockItem = {
id: "test-task-id",
number: 1,
ts: Date.now(),
task: "Test Task",
tokensIn: 100,
tokensOut: 50,
totalCost: 0.01,
}
beforeEach(() => {
vi.clearAllMocks()
mockUseTranslation.mockReturnValue({
t: mockT,
i18n: {} as any,
ready: true,
} as any)
})
test("renders share button", () => {
render(<ShareButton item={mockItem} />)
const button = screen.getByRole("button")
expect(button).toBeInTheDocument()
})
test("opens popover when clicked", async () => {
render(<ShareButton item={mockItem} />)
const button = screen.getByRole("button")
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
})
test("sends organization share message when organization button clicked", async () => {
render(<ShareButton item={mockItem} />)
// Open popover
const button = screen.getByRole("button")
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Click organization share button
const orgButton = screen.getByText("chat:task.shareWithOrganization")
fireEvent.click(orgButton)
expect(mockVscode.postMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "organization",
})
})
test("sends public share message when public button clicked", async () => {
render(<ShareButton item={mockItem} />)
// Open popover
const button = screen.getByRole("button")
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText("chat:task.sharePublicly")).toBeInTheDocument()
})
// Click public share button
const publicButton = screen.getByText("chat:task.sharePublicly")
fireEvent.click(publicButton)
expect(mockVscode.postMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "public",
})
})
test("displays success message when shareTaskSuccess message received", async () => {
const mockAddEventListener = vi.fn()
const mockRemoveEventListener = vi.fn()
// Mock window.addEventListener
Object.defineProperty(window, "addEventListener", {
value: mockAddEventListener,
writable: true,
})
Object.defineProperty(window, "removeEventListener", {
value: mockRemoveEventListener,
writable: true,
})
render(<ShareButton item={mockItem} />)
// Get the message event listener that was registered
const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1]
expect(messageListener).toBeDefined()
// Open popover first
const button = screen.getByRole("button")
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Simulate receiving a shareTaskSuccess message
const mockEvent = {
data: {
type: "shareTaskSuccess",
visibility: "organization",
text: "https://example.com/share/123",
},
}
messageListener(mockEvent)
await waitFor(() => {
expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument()
})
})
test("displays different success messages based on visibility", async () => {
const mockAddEventListener = vi.fn()
Object.defineProperty(window, "addEventListener", {
value: mockAddEventListener,
writable: true,
})
render(<ShareButton item={mockItem} />)
const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1]
// Open popover
const button = screen.getByRole("button")
fireEvent.click(button)
await waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Test public visibility success message
const publicEvent = {
data: {
type: "shareTaskSuccess",
visibility: "public",
text: "https://example.com/share/456",
},
}
messageListener(publicEvent)
await waitFor(() => {
expect(screen.getByText("chat:task.shareSuccessPublic")).toBeInTheDocument()
})
})
test("auto-hides success message after 5 seconds", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true })
const mockAddEventListener = vi.fn()
Object.defineProperty(window, "addEventListener", {
value: mockAddEventListener,
writable: true,
})
render(<ShareButton item={mockItem} />)
const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1]
// Open popover
const button = screen.getByRole("button")
fireEvent.click(button)
await vi.waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Simulate success message
const mockEvent = {
data: {
type: "shareTaskSuccess",
visibility: "organization",
text: "https://example.com/share/123",
},
}
messageListener(mockEvent)
await vi.waitFor(() => {
expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument()
})
// Fast-forward 5 seconds
await vi.advanceTimersByTimeAsync(5000)
// The success message and share options should both be gone (popover closed)
expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument()
expect(screen.queryByText("chat:task.shareWithOrganization")).not.toBeInTheDocument()
vi.useRealTimers()
})
test("clears previous success state when sharing again", async () => {
vi.useFakeTimers({ shouldAdvanceTime: true })
const mockAddEventListener = vi.fn()
Object.defineProperty(window, "addEventListener", {
value: mockAddEventListener,
writable: true,
})
render(<ShareButton item={mockItem} />)
const messageListener = mockAddEventListener.mock.calls.find((call) => call[0] === "message")?.[1]
// Open popover
const button = screen.getByRole("button")
fireEvent.click(button)
await vi.waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Click organization share button first time
const orgButton = screen.getByText("chat:task.shareWithOrganization")
fireEvent.click(orgButton)
// Verify first share message was sent
expect(mockVscode.postMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "organization",
})
// Clear mock to track new calls
mockVscode.postMessage.mockClear()
// Show success message
const mockEvent = {
data: {
type: "shareTaskSuccess",
visibility: "organization",
text: "https://example.com/share/123",
},
}
messageListener(mockEvent)
await vi.waitFor(() => {
expect(screen.getByText("chat:task.shareSuccessOrganization")).toBeInTheDocument()
})
// Wait for success message to auto-hide after 5 seconds
await vi.advanceTimersByTimeAsync(5000)
// Success message should be gone and popover should be closed
expect(screen.queryByText("chat:task.shareSuccessOrganization")).not.toBeInTheDocument()
// Open popover again
fireEvent.click(button)
await vi.waitFor(() => {
expect(screen.getByText("chat:task.shareWithOrganization")).toBeInTheDocument()
})
// Click share again
const orgButton2 = screen.getByText("chat:task.shareWithOrganization")
fireEvent.click(orgButton2)
// Verify the share message was sent again (no success message should be showing)
expect(mockVscode.postMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "organization",
})
vi.useRealTimers()
})
})

View file

@ -0,0 +1,315 @@
import { render, screen, fireEvent } from "@testing-library/react"
import { vi, describe, it, expect, beforeEach } from "vitest"
import { TaskActions } from "../TaskActions"
import type { HistoryItem } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { useExtensionState } from "@/context/ExtensionStateContext"
// Mock scrollIntoView for JSDOM
Object.defineProperty(Element.prototype, "scrollIntoView", {
value: vi.fn(),
writable: true,
})
// Mock the vscode utility
vi.mock("@/utils/vscode", () => ({
vscode: {
postMessage: vi.fn(),
},
}))
// Mock the useExtensionState hook
vi.mock("@/context/ExtensionStateContext", () => ({
useExtensionState: vi.fn(),
}))
const mockPostMessage = vi.mocked(vscode.postMessage)
const mockUseExtensionState = vi.mocked(useExtensionState)
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"chat:task.share": "Share task",
"chat:task.export": "Export task history",
"chat:task.delete": "Delete Task (Shift + Click to skip confirmation)",
"chat:task.shareWithOrganization": "Share with Organization",
"chat:task.shareWithOrganizationDescription": "Only members of your organization can access",
"chat:task.sharePublicly": "Share Publicly",
"chat:task.sharePubliclyDescription": "Anyone with the link can access",
"chat:task.connectToCloud": "Connect to Cloud",
"chat:task.connectToCloudDescription": "Sign in to Roo Code Cloud to share tasks",
"chat:task.sharingDisabledByOrganization": "Sharing disabled by organization",
"account:cloudBenefitsTitle": "Connect to Roo Code Cloud",
"account:cloudBenefitsSubtitle": "Sign in to Roo Code Cloud to share tasks",
"account:cloudBenefitHistory": "Access your task history from anywhere",
"account:cloudBenefitSharing": "Share tasks with your team",
"account:cloudBenefitMetrics": "Track usage and costs",
"account:connect": "Connect",
}
return translations[key] || key
},
}),
initReactI18next: {
type: "3rdParty",
init: vi.fn(),
},
}))
// Mock pretty-bytes
vi.mock("pretty-bytes", () => ({
default: (bytes: number) => `${bytes} B`,
}))
describe("TaskActions", () => {
const mockItem: HistoryItem = {
id: "test-task-id",
number: 1,
ts: Date.now(),
task: "Test task",
tokensIn: 100,
tokensOut: 200,
totalCost: 0.01,
size: 1024,
}
beforeEach(() => {
vi.clearAllMocks()
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: {
organizationName: "Test Organization",
},
} as any)
})
describe("Share Button Visibility", () => {
it("renders share button when item has id", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
expect(shareButton).toBeInTheDocument()
})
it("does not render share button when item has no id", () => {
render(<TaskActions item={undefined} buttonsDisabled={false} />)
const shareButton = screen.queryByTitle("Share task")
expect(shareButton).not.toBeInTheDocument()
})
it("renders share button even when not authenticated", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: false,
cloudIsAuthenticated: false,
} as any)
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
expect(shareButton).toBeInTheDocument()
})
})
describe("Authenticated User Share Flow", () => {
it("shows organization and public share options when authenticated and sharing enabled", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
expect(screen.getByText("Share with Organization")).toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
})
it("sends shareCurrentTask message when organization option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
const orgOption = screen.getByText("Share with Organization")
fireEvent.click(orgOption)
expect(mockPostMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "organization",
})
})
it("sends shareCurrentTask message when public option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
const publicOption = screen.getByText("Share Publicly")
fireEvent.click(publicOption)
expect(mockPostMessage).toHaveBeenCalledWith({
type: "shareCurrentTask",
visibility: "public",
})
})
it("does not show organization option when user is not in an organization", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: {
// No organizationName property
},
} as any)
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
})
})
describe("Unauthenticated User Login Flow", () => {
beforeEach(() => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: false,
cloudIsAuthenticated: false,
} as any)
})
it("shows connect to cloud option when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument()
expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument()
expect(screen.getByText("Connect")).toBeInTheDocument()
})
it("does not show organization and public options when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument()
})
it("sends rooCloudSignIn message when connect to cloud is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Share task")
fireEvent.click(shareButton)
const connectOption = screen.getByText("Connect")
fireEvent.click(connectOption)
expect(mockPostMessage).toHaveBeenCalledWith({
type: "rooCloudSignIn",
})
})
})
describe("Mixed Authentication States", () => {
it("shows disabled share button when authenticated but sharing not enabled", () => {
mockUseExtensionState.mockReturnValue({
sharingEnabled: false,
cloudIsAuthenticated: true,
} as any)
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const shareButton = screen.getByTitle("Sharing disabled by organization")
expect(shareButton).toBeInTheDocument()
expect(shareButton).toBeDisabled()
// Should not have a popover when sharing is disabled
fireEvent.click(shareButton)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.queryByText("Connect to Cloud")).not.toBeInTheDocument()
})
it("automatically opens popover when user becomes authenticated", () => {
// Start with unauthenticated state
mockUseExtensionState.mockReturnValue({
sharingEnabled: false,
cloudIsAuthenticated: false,
} as any)
const { rerender } = render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Verify popover is not open initially
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
// Simulate user becoming authenticated
mockUseExtensionState.mockReturnValue({
sharingEnabled: true,
cloudIsAuthenticated: true,
cloudUserInfo: {
organizationName: "Test Organization",
},
} as any)
rerender(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Verify popover automatically opens and shows sharing options
expect(screen.getByText("Share with Organization")).toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
})
})
describe("Other Actions", () => {
it("renders export button", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const exportButton = screen.getByTitle("Export task history")
expect(exportButton).toBeInTheDocument()
})
it("sends exportCurrentTask message when export button is clicked", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const exportButton = screen.getByTitle("Export task history")
fireEvent.click(exportButton)
expect(mockPostMessage).toHaveBeenCalledWith({
type: "exportCurrentTask",
})
})
it("renders delete button and file size when item has size", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const deleteButton = screen.getByTitle("Delete Task (Shift + Click to skip confirmation)")
expect(deleteButton).toBeInTheDocument()
expect(screen.getByText("1024 B")).toBeInTheDocument()
})
it("does not render delete button when item has no size", () => {
const itemWithoutSize = { ...mockItem, size: 0 }
render(<TaskActions item={itemWithoutSize} buttonsDisabled={false} />)
const deleteButton = screen.queryByTitle("Delete Task (Shift + Click to skip confirmation)")
expect(deleteButton).not.toBeInTheDocument()
})
})
describe("Button States", () => {
it("disables buttons when buttonsDisabled is true", () => {
render(<TaskActions item={mockItem} buttonsDisabled={true} />)
const shareButton = screen.getByTitle("Share task")
const exportButton = screen.getByTitle("Export task history")
expect(shareButton).toBeDisabled()
expect(exportButton).toBeDisabled()
})
})
})

View file

@ -45,7 +45,7 @@ const TelemetryBanner = () => {
window.postMessage({
type: "action",
action: "settingsButtonClicked",
values: { section: "advanced" }, // Link directly to advanced settings with telemetry controls
values: { section: "about" }, // Link directly to about settings with telemetry controls
})
}
@ -54,7 +54,12 @@ const TelemetryBanner = () => {
<div>
<strong>{t("welcome:telemetry.title")}</strong>
<div className="mt-1">
{t("welcome:telemetry.anonymousTelemetry")}
<Trans
i18nKey="welcome:telemetry.anonymousTelemetry"
components={{
privacyLink: <VSCodeLink href="https://roocode.com/privacy" />,
}}
/>
<div className="mt-1">
<Trans
i18nKey="welcome:telemetry.changeSettings"

View file

@ -48,7 +48,12 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro
{t("settings:footer.telemetry.label")}
</VSCodeCheckbox>
<p className="text-vscode-descriptionForeground text-sm mt-0">
{t("settings:footer.telemetry.description")}
<Trans
i18nKey="settings:footer.telemetry.description"
components={{
privacyLink: <VSCodeLink href="https://roocode.com/privacy" />,
}}
/>
</p>
</div>

View file

@ -1,8 +1,14 @@
{
"title": "Compte",
"profilePicture": "Imatge de perfil",
"unknownUser": "Usuari desconegut",
"logOut": "Tancar sessió",
"testApiAuthentication": "Provar autenticació d'API",
"signIn": "Connecta't a Roo Code Cloud"
"signIn": "Connecta't a Roo Code Cloud",
"connect": "Connecta",
"cloudBenefitsTitle": "Connecta't a Roo Code Cloud",
"cloudBenefitsSubtitle": "Sincronitza els teus prompts i telemetria per habilitar:",
"cloudBenefitHistory": "Historial de tasques en línia",
"cloudBenefitSharing": "Funcions de compartició i col·laboració",
"cloudBenefitMetrics": "Mètriques d'ús basades en tasques, tokens i costos",
"visitCloudWebsite": "Visita Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Compartir amb l'organització",
"shareWithOrganizationDescription": "Només els membres de la teva organització poden accedir",
"sharePublicly": "Compartir públicament",
"sharePubliclyDescription": "Qualsevol amb l'enllaç pot accedir"
"sharePubliclyDescription": "Qualsevol amb l'enllaç pot accedir",
"connectToCloud": "Connecta al núvol",
"connectToCloudDescription": "Inicia sessió a Roo Code Cloud per compartir tasques",
"sharingDisabledByOrganization": "Compartició deshabilitada per l'organització",
"shareSuccessOrganization": "Enllaç d'organització copiat al porta-retalls",
"shareSuccessPublic": "Enllaç públic copiat al porta-retalls"
},
"unpin": "Desfixar",
"pin": "Fixar",

View file

@ -1,8 +1,14 @@
{
"title": "Konto",
"profilePicture": "Profilbild",
"unknownUser": "Unbekannter Benutzer",
"logOut": "Abmelden",
"testApiAuthentication": "API-Authentifizierung testen",
"signIn": "Mit Roo Code Cloud verbinden"
"signIn": "Mit Roo Code Cloud verbinden",
"connect": "Verbinden",
"cloudBenefitsTitle": "Mit Roo Code Cloud verbinden",
"cloudBenefitsSubtitle": "Synchronisiere deine Prompts und Telemetrie, um folgendes zu aktivieren:",
"cloudBenefitHistory": "Online-Aufgabenverlauf",
"cloudBenefitSharing": "Freigabe- und Kollaborationsfunktionen",
"cloudBenefitMetrics": "Aufgaben-, Token- und kostenbasierte Nutzungsmetriken",
"visitCloudWebsite": "Roo Code Cloud besuchen"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Mit Organisation teilen",
"shareWithOrganizationDescription": "Nur Mitglieder deiner Organisation können zugreifen",
"sharePublicly": "Öffentlich teilen",
"sharePubliclyDescription": "Jeder mit dem Link kann zugreifen"
"sharePubliclyDescription": "Jeder mit dem Link kann zugreifen",
"connectToCloud": "Mit Cloud verbinden",
"connectToCloudDescription": "Melde dich bei Roo Code Cloud an, um Aufgaben zu teilen",
"sharingDisabledByOrganization": "Freigabe von der Organisation deaktiviert",
"shareSuccessOrganization": "Organisationslink in die Zwischenablage kopiert",
"shareSuccessPublic": "Öffentlicher Link in die Zwischenablage kopiert"
},
"unpin": "Lösen von oben",
"pin": "Anheften",

View file

@ -1,8 +1,14 @@
{
"title": "Account",
"profilePicture": "Profile picture",
"unknownUser": "Unknown User",
"logOut": "Log out",
"testApiAuthentication": "Test API Authentication",
"signIn": "Connect to Roo Code Cloud"
"signIn": "Connect to Roo Code Cloud",
"connect": "Connect",
"cloudBenefitsTitle": "Connect to Roo Code Cloud",
"cloudBenefitsSubtitle": "Sync your prompts and telemetry to enable:",
"cloudBenefitHistory": "Online task history",
"cloudBenefitSharing": "Sharing and collaboration features",
"cloudBenefitMetrics": "Task, token, and cost-based usage metrics",
"visitCloudWebsite": "Visit Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Share with Organization",
"shareWithOrganizationDescription": "Only members of your organization can access",
"sharePublicly": "Share Publicly",
"sharePubliclyDescription": "Anyone with the link can access"
"sharePubliclyDescription": "Anyone with the link can access",
"connectToCloud": "Connect to Cloud",
"connectToCloudDescription": "Sign in to Roo Code Cloud to share tasks",
"sharingDisabledByOrganization": "Sharing disabled by organization",
"shareSuccessOrganization": "Organization link copied to clipboard",
"shareSuccessPublic": "Public link copied to clipboard"
},
"unpin": "Unpin",
"pin": "Pin",

View file

@ -564,7 +564,7 @@
"feedback": "If you have any questions or feedback, feel free to open an issue at <githubLink>github.com/RooCodeInc/Roo-Code</githubLink> or join <redditLink>reddit.com/r/RooCode</redditLink> or <discordLink>discord.gg/roocode</discordLink>",
"telemetry": {
"label": "Allow anonymous error and usage reporting",
"description": "Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or personal information is ever sent. See our privacy policy for more details."
"description": "Help improve Roo Code by sending anonymous usage data and error reports. No code, prompts, or personal information is ever sent (unless you connect to Roo Code Cloud). See our <privacyLink>privacy policy</privacyLink> for more details."
},
"settings": {
"import": "Import",

View file

@ -17,7 +17,7 @@
"startCustom": "Or you can bring your provider API key:",
"telemetry": {
"title": "Help Improve Roo Code",
"anonymousTelemetry": "Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or personal information is ever sent.",
"anonymousTelemetry": "Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or personal information is ever sent (unless you connect to Roo Code Cloud). See our <privacyLink>privacy policy</privacyLink> for more details.",
"changeSettings": "You can always change this at the bottom of the <settingsLink>settings</settingsLink>",
"settings": "settings",
"allow": "Allow",

View file

@ -1,8 +1,14 @@
{
"title": "Cuenta",
"profilePicture": "Foto de perfil",
"unknownUser": "Usuario desconocido",
"logOut": "Cerrar sesión",
"testApiAuthentication": "Probar autenticación de API",
"signIn": "Conectar a Roo Code Cloud"
"signIn": "Conectar a Roo Code Cloud",
"connect": "Conectar",
"cloudBenefitsTitle": "Conectar a Roo Code Cloud",
"cloudBenefitsSubtitle": "Sincroniza tus prompts y telemetría para habilitar:",
"cloudBenefitHistory": "Historial de tareas en línea",
"cloudBenefitSharing": "Funciones de compartir y colaboración",
"cloudBenefitMetrics": "Métricas de uso basadas en tareas, tokens y costos",
"visitCloudWebsite": "Visitar Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Compartir con organización",
"shareWithOrganizationDescription": "Solo los miembros de tu organización pueden acceder",
"sharePublicly": "Compartir públicamente",
"sharePubliclyDescription": "Cualquiera con el enlace puede acceder"
"sharePubliclyDescription": "Cualquiera con el enlace puede acceder",
"connectToCloud": "Conectar al Cloud",
"connectToCloudDescription": "Inicia sesión en Roo Code Cloud para compartir tareas",
"sharingDisabledByOrganization": "Compartir deshabilitado por la organización",
"shareSuccessOrganization": "Enlace de organización copiado al portapapeles",
"shareSuccessPublic": "Enlace público copiado al portapapeles"
},
"unpin": "Desfijar",
"pin": "Fijar",

View file

@ -1,8 +1,14 @@
{
"title": "Compte",
"profilePicture": "Photo de profil",
"unknownUser": "Utilisateur inconnu",
"logOut": "Déconnexion",
"testApiAuthentication": "Tester l'authentification API",
"signIn": "Se connecter à Roo Code Cloud"
"signIn": "Se connecter à Roo Code Cloud",
"connect": "Se connecter",
"cloudBenefitsTitle": "Se connecter à Roo Code Cloud",
"cloudBenefitsSubtitle": "Synchronise tes prompts et télémétrie pour activer :",
"cloudBenefitHistory": "Historique des tâches en ligne",
"cloudBenefitSharing": "Fonctionnalités de partage et collaboration",
"cloudBenefitMetrics": "Métriques d'utilisation basées sur les tâches, tokens et coûts",
"visitCloudWebsite": "Visiter Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Partager avec l'organisation",
"shareWithOrganizationDescription": "Seuls les membres de ton organisation peuvent accéder",
"sharePublicly": "Partager publiquement",
"sharePubliclyDescription": "Toute personne avec le lien peut accéder"
"sharePubliclyDescription": "Toute personne avec le lien peut accéder",
"connectToCloud": "Se connecter au Cloud",
"connectToCloudDescription": "Connecte-toi à Roo Code Cloud pour partager des tâches",
"sharingDisabledByOrganization": "Partage désactivé par l'organisation",
"shareSuccessOrganization": "Lien d'organisation copié dans le presse-papiers",
"shareSuccessPublic": "Lien public copié dans le presse-papiers"
},
"unpin": "Désépingler",
"pin": "Épingler",

View file

@ -1,8 +1,14 @@
{
"title": "खाता",
"profilePicture": "प्रोफाइल चित्र",
"unknownUser": "अज्ञात उपयोगकर्ता",
"logOut": "लॉग आउट",
"testApiAuthentication": "API प्रमाणीकरण का परीक्षण करें",
"signIn": "Roo Code Cloud से कनेक्ट करें"
"signIn": "Roo Code Cloud से कनेक्ट करें",
"connect": "कनेक्ट करें",
"cloudBenefitsTitle": "Roo Code Cloud से कनेक्ट करें",
"cloudBenefitsSubtitle": "निम्नलिखित को सक्षम करने के लिए अपने prompts और telemetry को sync करें:",
"cloudBenefitHistory": "ऑनलाइन कार्य इतिहास",
"cloudBenefitSharing": "साझाकरण और सहयोग सुविधाएं",
"cloudBenefitMetrics": "कार्य, token और लागत आधारित उपयोग मेट्रिक्स",
"visitCloudWebsite": "Roo Code Cloud पर जाएं"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "संगठन के साथ साझा करें",
"shareWithOrganizationDescription": "केवल आपके संगठन के सदस्य पहुंच सकते हैं",
"sharePublicly": "सार्वजनिक रूप से साझा करें",
"sharePubliclyDescription": "लिंक वाला कोई भी व्यक्ति पहुंच सकता है"
"sharePubliclyDescription": "लिंक वाला कोई भी व्यक्ति पहुंच सकता है",
"connectToCloud": "Cloud से कनेक्ट करें",
"connectToCloudDescription": "कार्य साझा करने के लिए Roo Code Cloud में साइन इन करें",
"sharingDisabledByOrganization": "संगठन द्वारा साझाकरण अक्षम किया गया",
"shareSuccessOrganization": "संगठन लिंक क्लिपबोर्ड में कॉपी किया गया",
"shareSuccessPublic": "सार्वजनिक लिंक क्लिपबोर्ड में कॉपी किया गया"
},
"unpin": "पिन करें",
"pin": "अवपिन करें",

View file

@ -1,8 +1,14 @@
{
"title": "Akun",
"profilePicture": "Foto profil",
"unknownUser": "Pengguna Tidak Dikenal",
"logOut": "Keluar",
"testApiAuthentication": "Uji Autentikasi API",
"signIn": "Hubungkan ke Roo Code Cloud"
"signIn": "Hubungkan ke Roo Code Cloud",
"connect": "Hubungkan",
"cloudBenefitsTitle": "Hubungkan ke Roo Code Cloud",
"cloudBenefitsSubtitle": "Sinkronkan prompt dan telemetri kamu untuk mengaktifkan:",
"cloudBenefitHistory": "Riwayat tugas online",
"cloudBenefitSharing": "Fitur berbagi dan kolaborasi",
"cloudBenefitMetrics": "Metrik penggunaan berdasarkan tugas, token, dan biaya",
"visitCloudWebsite": "Kunjungi Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Bagikan dengan organisasi",
"shareWithOrganizationDescription": "Hanya anggota organisasi Anda yang dapat mengakses",
"sharePublicly": "Bagikan secara publik",
"sharePubliclyDescription": "Siapa pun dengan tautan dapat mengakses"
"sharePubliclyDescription": "Siapa pun dengan tautan dapat mengakses",
"connectToCloud": "Hubungkan ke Cloud",
"connectToCloudDescription": "Masuk ke Roo Code Cloud untuk berbagi tugas",
"sharingDisabledByOrganization": "Berbagi dinonaktifkan oleh organisasi",
"shareSuccessOrganization": "Tautan organisasi disalin ke clipboard",
"shareSuccessPublic": "Tautan publik disalin ke clipboard"
},
"history": {
"title": "Riwayat"

View file

@ -1,8 +1,14 @@
{
"title": "Account",
"profilePicture": "Immagine del profilo",
"unknownUser": "Utente sconosciuto",
"logOut": "Disconnetti",
"testApiAuthentication": "Verifica autenticazione API",
"signIn": "Connetti a Roo Code Cloud"
"signIn": "Connetti a Roo Code Cloud",
"connect": "Connetti",
"cloudBenefitsTitle": "Connetti a Roo Code Cloud",
"cloudBenefitsSubtitle": "Sincronizza i tuoi prompt e telemetria per abilitare:",
"cloudBenefitHistory": "Cronologia attività online",
"cloudBenefitSharing": "Funzionalità di condivisione e collaborazione",
"cloudBenefitMetrics": "Metriche di utilizzo basate su attività, token e costi",
"visitCloudWebsite": "Visita Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Condividi con l'organizzazione",
"shareWithOrganizationDescription": "Solo i membri della tua organizzazione possono accedere",
"sharePublicly": "Condividi pubblicamente",
"sharePubliclyDescription": "Chiunque con il link può accedere"
"sharePubliclyDescription": "Chiunque con il link può accedere",
"connectToCloud": "Connetti al Cloud",
"connectToCloudDescription": "Accedi a Roo Code Cloud per condividere attività",
"sharingDisabledByOrganization": "Condivisione disabilitata dall'organizzazione",
"shareSuccessOrganization": "Link organizzazione copiato negli appunti",
"shareSuccessPublic": "Link pubblico copiato negli appunti"
},
"unpin": "Rilascia",
"pin": "Fissa",

View file

@ -1,8 +1,14 @@
{
"title": "アカウント",
"profilePicture": "プロフィール画像",
"unknownUser": "不明なユーザー",
"logOut": "ログアウト",
"testApiAuthentication": "API認証をテスト",
"signIn": "Roo Code Cloud に接続"
"signIn": "Roo Code Cloud に接続",
"connect": "接続",
"cloudBenefitsTitle": "Roo Code Cloudに接続",
"cloudBenefitsSubtitle": "プロンプトとテレメトリを同期して以下を有効にする:",
"cloudBenefitHistory": "オンラインタスク履歴",
"cloudBenefitSharing": "共有とコラボレーション機能",
"cloudBenefitMetrics": "タスク、Token、コストベースの使用メトリクス",
"visitCloudWebsite": "Roo Code Cloudを訪問"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "組織と共有",
"shareWithOrganizationDescription": "組織のメンバーのみがアクセスできます",
"sharePublicly": "公開で共有",
"sharePubliclyDescription": "リンクを持つ誰でもアクセスできます"
"sharePubliclyDescription": "リンクを持つ誰でもアクセスできます",
"connectToCloud": "クラウドに接続",
"connectToCloudDescription": "タスクを共有するためにRoo Code Cloudにサインイン",
"sharingDisabledByOrganization": "組織により共有が無効化されています",
"shareSuccessOrganization": "組織リンクをクリップボードにコピーしました",
"shareSuccessPublic": "公開リンクをクリップボードにコピーしました"
},
"unpin": "ピン留めを解除",
"pin": "ピン留め",

View file

@ -1,8 +1,14 @@
{
"title": "계정",
"profilePicture": "프로필 사진",
"unknownUser": "알 수 없는 사용자",
"logOut": "로그아웃",
"testApiAuthentication": "API 인증 테스트",
"signIn": "Roo Code Cloud에 연결"
"signIn": "Roo Code Cloud에 연결",
"connect": "연결",
"cloudBenefitsTitle": "Roo Code Cloud에 연결",
"cloudBenefitsSubtitle": "프롬프트와 텔레메트리를 동기화하여 다음을 활성화:",
"cloudBenefitHistory": "온라인 작업 기록",
"cloudBenefitSharing": "공유 및 협업 기능",
"cloudBenefitMetrics": "작업, 토큰, 비용 기반 사용 메트릭",
"visitCloudWebsite": "Roo Code Cloud 방문"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "조직과 공유",
"shareWithOrganizationDescription": "조직 구성원만 액세스할 수 있습니다",
"sharePublicly": "공개적으로 공유",
"sharePubliclyDescription": "링크가 있는 누구나 액세스할 수 있습니다"
"sharePubliclyDescription": "링크가 있는 누구나 액세스할 수 있습니다",
"connectToCloud": "클라우드에 연결",
"connectToCloudDescription": "작업을 공유하려면 Roo Code Cloud에 로그인하세요",
"sharingDisabledByOrganization": "조직에서 공유가 비활성화됨",
"shareSuccessOrganization": "조직 링크가 클립보드에 복사되었습니다",
"shareSuccessPublic": "공개 링크가 클립보드에 복사되었습니다"
},
"unpin": "고정 해제하기",
"pin": "고정하기",

View file

@ -1,8 +1,14 @@
{
"title": "Account",
"profilePicture": "Profielfoto",
"unknownUser": "Onbekende gebruiker",
"logOut": "Uitloggen",
"testApiAuthentication": "API-authenticatie testen",
"signIn": "Verbind met Roo Code Cloud"
"signIn": "Verbind met Roo Code Cloud",
"connect": "Verbinden",
"cloudBenefitsTitle": "Verbind met Roo Code Cloud",
"cloudBenefitsSubtitle": "Synchroniseer je prompts en telemetrie om het volgende in te schakelen:",
"cloudBenefitHistory": "Online taakgeschiedenis",
"cloudBenefitSharing": "Deel- en samenwerkingsfuncties",
"cloudBenefitMetrics": "Taak-, token- en kostengebaseerde gebruiksstatistieken",
"visitCloudWebsite": "Bezoek Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Delen met organisatie",
"shareWithOrganizationDescription": "Alleen leden van je organisatie kunnen toegang krijgen",
"sharePublicly": "Openbaar delen",
"sharePubliclyDescription": "Iedereen met de link kan toegang krijgen"
"sharePubliclyDescription": "Iedereen met de link kan toegang krijgen",
"connectToCloud": "Verbind met Cloud",
"connectToCloudDescription": "Meld je aan bij Roo Code Cloud om taken te delen",
"sharingDisabledByOrganization": "Delen uitgeschakeld door organisatie",
"shareSuccessOrganization": "Organisatielink gekopieerd naar klembord",
"shareSuccessPublic": "Openbare link gekopieerd naar klembord"
},
"unpin": "Losmaken",
"pin": "Vastmaken",

View file

@ -1,8 +1,14 @@
{
"title": "Konto",
"profilePicture": "Zdjęcie profilowe",
"unknownUser": "Nieznany użytkownik",
"logOut": "Wyloguj",
"testApiAuthentication": "Testuj uwierzytelnianie API",
"signIn": "Połącz z Roo Code Cloud"
"signIn": "Połącz z Roo Code Cloud",
"connect": "Połącz",
"cloudBenefitsTitle": "Połącz z Roo Code Cloud",
"cloudBenefitsSubtitle": "Synchronizuj swoje prompty i telemetrię, aby włączyć:",
"cloudBenefitHistory": "Historia zadań online",
"cloudBenefitSharing": "Funkcje udostępniania i współpracy",
"cloudBenefitMetrics": "Metryki użycia oparte na zadaniach, tokenach i kosztach",
"visitCloudWebsite": "Odwiedź Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Udostępnij organizacji",
"shareWithOrganizationDescription": "Tylko członkowie twojej organizacji mogą uzyskać dostęp",
"sharePublicly": "Udostępnij publicznie",
"sharePubliclyDescription": "Każdy z linkiem może uzyskać dostęp"
"sharePubliclyDescription": "Każdy z linkiem może uzyskać dostęp",
"connectToCloud": "Połącz z chmurą",
"connectToCloudDescription": "Zaloguj się do Roo Code Cloud, aby udostępniać zadania",
"sharingDisabledByOrganization": "Udostępnianie wyłączone przez organizację",
"shareSuccessOrganization": "Link organizacji skopiowany do schowka",
"shareSuccessPublic": "Link publiczny skopiowany do schowka"
},
"unpin": "Odepnij",
"pin": "Przypnij",

View file

@ -1,8 +1,14 @@
{
"title": "Conta",
"profilePicture": "Foto de perfil",
"unknownUser": "Usuário desconhecido",
"logOut": "Sair",
"testApiAuthentication": "Testar Autenticação de API",
"signIn": "Conectar ao Roo Code Cloud"
"signIn": "Conectar ao Roo Code Cloud",
"connect": "Conectar",
"cloudBenefitsTitle": "Conectar ao Roo Code Cloud",
"cloudBenefitsSubtitle": "Sincronize seus prompts e telemetria para habilitar:",
"cloudBenefitHistory": "Histórico de tarefas online",
"cloudBenefitSharing": "Recursos de compartilhamento e colaboração",
"cloudBenefitMetrics": "Métricas de uso baseadas em tarefas, tokens e custos",
"visitCloudWebsite": "Visitar Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Compartilhar com organização",
"shareWithOrganizationDescription": "Apenas membros da sua organização podem acessar",
"sharePublicly": "Compartilhar publicamente",
"sharePubliclyDescription": "Qualquer pessoa com o link pode acessar"
"sharePubliclyDescription": "Qualquer pessoa com o link pode acessar",
"connectToCloud": "Conectar ao Cloud",
"connectToCloudDescription": "Entre no Roo Code Cloud para compartilhar tarefas",
"sharingDisabledByOrganization": "Compartilhamento desabilitado pela organização",
"shareSuccessOrganization": "Link da organização copiado para a área de transferência",
"shareSuccessPublic": "Link público copiado para a área de transferência"
},
"unpin": "Desfixar",
"pin": "Fixar",

View file

@ -1,8 +1,14 @@
{
"title": "Учетная запись",
"profilePicture": "Фото профиля",
"unknownUser": "Неизвестный пользователь",
"logOut": "Выход",
"testApiAuthentication": "Проверить аутентификацию API",
"signIn": "Подключиться к Roo Code Cloud"
"signIn": "Подключиться к Roo Code Cloud",
"connect": "Подключиться",
"cloudBenefitsTitle": "Подключиться к Roo Code Cloud",
"cloudBenefitsSubtitle": "Синхронизируй свои промпты и телеметрию, чтобы включить:",
"cloudBenefitHistory": "Онлайн-история задач",
"cloudBenefitSharing": "Функции обмена и совместной работы",
"cloudBenefitMetrics": "Метрики использования на основе задач, токенов и затрат",
"visitCloudWebsite": "Посетить Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Поделиться с организацией",
"shareWithOrganizationDescription": "Только члены вашей организации могут получить доступ",
"sharePublicly": "Поделиться публично",
"sharePubliclyDescription": "Любой, у кого есть ссылка, может получить доступ"
"sharePubliclyDescription": "Любой, у кого есть ссылка, может получить доступ",
"connectToCloud": "Подключиться к облаку",
"connectToCloudDescription": "Войди в Roo Code Cloud, чтобы делиться задачами",
"sharingDisabledByOrganization": "Обмен отключен организацией",
"shareSuccessOrganization": "Ссылка организации скопирована в буфер обмена",
"shareSuccessPublic": "Публичная ссылка скопирована в буфер обмена"
},
"unpin": "Открепить",
"pin": "Закрепить",

View file

@ -1,8 +1,14 @@
{
"title": "Hesap",
"profilePicture": "Profil resmi",
"unknownUser": "Bilinmeyen kullanıcı",
"logOut": ıkış yap",
"testApiAuthentication": "API Kimlik Doğrulamayı Test Et",
"signIn": "Roo Code Cloud'a bağlan"
"signIn": "Roo Code Cloud'a bağlan",
"connect": "Bağlan",
"cloudBenefitsTitle": "Roo Code Cloud'a bağlan",
"cloudBenefitsSubtitle": "Aşağıdakileri etkinleştirmek için promptlarını ve telemetriyi senkronize et:",
"cloudBenefitHistory": "Çevrimiçi görev geçmişi",
"cloudBenefitSharing": "Paylaşım ve işbirliği özellikleri",
"cloudBenefitMetrics": "Görev, token ve maliyet tabanlı kullanım metrikleri",
"visitCloudWebsite": "Roo Code Cloud'u ziyaret et"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Kuruluşla paylaş",
"shareWithOrganizationDescription": "Sadece kuruluşunuzun üyeleri erişebilir",
"sharePublicly": "Herkese açık paylaş",
"sharePubliclyDescription": "Bağlantıya sahip herkes erişebilir"
"sharePubliclyDescription": "Bağlantıya sahip herkes erişebilir",
"connectToCloud": "Buluta bağlan",
"connectToCloudDescription": "Görevleri paylaşmak için Roo Code Cloud'a giriş yap",
"sharingDisabledByOrganization": "Paylaşım kuruluş tarafından devre dışı bırakıldı",
"shareSuccessOrganization": "Organizasyon bağlantısı panoya kopyalandı",
"shareSuccessPublic": "Genel bağlantı panoya kopyalandı"
},
"unpin": "Sabitlemeyi iptal et",
"pin": "Sabitle",

View file

@ -1,8 +1,14 @@
{
"title": "Tài khoản",
"profilePicture": "Ảnh hồ sơ",
"unknownUser": "Người dùng không xác định",
"logOut": "Đăng xuất",
"testApiAuthentication": "Kiểm tra xác thực API",
"signIn": "Kết nối với Roo Code Cloud"
"signIn": "Kết nối với Roo Code Cloud",
"connect": "Kết nối",
"cloudBenefitsTitle": "Kết nối với Roo Code Cloud",
"cloudBenefitsSubtitle": "Đồng bộ prompts và telemetry của bạn để kích hoạt:",
"cloudBenefitHistory": "Lịch sử tác vụ trực tuyến",
"cloudBenefitSharing": "Tính năng chia sẻ và cộng tác",
"cloudBenefitMetrics": "Số liệu sử dụng dựa trên tác vụ, token và chi phí",
"visitCloudWebsite": "Truy cập Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "Chia sẻ với tổ chức",
"shareWithOrganizationDescription": "Chỉ thành viên tổ chức của bạn mới có thể truy cập",
"sharePublicly": "Chia sẻ công khai",
"sharePubliclyDescription": "Bất kỳ ai có liên kết đều có thể truy cập"
"sharePubliclyDescription": "Bất kỳ ai có liên kết đều có thể truy cập",
"connectToCloud": "Kết nối với Cloud",
"connectToCloudDescription": "Đăng nhập vào Roo Code Cloud để chia sẻ tác vụ",
"sharingDisabledByOrganization": "Chia sẻ bị tổ chức vô hiệu hóa",
"shareSuccessOrganization": "Liên kết tổ chức đã được sao chép vào clipboard",
"shareSuccessPublic": "Liên kết công khai đã được sao chép vào clipboard"
},
"unpin": "Bỏ ghim khỏi đầu",
"pin": "Ghim lên đầu",

View file

@ -1,8 +1,14 @@
{
"title": "账户",
"profilePicture": "头像",
"unknownUser": "未知用户",
"logOut": "退出登录",
"testApiAuthentication": "测试 API 认证",
"signIn": "连接到 Roo Code Cloud"
"signIn": "连接到 Roo Code Cloud",
"connect": "连接",
"cloudBenefitsTitle": "连接到 Roo Code Cloud",
"cloudBenefitsSubtitle": "同步你的提示词和遥测数据以启用:",
"cloudBenefitHistory": "在线任务历史",
"cloudBenefitSharing": "共享和协作功能",
"cloudBenefitMetrics": "基于任务、Token 和成本的使用指标",
"visitCloudWebsite": "访问 Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "与组织分享",
"shareWithOrganizationDescription": "仅组织成员可访问",
"sharePublicly": "公开分享",
"sharePubliclyDescription": "任何拥有链接的人都可访问"
"sharePubliclyDescription": "任何拥有链接的人都可访问",
"connectToCloud": "连接到云端",
"connectToCloudDescription": "登录 Roo Code Cloud 以分享任务",
"sharingDisabledByOrganization": "组织已禁用分享功能",
"shareSuccessOrganization": "组织链接已复制到剪贴板",
"shareSuccessPublic": "公开链接已复制到剪贴板"
},
"unpin": "取消置顶",
"pin": "置顶",

View file

@ -1,8 +1,14 @@
{
"title": "帳戶",
"profilePicture": "個人圖片",
"unknownUser": "未知使用者",
"logOut": "登出",
"testApiAuthentication": "測試 API 認證",
"signIn": "連接到 Roo Code Cloud"
"signIn": "連接到 Roo Code Cloud",
"connect": "連接",
"cloudBenefitsTitle": "連接到 Roo Code Cloud",
"cloudBenefitsSubtitle": "同步你的提示詞和遙測資料以啟用:",
"cloudBenefitHistory": "線上工作歷史",
"cloudBenefitSharing": "分享和協作功能",
"cloudBenefitMetrics": "基於工作、Token 和成本的使用指標",
"visitCloudWebsite": "造訪 Roo Code Cloud"
}

View file

@ -16,7 +16,12 @@
"shareWithOrganization": "與組織分享",
"shareWithOrganizationDescription": "僅組織成員可存取",
"sharePublicly": "公開分享",
"sharePubliclyDescription": "任何擁有連結的人都可存取"
"sharePubliclyDescription": "任何擁有連結的人都可存取",
"connectToCloud": "連接到雲端",
"connectToCloudDescription": "登入 Roo Code Cloud 以分享工作",
"sharingDisabledByOrganization": "組織已停用分享功能",
"shareSuccessOrganization": "組織連結已複製到剪貼簿",
"shareSuccessPublic": "公開連結已複製到剪貼簿"
},
"unpin": "取消置頂",
"pin": "置頂",

View file

@ -42,6 +42,9 @@ Object.defineProperty(window, "matchMedia", {
})),
})
// Mock scrollIntoView which is not available in jsdom
Element.prototype.scrollIntoView = vi.fn()
// Suppress console.log during tests to reduce noise.
// Keep console.error for actual errors.
const originalConsoleLog = console.log