Roo Code Cloud (#4069)

Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
This commit is contained in:
Chris Estreich 2025-05-28 20:43:30 -07:00 committed by GitHub
parent 4cf240f3db
commit 7820b7517a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
174 changed files with 4549 additions and 1237 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Roo Code Cloud

View file

@ -1 +1,5 @@
POSTHOG_API_KEY=key-goes-here
# Roo Code Cloud / Local Development
CLERK_BASE_URL=https://epic-chamois-85.clerk.accounts.dev
ROO_CODE_API_URL=http://localhost:3000

View file

@ -133,10 +133,3 @@ jobs:
- name: Run integration tests
working-directory: apps/vscode-e2e
run: xvfb-run -a pnpm test:ci
qa:
needs: [check-translations, knip, compile, platform-unit-test, integration-test]
runs-on: ubuntu-latest
steps:
- name: NO-OP
run: echo "All tests passed."

View file

@ -1,4 +1,4 @@
name: "CodeQL Advanced"
name: CodeQL Advanced
on:
push:

View file

@ -1,4 +1,5 @@
name: Publish Extension
on:
pull_request:
types: [closed]

View file

@ -1,8 +1,6 @@
name: Nightly Publish
on:
# push:
# branches: [main]
workflow_run:
workflows: ["Code QA Roo Code"]
types:

View file

@ -0,0 +1,4 @@
import { config } from "@roo-code/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [...config]

View file

@ -0,0 +1,25 @@
{
"name": "@roo-code/cloud",
"description": "Roo Code Cloud VSCode integration.",
"private": true,
"type": "module",
"exports": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "vitest --globals --run",
"clean": "rimraf dist .turbo"
},
"dependencies": {
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"axios": "^1.7.4"
},
"devDependencies": {
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@types/node": "^22.15.20",
"@types/vscode": "^1.84.0",
"vitest": "^3.1.3"
}
}

View file

@ -0,0 +1,395 @@
import crypto from "crypto"
import EventEmitter from "events"
import axios from "axios"
import * as vscode from "vscode"
import type { CloudUserInfo } from "@roo-code/types"
import { CloudServiceCallbacks } from "./types"
import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
import { RefreshTimer } from "./RefreshTimer"
export interface AuthServiceEvents {
"active-session": [data: { previousState: AuthState }]
"logged-out": [data: { previousState: AuthState }]
}
const CLIENT_TOKEN_KEY = "clerk-client-token"
const SESSION_ID_KEY = "clerk-session-id"
const AUTH_STATE_KEY = "clerk-auth-state"
type AuthState = "initializing" | "logged-out" | "active-session" | "inactive-session"
export class AuthService extends EventEmitter<AuthServiceEvents> {
private context: vscode.ExtensionContext
private userChanged: CloudServiceCallbacks["userChanged"]
private timer: RefreshTimer
private state: AuthState = "initializing"
private clientToken: string | null = null
private sessionToken: string | null = null
private sessionId: string | null = null
constructor(context: vscode.ExtensionContext, userChanged: CloudServiceCallbacks["userChanged"]) {
super()
this.context = context
this.userChanged = userChanged
this.timer = new RefreshTimer({
callback: async () => {
await this.refreshSession()
return true
},
successInterval: 50_000,
initialBackoffMs: 1_000,
maxBackoffMs: 300_000,
})
}
/**
* Initialize the auth state
*
* This method loads tokens from storage and determines the current auth state.
* It also starts the refresh timer if we have an active session.
*/
public async initialize(): Promise<void> {
if (this.state !== "initializing") {
console.log("[auth] initialize() called after already initialized")
return
}
try {
this.clientToken = (await this.context.secrets.get(CLIENT_TOKEN_KEY)) || null
this.sessionId = this.context.globalState.get<string>(SESSION_ID_KEY) || null
// Determine initial state.
if (!this.clientToken || !this.sessionId) {
// TODO: it may be possible to get a new session with the client,
// but the obvious Clerk endpoints don't support that.
const previousState = this.state
this.state = "logged-out"
this.emit("logged-out", { previousState })
} else {
this.state = "inactive-session"
this.timer.start()
}
console.log(`[auth] Initialized with state: ${this.state}`)
} catch (error) {
console.error(`[auth] Error initializing AuthService: ${error}`)
this.state = "logged-out"
}
}
/**
* Start the login process
*
* This method initiates the authentication flow by generating a state parameter
* and opening the browser to the authorization URL.
*/
public async login(): Promise<void> {
try {
// Generate a cryptographically random state parameter.
const state = crypto.randomBytes(16).toString("hex")
await this.context.globalState.update(AUTH_STATE_KEY, state)
const uri = vscode.Uri.parse(`${getRooCodeApiUrl()}/extension/sign-in?state=${state}`)
await vscode.env.openExternal(uri)
} catch (error) {
console.error(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`)
}
}
/**
* Handle the callback from Roo Code Cloud
*
* This method is called when the user is redirected back to the extension
* after authenticating with Roo Code Cloud.
*
* @param code The authorization code from the callback
* @param state The state parameter from the callback
*/
public async handleCallback(code: string | null, state: string | null): Promise<void> {
if (!code || !state) {
vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url")
return
}
try {
// Validate state parameter to prevent CSRF attacks.
const storedState = this.context.globalState.get(AUTH_STATE_KEY)
if (state !== storedState) {
console.log("[auth] State mismatch in callback")
throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
}
const { clientToken, sessionToken, sessionId } = await this.clerkSignIn(code)
await this.context.secrets.store(CLIENT_TOKEN_KEY, clientToken)
await this.context.globalState.update(SESSION_ID_KEY, sessionId)
this.clientToken = clientToken
this.sessionId = sessionId
this.sessionToken = sessionToken
const previousState = this.state
this.state = "active-session"
this.emit("active-session", { previousState })
this.timer.start()
if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
console.log("[auth] Successfully authenticated with Roo Code Cloud")
} catch (error) {
console.log(`[auth] Error handling Roo Code Cloud callback: ${error}`)
const previousState = this.state
this.state = "logged-out"
this.emit("logged-out", { previousState })
throw new Error(`Failed to handle Roo Code Cloud callback: ${error}`)
}
}
/**
* Log out
*
* This method removes all stored tokens and stops the refresh timer.
*/
public async logout(): Promise<void> {
try {
this.timer.stop()
await this.context.secrets.delete(CLIENT_TOKEN_KEY)
await this.context.globalState.update(SESSION_ID_KEY, undefined)
await this.context.globalState.update(AUTH_STATE_KEY, undefined)
const oldClientToken = this.clientToken
const oldSessionId = this.sessionId
this.clientToken = null
this.sessionToken = null
this.sessionId = null
const previousState = this.state
this.state = "logged-out"
this.emit("logged-out", { previousState })
if (oldClientToken && oldSessionId) {
await this.clerkLogout(oldClientToken, oldSessionId)
}
if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
vscode.window.showInformationMessage("Logged out from Roo Code Cloud")
console.log("[auth] Logged out from Roo Code Cloud")
} catch (error) {
console.log(`[auth] Error logging out from Roo Code Cloud: ${error}`)
throw new Error(`Failed to log out from Roo Code Cloud: ${error}`)
}
}
public getState(): AuthState {
return this.state
}
public getSessionToken(): string | undefined {
if (this.state === "active-session" && this.sessionToken) {
return this.sessionToken
}
return
}
/**
* Check if the user is authenticated
*
* @returns True if the user is authenticated (has an active or inactive session)
*/
public isAuthenticated(): boolean {
return this.state === "active-session" || this.state === "inactive-session"
}
public hasActiveSession(): boolean {
return this.state === "active-session"
}
/**
* Refresh the session
*
* This method refreshes the session token using the client token.
*/
private async refreshSession() {
if (!this.sessionId || !this.clientToken) {
console.log("[auth] Cannot refresh session: missing session ID or token")
this.state = "inactive-session"
return
}
const previousState = this.state
this.sessionToken = await this.clerkCreateSessionToken()
this.state = "active-session"
if (previousState !== "active-session") {
this.emit("active-session", { previousState })
if (this.userChanged) {
this.getUserInfo().then(this.userChanged)
}
}
}
/**
* Extract user information from the ID token
*
* @returns User information from ID token claims or null if no ID token available
*/
public async getUserInfo(): Promise<CloudUserInfo | undefined> {
if (!this.clientToken) {
return undefined
}
return await this.clerkMe()
}
private async clerkSignIn(
ticket: string,
): Promise<{ clientToken: string; sessionToken: string; sessionId: string }> {
const formData = new URLSearchParams()
formData.append("strategy", "ticket")
formData.append("ticket", ticket)
const response = await axios.post(`${getClerkBaseUrl()}/v1/client/sign_ins`, formData, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": this.userAgent(),
},
})
// 3. Extract the client token from the Authorization header.
const clientToken = response.headers.authorization
if (!clientToken) {
throw new Error("No authorization header found in the response")
}
// 4. Find the session using created_session_id and extract the JWT.
const createdSessionId = response.data?.response?.created_session_id
if (!createdSessionId) {
throw new Error("No session ID found in the response")
}
// Find the session in the client sessions array.
const session = response.data?.client?.sessions?.find((s: { id: string }) => s.id === createdSessionId)
if (!session) {
throw new Error("Session not found in the response")
}
// Extract the session token (JWT) and store it.
const sessionToken = session.last_active_token?.jwt
if (!sessionToken) {
throw new Error("Session does not have a token")
}
return { clientToken, sessionToken, sessionId: session.id }
}
private async clerkCreateSessionToken(): Promise<string> {
const formData = new URLSearchParams()
formData.append("_is_native", "1")
const response = await axios.post(
`${getClerkBaseUrl()}/v1/client/sessions/${this.sessionId}/tokens`,
formData,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Bearer ${this.clientToken}`,
"User-Agent": this.userAgent(),
},
},
)
const sessionToken = response.data?.jwt
if (!sessionToken) {
throw new Error("No JWT found in refresh response")
}
return sessionToken
}
private async clerkMe(): Promise<CloudUserInfo> {
const response = await axios.get(`${getClerkBaseUrl()}/v1/me`, {
headers: {
Authorization: `Bearer ${this.clientToken}`,
"User-Agent": this.userAgent(),
},
})
const userData = response.data?.response
if (!userData) {
throw new Error("No response user data")
}
const userInfo: CloudUserInfo = {}
userInfo.name = `${userData?.first_name} ${userData?.last_name}`
const primaryEmailAddressId = userData?.primary_email_address_id
const emailAddresses = userData?.email_addresses
if (primaryEmailAddressId && emailAddresses) {
userInfo.email = emailAddresses.find(
(email: { id: string }) => primaryEmailAddressId === email?.id,
)?.email_address
}
userInfo.picture = userData?.image_url
return userInfo
}
private async clerkLogout(clientToken: string, sessionId: string): Promise<void> {
const formData = new URLSearchParams()
formData.append("_is_native", "1")
await axios.post(`${getClerkBaseUrl()}/v1/client/sessions/${sessionId}/remove`, formData, {
headers: {
Authorization: `Bearer ${clientToken}`,
"User-Agent": this.userAgent(),
},
})
}
private userAgent(): string {
return `Roo-Code ${this.context.extension?.packageJSON?.version}`
}
private static _instance: AuthService | null = null
static get instance() {
if (!this._instance) {
throw new Error("AuthService not initialized")
}
return this._instance
}
static async createInstance(context: vscode.ExtensionContext, userChanged: CloudServiceCallbacks["userChanged"]) {
if (this._instance) {
throw new Error("AuthService instance already created")
}
this._instance = new AuthService(context, userChanged)
await this._instance.initialize()
return this._instance
}
}

View file

@ -0,0 +1,157 @@
import * as vscode from "vscode"
import type { CloudUserInfo, TelemetryEvent, OrganizationAllowList } 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"
export class CloudService {
private static _instance: CloudService | null = null
private context: vscode.ExtensionContext
private callbacks: CloudServiceCallbacks
private authService: AuthService | null = null
private settingsService: SettingsService | null = null
private telemetryClient: TelemetryClient | null = null
private isInitialized = false
private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) {
this.context = context
this.callbacks = callbacks
}
public async initialize(): Promise<void> {
if (this.isInitialized) {
return
}
try {
this.authService = await AuthService.createInstance(this.context, (userInfo) => {
this.callbacks.userChanged?.(userInfo)
})
this.settingsService = await SettingsService.createInstance(this.context, () =>
this.callbacks.settingsChanged?.(),
)
this.telemetryClient = new TelemetryClient(this.authService)
try {
TelemetryService.instance.register(this.telemetryClient)
} catch (error) {
console.warn("[CloudService] Failed to register TelemetryClient:", error)
}
this.isInitialized = true
} catch (error) {
console.error("[CloudService] Failed to initialize:", error)
throw new Error(`Failed to initialize CloudService: ${error}`)
}
}
// AuthService
public async login(): Promise<void> {
this.ensureInitialized()
return this.authService!.login()
}
public async logout(): Promise<void> {
this.ensureInitialized()
return this.authService!.logout()
}
public isAuthenticated(): boolean {
this.ensureInitialized()
return this.authService!.isAuthenticated()
}
public hasActiveSession(): boolean {
this.ensureInitialized()
return this.authService!.hasActiveSession()
}
public async getUserInfo(): Promise<CloudUserInfo | undefined> {
this.ensureInitialized()
return this.authService!.getUserInfo()
}
public getAuthState(): string {
this.ensureInitialized()
return this.authService!.getState()
}
public async handleAuthCallback(code: string | null, state: string | null): Promise<void> {
this.ensureInitialized()
return this.authService!.handleCallback(code, state)
}
// SettingsService
public getAllowList(): OrganizationAllowList {
this.ensureInitialized()
return this.settingsService!.getAllowList()
}
// TelemetryClient
public captureEvent(event: TelemetryEvent): void {
this.ensureInitialized()
this.telemetryClient!.capture(event)
}
// Lifecycle
public dispose(): void {
if (this.settingsService) {
this.settingsService.dispose()
}
this.isInitialized = false
}
private ensureInitialized(): void {
if (!this.isInitialized || !this.authService || !this.settingsService || !this.telemetryClient) {
throw new Error("CloudService not initialized.")
}
}
static get instance(): CloudService {
if (!this._instance) {
throw new Error("CloudService not initialized")
}
return this._instance
}
static async createInstance(
context: vscode.ExtensionContext,
callbacks: CloudServiceCallbacks = {},
): Promise<CloudService> {
if (this._instance) {
throw new Error("CloudService instance already created")
}
this._instance = new CloudService(context, callbacks)
await this._instance.initialize()
return this._instance
}
static hasInstance(): boolean {
return this._instance !== null && this._instance.isInitialized
}
static resetInstance(): void {
if (this._instance) {
this._instance.dispose()
this._instance = null
}
}
static isEnabled(): boolean {
return !!this._instance?.isAuthenticated()
}
}

