mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Roo Code Cloud (#4069)
Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
This commit is contained in:
parent
4cf240f3db
commit
7820b7517a
174 changed files with 4549 additions and 1237 deletions
5
.changeset/swift-carrots-doubt.md
Normal file
5
.changeset/swift-carrots-doubt.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Roo Code Cloud
|
||||
|
|
@ -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
|
||||
|
|
|
|||
7
.github/workflows/code-qa.yml
vendored
7
.github/workflows/code-qa.yml
vendored
|
|
@ -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."
|
||||
|
|
|
|||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
|
|
@ -1,4 +1,4 @@
|
|||
name: "CodeQL Advanced"
|
||||
name: CodeQL Advanced
|
||||
|
||||
on:
|
||||
push:
|
||||
|
|
|
|||
1
.github/workflows/marketplace-publish.yml
vendored
1
.github/workflows/marketplace-publish.yml
vendored
|
|
@ -1,4 +1,5 @@
|
|||
name: Publish Extension
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
|
|
|||
2
.github/workflows/nightly-publish.yml
vendored
2
.github/workflows/nightly-publish.yml
vendored
|
|
@ -1,8 +1,6 @@
|
|||
name: Nightly Publish
|
||||
|
||||
on:
|
||||
# push:
|
||||
# branches: [main]
|
||||
workflow_run:
|
||||
workflows: ["Code QA Roo Code"]
|
||||
types:
|
||||
|
|
|
|||
4
packages/cloud/eslint.config.mjs
Normal file
4
packages/cloud/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
25
packages/cloud/package.json
Normal file
25
packages/cloud/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
395
packages/cloud/src/AuthService.ts
Normal file
395
packages/cloud/src/AuthService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
157
packages/cloud/src/CloudService.ts
Normal file
157
packages/cloud/src/CloudService.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
2
packages/cloud/src/Config.ts
Normal file
2
packages/cloud/src/Config.ts
Normal 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"
|
||||
|
|
@ -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)
|
||||
}
|
||||
87
packages/cloud/src/RooCodeTelemetryClient.ts
Normal file
87
packages/cloud/src/RooCodeTelemetryClient.ts
Normal 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() {}
|
||||
}
|
||||
137
packages/cloud/src/SettingsService.ts
Normal file
137
packages/cloud/src/SettingsService.ts
Normal 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
|
||||
}
|
||||
}
|
||||
87
packages/cloud/src/TelemetryClient.ts
Normal file
87
packages/cloud/src/TelemetryClient.ts
Normal 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() {}
|
||||
}
|
||||
50
packages/cloud/src/__mocks__/vscode.ts
Normal file
50
packages/cloud/src/__mocks__/vscode.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
}
|
||||
238
packages/cloud/src/__tests__/CloudService.test.ts
Normal file
238
packages/cloud/src/__tests__/CloudService.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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()
|
||||
250
packages/cloud/src/__tests__/RooCodeTelemetryClient.test.ts
Normal file
250
packages/cloud/src/__tests__/RooCodeTelemetryClient.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
250
packages/cloud/src/__tests__/TelemetryClient.test.ts
Normal file
250
packages/cloud/src/__tests__/TelemetryClient.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
1
packages/cloud/src/index.ts
Normal file
1
packages/cloud/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from "./CloudService"
|
||||
6
packages/cloud/src/types.ts
Normal file
6
packages/cloud/src/types.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { CloudUserInfo } from "@roo-code/types"
|
||||
|
||||
export interface CloudServiceCallbacks {
|
||||
userChanged?: (userInfo: CloudUserInfo | undefined) => void
|
||||
settingsChanged?: () => void
|
||||
}
|
||||
5
packages/cloud/tsconfig.json
Normal file
5
packages/cloud/tsconfig.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/vscode-library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
13
packages/cloud/vitest.config.ts
Normal file
13
packages/cloud/vitest.config.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
})
|
||||
12
packages/config-typescript/vscode-library.json
Normal file
12
packages/config-typescript/vscode-library.json
Normal 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
|
||||
}
|
||||
}
|
||||
4
packages/telemetry/eslint.config.mjs
Normal file
4
packages/telemetry/eslint.config.mjs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import { config } from "@roo-code/config-eslint/base"
|
||||
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
export default [...config]
|
||||
25
packages/telemetry/package.json
Normal file
25
packages/telemetry/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
})
|
||||
3
packages/telemetry/src/index.ts
Normal file
3
packages/telemetry/src/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export * from "./BaseTelemetryClient"
|
||||
export * from "./PostHogTelemetryClient"
|
||||
export * from "./TelemetryService"
|
||||
5
packages/telemetry/tsconfig.json
Normal file
5
packages/telemetry/tsconfig.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "@roo-code/config-typescript/vscode-library.json",
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
8
packages/telemetry/vitest.config.ts
Normal file
8
packages/telemetry/vitest.config.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from "vitest/config"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
},
|
||||
})
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
49
packages/types/src/cloud.ts
Normal file
49
packages/types/src/cloud.ts
Normal 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>
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export const commandIds = [
|
|||
"mcpButtonClicked",
|
||||
"historyButtonClicked",
|
||||
"popoutButtonClicked",
|
||||
"accountButtonClicked",
|
||||
"settingsButtonClicked",
|
||||
|
||||
"openInNewTab",
|
||||
|
|
|
|||
65
pnpm-lock.yaml
generated
65
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "コンテキストは最近圧縮されました;この試行をスキップします",
|
||||
|
|
|
|||
|
|
@ -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": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "Контекст был недавно сжат; пропускаем эту попытку",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": "上下文最近已压缩;跳过此次尝试",
|
||||
|
|
|
|||
|
|
@ -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": "上下文最近已壓縮;跳過此次嘗試",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 सक्षम करें।"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 を有効にする。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 사용 설정"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
export * from "./TelemetryService"
|
||||
export * from "./types"
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
Loading…
Add table
Reference in a new issue