View file

@ -0,0 +1,2 @@
export const getClerkBaseUrl = () => process.env.CLERK_BASE_URL || "https://clerk.roocode.com"
export const getRooCodeApiUrl = () => process.env.ROO_CODE_API_URL || "https://app.roocode.com"

View file

@ -146,7 +146,7 @@ export class RefreshTimer {
const result = await this.callback()
this.scheduleNextAttempt(result)
} catch (error) {
} catch (_error) {
// Treat errors as failed attempts
this.scheduleNextAttempt(false)
}

View file

@ -0,0 +1,87 @@
import { TelemetryEventName, type TelemetryEvent, rooCodeTelemetryEventSchema } from "@roo-code/types"
import { BaseTelemetryClient } from "@roo-code/telemetry"
import { getRooCodeApiUrl } from "./Config"
import { AuthService } from "./AuthService"
export class RooCodeTelemetryClient extends BaseTelemetryClient {
constructor(
private authService: AuthService,
debug = false,
) {
super(
{
type: "exclude",
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
},
debug,
)
}
private async fetch(path: string, options: RequestInit) {
if (!this.authService.isAuthenticated()) {
return
}
const token = this.authService.getSessionToken()
if (!token) {
console.error(`[RooCodeTelemetryClient#fetch] Unauthorized: No session token available.`)
return
}
const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
...options,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
})
if (!response.ok) {
console.error(
`[RooCodeTelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
)
}
}
public override async capture(event: TelemetryEvent) {
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
if (this.debug) {
console.info(`[RooCodeTelemetryClient#capture] Skipping event: ${event.event}`)
}
return
}
const payload = {
type: event.event,
properties: await this.getEventProperties(event),
}
if (this.debug) {
console.info(`[RooCodeTelemetryClient#capture] ${JSON.stringify(payload)}`)
}
const result = rooCodeTelemetryEventSchema.safeParse(payload)
if (!result.success) {
console.error(
`[RooCodeTelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
)
return
}
try {
await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
} catch (error) {
console.error(`[RooCodeTelemetryClient#capture] Error sending telemetry event: ${error}`)
}
}
public override updateTelemetryState(_didUserOptIn: boolean) {}
public override isTelemetryEnabled(): boolean {
return true
}
public override async shutdown() {}
}

View file

@ -0,0 +1,137 @@
import * as vscode from "vscode"
import {
ORGANIZATION_ALLOW_ALL,
OrganizationAllowList,
OrganizationSettings,
organizationSettingsSchema,
} from "@roo-code/types"
import { getRooCodeApiUrl } from "./Config"
import { AuthService } from "./AuthService"
import { RefreshTimer } from "./RefreshTimer"
const ORGANIZATION_SETTINGS_CACHE_KEY = "organization-settings"
export class SettingsService {
private static _instance: SettingsService | null = null
private context: vscode.ExtensionContext
private authService: AuthService
private settings: OrganizationSettings | undefined = undefined
private timer: RefreshTimer
private constructor(context: vscode.ExtensionContext, authService: AuthService, callback: () => void) {
this.context = context
this.authService = authService
this.timer = new RefreshTimer({
callback: async () => {
await this.fetchSettings(callback)
return true
},
successInterval: 30000,
initialBackoffMs: 1000,
maxBackoffMs: 30000,
})
}
public initialize(): void {
this.loadCachedSettings()
this.authService.on("active-session", () => {
this.timer.start()
})
this.authService.on("logged-out", () => {
this.timer.stop()
this.removeSettings()
})
if (this.authService.hasActiveSession()) {
this.timer.start()
}
}
private async fetchSettings(callback: () => void): Promise<void> {
const token = this.authService.getSessionToken()
if (!token) {
return
}
try {
const response = await fetch(`${getRooCodeApiUrl()}/api/organization-settings`, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (!response.ok) {
console.error(`Failed to fetch organization settings: ${response.status} ${response.statusText}`)
return
}
const data = await response.json()
const result = organizationSettingsSchema.safeParse(data)
if (!result.success) {
console.error("Invalid organization settings format:", result.error)
return
}
const newSettings = result.data
if (!this.settings || this.settings.version !== newSettings.version) {
this.settings = newSettings
await this.cacheSettings()
callback()
}
} catch (error) {
console.error("Error fetching organization settings:", error)
}
}
private async cacheSettings(): Promise<void> {
await this.context.globalState.update(ORGANIZATION_SETTINGS_CACHE_KEY, this.settings)
}
private loadCachedSettings(): void {
this.settings = this.context.globalState.get<OrganizationSettings>(ORGANIZATION_SETTINGS_CACHE_KEY)
}
public getAllowList(): OrganizationAllowList {
return this.settings?.allowList || ORGANIZATION_ALLOW_ALL
}
public getSettings(): OrganizationSettings | undefined {
return this.settings
}
public async removeSettings(): Promise<void> {
this.settings = undefined
await this.cacheSettings()
}
public dispose(): void {
this.timer.stop()
}
static get instance() {
if (!this._instance) {
throw new Error("SettingsService not initialized")
}
return this._instance
}
static async createInstance(context: vscode.ExtensionContext, callback: () => void) {
if (this._instance) {
throw new Error("SettingsService instance already created")
}
this._instance = new SettingsService(context, AuthService.instance, callback)
this._instance.initialize()
return this._instance
}
}

View file

@ -0,0 +1,87 @@
import { TelemetryEventName, type TelemetryEvent, rooCodeTelemetryEventSchema } from "@roo-code/types"
import { BaseTelemetryClient } from "@roo-code/telemetry"
import { getRooCodeApiUrl } from "./Config"
import { AuthService } from "./AuthService"
export class TelemetryClient extends BaseTelemetryClient {
constructor(
private authService: AuthService,
debug = false,
) {
super(
{
type: "exclude",
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
},
debug,
)
}
private async fetch(path: string, options: RequestInit) {
if (!this.authService.isAuthenticated()) {
return
}
const token = this.authService.getSessionToken()
if (!token) {
console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`)
return
}
const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
...options,
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
})
if (!response.ok) {
console.error(
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
)
}
}
public override async capture(event: TelemetryEvent) {
if (!this.isTelemetryEnabled() || !this.isEventCapturable(event.event)) {
if (this.debug) {
console.info(`[TelemetryClient#capture] Skipping event: ${event.event}`)
}
return
}
const payload = {
type: event.event,
properties: await this.getEventProperties(event),
}
if (this.debug) {
console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`)
}
const result = rooCodeTelemetryEventSchema.safeParse(payload)
if (!result.success) {
console.error(
`[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
)
return
}
try {
await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
} catch (error) {
console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
}
}
public override updateTelemetryState(_didUserOptIn: boolean) {}
public override isTelemetryEnabled(): boolean {
return true
}
public override async shutdown() {}
}

View file

@ -0,0 +1,50 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { vi } from "vitest"
export const window = {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
}
export const env = {
openExternal: vi.fn(),
}
export const Uri = {
parse: vi.fn((uri: string) => ({ toString: () => uri })),
}
export interface ExtensionContext {
secrets: {
get: (key: string) => Promise<string | undefined>
store: (key: string, value: string) => Promise<void>
delete: (key: string) => Promise<void>
}
globalState: {
get: <T>(key: string) => T | undefined
update: (key: string, value: any) => Promise<void>
}
extension?: {
packageJSON?: {
version?: string
}
}
}
// Mock implementation for tests
export const mockExtensionContext: ExtensionContext = {
secrets: {
get: vi.fn().mockResolvedValue(undefined),
store: vi.fn().mockResolvedValue(undefined),
delete: vi.fn().mockResolvedValue(undefined),
},
globalState: {
get: vi.fn().mockReturnValue(undefined),
update: vi.fn().mockResolvedValue(undefined),
},
extension: {
packageJSON: {
version: "1.0.0",
},
},
}

View file

@ -0,0 +1,238 @@
// npx vitest run src/__tests__/CloudService.test.ts
import * as vscode from "vscode"
import { CloudService } from "../CloudService"
import { AuthService } from "../AuthService"
import { SettingsService } from "../SettingsService"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudServiceCallbacks } from "../types"
vi.mock("vscode", () => ({
ExtensionContext: vi.fn(),
window: {
showInformationMessage: vi.fn(),
showErrorMessage: vi.fn(),
},
env: {
openExternal: vi.fn(),
},
Uri: {
parse: vi.fn(),
},
}))
vi.mock("@roo-code/telemetry")
vi.mock("../AuthService")
vi.mock("../SettingsService")
describe("CloudService", () => {
let mockContext: vscode.ExtensionContext
let mockAuthService: {
initialize: ReturnType<typeof vi.fn>
login: ReturnType<typeof vi.fn>
logout: ReturnType<typeof vi.fn>
isAuthenticated: ReturnType<typeof vi.fn>
hasActiveSession: ReturnType<typeof vi.fn>
getUserInfo: ReturnType<typeof vi.fn>
getState: ReturnType<typeof vi.fn>
getSessionToken: ReturnType<typeof vi.fn>
handleCallback: ReturnType<typeof vi.fn>
on: ReturnType<typeof vi.fn>
off: ReturnType<typeof vi.fn>
once: ReturnType<typeof vi.fn>
emit: ReturnType<typeof vi.fn>
}
let mockSettingsService: {
initialize: ReturnType<typeof vi.fn>
getSettings: ReturnType<typeof vi.fn>
getAllowList: ReturnType<typeof vi.fn>
dispose: ReturnType<typeof vi.fn>
}
let mockTelemetryService: {
hasInstance: ReturnType<typeof vi.fn>
instance: {
register: ReturnType<typeof vi.fn>
}
}
beforeEach(() => {
CloudService.resetInstance()
mockContext = {
secrets: {
get: vi.fn(),
store: vi.fn(),
delete: vi.fn(),
},
globalState: {
get: vi.fn(),
update: vi.fn(),
},
extension: {
packageJSON: {
version: "1.0.0",
},
},
} as unknown as vscode.ExtensionContext
mockAuthService = {
initialize: vi.fn(),
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: vi.fn().mockReturnValue(false),
hasActiveSession: vi.fn().mockReturnValue(false),
getUserInfo: vi.fn(),
getState: vi.fn().mockReturnValue("logged-out"),
getSessionToken: vi.fn(),
handleCallback: vi.fn(),
on: vi.fn(),
off: vi.fn(),
once: vi.fn(),
emit: vi.fn(),
}
mockSettingsService = {
initialize: vi.fn(),
getSettings: vi.fn(),
getAllowList: vi.fn(),
dispose: vi.fn(),
}
mockTelemetryService = {
hasInstance: vi.fn().mockReturnValue(true),
instance: {
register: vi.fn(),
},
}
vi.mocked(AuthService.createInstance).mockResolvedValue(mockAuthService as unknown as AuthService)
Object.defineProperty(AuthService, "instance", { get: () => mockAuthService, configurable: true })
vi.mocked(SettingsService.createInstance).mockResolvedValue(mockSettingsService as unknown as SettingsService)
Object.defineProperty(SettingsService, "instance", { get: () => mockSettingsService, configurable: true })
vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
Object.defineProperty(TelemetryService, "instance", {
get: () => mockTelemetryService.instance,
configurable: true,
})
})
afterEach(() => {
vi.clearAllMocks()
CloudService.resetInstance()
})
describe("createInstance", () => {
it("should create and initialize CloudService instance", async () => {
const callbacks = { userChanged: vi.fn(), settingsChanged: vi.fn() }
const cloudService = await CloudService.createInstance(mockContext, callbacks)
expect(cloudService).toBeInstanceOf(CloudService)
expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
expect(SettingsService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
})
it("should throw error if instance already exists", async () => {
await CloudService.createInstance(mockContext)
await expect(CloudService.createInstance(mockContext)).rejects.toThrow(
"CloudService instance already created",
)
})
})
describe("authentication methods", () => {
let cloudService: CloudService
let callbacks: CloudServiceCallbacks
beforeEach(async () => {
callbacks = { userChanged: vi.fn(), settingsChanged: vi.fn() }
cloudService = await CloudService.createInstance(mockContext, callbacks)
})
it("should delegate login to AuthService", async () => {
await cloudService.login()
expect(mockAuthService.login).toHaveBeenCalled()
})
it("should delegate logout to AuthService", async () => {
await cloudService.logout()
expect(mockAuthService.logout).toHaveBeenCalled()
})
it("should delegate isAuthenticated to AuthService", () => {
const result = cloudService.isAuthenticated()
expect(mockAuthService.isAuthenticated).toHaveBeenCalled()
expect(result).toBe(false)
})
it("should delegate hasActiveSession to AuthService", () => {
const result = cloudService.hasActiveSession()
expect(mockAuthService.hasActiveSession).toHaveBeenCalled()
expect(result).toBe(false)
})
it("should delegate getUserInfo to AuthService", async () => {
await cloudService.getUserInfo()
expect(mockAuthService.getUserInfo).toHaveBeenCalled()
})
it("should delegate getAuthState to AuthService", () => {
const result = cloudService.getAuthState()
expect(mockAuthService.getState).toHaveBeenCalled()
expect(result).toBe("logged-out")
})
it("should delegate handleAuthCallback to AuthService", async () => {
await cloudService.handleAuthCallback("code", "state")
expect(mockAuthService.handleCallback).toHaveBeenCalledWith("code", "state")
})
})
describe("organization settings methods", () => {
let cloudService: CloudService
beforeEach(async () => {
cloudService = await CloudService.createInstance(mockContext)
})
it("should delegate getAllowList to SettingsService", () => {
cloudService.getAllowList()
expect(mockSettingsService.getAllowList).toHaveBeenCalled()
})
})
describe("error handling", () => {
it("should throw error when accessing methods before initialization", () => {
expect(() => CloudService.instance.login()).toThrow("CloudService not initialized")
})
it("should throw error when accessing instance before creation", () => {
expect(() => CloudService.instance).toThrow("CloudService not initialized")
})
})
describe("hasInstance", () => {
it("should return false when no instance exists", () => {
expect(CloudService.hasInstance()).toBe(false)
})
it("should return true when instance exists and is initialized", async () => {
await CloudService.createInstance(mockContext)
expect(CloudService.hasInstance()).toBe(true)
})
})
describe("dispose", () => {
it("should dispose of all services and clean up", async () => {
const cloudService = await CloudService.createInstance(mockContext)
cloudService.dispose()
expect(mockSettingsService.dispose).toHaveBeenCalled()
})
})
})

View file

@ -1,27 +1,27 @@
import { RefreshTimer } from "../refresh-timer"
// npx vitest run --globals src/__tests__/RefreshTimer.test.ts
// Mock timers
jest.useFakeTimers()
import { Mock } from "vitest"
import { RefreshTimer } from "../RefreshTimer"
vi.useFakeTimers()
describe("RefreshTimer", () => {
let mockCallback: jest.Mock
let mockCallback: Mock
let refreshTimer: RefreshTimer
beforeEach(() => {
// Reset mocks before each test
mockCallback = jest.fn()
// Default mock implementation returns success
mockCallback = vi.fn()
mockCallback.mockResolvedValue(true)
})
afterEach(() => {
// Clean up after each test
if (refreshTimer) {
refreshTimer.stop()
}
jest.clearAllTimers()
jest.clearAllMocks()
vi.clearAllTimers()
vi.clearAllMocks()
})
it("should execute callback immediately when started", () => {
@ -50,7 +50,7 @@ describe("RefreshTimer", () => {
expect(mockCallback).toHaveBeenCalledTimes(1)
// Fast-forward 50 seconds
jest.advanceTimersByTime(50000)
vi.advanceTimersByTime(50000)
// Callback should be called again
expect(mockCallback).toHaveBeenCalledTimes(2)
@ -72,7 +72,7 @@ describe("RefreshTimer", () => {
expect(mockCallback).toHaveBeenCalledTimes(1)
// Fast-forward 1 second
jest.advanceTimersByTime(1000)
vi.advanceTimersByTime(1000)
// Callback should be called again
expect(mockCallback).toHaveBeenCalledTimes(2)
@ -81,7 +81,7 @@ describe("RefreshTimer", () => {
await Promise.resolve()
// Fast-forward 2 seconds
jest.advanceTimersByTime(2000)
vi.advanceTimersByTime(2000)
// Callback should be called again
expect(mockCallback).toHaveBeenCalledTimes(3)
@ -103,13 +103,13 @@ describe("RefreshTimer", () => {
// Fast-forward through multiple failures to reach max backoff
await Promise.resolve() // First attempt
jest.advanceTimersByTime(1000)
vi.advanceTimersByTime(1000)
await Promise.resolve() // Second attempt (backoff = 2000ms)
jest.advanceTimersByTime(2000)
vi.advanceTimersByTime(2000)
await Promise.resolve() // Third attempt (backoff = 4000ms)
jest.advanceTimersByTime(4000)
vi.advanceTimersByTime(4000)
await Promise.resolve() // Fourth attempt (backoff would be 8000ms but max is 5000ms)
@ -132,13 +132,13 @@ describe("RefreshTimer", () => {
await Promise.resolve()
// Fast-forward 1 second
jest.advanceTimersByTime(1000)
vi.advanceTimersByTime(1000)
// Second attempt (succeeds)
await Promise.resolve()
// Fast-forward 5 seconds
jest.advanceTimersByTime(5000)
vi.advanceTimersByTime(5000)
// Third attempt (fails)
await Promise.resolve()
@ -173,7 +173,7 @@ describe("RefreshTimer", () => {
refreshTimer.stop()
// Fast-forward a long time
jest.advanceTimersByTime(1000000)
vi.advanceTimersByTime(1000000)
// Callback should only have been called once (the initial call)
expect(mockCallback).toHaveBeenCalledTimes(1)
@ -191,10 +191,10 @@ describe("RefreshTimer", () => {
// Fast-forward through a few failures
await Promise.resolve()
jest.advanceTimersByTime(1000)
vi.advanceTimersByTime(1000)
await Promise.resolve()
jest.advanceTimersByTime(2000)
vi.advanceTimersByTime(2000)
// Reset the timer
refreshTimer.reset()

View file

@ -0,0 +1,250 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// npx vitest run src/__tests__/RooCodeTelemetryClient.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
import { RooCodeTelemetryClient } from "../RooCodeTelemetryClient"
const mockFetch = vi.fn()
global.fetch = mockFetch as any
describe("RooCodeTelemetryClient", () => {
const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
return instance[propertyName]
}
let mockAuthService: any
beforeEach(() => {
vi.clearAllMocks()
// Create a mock AuthService instead of using the singleton
mockAuthService = {
getSessionToken: vi.fn().mockReturnValue("mock-token"),
getState: vi.fn().mockReturnValue("active-session"),
isAuthenticated: vi.fn().mockReturnValue(true),
hasActiveSession: vi.fn().mockReturnValue(true),
}
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({}),
})
vi.spyOn(console, "info").mockImplementation(() => {})
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("isEventCapturable", () => {
it("should return true for events not in exclude list", () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
})
it("should return false for events in exclude list", () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
})
})
describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
mode: "override", // This should override the provider's mode.
},
})
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "override", // Event property takes precedence.
customProp: "value",
})
expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
})
it("should handle errors from provider gracefully", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
const consoleErrorSpy = vi.spyOn(console, "error")
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Error getting telemetry properties: Provider error"),
)
})
it("should return event properties when no provider is set", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
})
})
describe("capture", () => {
it("should not capture events that are not capturable", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
await client.capture({
event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not send request when schema validation fails", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
})
it("should send request when event is capturable and validation passes", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
const providerProperties = {
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}
const eventProperties = {
taskId: "test-task-id",
}
const mockValidatedData = {
type: TelemetryEventName.TASK_CREATED,
properties: {
...providerProperties,
taskId: "test-task-id",
},
}
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}
client.setProvider(mockProvider)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: eventProperties,
})
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({
method: "POST",
body: JSON.stringify(mockValidatedData),
}),
)
})
it("should handle fetch errors gracefully", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
mockFetch.mockRejectedValue(new Error("Network error"))
await expect(
client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
}),
).resolves.not.toThrow()
})
})
describe("telemetry state methods", () => {
it("should always return true for isTelemetryEnabled", () => {
const client = new RooCodeTelemetryClient(mockAuthService)
expect(client.isTelemetryEnabled()).toBe(true)
})
it("should have empty implementations for updateTelemetryState and shutdown", async () => {
const client = new RooCodeTelemetryClient(mockAuthService)
client.updateTelemetryState(true)
await client.shutdown()
})
})
})

View file

@ -0,0 +1,250 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// npx vitest run src/__tests__/TelemetryClient.test.ts
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
import { TelemetryClient } from "../TelemetryClient"
const mockFetch = vi.fn()
global.fetch = mockFetch as any
describe("TelemetryClient", () => {
const getPrivateProperty = <T>(instance: any, propertyName: string): T => {
return instance[propertyName]
}
let mockAuthService: any
beforeEach(() => {
vi.clearAllMocks()
// Create a mock AuthService instead of using the singleton
mockAuthService = {
getSessionToken: vi.fn().mockReturnValue("mock-token"),
getState: vi.fn().mockReturnValue("active-session"),
isAuthenticated: vi.fn().mockReturnValue(true),
hasActiveSession: vi.fn().mockReturnValue(true),
}
mockFetch.mockResolvedValue({
ok: true,
json: vi.fn().mockResolvedValue({}),
})
vi.spyOn(console, "info").mockImplementation(() => {})
vi.spyOn(console, "error").mockImplementation(() => {})
})
afterEach(() => {
vi.restoreAllMocks()
})
describe("isEventCapturable", () => {
it("should return true for events not in exclude list", () => {
const client = new TelemetryClient(mockAuthService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CREATED)).toBe(true)
expect(isEventCapturable(TelemetryEventName.LLM_COMPLETION)).toBe(true)
expect(isEventCapturable(TelemetryEventName.MODE_SWITCH)).toBe(true)
expect(isEventCapturable(TelemetryEventName.TOOL_USED)).toBe(true)
})
it("should return false for events in exclude list", () => {
const client = new TelemetryClient(mockAuthService)
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
"isEventCapturable",
).bind(client)
expect(isEventCapturable(TelemetryEventName.TASK_CONVERSATION_MESSAGE)).toBe(false)
})
})
describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = new TelemetryClient(mockAuthService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}),
}
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
mode: "override", // This should override the provider's mode.
},
})
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "override", // Event property takes precedence.
customProp: "value",
})
expect(mockProvider.getTelemetryProperties).toHaveBeenCalledTimes(1)
})
it("should handle errors from provider gracefully", async () => {
const client = new TelemetryClient(mockAuthService)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
const consoleErrorSpy = vi.spyOn(console, "error")
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining("Error getting telemetry properties: Provider error"),
)
})
it("should return event properties when no provider is set", async () => {
const client = new TelemetryClient(mockAuthService)
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)
const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: { customProp: "value" },
})
expect(result).toEqual({ customProp: "value" })
})
})
describe("capture", () => {
it("should not capture events that are not capturable", async () => {
const client = new TelemetryClient(mockAuthService)
await client.capture({
event: TelemetryEventName.TASK_CONVERSATION_MESSAGE, // In exclude list.
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
})
it("should not send request when schema validation fails", async () => {
const client = new TelemetryClient(mockAuthService)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
})
it("should send request when event is capturable and validation passes", async () => {
const client = new TelemetryClient(mockAuthService)
const providerProperties = {
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}
const eventProperties = {
taskId: "test-task-id",
}
const mockValidatedData = {
type: TelemetryEventName.TASK_CREATED,
properties: {
...providerProperties,
taskId: "test-task-id",
},
}
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}
client.setProvider(mockProvider)
await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: eventProperties,
})
expect(mockFetch).toHaveBeenCalledWith(
"https://app.roocode.com/api/events",
expect.objectContaining({
method: "POST",
body: JSON.stringify(mockValidatedData),
}),
)
})
it("should handle fetch errors gracefully", async () => {
const client = new TelemetryClient(mockAuthService)
mockFetch.mockRejectedValue(new Error("Network error"))
await expect(
client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
}),
).resolves.not.toThrow()
})
})
describe("telemetry state methods", () => {
it("should always return true for isTelemetryEnabled", () => {
const client = new TelemetryClient(mockAuthService)
expect(client.isTelemetryEnabled()).toBe(true)
})
it("should have empty implementations for updateTelemetryState and shutdown", async () => {
const client = new TelemetryClient(mockAuthService)
client.updateTelemetryState(true)
await client.shutdown()
})
})
})

View file

@ -0,0 +1 @@
export * from "./CloudService"

View file

@ -0,0 +1,6 @@
import { CloudUserInfo } from "@roo-code/types"
export interface CloudServiceCallbacks {
userChanged?: (userInfo: CloudUserInfo | undefined) => void
settingsChanged?: () => void
}

View file

@ -0,0 +1,5 @@
{
"extends": "@roo-code/config-typescript/vscode-library.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,13 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
globals: true,
environment: "node",
},
resolve: {
alias: {
vscode: new URL("./src/__mocks__/vscode.ts", import.meta.url).pathname,
},
},
})

View file

@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"types": ["vitest/globals"],
"outDir": "dist",
"module": "esnext",
"moduleResolution": "Bundler",
"noUncheckedIndexedAccess": false,
"useUnknownInCatchVariables": false
}
}

View file

@ -0,0 +1,4 @@
import { config } from "@roo-code/config-eslint/base"
/** @type {import("eslint").Linter.Config} */
export default [...config]

View file

@ -0,0 +1,25 @@
{
"name": "@roo-code/telemetry",
"description": "Roo Code telemetry service and clients.",
"private": true,
"type": "module",
"exports": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext=ts --max-warnings=0",
"check-types": "tsc --noEmit",
"test": "vitest --globals --run",
"clean": "rimraf dist .turbo"
},
"dependencies": {
"@roo-code/types": "workspace:^",
"posthog-node": "^4.7.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@types/node": "^22.15.20",
"@types/vscode": "^1.84.0",
"vitest": "^3.1.3"
}
}

View file

@ -1,6 +1,10 @@
import { TelemetryEvent, TelemetryEventName } from "@roo-code/types"
import { TelemetryClient, TelemetryPropertiesProvider, TelemetryEventSubscription } from "../types"
import {
TelemetryEvent,
TelemetryEventName,
TelemetryClient,
TelemetryPropertiesProvider,
TelemetryEventSubscription,
} from "@roo-code/types"
export abstract class BaseTelemetryClient implements TelemetryClient {
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null

View file

@ -14,11 +14,11 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
private client: PostHog
private distinctId: string = vscode.env.machineId
private constructor(debug = false) {
constructor(debug = false) {
super(
{
type: "exclude",
events: [TelemetryEventName.LLM_COMPLETION],
events: [TelemetryEventName.TASK_MESSAGE, TelemetryEventName.LLM_COMPLETION],
},
debug,
)
@ -75,14 +75,4 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
public override async shutdown(): Promise<void> {
await this.client.shutdown()
}
private static _instance: PostHogTelemetryClient | null = null
public static getInstance(): PostHogTelemetryClient {
if (!PostHogTelemetryClient._instance) {
PostHogTelemetryClient._instance = new PostHogTelemetryClient()
}
return PostHogTelemetryClient._instance
}
}

View file

@ -1,38 +1,17 @@
import * as vscode from "vscode"
import { ZodError } from "zod"
import { TelemetryEventName } from "@roo-code/types"
import { logger } from "../../utils/logging"
import { PostHogTelemetryClient } from "./clients/PostHogTelemetryClient"
import { type TelemetryClient, type TelemetryPropertiesProvider } from "./types"
import { type TelemetryClient, type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
/**
* TelemetryService wrapper class that defers initialization.
* This ensures that we only create the various clients after environment
* variables are loaded.
*/
class TelemetryService {
private clients: TelemetryClient[] = []
private initialized = false
export class TelemetryService {
constructor(private clients: TelemetryClient[]) {}
/**
* Initialize the telemetry client. This should be called after environment
* variables are loaded.
*/
public async initialize(context: vscode.ExtensionContext): Promise<void> {
if (this.initialized) {
return
}
this.initialized = true
try {
this.clients.push(PostHogTelemetryClient.getInstance())
} catch (error) {
console.warn("Failed to initialize telemetry service:", error)
}
public register(client: TelemetryClient): void {
this.clients.push(client)
}
/**
@ -44,8 +23,6 @@ class TelemetryService {
if (this.isReady) {
this.clients.forEach((client) => client.setProvider(provider))
}
logger.debug("TelemetryService: ClineProvider reference set")
}
/**
@ -54,7 +31,7 @@ class TelemetryService {
* @returns Whether the service is ready to use
*/
private get isReady(): boolean {
return this.initialized && this.clients.length > 0
return this.clients.length > 0
}
/**
@ -74,7 +51,8 @@ class TelemetryService {
* @param eventName The event name to capture
* @param properties The event properties
*/
public captureEvent(eventName: TelemetryEventName, properties?: any): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public captureEvent(eventName: TelemetryEventName, properties?: Record<string, any>): void {
if (!this.isReady) {
return
}
@ -197,6 +175,27 @@ class TelemetryService {
this.clients.forEach((client) => client.shutdown())
}
}
export const telemetryService = new TelemetryService()
private static _instance: TelemetryService | null = null
static createInstance(clients: TelemetryClient[] = []) {
if (this._instance) {
throw new Error("TelemetryService instance already created")
}
this._instance = new TelemetryService(clients)
return this._instance
}
static get instance() {
if (!this._instance) {
throw new Error("TelemetryService not initialized")
}
return this._instance
}
static hasInstance(): boolean {
return this._instance !== null
}
}

View file

@ -1,21 +1,23 @@
// npx jest src/services/telemetry/clients/__tests__/PostHogTelemetryClient.test.ts
/* eslint-disable @typescript-eslint/no-explicit-any */
// npx vitest run src/__tests__/PostHogTelemetryClient.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest"
import * as vscode from "vscode"
import { PostHog } from "posthog-node"
import { TelemetryEventName } from "@roo-code/types"
import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
import { TelemetryPropertiesProvider } from "../../types"
import { PostHogTelemetryClient } from "../PostHogTelemetryClient"
jest.mock("posthog-node")
vi.mock("posthog-node")
jest.mock("vscode", () => ({
vi.mock("vscode", () => ({
env: {
machineId: "test-machine-id",
},
workspace: {
getConfiguration: jest.fn(),
getConfiguration: vi.fn(),
},
}))
@ -24,37 +26,29 @@ describe("PostHogTelemetryClient", () => {
return instance[propertyName]
}
let mockPostHogClient: jest.Mocked<PostHog>
let mockPostHogClient: any
beforeEach(() => {
jest.clearAllMocks()
vi.clearAllMocks()
mockPostHogClient = {
capture: jest.fn(),
optIn: jest.fn(),
optOut: jest.fn(),
shutdown: jest.fn().mockResolvedValue(undefined),
} as unknown as jest.Mocked<PostHog>
;(PostHog as unknown as jest.Mock).mockImplementation(() => mockPostHogClient)
capture: vi.fn(),
optIn: vi.fn(),
optOut: vi.fn(),
shutdown: vi.fn().mockResolvedValue(undefined),
}
;(PostHog as any).mockImplementation(() => mockPostHogClient)
// @ts-ignore - Accessing private static property for testing
// @ts-expect-error - Accessing private static property for testing
PostHogTelemetryClient._instance = undefined
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
get: jest.fn().mockReturnValue("all"),
})
})
describe("getInstance", () => {
it("should return the same instance when called multiple times", () => {
const instance1 = PostHogTelemetryClient.getInstance()
const instance2 = PostHogTelemetryClient.getInstance()
expect(instance1).toBe(instance2)
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
})
describe("isEventCapturable", () => {
it("should return true for events not in exclude list", () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
@ -66,7 +60,7 @@ describe("PostHogTelemetryClient", () => {
})
it("should return false for events in exclude list", () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
const isEventCapturable = getPrivateProperty<(eventName: TelemetryEventName) => boolean>(
client,
@ -79,10 +73,10 @@ describe("PostHogTelemetryClient", () => {
describe("getEventProperties", () => {
it("should merge provider properties with event properties", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: jest.fn().mockResolvedValue({
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
@ -120,13 +114,13 @@ describe("PostHogTelemetryClient", () => {
})
it("should handle errors from provider gracefully", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: jest.fn().mockRejectedValue(new Error("Provider error")),
getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")),
}
const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation()
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
client.setProvider(mockProvider)
const getEventProperties = getPrivateProperty<
@ -147,7 +141,7 @@ describe("PostHogTelemetryClient", () => {
})
it("should return event properties when no provider is set", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
@ -164,7 +158,7 @@ describe("PostHogTelemetryClient", () => {
describe("capture", () => {
it("should not capture events when telemetry is disabled", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
client.updateTelemetryState(false)
await client.capture({
@ -176,7 +170,7 @@ describe("PostHogTelemetryClient", () => {
})
it("should not capture events that are not capturable", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
await client.capture({
@ -188,11 +182,11 @@ describe("PostHogTelemetryClient", () => {
})
it("should capture events when telemetry is enabled and event is capturable", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)
const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: jest.fn().mockResolvedValue({
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
@ -222,10 +216,10 @@ describe("PostHogTelemetryClient", () => {
describe("updateTelemetryState", () => {
it("should enable telemetry when user opts in and global telemetry is enabled", () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
get: jest.fn().mockReturnValue("all"),
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
client.updateTelemetryState(true)
@ -235,10 +229,10 @@ describe("PostHogTelemetryClient", () => {
})
it("should disable telemetry when user opts out", () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
get: jest.fn().mockReturnValue("all"),
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("all"),
})
client.updateTelemetryState(false)
@ -248,10 +242,10 @@ describe("PostHogTelemetryClient", () => {
})
it("should disable telemetry when global telemetry is disabled, regardless of user opt-in", () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
;(vscode.workspace.getConfiguration as jest.Mock).mockReturnValue({
get: jest.fn().mockReturnValue("off"),
;(vscode.workspace.getConfiguration as any).mockReturnValue({
get: vi.fn().mockReturnValue("off"),
})
client.updateTelemetryState(true)
@ -262,7 +256,7 @@ describe("PostHogTelemetryClient", () => {
describe("shutdown", () => {
it("should call shutdown on the PostHog client", async () => {
const client = PostHogTelemetryClient.getInstance()
const client = new PostHogTelemetryClient()
await client.shutdown()
expect(mockPostHogClient.shutdown).toHaveBeenCalled()
})

View file

@ -0,0 +1,3 @@
export * from "./BaseTelemetryClient"
export * from "./PostHogTelemetryClient"
export * from "./TelemetryService"

View file

@ -0,0 +1,5 @@
{
"extends": "@roo-code/config-typescript/vscode-library.json",
"include": ["src"],
"exclude": ["node_modules"]
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from "vitest/config"
export default defineConfig({
test: {
globals: true,
environment: "node",
},
})

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.19.0",
"version": "1.22.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",

View file

@ -0,0 +1,49 @@
import { z } from "zod"
export interface CloudUserInfo {
name?: string
email?: string
picture?: string
}
/**
* Organization Allow List
*/
export const organizationAllowListSchema = z.object({
allowAll: z.boolean(),
providers: z.record(
z.object({
allowAll: z.boolean(),
models: z.array(z.string()).optional(),
}),
),
})
export type OrganizationAllowList = z.infer<typeof organizationAllowListSchema>
export const ORGANIZATION_ALLOW_ALL: OrganizationAllowList = {
allowAll: true,
providers: {},
} as const
/**
* Organization Settings
*/
export const organizationSettingsSchema = z.object({
version: z.number(),
defaultSettings: z
.object({
enableCheckpoints: z.boolean().optional(),
maxOpenTabsContext: z.number().optional(),
maxWorkspaceFiles: z.number().optional(),
showRooIgnoredFiles: z.boolean().optional(),
maxReadFileLine: z.number().optional(),
fuzzyMatchThreshold: z.number().optional(),
})
.optional(),
allowList: organizationAllowListSchema,
})
export type OrganizationSettings = z.infer<typeof organizationSettingsSchema>

View file

@ -1,5 +1,6 @@
export * from "./api.js"
export * from "./codebase-index.js"
export * from "./cloud.js"
export * from "./experiment.js"
export * from "./global-settings.js"
export * from "./history.js"

View file

@ -1,6 +1,7 @@
import { z } from "zod"
import { providerNames } from "./provider-settings.js"
import { clineMessageSchema } from "./message.js"
/**
* TelemetrySetting
@ -20,6 +21,7 @@ export enum TelemetryEventName {
TASK_CREATED = "Task Created",
TASK_RESTARTED = "Task Reopened",
TASK_COMPLETED = "Task Completed",
TASK_MESSAGE = "Task Message",
TASK_CONVERSATION_MESSAGE = "Conversation Message",
LLM_COMPLETION = "LLM Completion",
MODE_SWITCH = "Mode Switched",
@ -87,14 +89,6 @@ export type TelemetryEvent = {
* RooCodeTelemetryEvent
*/
const completionPropertiesSchema = z.object({
inputTokens: z.number(),
outputTokens: z.number(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
cost: z.number().optional(),
})
export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
z.object({
type: z.enum([
@ -116,19 +110,56 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.SHELL_INTEGRATION_ERROR,
TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR,
]),
properties: telemetryPropertiesSchema,
}),
z.object({
type: z.literal(TelemetryEventName.TASK_MESSAGE),
properties: z.object({
...appPropertiesSchema.shape,
...taskPropertiesSchema.shape,
taskId: z.string(),
message: clineMessageSchema,
}),
}),
z.object({
type: z.literal(TelemetryEventName.LLM_COMPLETION),
properties: z.object({
...appPropertiesSchema.shape,
...taskPropertiesSchema.shape,
...completionPropertiesSchema.shape,
...telemetryPropertiesSchema.shape,
inputTokens: z.number(),
outputTokens: z.number(),
cacheReadTokens: z.number().optional(),
cacheWriteTokens: z.number().optional(),
cost: z.number().optional(),
}),
}),
])
export type RooCodeTelemetryEvent = z.infer<typeof rooCodeTelemetryEventSchema>
/**
* TelemetryEventSubscription
*/
export type TelemetryEventSubscription =
| { type: "include"; events: TelemetryEventName[] }
| { type: "exclude"; events: TelemetryEventName[] }
/**
* TelemetryPropertiesProvider
*/
export interface TelemetryPropertiesProvider {
getTelemetryProperties(): Promise<TelemetryProperties>
}
/**
* TelemetryClient
*/
export interface TelemetryClient {
subscription?: TelemetryEventSubscription
setProvider(provider: TelemetryPropertiesProvider): void
capture(options: TelemetryEvent): Promise<void>
updateTelemetryState(didUserOptIn: boolean): void
isTelemetryEnabled(): boolean
shutdown(): Promise<void>
}

View file

@ -34,6 +34,7 @@ export const commandIds = [
"mcpButtonClicked",
"historyButtonClicked",
"popoutButtonClicked",
"accountButtonClicked",
"settingsButtonClicked",
"openInNewTab",

65
pnpm-lock.yaml generated
View file

@ -118,6 +118,34 @@ importers:
specifier: ^3.1.3
version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.20)(jiti@2.4.2)(jsdom@20.0.3)(lightningcss@1.29.2)(tsx@4.19.4)(yaml@2.8.0)
packages/cloud:
dependencies:
'@roo-code/telemetry':
specifier: workspace:^
version: link:../telemetry
'@roo-code/types':
specifier: workspace:^
version: link:../types
axios:
specifier: ^1.7.4
version: 1.9.0
devDependencies:
'@roo-code/config-eslint':
specifier: workspace:^
version: link:../config-eslint
'@roo-code/config-typescript':
specifier: workspace:^
version: link:../config-typescript
'@types/node':
specifier: ^22.15.20
version: 22.15.20
'@types/vscode':
specifier: ^1.84.0
version: 1.100.0
vitest:
specifier: ^3.1.3
version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.20)(jiti@2.4.2)(jsdom@20.0.3)(lightningcss@1.29.2)(tsx@4.19.4)(yaml@2.8.0)
packages/config-eslint:
devDependencies:
'@eslint/js':
@ -153,6 +181,34 @@ importers:
packages/config-typescript: {}
packages/telemetry:
dependencies:
'@roo-code/types':
specifier: workspace:^
version: link:../types
posthog-node:
specifier: ^4.7.0
version: 4.17.2
zod:
specifier: ^3.24.2
version: 3.24.4
devDependencies:
'@roo-code/config-eslint':
specifier: workspace:^
version: link:../config-eslint
'@roo-code/config-typescript':
specifier: workspace:^
version: link:../config-typescript
'@types/node':
specifier: ^22.15.20
version: 22.15.20
'@types/vscode':
specifier: ^1.84.0
version: 1.100.0
vitest:
specifier: ^3.1.3
version: 3.1.3(@types/debug@4.1.12)(@types/node@22.15.20)(jiti@2.4.2)(jsdom@20.0.3)(lightningcss@1.29.2)(tsx@4.19.4)(yaml@2.8.0)
packages/types:
dependencies:
zod:
@ -204,6 +260,12 @@ importers:
'@qdrant/js-client-rest':
specifier: ^1.14.0
version: 1.14.0(typescript@5.8.3)
'@roo-code/cloud':
specifier: workspace:^
version: link:../packages/cloud
'@roo-code/telemetry':
specifier: workspace:^
version: link:../packages/telemetry
'@roo-code/types':
specifier: workspace:^
version: link:../packages/types
@ -300,9 +362,6 @@ importers:
pkce-challenge:
specifier: ^4.1.0
version: 4.1.0
posthog-node:
specifier: ^4.7.0
version: 4.17.2
pretty-bytes:
specifier: ^6.1.1
version: 6.1.1

View file

@ -1,11 +1,14 @@
import * as vscode from "vscode"
import { CloudService } from "@roo-code/cloud"
import { ClineProvider } from "../core/webview/ClineProvider"
export const handleUri = async (uri: vscode.Uri) => {
const path = uri.path
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
const visibleProvider = ClineProvider.getVisibleInstance()
if (!visibleProvider) {
return
}
@ -32,6 +35,12 @@ export const handleUri = async (uri: vscode.Uri) => {
}
break
}
case "/auth/clerk/callback": {
const code = query.get("code")
const state = query.get("state")
await CloudService.instance.handleAuthCallback(code, state)
break
}
default:
break
}

View file

@ -2,12 +2,12 @@ import * as vscode from "vscode"
import delay from "delay"
import type { CommandId } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Package } from "../shared/package"
import { getCommand } from "../utils/commands"
import { ClineProvider } from "../core/webview/ClineProvider"
import { ContextProxy } from "../core/config/ContextProxy"
import { telemetryService } from "../services/telemetry/TelemetryService"
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
import { handleNewTask } from "./handleTask"
@ -70,6 +70,17 @@ export const registerCommands = (options: RegisterCommandOptions) => {
const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOptions): Record<CommandId, any> => ({
activationCompleted: () => {},
accountButtonClicked: () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) {
return
}
TelemetryService.instance.captureTitleButtonClicked("account")
visibleProvider.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
},
plusButtonClicked: async () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
@ -77,7 +88,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
telemetryService.captureTitleButtonClicked("plus")
TelemetryService.instance.captureTitleButtonClicked("plus")
await visibleProvider.removeClineFromStack()
await visibleProvider.postStateToWebview()
@ -90,7 +101,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
telemetryService.captureTitleButtonClicked("mcp")
TelemetryService.instance.captureTitleButtonClicked("mcp")
visibleProvider.postMessageToWebview({ type: "action", action: "mcpButtonClicked" })
},
@ -101,12 +112,12 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
telemetryService.captureTitleButtonClicked("prompts")
TelemetryService.instance.captureTitleButtonClicked("prompts")
visibleProvider.postMessageToWebview({ type: "action", action: "promptsButtonClicked" })
},
popoutButtonClicked: () => {
telemetryService.captureTitleButtonClicked("popout")
TelemetryService.instance.captureTitleButtonClicked("popout")
return openClineInNewTab({ context, outputChannel })
},
@ -118,7 +129,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
telemetryService.captureTitleButtonClicked("settings")
TelemetryService.instance.captureTitleButtonClicked("settings")
visibleProvider.postMessageToWebview({ type: "action", action: "settingsButtonClicked" })
// Also explicitly post the visibility message to trigger scroll reliably
@ -131,7 +142,7 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
return
}
telemetryService.captureTitleButtonClicked("history")
TelemetryService.instance.captureTitleButtonClicked("history")
visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
},

View file

@ -2,12 +2,11 @@ import cloneDeep from "clone-deep"
import { serializeError } from "serialize-error"
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse } from "../../shared/tools"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { fetchInstructionsTool } from "../tools/fetchInstructionsTool"
import { listFilesTool } from "../tools/listFilesTool"
import { readFileTool } from "../tools/readFileTool"
@ -320,7 +319,7 @@ export async function presentAssistantMessage(cline: Task) {
if (!block.partial) {
cline.recordToolUsage(block.name)
telemetryService.captureToolUsage(cline.taskId, block.name)
TelemetryService.instance.captureToolUsage(cline.taskId, block.name)
}
// Validate tool use before execution.
@ -368,7 +367,7 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say("user_feedback", text, images)
// Track tool repetition in telemetry.
telemetryService.captureConsecutiveMistakeError(cline.taskId)
TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
}
// Return tool result message about the repetition

View file

@ -1,6 +1,8 @@
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
import { getWorkspacePath } from "../../utils/path"
@ -10,7 +12,6 @@ import { getApiMetrics } from "../../shared/getApiMetrics"
import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { CheckpointServiceOptions, RepoPerTaskCheckpointService } from "../../services/checkpoints"
export function getCheckpointService(cline: Task) {
@ -166,7 +167,7 @@ export async function checkpointSave(cline: Task, force = false) {
return
}
telemetryService.captureCheckpointCreated(cline.taskId)
TelemetryService.instance.captureCheckpointCreated(cline.taskId)
// Start the checkpoint process in the background.
return service.saveCheckpoint(`Task: ${cline.taskId}, Time: ${Date.now()}`, { allowEmpty: force }).catch((err) => {
@ -198,7 +199,7 @@ export async function checkpointRestore(cline: Task, { ts, commitHash, mode }: C
try {
await service.restoreCheckpoint(commitHash)
telemetryService.captureCheckpointRestored(cline.taskId)
TelemetryService.instance.captureCheckpointRestored(cline.taskId)
await provider?.postMessageToWebview({ type: "currentCheckpointUpdated", text: commitHash })
if (mode === "restore") {
@ -256,7 +257,7 @@ export async function checkpointDiff(cline: Task, { ts, previousCommitHash, comm
return
}
telemetryService.captureCheckpointDiffed(cline.taskId)
TelemetryService.instance.captureCheckpointDiffed(cline.taskId)
if (!previousCommitHash && mode === "checkpoint") {
const previousCheckpoint = cline.clineMessages

View file

@ -1,18 +1,23 @@
// npx jest core/condense/__tests__/index.test.ts
import { describe, expect, it, jest, beforeEach } from "@jest/globals"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandler } from "../../../api"
import { ApiMessage } from "../../task-persistence/apiMessages"
import { maybeRemoveImageBlocks } from "../../../api/transform/image-cleaning"
import { summarizeConversation, getMessagesSinceLastSummary, N_MESSAGES_TO_KEEP } from "../index"
import { telemetryService } from "../../../services/telemetry/TelemetryService"
// Mock dependencies
jest.mock("../../../api/transform/image-cleaning", () => ({
maybeRemoveImageBlocks: jest.fn((messages: ApiMessage[], _apiHandler: ApiHandler) => [...messages]),
}))
jest.mock("../../../services/telemetry/TelemetryService", () => ({
telemetryService: {
captureContextCondensed: jest.fn(),
jest.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureContextCondensed: jest.fn(),
},
},
}))
@ -524,7 +529,7 @@ describe("summarizeConversation with custom settings", () => {
jest.clearAllMocks()
// Reset telemetry mock
;(telemetryService.captureContextCondensed as jest.Mock).mockClear()
;(TelemetryService.instance.captureContextCondensed as jest.Mock).mockClear()
// Setup mock API handlers
mockMainApiHandler = {
@ -729,7 +734,7 @@ describe("summarizeConversation with custom settings", () => {
)
// Verify telemetry was called with custom prompt flag
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
taskId,
false,
true, // usedCustomPrompt
@ -753,7 +758,7 @@ describe("summarizeConversation with custom settings", () => {
)
// Verify telemetry was called with custom API handler flag
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
taskId,
false,
false, // usedCustomPrompt
@ -777,7 +782,7 @@ describe("summarizeConversation with custom settings", () => {
)
// Verify telemetry was called with both flags
expect(telemetryService.captureContextCondensed).toHaveBeenCalledWith(
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
taskId,
true, // isAutomaticTrigger
true, // usedCustomPrompt

View file

@ -1,9 +1,11 @@
import Anthropic from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { t } from "../../i18n"
import { ApiHandler } from "../../api"
import { ApiMessage } from "../task-persistence/apiMessages"
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
import { telemetryService } from "../../services/telemetry/TelemetryService"
export const N_MESSAGES_TO_KEEP = 3
@ -88,14 +90,16 @@ export async function summarizeConversation(
customCondensingPrompt?: string,
condensingApiHandler?: ApiHandler,
): Promise<SummarizeResponse> {
telemetryService.captureContextCondensed(
TelemetryService.instance.captureContextCondensed(
taskId,
isAutomaticTrigger ?? false,
!!customCondensingPrompt?.trim(),
!!condensingApiHandler,
)
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
if (messagesToSummarize.length <= 1) {
const error =
messages.length <= N_MESSAGES_TO_KEEP + 1
@ -103,20 +107,25 @@ export async function summarizeConversation(
: t("common:errors.condensed_recently")
return { ...response, error }
}
const keepMessages = messages.slice(-N_MESSAGES_TO_KEEP)
// Check if there's a recent summary in the messages we're keeping
const recentSummaryExists = keepMessages.some((message) => message.isSummary)
if (recentSummaryExists) {
const error = t("common:errors.condensed_recently")
return { ...response, error }
}
const finalRequestMessage: Anthropic.MessageParam = {
role: "user",
content: "Summarize the conversation so far, as described in the prompt instructions.",
}
const requestMessages = maybeRemoveImageBlocks([...messagesToSummarize, finalRequestMessage], apiHandler).map(
({ role, content }) => ({ role, content }),
)
// Note: this doesn't need to be a stream, consider using something like apiHandler.completePrompt
// Use custom prompt if provided and non-empty, otherwise use the default SUMMARY_PROMPT
const promptToUse = customCondensingPrompt?.trim() ? customCondensingPrompt.trim() : SUMMARY_PROMPT
@ -129,7 +138,9 @@ export async function summarizeConversation(
console.warn(
"Chosen API handler for condensing does not support message creation or is invalid, falling back to main apiHandler.",
)
handlerToUse = apiHandler // Fallback to the main, presumably valid, apiHandler
// Ensure the main apiHandler itself is valid before this point or add another check.
if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
// This case should ideally not happen if main apiHandler is always valid.
@ -142,9 +153,11 @@ export async function summarizeConversation(
}
const stream = handlerToUse.createMessage(promptToUse, requestMessages)
let summary = ""
let cost = 0
let outputTokens = 0
for await (const chunk of stream) {
if (chunk.type === "text") {
summary += chunk.text
@ -154,28 +167,35 @@ export async function summarizeConversation(
outputTokens = chunk.outputTokens ?? 0
}
}
summary = summary.trim()
if (summary.length === 0) {
const error = t("common:errors.condense_failed")
return { ...response, cost, error }
}
const summaryMessage: ApiMessage = {
role: "assistant",
content: summary,
ts: keepMessages[0].ts,
isSummary: true,
}
const newMessages = [...messages.slice(0, -N_MESSAGES_TO_KEEP), summaryMessage, ...keepMessages]
// Count the tokens in the context for the next API request
// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens
const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
const contextMessages = outputTokens
? [systemPromptMessage, ...keepMessages]
: [systemPromptMessage, summaryMessage, ...keepMessages]
const contextBlocks = contextMessages.flatMap((message) =>
typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content,
)
const newContextTokens = outputTokens + (await apiHandler.countTokens(contextBlocks))
if (newContextTokens >= prevContextTokens) {
const error = t("common:errors.condense_context_grew")
@ -187,9 +207,11 @@ export async function summarizeConversation(
/* Returns the list of all messages since the last summary message, including the summary. Returns all messages if there is no summary. */
export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] {
let lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary)
if (lastSummaryIndexReverse === -1) {
return messages
}
const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1
return messages.slice(lastSummaryIndex)
}

View file

@ -15,9 +15,9 @@ import {
globalSettingsSchema,
isSecretStateKey,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { logger } from "../../utils/logging"
import { telemetryService } from "../../services/telemetry/TelemetryService"
type GlobalStateKey = keyof GlobalState
type SecretStateKey = keyof SecretState
@ -162,7 +162,7 @@ export class ContextProxy {
return globalSettingsSchema.parse(values)
} catch (error) {
if (error instanceof ZodError) {
telemetryService.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
}
return GLOBAL_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as GlobalSettings)
@ -180,7 +180,7 @@ export class ContextProxy {
return providerSettingsSchema.parse(values)
} catch (error) {
if (error instanceof ZodError) {
telemetryService.captureSchemaValidationError({ schemaName: "ProviderSettings", error })
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ProviderSettings", error })
}
return PROVIDER_SETTINGS_KEYS.reduce((acc, key) => ({ ...acc, [key]: values[key] }), {} as ProviderSettings)
@ -248,7 +248,7 @@ export class ContextProxy {
return Object.fromEntries(Object.entries(globalSettings).filter(([_, value]) => value !== undefined))
} catch (error) {
if (error instanceof ZodError) {
telemetryService.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
TelemetryService.instance.captureSchemaValidationError({ schemaName: "GlobalSettings", error })
}
return undefined

View file

@ -6,9 +6,9 @@ import {
providerSettingsSchema,
providerSettingsSchemaDiscriminated,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Mode, modes } from "../../shared/modes"
import { telemetryService } from "../../services/telemetry/TelemetryService"
const providerSettingsWithIdSchema = providerSettingsSchema.extend({ id: z.string().optional() })
const discriminatedProviderSettingsWithIdSchema = providerSettingsSchemaDiscriminated.and(
@ -469,7 +469,10 @@ export class ProviderSettingsManager {
}
} catch (error) {
if (error instanceof ZodError) {
telemetryService.captureSchemaValidationError({ schemaName: "ProviderProfiles", error })
TelemetryService.instance.captureSchemaValidationError({
schemaName: "ProviderProfiles",
error,
})
}
throw new Error(`Failed to read provider profiles from secrets: ${error}`)

View file

@ -6,6 +6,7 @@ import * as path from "path"
import * as vscode from "vscode"
import type { ProviderName } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { importSettings, exportSettings } from "../importExport"
import { ProviderSettingsManager } from "../ProviderSettingsManager"
@ -41,6 +42,10 @@ describe("importExport", () => {
beforeEach(() => {
jest.clearAllMocks()
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
mockProviderSettingsManager = {
export: jest.fn(),
import: jest.fn(),

View file

@ -6,8 +6,7 @@ import * as vscode from "vscode"
import { z, ZodError } from "zod"
import { globalSettingsSchema } from "@roo-code/types"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetryService } from "@roo-code/telemetry"
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
import { ContextProxy } from "./ContextProxy"
@ -84,7 +83,7 @@ export const importSettings = async ({ providerSettingsManager, contextProxy, cu
if (e instanceof ZodError) {
error = e.issues.map((issue) => `[${issue.path.join(".")}]: ${issue.message}`).join("\n")
telemetryService.captureSchemaValidationError({ schemaName: "ImportExport", error: e })
TelemetryService.instance.captureSchemaValidationError({ schemaName: "ImportExport", error: e })
} else if (e instanceof Error) {
error = e.message
}

File diff suppressed because it is too large Load diff

View file

@ -1,8 +1,10 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { ApiHandler } from "../../api"
import { summarizeConversation, SummarizeResponse } from "../condense"
import { ApiMessage } from "../task-persistence/apiMessages"
import { telemetryService } from "../../services/telemetry/TelemetryService"
/**
* Default percentage of the context window to use as a buffer when deciding when to truncate
@ -36,7 +38,7 @@ export async function estimateTokenCount(
* @returns {ApiMessage[]} The truncated conversation messages.
*/
export function truncateConversation(messages: ApiMessage[], fracToRemove: number, taskId: string): ApiMessage[] {
telemetryService.captureSlidingWindowTruncation(taskId)
TelemetryService.instance.captureSlidingWindowTruncation(taskId)
const truncatedMessages = [messages[0]]
const rawMessagesToRemove = Math.floor((messages.length - 1) * fracToRemove)
const messagesToRemove = rawMessagesToRemove - (rawMessagesToRemove % 2)

View file

@ -8,18 +8,21 @@ import delay from "delay"
import pWaitFor from "p-wait-for"
import { serializeError } from "serialize-error"
import type {
ProviderSettings,
TokenUsage,
ToolUsage,
ToolName,
ContextCondense,
ClineAsk,
ClineMessage,
ClineSay,
ToolProgressStatus,
HistoryItem,
import {
type ProviderSettings,
type TokenUsage,
type ToolUsage,
type ToolName,
type ContextCondense,
type ClineAsk,
type ClineMessage,
type ClineSay,
type ToolProgressStatus,
type HistoryItem,
TelemetryEventName,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService } from "@roo-code/cloud"
// api
import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
@ -41,7 +44,6 @@ import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { BrowserSession } from "../../services/browser/BrowserSession"
import { McpHub } from "../../services/mcp/McpHub"
import { McpServerManager } from "../../services/mcp/McpServerManager"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { RepoPerTaskCheckpointService } from "../../services/checkpoints"
// integrations
@ -243,9 +245,9 @@ export class Task extends EventEmitter<ClineEvents> {
this.taskNumber = taskNumber
if (historyItem) {
telemetryService.captureTaskRestarted(this.taskId)
TelemetryService.instance.captureTaskRestarted(this.taskId)
} else {
telemetryService.captureTaskCreated(this.taskId)
TelemetryService.instance.captureTaskCreated(this.taskId)
}
this.diffStrategy = new MultiSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
@ -321,6 +323,15 @@ export class Task extends EventEmitter<ClineEvents> {
await this.providerRef.deref()?.postStateToWebview()
this.emit("message", { action: "created", message })
await this.saveClineMessages()
const shouldCaptureMessage = message.partial !== true && CloudService.isEnabled()
if (shouldCaptureMessage) {
CloudService.instance.captureEvent({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId: this.taskId, message },
})
}
}
public async overwriteClineMessages(newMessages: ClineMessage[]) {
@ -331,6 +342,15 @@ export class Task extends EventEmitter<ClineEvents> {
private async updateClineMessage(partialMessage: ClineMessage) {
await this.providerRef.deref()?.postMessageToWebview({ type: "partialMessage", partialMessage })
this.emit("message", { action: "updated", message: partialMessage })
const shouldCaptureMessage = partialMessage.partial !== true && CloudService.isEnabled()
if (shouldCaptureMessage) {
CloudService.instance.captureEvent({
event: TelemetryEventName.TASK_MESSAGE,
properties: { taskId: this.taskId, message: partialMessage },
})
}
}
private async saveClineMessages() {
@ -1066,7 +1086,7 @@ export class Task extends EventEmitter<ClineEvents> {
await this.say("user_feedback", text, images)
// Track consecutive mistake errors in telemetry.
telemetryService.captureConsecutiveMistakeError(this.taskId)
TelemetryService.instance.captureConsecutiveMistakeError(this.taskId)
}
this.consecutiveMistakeCount = 0
@ -1125,7 +1145,7 @@ export class Task extends EventEmitter<ClineEvents> {
const finalUserContent = [...parsedUserContent, { type: "text" as const, text: environmentDetails }]
await this.addToApiConversationHistory({ role: "user", content: finalUserContent })
telemetryService.captureConversationMessage(this.taskId, "user")
TelemetryService.instance.captureConversationMessage(this.taskId, "user")
// Since we sent off a placeholder api_req_started message to update the
// webview while waiting to actually start the API request (to load
@ -1345,7 +1365,7 @@ export class Task extends EventEmitter<ClineEvents> {
cacheReadTokens > 0 ||
typeof totalCost !== "undefined"
) {
telemetryService.captureLlmCompletion(this.taskId, {
TelemetryService.instance.captureLlmCompletion(this.taskId, {
inputTokens,
outputTokens,
cacheWriteTokens,
@ -1399,7 +1419,7 @@ export class Task extends EventEmitter<ClineEvents> {
content: [{ type: "text", text: assistantMessage }],
})
telemetryService.captureConversationMessage(this.taskId, "assistant")
TelemetryService.instance.captureConversationMessage(this.taskId, "assistant")
// NOTE: This comment is here for future reference - this was a
// workaround for `userMessageContent` not getting set to true.

View file

@ -1,4 +1,4 @@
// npx jest src/core/task/__tests__/Task.test.ts
// npx jest core/task/__tests__/Task.test.ts
import * as os from "os"
import * as path from "path"
@ -7,6 +7,7 @@ import * as vscode from "vscode"
import { Anthropic } from "@anthropic-ai/sdk"
import type { GlobalState, ProviderSettings, ModelInfo } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../Task"
import { ClineProvider } from "../../webview/ClineProvider"
@ -126,10 +127,9 @@ jest.mock("../../environment/getEnvironmentDetails", () => ({
getEnvironmentDetails: jest.fn().mockResolvedValue(""),
}))
// Mock RooIgnoreController
jest.mock("../../ignore/RooIgnoreController")
// Mock storagePathManager to prevent dynamic import issues
// Mock storagePathManager to prevent dynamic import issues.
jest.mock("../../../utils/storage", () => ({
getTaskDirectoryPath: jest
.fn()
@ -139,14 +139,12 @@ jest.mock("../../../utils/storage", () => ({
.mockImplementation((globalStoragePath) => Promise.resolve(`${globalStoragePath}/settings`)),
}))
// Mock fileExistsAtPath
jest.mock("../../../utils/fs", () => ({
fileExistsAtPath: jest.fn().mockImplementation((filePath) => {
return filePath.includes("ui_messages.json") || filePath.includes("api_conversation_history.json")
}),
}))
// Mock fs/promises
const mockMessages = [
{
ts: Date.now(),
@ -163,6 +161,10 @@ describe("Cline", () => {
let mockExtensionContext: vscode.ExtensionContext
beforeEach(() => {
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
// Setup mock extension context
const storageUri = {
fsPath: path.join(os.tmpdir(), "test-storage"),

View file

@ -1,6 +1,8 @@
import path from "path"
import fs from "fs/promises"
import { TelemetryService } from "@roo-code/telemetry"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { getReadablePath } from "../../utils/path"
import { Task } from "../task/Task"
@ -9,7 +11,6 @@ import { formatResponse } from "../prompts/responses"
import { fileExistsAtPath } from "../../utils/fs"
import { addLineNumbers } from "../../integrations/misc/extract-text"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
export async function applyDiffTool(
@ -103,7 +104,7 @@ export async function applyDiffTool(
const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
cline.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
let formattedError = ""
telemetryService.captureDiffApplicationError(cline.taskId, currentCount)
TelemetryService.instance.captureDiffApplicationError(cline.taskId, currentCount)
if (diffResult.failParts && diffResult.failParts.length > 0) {
for (const failPart of diffResult.failParts) {

View file

@ -1,5 +1,7 @@
import Anthropic from "@anthropic-ai/sdk"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
import {
ToolResponse,
@ -12,7 +14,6 @@ import {
AskFinishSubTaskApproval,
} from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { type ExecuteCommandOptions, executeCommand } from "./executeCommandTool"
export async function attemptCompletionTool(
@ -45,7 +46,7 @@ export async function attemptCompletionTool(
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
await cline.say("completion_result", removeClosingTag("result", result), undefined, false)
telemetryService.captureTaskCompleted(cline.taskId)
TelemetryService.instance.captureTaskCompleted(cline.taskId)
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
@ -71,7 +72,7 @@ export async function attemptCompletionTool(
if (lastMessage && lastMessage.ask !== "command") {
// Haven't sent a command message yet so first send completion_result then command.
await cline.say("completion_result", result, undefined, false)
telemetryService.captureTaskCompleted(cline.taskId)
TelemetryService.instance.captureTaskCompleted(cline.taskId)
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
}
@ -96,7 +97,7 @@ export async function attemptCompletionTool(
commandResult = execCommandResult
} else {
await cline.say("completion_result", result, undefined, false)
telemetryService.captureTaskCompleted(cline.taskId)
TelemetryService.instance.captureTaskCompleted(cline.taskId)
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage(), cline.toolUsage)
}

View file

@ -4,13 +4,13 @@ import * as path from "path"
import delay from "delay"
import { CommandExecutionStatus } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { Task } from "../task/Task"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag, ToolResponse } from "../../shared/tools"
import { formatResponse } from "../prompts/responses"
import { unescapeHtmlEntities } from "../../utils/text-normalization"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
import { Terminal } from "../../integrations/terminal/Terminal"
@ -192,7 +192,7 @@ export async function executeCommand(
if (terminalProvider === "vscode") {
callbacks.onNoShellIntegration = async (error: string) => {
telemetryService.captureShellIntegrationError(cline.taskId)
TelemetryService.instance.captureShellIntegrationError(cline.taskId)
shellIntegrationError = error
}
}

View file

@ -9,19 +9,23 @@ import axios from "axios"
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import type {
GlobalState,
ProviderName,
ProviderSettings,
RooCodeSettings,
ProviderSettingsEntry,
TelemetryProperties,
CodeActionId,
CodeActionName,
TerminalActionId,
TerminalActionPromptType,
HistoryItem,
import {
type GlobalState,
type ProviderName,
type ProviderSettings,
type RooCodeSettings,
type ProviderSettingsEntry,
type TelemetryProperties,
type TelemetryPropertiesProvider,
type CodeActionId,
type CodeActionName,
type TerminalActionId,
type TerminalActionPromptType,
type HistoryItem,
ORGANIZATION_ALLOW_ALL,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService } from "@roo-code/cloud"
import { t } from "../../i18n"
import { setPanel } from "../../activate/registerCommands"
@ -53,11 +57,11 @@ import { Task, TaskOptions } from "../task/Task"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { getSystemPromptFilePath } from "../prompts/sections/custom-system-prompt"
import { TelemetryPropertiesProvider, telemetryService } from "../../services/telemetry"
import { getWorkspacePath } from "../../utils/path"
import { webviewMessageHandler } from "./webviewMessageHandler"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels"
import { ProfileValidator } from "../../shared/ProfileValidator"
/**
* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@ -68,6 +72,12 @@ export type ClineProviderEvents = {
clineCreated: [cline: Task]
}
class OrganizationAllowListViolationError extends Error {
constructor(message: string) {
super(message)
}
}
export class ClineProvider
extends EventEmitter<ClineProviderEvents>
implements vscode.WebviewViewProvider, TelemetryPropertiesProvider
@ -114,7 +124,7 @@ export class ClineProvider
// Register this provider with the telemetry service to enable it to add
// properties like mode and provider.
telemetryService.setProvider(this)
TelemetryService.instance.setProvider(this)
this._workspaceTracker = new WorkspaceTracker(this)
@ -288,7 +298,7 @@ export class ClineProvider
params: Record<string, string | any[]>,
): Promise<void> {
// Capture telemetry for code action usage
telemetryService.captureCodeActionUsed(promptType)
TelemetryService.instance.captureCodeActionUsed(promptType)
const visibleProvider = await ClineProvider.getInstance()
@ -314,7 +324,7 @@ export class ClineProvider
promptType: TerminalActionPromptType,
params: Record<string, string | any[]>,
): Promise<void> {
telemetryService.captureCodeActionUsed(promptType)
TelemetryService.instance.captureCodeActionUsed(promptType)
const visibleProvider = await ClineProvider.getInstance()
@ -330,7 +340,15 @@ export class ClineProvider
return
}
await visibleProvider.initClineWithTask(prompt)
try {
await visibleProvider.initClineWithTask(prompt)
} catch (error) {
if (error instanceof OrganizationAllowListViolationError) {
// Errors from terminal commands seem to get swallowed / ignored.
vscode.window.showErrorMessage(error.message)
}
throw error
}
}
async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) {
@ -494,12 +512,17 @@ export class ClineProvider
) {
const {
apiConfiguration,
organizationAllowList,
diffEnabled: enableDiff,
enableCheckpoints,
fuzzyMatchThreshold,
experiments,
} = await this.getState()
if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
}
const cline = new Task({
provider: this,
apiConfiguration,
@ -628,7 +651,7 @@ export class ClineProvider
"default-src 'none'",
`font-src ${webview.cspSource}`,
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} data:`,
`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
`media-src ${webview.cspSource}`,
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
@ -713,7 +736,7 @@ export class ClineProvider
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://us-assets.i.posthog.com 'strict-dynamic'; connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<script nonce="${nonce}">
@ -752,7 +775,7 @@ export class ClineProvider
const cline = this.getCurrentCline()
if (cline) {
telemetryService.captureModeSwitch(cline.taskId, newMode)
TelemetryService.instance.captureModeSwitch(cline.taskId, newMode)
cline.emit("taskModeSwitched", cline.taskId, newMode)
}
@ -1273,6 +1296,7 @@ export class ClineProvider
maxReadFileLine,
terminalCompressProgressBar,
historyPreviewCollapsed,
organizationAllowList,
condensingApiConfigId,
customCondensingPrompt,
codebaseIndexConfig,
@ -1367,6 +1391,7 @@ export class ClineProvider
terminalCompressProgressBar: terminalCompressProgressBar ?? true,
hasSystemPromptOverride,
historyPreviewCollapsed: historyPreviewCollapsed ?? false,
organizationAllowList,
condensingApiConfigId,
customCondensingPrompt,
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
@ -1401,6 +1426,16 @@ export class ClineProvider
providerSettings.apiProvider = apiProvider
}
let organizationAllowList = ORGANIZATION_ALLOW_ALL
try {
organizationAllowList = await CloudService.instance.getAllowList()
} catch (error) {
console.error(
`[getState] failed to get organization allow list: ${error instanceof Error ? error.message : String(error)}`,
)
}
// Return the same structure as before
return {
apiConfiguration: providerSettings,
@ -1469,6 +1504,7 @@ export class ClineProvider
showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true,
maxReadFileLine: stateValues.maxReadFileLine ?? -1,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
organizationAllowList,
// Explicitly add condensing settings
condensingApiConfigId: stateValues.condensingApiConfigId,
customCondensingPrompt: stateValues.customCondensingPrompt,

View file

@ -1,10 +1,11 @@
// npx jest src/core/webview/__tests__/ClineProvider.test.ts
// npx jest core/webview/__tests__/ClineProvider.test.ts
import Anthropic from "@anthropic-ai/sdk"
import * as vscode from "vscode"
import axios from "axios"
import type { ProviderSettingsEntry, ClineMessage } from "@roo-code/types"
import { type ProviderSettingsEntry, type ClineMessage, ORGANIZATION_ALLOW_ALL } from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
import { defaultModeSlug } from "../../../shared/modes"
@ -94,13 +95,11 @@ jest.mock("../../../services/browser/browserDiscovery", () => ({
}),
}))
// Initialize mocks
const mockAddCustomInstructions = jest.fn().mockResolvedValue("Combined instructions")
;(jest.requireMock("../../prompts/sections/custom-instructions") as any).addCustomInstructions =
mockAddCustomInstructions
// Mock delay module
jest.mock("delay", () => {
const delayFn = (_ms: number) => Promise.resolve()
delayFn.createDelay = () => delayFn
@ -109,7 +108,7 @@ jest.mock("delay", () => {
return delayFn
})
// MCP-related modules are mocked once above (lines 87-109)
// MCP-related modules are mocked once above (lines 87-109).
jest.mock(
"@modelcontextprotocol/sdk/client/index.js",
@ -240,10 +239,12 @@ describe("ClineProvider", () => {
let updateGlobalStateSpy: jest.SpyInstance<ClineProvider["contextProxy"]["updateGlobalState"]>
beforeEach(() => {
// Reset mocks
jest.clearAllMocks()
// Mock context
if (!TelemetryService.hasInstance()) {
TelemetryService.createInstance([])
}
const globalState: Record<string, string | undefined> = {
mode: "architect",
currentApiConfigName: "current-config",
@ -422,6 +423,7 @@ describe("ClineProvider", () => {
showRooIgnoredFiles: true,
renderContext: "sidebar",
maxReadFileLine: 500,
organizationAllowList: ORGANIZATION_ALLOW_ALL,
autoCondenseContext: true,
autoCondenseContextPercent: 100,
}

View file

@ -3,13 +3,16 @@ import fs from "fs/promises"
import pWaitFor from "p-wait-for"
import * as vscode from "vscode"
import type { Language, ProviderSettings, GlobalState } from "@roo-code/types"
import { type Language, type ProviderSettings, type GlobalState, TelemetryEventName } from "@roo-code/types"
import { CloudService } from "@roo-code/cloud"
import { TelemetryService } from "@roo-code/telemetry"
import { ClineProvider } from "./ClineProvider"
import { changeLanguage, t } from "../../i18n"
import { Package } from "../../shared/package"
import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
import { supportPrompt } from "../../shared/support-prompt"
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
import { checkExistKey } from "../../shared/checkExistApiConfig"
import { experimentDefault } from "../../shared/experiments"
@ -29,7 +32,6 @@ import { getOllamaModels } from "../../api/providers/ollama"
import { getVsCodeLmModels } from "../../api/providers/vscode-lm"
import { getLmStudioModels } from "../../api/providers/lmstudio"
import { openMention } from "../mentions"
import { telemetryService } from "../../services/telemetry"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { getWorkspacePath } from "../../utils/path"
import { Mode, defaultModeSlug } from "../../shared/modes"
@ -104,6 +106,16 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
await Promise.all([
await updateGlobalState("listApiConfigMeta", listApiConfig),
await provider.postMessageToWebview({ type: "listApiConfig", listApiConfig }),
async () => {
try {
if (CloudService.instance.hasActiveSession()) {
const userInfo = await CloudService.instance.getUserInfo()
provider.postMessageToWebview({ type: "authenticatedUser", userInfo })
}
} catch (error) {
provider.log(`AuthService#getUserInfo failed: ${error}`)
}
},
])
})
.catch((error) =>
@ -116,7 +128,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
provider.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
TelemetryService.instance.updateTelemetryState(isOptedIn)
})
provider.isViewLaunched = true
@ -1018,7 +1030,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
// Capture telemetry for prompt enhancement.
const currentCline = provider.getCurrentCline()
telemetryService.capturePromptEnhanced(currentCline?.taskId)
TelemetryService.instance.capturePromptEnhanced(currentCline?.taskId)
await provider.postMessageToWebview({ type: "enhancedPrompt", text: enhancedPrompt })
} catch (error) {
@ -1322,10 +1334,38 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
const telemetrySetting = message.text as TelemetrySetting
await updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
TelemetryService.instance.updateTelemetryState(isOptedIn)
await provider.postStateToWebview()
break
}
case "accountButtonClicked": {
// Navigate to the account tab.
provider.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
break
}
case "rooCloudSignIn": {
try {
TelemetryService.instance.captureEvent(TelemetryEventName.AUTHENTICATION_INITIATED)
await CloudService.instance.login()
} catch (error) {
provider.log(`AuthService#login failed: ${error}`)
vscode.window.showErrorMessage("Sign in failed.")
}
break
}
case "rooCloudSignOut": {
try {
await CloudService.instance.logout()
await provider.postStateToWebview()
provider.postMessageToWebview({ type: "authenticatedUser", userInfo: undefined })
} catch (error) {
provider.log(`AuthService#logout failed: ${error}`)
vscode.window.showErrorMessage("Sign out failed.")
}
break
}
case "codebaseIndexConfig": {
const codebaseIndexConfig = message.values ?? {
codebaseIndexEnabled: false,

View file

@ -99,9 +99,6 @@ async function main() {
entryPoints: ["extension.ts"],
outfile: "dist/extension.js",
external: ["vscode"],
alias: {
"@roo-code/types": path.resolve(__dirname, "../packages/types/dist/index.js"),
},
}
/**
@ -111,9 +108,6 @@ async function main() {
...buildOptions,
entryPoints: ["workers/countTokens.ts"],
outdir: "dist/workers",
alias: {
"@roo-code/types": path.resolve(__dirname, "../packages/types/dist/index.js"),
},
}
const [extensionCtx, workerCtx] = await Promise.all([

View file

@ -12,6 +12,9 @@ try {
console.warn("Failed to load environment variables:", e)
}
import { CloudService } from "@roo-code/cloud"
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
import { Package } from "./shared/package"
@ -21,7 +24,6 @@ import { ClineProvider } from "./core/webview/ClineProvider"
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { McpServerManager } from "./services/mcp/McpServerManager"
import { telemetryService } from "./services/telemetry/TelemetryService"
import { CodeIndexManager } from "./services/code-index/manager"
import { migrateSettings } from "./utils/migrateSettings"
import { API } from "./extension/api"
@ -57,8 +59,21 @@ export async function activate(context: vscode.ExtensionContext) {
// Migrate old settings to new
await migrateSettings(context, outputChannel)
// Initialize telemetry service after environment variables are loaded.
telemetryService.initialize(context)
// Initialize telemetry service.
const telemetryService = TelemetryService.createInstance()
try {
telemetryService.register(new PostHogTelemetryClient())
} catch (error) {
console.warn("Failed to register PostHogTelemetryClient:", error)
}
// Initialize Roo Code Cloud service.
await CloudService.createInstance(context, {
userChanged: (userInfo) =>
ClineProvider.getVisibleInstance()?.postMessageToWebview({ type: "authenticatedUser", userInfo }),
settingsChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),
})
// Initialize i18n for internationalization support
initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language))
@ -86,7 +101,7 @@ export async function activate(context: vscode.ExtensionContext) {
}
const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, codeIndexManager)
telemetryService.setProvider(provider)
TelemetryService.instance.setProvider(provider)
if (codeIndexManager) {
context.subscriptions.push(codeIndexManager)
@ -147,18 +162,29 @@ export async function activate(context: vscode.ExtensionContext) {
// Watch the core files and automatically reload the extension host.
if (process.env.NODE_ENV === "development") {
console.log(`♻️♻️♻️ Core auto-reloading is ENABLED! Watching for changes in ${context.extensionPath}/**/*.ts`)
const pattern = "**/*.ts"
const watcher = vscode.workspace.createFileSystemWatcher(
new vscode.RelativePattern(context.extensionPath, "**/*.ts"),
const watchPaths = [
{ path: context.extensionPath, name: "extension" },
{ path: path.join(context.extensionPath, "../packages/types"), name: "types" },
{ path: path.join(context.extensionPath, "../packages/telemetry"), name: "telemetry" },
{ path: path.join(context.extensionPath, "../packages/cloud"), name: "cloud" },
]
console.log(
`♻️♻️♻️ Core auto-reloading is ENABLED. Watching for changes in: ${watchPaths.map(({ name }) => name).join(", ")}`,
)
watcher.onDidChange((uri) => {
console.log(`♻️ File changed: ${uri.fsPath}. Reloading host…`)
vscode.commands.executeCommand("workbench.action.reloadWindow")
})
watchPaths.forEach(({ path: watchPath, name }) => {
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(watchPath, pattern))
context.subscriptions.push(watcher)
watcher.onDidChange((uri) => {
console.log(`♻️ ${name} file changed: ${uri.fsPath}. Reloading host…`)
vscode.commands.executeCommand("workbench.action.reloadWindow")
})
context.subscriptions.push(watcher)
})
}
return new API(outputChannel, provider, socketPath, enableLogging)
@ -168,6 +194,6 @@ export async function activate(context: vscode.ExtensionContext) {
export async function deactivate() {
outputChannel.appendLine(`${Package.name} extension deactivated`)
await McpServerManager.cleanup(extensionContext)
telemetryService.shutdown()
TelemetryService.instance.shutdown()
TerminalRegistry.cleanup()
}

View file

@ -58,6 +58,7 @@
"cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}",
"settings_import_failed": "Ha fallat la importació de la configuració: {{error}}.",
"mistake_limit_guidance": "Això pot indicar un error en el procés de pensament del model o la incapacitat d'utilitzar una eina correctament, que es pot mitigar amb orientació de l'usuari (p. ex. \"Prova de dividir la tasca en passos més petits\").",
"violated_organization_allowlist": "Ha fallat l'execució de la tasca: el perfil actual infringeix la configuració de la teva organització",
"condense_failed": "Ha fallat la condensació del context",
"condense_not_enough_messages": "No hi ha prou missatges per condensar el context",
"condensed_recently": "El context s'ha condensat recentment; s'omet aquest intent",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}",
"settings_import_failed": "Fehler beim Importieren der Einstellungen: {{error}}.",
"mistake_limit_guidance": "Dies kann auf einen Fehler im Denkprozess des Modells oder die Unfähigkeit hinweisen, ein Tool richtig zu verwenden, was durch Benutzerführung behoben werden kann (z.B. \"Versuche, die Aufgabe in kleinere Schritte zu unterteilen\").",
"violated_organization_allowlist": "Aufgabe konnte nicht ausgeführt werden: Das aktuelle Profil verstößt gegen die Einstellungen deiner Organisation",
"condense_failed": "Fehler beim Verdichten des Kontexts",
"condense_not_enough_messages": "Nicht genügend Nachrichten zum Verdichten des Kontexts",
"condensed_recently": "Kontext wurde kürzlich verdichtet; dieser Versuch wird übersprungen",

View file

@ -54,6 +54,7 @@
"failed_update_project_mcp": "Failed to update project MCP servers",
"settings_import_failed": "Settings import failed: {{error}}.",
"mistake_limit_guidance": "This may indicate a failure in the model's thought process or inability to use a tool properly, which can be mitigated with some user guidance (e.g. \"Try breaking down the task into smaller steps\").",
"violated_organization_allowlist": "Failed to run task: the current profile violates your organization settings",
"condense_failed": "Failed to condense context",
"condense_not_enough_messages": "Not enough messages to condense context",
"condensed_recently": "Context was condensed recently; skipping this attempt",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}",
"settings_import_failed": "Error al importar la configuración: {{error}}.",
"mistake_limit_guidance": "Esto puede indicar un fallo en el proceso de pensamiento del modelo o la incapacidad de usar una herramienta correctamente, lo cual puede mitigarse con orientación del usuario (ej. \"Intenta dividir la tarea en pasos más pequeños\").",
"violated_organization_allowlist": "Error al ejecutar la tarea: el perfil actual infringe la configuración de tu organización",
"condense_failed": "Error al condensar el contexto",
"condense_not_enough_messages": "No hay suficientes mensajes para condensar el contexto",
"condensed_recently": "El contexto se condensó recientemente; se omite este intento",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}",
"settings_import_failed": "Échec de l'importation des paramètres : {{error}}",
"mistake_limit_guidance": "Cela peut indiquer un échec dans le processus de réflexion du modèle ou une incapacité à utiliser un outil correctement, ce qui peut être atténué avec des conseils de l'utilisateur (par ex. \"Essaie de diviser la tâche en étapes plus petites\").",
"violated_organization_allowlist": "Échec de l'exécution de la tâche : le profil actuel enfreint les paramètres de votre organisation",
"condense_failed": "Échec de la condensation du contexte",
"condense_not_enough_messages": "Pas assez de messages pour condenser le contexte",
"condensed_recently": "Le contexte a été condensé récemment ; cette tentative est ignorée",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}",
"settings_import_failed": "सेटिंग्स इम्पोर्ट करने में विफल: {{error}}।",
"mistake_limit_guidance": "यह मॉडल की सोच प्रक्रिया में विफलता या किसी टूल का सही उपयोग न कर पाने का संकेत हो सकता है, जिसे उपयोगकर्ता के मार्गदर्शन से ठीक किया जा सकता है (जैसे \"कार्य को छोटे चरणों में बांटने की कोशिश करें\")।",
"violated_organization_allowlist": "कार्य चलाने में विफल: वर्तमान प्रोफ़ाइल आपके संगठन की सेटिंग्स का उल्लंघन करती है",
"condense_failed": "संदर्भ को संक्षिप्त करने में विफल",
"condense_not_enough_messages": "संदर्भ को संक्षिप्त करने के लिए पर्याप्त संदेश नहीं हैं",
"condensed_recently": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}",
"settings_import_failed": "Importazione delle impostazioni fallita: {{error}}.",
"mistake_limit_guidance": "Questo può indicare un fallimento nel processo di pensiero del modello o l'incapacità di utilizzare correttamente uno strumento, che può essere mitigato con la guida dell'utente (ad es. \"Prova a suddividere l'attività in passaggi più piccoli\").",
"violated_organization_allowlist": "Impossibile eseguire l'attività: il profilo corrente viola le impostazioni della tua organizzazione",
"condense_failed": "Impossibile condensare il contesto",
"condense_not_enough_messages": "Non ci sono abbastanza messaggi per condensare il contesto",
"condensed_recently": "Il contesto è stato condensato di recente; questo tentativo viene saltato",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "パス {{path}} にアクセスできません:{{error}}",
"settings_import_failed": "設定のインポートに失敗しました:{{error}}",
"mistake_limit_guidance": "これは、モデルの思考プロセスの失敗やツールを適切に使用できないことを示している可能性があり、ユーザーのガイダンスによって軽減できます(例:「タスクをより小さなステップに分割してみてください」)。",
"violated_organization_allowlist": "タスクの実行に失敗しました: 現在のプロファイルは組織の設定に違反しています",
"condense_failed": "コンテキストの圧縮に失敗しました",
"condense_not_enough_messages": "コンテキストを圧縮するのに十分なメッセージがありません",
"condensed_recently": "コンテキストは最近圧縮されました;この試行をスキップします",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}",
"settings_import_failed": "설정 가져오기 실패: {{error}}.",
"mistake_limit_guidance": "이는 모델의 사고 과정 실패나 도구를 제대로 사용하지 못하는 것을 나타낼 수 있으며, 사용자 가이드를 통해 완화할 수 있습니다 (예: \"작업을 더 작은 단계로 나누어 시도해보세요\").",
"violated_organization_allowlist": "작업 실행 실패: 현재 프로필이 조직 설정을 위반합니다",
"condense_failed": "컨텍스트 압축에 실패했습니다",
"condense_not_enough_messages": "컨텍스트를 압축할 메시지가 충분하지 않습니다",
"condensed_recently": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다",

View file

@ -54,6 +54,7 @@
"failed_update_project_mcp": "Bijwerken van project MCP-servers mislukt",
"settings_import_failed": "Importeren van instellingen mislukt: {{error}}.",
"mistake_limit_guidance": "Dit kan duiden op een fout in het denkproces van het model of het onvermogen om een tool correct te gebruiken, wat kan worden verminderd met gebruikersbegeleiding (bijv. \"Probeer de taak op te delen in kleinere stappen\").",
"violated_organization_allowlist": "Taak uitvoeren mislukt: het huidige profiel schendt de instellingen van uw organisatie",
"condense_failed": "Comprimeren van context mislukt",
"condense_not_enough_messages": "Niet genoeg berichten om context te comprimeren",
"condensed_recently": "Context is recent gecomprimeerd; deze poging wordt overgeslagen",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}",
"settings_import_failed": "Nie udało się zaimportować ustawień: {{error}}.",
"mistake_limit_guidance": "To może wskazywać na błąd w procesie myślowym modelu lub niezdolność do prawidłowego użycia narzędzia, co można złagodzić poprzez wskazówki użytkownika (np. \"Spróbuj podzielić zadanie na mniejsze kroki\").",
"violated_organization_allowlist": "Nie udało się uruchomić zadania: bieżący profil narusza ustawienia Twojej organizacji",
"condense_failed": "Nie udało się skondensować kontekstu",
"condense_not_enough_messages": "Za mało wiadomości do skondensowania kontekstu",
"condensed_recently": "Kontekst został niedawno skondensowany; pomijanie tej próby",

View file

@ -58,6 +58,7 @@
"cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}",
"settings_import_failed": "Falha ao importar configurações: {{error}}",
"mistake_limit_guidance": "Isso pode indicar uma falha no processo de pensamento do modelo ou incapacidade de usar uma ferramenta adequadamente, o que pode ser mitigado com orientação do usuário (ex. \"Tente dividir a tarefa em etapas menores\").",
"violated_organization_allowlist": "Falha ao executar a tarefa: o perfil atual viola as configurações da sua organização",
"condense_failed": "Falha ao condensar o contexto",
"condense_not_enough_messages": "Não há mensagens suficientes para condensar o contexto",
"condensed_recently": "O contexto foi condensado recentemente; pulando esta tentativa",

View file

@ -54,6 +54,7 @@
"failed_update_project_mcp": "Не удалось обновить серверы проекта MCP",
"settings_import_failed": "Не удалось импортировать настройки: {{error}}.",
"mistake_limit_guidance": "Это может указывать на сбой в процессе мышления модели или неспособность правильно использовать инструмент, что можно смягчить с помощью руководства пользователя (например, \"Попробуйте разбить задачу на более мелкие шаги\").",
"violated_organization_allowlist": "Не удалось выполнить задачу: текущий профиль нарушает настройки вашей организации",
"condense_failed": "Не удалось сжать контекст",
"condense_not_enough_messages": "Недостаточно сообщений для сжатия контекста",
"condensed_recently": "Контекст был недавно сжат; пропускаем эту попытку",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}",
"settings_import_failed": "Ayarlar içe aktarılamadı: {{error}}.",
"mistake_limit_guidance": "Bu, modelin düşünce sürecindeki bir başarısızlığı veya bir aracı düzgün kullanamama durumunu gösterebilir, bu da kullanıcı rehberliği ile hafifletilebilir (örn. \"Görevi daha küçük adımlara bölmeyi deneyin\").",
"violated_organization_allowlist": "Görev yürütülemedi: Geçerli profil kuruluşunuzun ayarlarını ihlal ediyor",
"condense_failed": "Bağlam sıkıştırılamadı",
"condense_not_enough_messages": "Bağlamı sıkıştırmak için yeterli mesaj yok",
"condensed_recently": "Bağlam yakın zamanda sıkıştırıldı; bu deneme atlanıyor",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}",
"settings_import_failed": "Nhập cài đặt thất bại: {{error}}.",
"mistake_limit_guidance": "Điều này có thể cho thấy sự thất bại trong quá trình suy nghĩ của mô hình hoặc không thể sử dụng công cụ đúng cách, có thể được giảm thiểu bằng hướng dẫn của người dùng (ví dụ: \"Hãy thử chia nhỏ nhiệm vụ thành các bước nhỏ hơn\").",
"violated_organization_allowlist": "Không thể chạy tác vụ: hồ sơ hiện tại vi phạm cài đặt của tổ chức của bạn",
"condense_failed": "Không thể nén ngữ cảnh",
"condense_not_enough_messages": "Không đủ tin nhắn để nén ngữ cảnh",
"condensed_recently": "Ngữ cảnh đã được nén gần đây; bỏ qua lần thử này",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "无法访问路径 {{path}}{{error}}",
"settings_import_failed": "设置导入失败:{{error}}。",
"mistake_limit_guidance": "这可能表明模型思维过程失败或无法正确使用工具,可通过用户指导来缓解(例如\"尝试将任务分解为更小的步骤\")。",
"violated_organization_allowlist": "执行任务失败:当前配置文件违反了您的组织设置",
"condense_failed": "压缩上下文失败",
"condense_not_enough_messages": "没有足够的对话来压缩上下文",
"condensed_recently": "上下文最近已压缩;跳过此次尝试",

View file

@ -54,6 +54,7 @@
"cannot_access_path": "無法存取路徑 {{path}}{{error}}",
"settings_import_failed": "設定匯入失敗:{{error}}。",
"mistake_limit_guidance": "這可能表明模型思維過程失敗或無法正確使用工具,可透過使用者指導來緩解(例如「嘗試將工作分解為更小的步驟」)。",
"violated_organization_allowlist": "執行工作失敗:目前設定檔違反了您的組織設定",
"condense_failed": "壓縮上下文失敗",
"condense_not_enough_messages": "沒有足夠的訊息來壓縮上下文",
"condensed_recently": "上下文最近已壓縮;跳過此次嘗試",

View file

@ -95,6 +95,12 @@
"title": "%command.openInEditor.title%",
"icon": "$(link-external)"
},
{
"command": "roo-cline.accountButtonClicked",
"title": "Account",
"icon": "$(account)",
"when": "config.roo-cline.rooCodeCloudEnabled"
},
{
"command": "roo-cline.settingsButtonClicked",
"title": "%command.settings.title%",
@ -229,8 +235,13 @@
"when": "view == roo-cline.SidebarProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"command": "roo-cline.accountButtonClicked",
"group": "navigation@6",
"when": "view == roo-cline.SidebarProvider && config.roo-cline.rooCodeCloudEnabled"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@7",
"when": "view == roo-cline.SidebarProvider"
}
],
@ -261,8 +272,13 @@
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
},
{
"command": "roo-cline.settingsButtonClicked",
"command": "roo-cline.accountButtonClicked",
"group": "navigation@6",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider && config.roo-cline.rooCodeCloudEnabled"
},
{
"command": "roo-cline.settingsButtonClicked",
"group": "navigation@7",
"when": "activeWebviewPanelId == roo-cline.TabPanelProvider"
}
]
@ -313,6 +329,11 @@
"type": "string",
"default": "",
"description": "%settings.customStoragePath.description%"
},
"roo-cline.rooCodeCloudEnabled": {
"type": "boolean",
"default": false,
"description": "%settings.rooCodeCloudEnabled.description%"
}
}
}
@ -340,6 +361,8 @@
"@google/genai": "^0.13.0",
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.9.0",
"@roo-code/cloud": "workspace:^",
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"@qdrant/js-client-rest": "^1.14.0",
"@types/lodash.debounce": "^4.0.9",
@ -373,7 +396,6 @@
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"pkce-challenge": "^4.1.0",
"posthog-node": "^4.7.0",
"pretty-bytes": "^6.1.1",
"ps-tree": "^1.2.0",
"puppeteer-chromium-resolver": "^23.0.0",

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Configuració per a l'API del model de llenguatge VSCode",
"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.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."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Einstellungen für die VSCode-Sprachmodell-API",
"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.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."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Configuración para la API del modelo de lenguaje VSCode",
"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.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."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Paramètres pour l'API du modèle de langage VSCode",
"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.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."
}

View file

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

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Impostazioni per l'API del modello linguistico VSCode",
"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.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."
}

View file

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

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Settings for VSCode Language Model API",
"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.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."
}

View file

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

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Instellingen voor VSCode Language Model API",
"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.customStoragePath.description": "Aangepast opslagpad. Laat leeg om de standaardlocatie te gebruiken. Ondersteunt absolute paden (bijv. 'D:\\RooCodeStorage')",
"settings.rooCodeCloudEnabled.description": "Roo Code Cloud inschakelen."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Ustawienia dla API modelu językowego VSCode",
"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.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."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Configurações para a API do modelo de linguagem do VSCode",
"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.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."
}

View file

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

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "VSCode dil modeli API'si için ayarlar",
"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.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."
}

View file

@ -27,5 +27,6 @@
"settings.vsCodeLmModelSelector.description": "Cài đặt cho API mô hình ngôn ngữ VSCode",
"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.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."
}

View file

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

View file

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

View file

@ -1,2 +0,0 @@
export * from "./TelemetryService"
export * from "./types"

View file

@ -1,19 +0,0 @@
import { TelemetryEventName, type TelemetryProperties, type TelemetryEvent } from "@roo-code/types"
export type TelemetryEventSubscription =
| { type: "include"; events: TelemetryEventName[] }
| { type: "exclude"; events: TelemetryEventName[] }
export interface TelemetryPropertiesProvider {
getTelemetryProperties(): Promise<TelemetryProperties>
}
export interface TelemetryClient {
subscription?: TelemetryEventSubscription
setProvider(provider: TelemetryPropertiesProvider): void
capture(options: TelemetryEvent): Promise<void>
updateTelemetryState(didUserOptIn: boolean): void
isTelemetryEnabled(): boolean
shutdown(): Promise<void>
}

View file

@ -7,6 +7,8 @@ import type {
TelemetrySetting,
ExperimentId,
ClineMessage,
OrganizationAllowList,
CloudUserInfo,
} from "@roo-code/types"
import { GitCommit } from "../utils/git"
@ -65,6 +67,7 @@ export interface ExtensionMessage {
| "setHistoryPreviewCollapsed"
| "commandExecutionStatus"
| "vsCodeSetting"
| "authenticatedUser"
| "condenseTaskContextResponse"
| "singleRouterModelFetchResponse"
| "indexingStatusUpdate"
@ -77,6 +80,7 @@ export interface ExtensionMessage {
| "settingsButtonClicked"
| "historyButtonClicked"
| "promptsButtonClicked"
| "accountButtonClicked"
| "didBecomeVisible"
| "focusInput"
invoke?: "newChat" | "sendMessage" | "primaryButtonClick" | "secondaryButtonClick" | "setChatBoxMessage"
@ -108,6 +112,8 @@ export interface ExtensionMessage {
error?: string
setting?: string
value?: any
userInfo?: CloudUserInfo
organizationAllowList?: OrganizationAllowList
}
export type ExtensionState = Pick<
@ -210,6 +216,9 @@ export type ExtensionState = Pick<
renderContext: "sidebar" | "editor"
settingsImportedAt?: number
historyPreviewCollapsed?: boolean
organizationAllowList: OrganizationAllowList
autoCondenseContext: boolean
autoCondenseContextPercent: number
}

Some files were not shown because too many files have changed in this diff Show more