Move @roo-code/cloud to the Roo-Code repo (#7503)

This commit is contained in:
Chris Estreich 2025-08-28 11:18:45 -07:00 committed by Hannes Rudolph
parent 8d94af5b7a
commit af00c627b7
20 changed files with 1723 additions and 1022 deletions

View file

@ -129,7 +129,6 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
private changeState(newState: AuthState): void {
const previousState = this.state
this.state = newState
this.log(`[auth] changeState: ${previousState} -> ${newState}`)
this.emit("auth-state-changed", { state: newState, previousState })
}
@ -163,6 +162,8 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
this.userInfo = null
this.changeState("logged-out")
this.log("[auth] Transitioned to logged-out state")
}
private transitionToAttemptingSession(credentials: AuthCredentials): void {
@ -175,6 +176,8 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
this.changeState("attempting-session")
this.timer.start()
this.log("[auth] Transitioned to attempting-session state")
}
private transitionToInactiveSession(): void {
@ -182,6 +185,8 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
this.userInfo = null
this.changeState("inactive-session")
this.log("[auth] Transitioned to inactive-session state")
}
/**
@ -417,6 +422,7 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
if (previousState !== "active-session") {
this.changeState("active-session")
this.log("[auth] Transitioned to active-session state")
this.fetchUserInfo()
} else {
this.state = "active-session"
@ -563,7 +569,11 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
)?.email_address
}
let extensionBridgeEnabled = true
// Check for extension_bridge_enabled in user's public metadata
let extensionBridgeEnabled = false
if (userData.public_metadata?.extension_bridge_enabled === true) {
extensionBridgeEnabled = true
}
// Fetch organization info if user is in organization context
try {
@ -579,7 +589,11 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
if (userMembership) {
this.setUserOrganizationInfo(userInfo, userMembership)
extensionBridgeEnabled = await this.isExtensionBridgeEnabledForOrganization(storedOrgId)
// Check organization public metadata for extension_bridge_enabled
// Organization setting takes precedence over user setting
if (await this.isExtensionBridgeEnabledForOrganization(storedOrgId)) {
extensionBridgeEnabled = true
}
this.log("[auth] User in organization context:", {
id: userMembership.organization.id,
@ -600,9 +614,10 @@ export class WebAuthService extends EventEmitter<AuthServiceEvents> implements A
if (primaryOrgMembership) {
this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
extensionBridgeEnabled = await this.isExtensionBridgeEnabledForOrganization(
primaryOrgMembership.organization.id,
)
// Check organization public metadata for extension_bridge_enabled
if (await this.isExtensionBridgeEnabledForOrganization(primaryOrgMembership.organization.id)) {
extensionBridgeEnabled = true
}
this.log("[auth] Legacy credentials: Found organization membership:", {
id: primaryOrgMembership.organization.id,

View file

@ -13,10 +13,6 @@ export const Uri = {
parse: vi.fn((uri: string) => ({ toString: () => uri })),
}
export const commands = {
executeCommand: vi.fn().mockResolvedValue(undefined),
}
export interface ExtensionContext {
secrets: {
get: (key: string) => Promise<string | undefined>

View file

@ -560,7 +560,7 @@ describe("WebAuthService", () => {
name: "John Doe",
email: "john@example.com",
picture: "https://example.com/avatar.jpg",
extensionBridgeEnabled: true,
extensionBridgeEnabled: false,
},
})
})
@ -725,7 +725,7 @@ describe("WebAuthService", () => {
name: "Jane Smith",
email: "jane@example.com",
picture: "https://example.com/jane.jpg",
extensionBridgeEnabled: true,
extensionBridgeEnabled: false,
})
})
@ -844,7 +844,7 @@ describe("WebAuthService", () => {
name: "John Doe",
email: undefined,
picture: undefined,
extensionBridgeEnabled: true,
extensionBridgeEnabled: false,
})
})
})
@ -969,7 +969,7 @@ describe("WebAuthService", () => {
name: "Test User",
email: undefined,
picture: undefined,
extensionBridgeEnabled: true,
extensionBridgeEnabled: false,
},
})
})

View file

@ -0,0 +1,290 @@
import crypto from "crypto"
import {
type TaskProviderLike,
type TaskLike,
type CloudUserInfo,
type ExtensionBridgeCommand,
type TaskBridgeCommand,
ConnectionState,
ExtensionSocketEvents,
TaskSocketEvents,
} from "@roo-code/types"
import { SocketConnectionManager } from "./SocketConnectionManager.js"
import { ExtensionManager } from "./ExtensionManager.js"
import { TaskManager } from "./TaskManager.js"
export interface ExtensionBridgeServiceOptions {
userId: string
socketBridgeUrl: string
token: string
provider: TaskProviderLike
sessionId?: string
}
export class ExtensionBridgeService {
private static instance: ExtensionBridgeService | null = null
// Core
private readonly userId: string
private readonly socketBridgeUrl: string
private readonly token: string
private readonly provider: TaskProviderLike
private readonly instanceId: string
// Managers
private connectionManager: SocketConnectionManager
private extensionManager: ExtensionManager
private taskManager: TaskManager
// Reconnection
private readonly MAX_RECONNECT_ATTEMPTS = Infinity
private readonly RECONNECT_DELAY = 1_000
private readonly RECONNECT_DELAY_MAX = 30_000
public static getInstance(): ExtensionBridgeService | null {
return ExtensionBridgeService.instance
}
public static async createInstance(options: ExtensionBridgeServiceOptions) {
console.log("[ExtensionBridgeService] createInstance")
ExtensionBridgeService.instance = new ExtensionBridgeService(options)
await ExtensionBridgeService.instance.initialize()
return ExtensionBridgeService.instance
}
public static resetInstance() {
if (ExtensionBridgeService.instance) {
console.log("[ExtensionBridgeService] resetInstance")
ExtensionBridgeService.instance.disconnect().catch(() => {})
ExtensionBridgeService.instance = null
}
}
public static async handleRemoteControlState(
userInfo: CloudUserInfo | null,
remoteControlEnabled: boolean | undefined,
options: ExtensionBridgeServiceOptions,
logger?: (message: string) => void,
) {
if (userInfo?.extensionBridgeEnabled && remoteControlEnabled) {
const existingService = ExtensionBridgeService.getInstance()
if (!existingService) {
try {
const service = await ExtensionBridgeService.createInstance(options)
const state = service.getConnectionState()
logger?.(`[ExtensionBridgeService#handleRemoteControlState] Instance created (state: ${state})`)
if (state !== ConnectionState.CONNECTED) {
logger?.(
`[ExtensionBridgeService#handleRemoteControlState] Service is not connected yet, will retry in background`,
)
}
} catch (error) {
const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to create instance: ${
error instanceof Error ? error.message : String(error)
}`
logger?.(message)
console.error(message)
}
} else {
const state = existingService.getConnectionState()
if (state === ConnectionState.FAILED || state === ConnectionState.DISCONNECTED) {
logger?.(
`[ExtensionBridgeService#handleRemoteControlState] Existing service is ${state}, attempting reconnection`,
)
existingService.reconnect().catch((error) => {
const message = `[ExtensionBridgeService#handleRemoteControlState] Reconnection failed: ${
error instanceof Error ? error.message : String(error)
}`
logger?.(message)
console.error(message)
})
}
}
} else {
const existingService = ExtensionBridgeService.getInstance()
if (existingService) {
try {
await existingService.disconnect()
ExtensionBridgeService.resetInstance()
logger?.(`[ExtensionBridgeService#handleRemoteControlState] Service disconnected and reset`)
} catch (error) {
const message = `[ExtensionBridgeService#handleRemoteControlState] Failed to disconnect and reset instance: ${
error instanceof Error ? error.message : String(error)
}`
logger?.(message)
console.error(message)
}
}
}
}
private constructor(options: ExtensionBridgeServiceOptions) {
this.userId = options.userId
this.socketBridgeUrl = options.socketBridgeUrl
this.token = options.token
this.provider = options.provider
this.instanceId = options.sessionId || crypto.randomUUID()
this.connectionManager = new SocketConnectionManager({
url: this.socketBridgeUrl,
socketOptions: {
query: {
token: this.token,
clientType: "extension",
instanceId: this.instanceId,
},
transports: ["websocket", "polling"],
reconnection: true,
reconnectionAttempts: this.MAX_RECONNECT_ATTEMPTS,
reconnectionDelay: this.RECONNECT_DELAY,
reconnectionDelayMax: this.RECONNECT_DELAY_MAX,
},
onConnect: () => this.handleConnect(),
onDisconnect: () => this.handleDisconnect(),
onReconnect: () => this.handleReconnect(),
})
this.extensionManager = new ExtensionManager(this.instanceId, this.userId, this.provider)
this.taskManager = new TaskManager()
}
private async initialize() {
// Populate the app and git properties before registering the instance.
await this.provider.getTelemetryProperties()
await this.connectionManager.connect()
this.setupSocketListeners()
}
private setupSocketListeners() {
const socket = this.connectionManager.getSocket()
if (!socket) {
console.error("[ExtensionBridgeService] Socket not available")
return
}
// Remove any existing listeners first to prevent duplicates.
socket.off(ExtensionSocketEvents.RELAYED_COMMAND)
socket.off(TaskSocketEvents.RELAYED_COMMAND)
socket.off("connected")
socket.on(ExtensionSocketEvents.RELAYED_COMMAND, (message: ExtensionBridgeCommand) => {
console.log(
`[ExtensionBridgeService] on(${ExtensionSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.instanceId}`,
)
this.extensionManager?.handleExtensionCommand(message)
})
socket.on(TaskSocketEvents.RELAYED_COMMAND, (message: TaskBridgeCommand) => {
console.log(
`[ExtensionBridgeService] on(${TaskSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.taskId}`,
)
this.taskManager.handleTaskCommand(message)
})
}
private async handleConnect() {
const socket = this.connectionManager.getSocket()
if (!socket) {
console.error("[ExtensionBridgeService] Socket not available after connect")
return
}
await this.extensionManager.onConnect(socket)
await this.taskManager.onConnect(socket)
}
private handleDisconnect() {
this.extensionManager.onDisconnect()
this.taskManager.onDisconnect()
}
private async handleReconnect() {
const socket = this.connectionManager.getSocket()
if (!socket) {
console.error("[ExtensionBridgeService] Socket not available after reconnect")
return
}
// Re-setup socket listeners to ensure they're properly configured
// after automatic reconnection (Socket.IO's built-in reconnection)
// The socket.off() calls in setupSocketListeners prevent duplicates
this.setupSocketListeners()
await this.extensionManager.onReconnect(socket)
await this.taskManager.onReconnect(socket)
}
// Task API
public async subscribeToTask(task: TaskLike): Promise<void> {
const socket = this.connectionManager.getSocket()
if (!socket || !this.connectionManager.isConnected()) {
console.warn("[ExtensionBridgeService] Cannot subscribe to task: not connected. Will retry when connected.")
this.taskManager.addPendingTask(task)
const state = this.connectionManager.getConnectionState()
if (state === ConnectionState.DISCONNECTED || state === ConnectionState.FAILED) {
this.initialize()
}
return
}
await this.taskManager.subscribeToTask(task, socket)
}
public async unsubscribeFromTask(taskId: string): Promise<void> {
const socket = this.connectionManager.getSocket()
if (!socket) {
return
}
await this.taskManager.unsubscribeFromTask(taskId, socket)
}
// Shared API
public getConnectionState(): ConnectionState {
return this.connectionManager.getConnectionState()
}
public async disconnect(): Promise<void> {
await this.extensionManager.cleanup(this.connectionManager.getSocket())
await this.taskManager.cleanup(this.connectionManager.getSocket())
await this.connectionManager.disconnect()
ExtensionBridgeService.instance = null
}
public async reconnect(): Promise<void> {
await this.connectionManager.reconnect()
// After a manual reconnect, we have a new socket instance
// so we need to set up listeners again.
this.setupSocketListeners()
}
}

View file

@ -0,0 +1,297 @@
import type { Socket } from "socket.io-client"
import {
type TaskProviderLike,
type ExtensionInstance,
type ExtensionBridgeCommand,
type ExtensionBridgeEvent,
RooCodeEventName,
TaskStatus,
ExtensionBridgeCommandName,
ExtensionBridgeEventName,
ExtensionSocketEvents,
HEARTBEAT_INTERVAL_MS,
} from "@roo-code/types"
export class ExtensionManager {
private instanceId: string
private userId: string
private provider: TaskProviderLike
private extensionInstance: ExtensionInstance
private heartbeatInterval: NodeJS.Timeout | null = null
private socket: Socket | null = null
constructor(instanceId: string, userId: string, provider: TaskProviderLike) {
this.instanceId = instanceId
this.userId = userId
this.provider = provider
this.extensionInstance = {
instanceId: this.instanceId,
userId: this.userId,
workspacePath: this.provider.cwd,
appProperties: this.provider.appProperties,
gitProperties: this.provider.gitProperties,
lastHeartbeat: Date.now(),
task: {
taskId: "",
taskStatus: TaskStatus.None,
},
taskHistory: [],
}
this.setupListeners()
}
public async onConnect(socket: Socket): Promise<void> {
this.socket = socket
await this.registerInstance(socket)
this.startHeartbeat(socket)
}
public onDisconnect(): void {
this.stopHeartbeat()
this.socket = null
}
public async onReconnect(socket: Socket): Promise<void> {
this.socket = socket
await this.registerInstance(socket)
this.startHeartbeat(socket)
}
public async cleanup(socket: Socket | null): Promise<void> {
this.stopHeartbeat()
if (socket) {
await this.unregisterInstance(socket)
}
this.socket = null
}
public handleExtensionCommand(message: ExtensionBridgeCommand): void {
if (message.instanceId !== this.instanceId) {
console.log(`[ExtensionManager] command -> instance id mismatch | ${this.instanceId}`, {
messageInstanceId: message.instanceId,
})
return
}
switch (message.type) {
case ExtensionBridgeCommandName.StartTask: {
console.log(`[ExtensionManager] command -> createTask() | ${message.instanceId}`, {
text: message.payload.text?.substring(0, 100) + "...",
hasImages: !!message.payload.images,
})
this.provider.createTask(message.payload.text, message.payload.images)
break
}
case ExtensionBridgeCommandName.StopTask: {
const instance = this.updateInstance()
if (instance.task.taskStatus === TaskStatus.Running) {
console.log(`[ExtensionManager] command -> cancelTask() | ${message.instanceId}`)
this.provider.cancelTask()
this.provider.postStateToWebview()
} else if (instance.task.taskId) {
console.log(`[ExtensionManager] command -> clearTask() | ${message.instanceId}`)
this.provider.clearTask()
this.provider.postStateToWebview()
}
break
}
case ExtensionBridgeCommandName.ResumeTask: {
console.log(`[ExtensionManager] command -> resumeTask() | ${message.instanceId}`, {
taskId: message.payload.taskId,
})
// Resume the task from history by taskId
this.provider.resumeTask(message.payload.taskId)
this.provider.postStateToWebview()
break
}
}
}
private async registerInstance(socket: Socket): Promise<void> {
const instance = this.updateInstance()
try {
socket.emit(ExtensionSocketEvents.REGISTER, instance)
console.log(
`[ExtensionManager] emit() -> ${ExtensionSocketEvents.REGISTER}`,
// instance,
)
} catch (error) {
console.error(
`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.REGISTER}: ${
error instanceof Error ? error.message : String(error)
}`,
)
return
}
}
private async unregisterInstance(socket: Socket): Promise<void> {
const instance = this.updateInstance()
try {
socket.emit(ExtensionSocketEvents.UNREGISTER, instance)
console.log(
`[ExtensionManager] emit() -> ${ExtensionSocketEvents.UNREGISTER}`,
// instance,
)
} catch (error) {
console.error(
`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.UNREGISTER}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
private startHeartbeat(socket: Socket): void {
this.stopHeartbeat()
this.heartbeatInterval = setInterval(async () => {
const instance = this.updateInstance()
try {
socket.emit(ExtensionSocketEvents.HEARTBEAT, instance)
// console.log(
// `[ExtensionManager] emit() -> ${ExtensionSocketEvents.HEARTBEAT}`,
// instance,
// );
} catch (error) {
console.error(
`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.HEARTBEAT}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}, HEARTBEAT_INTERVAL_MS)
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = null
}
}
private setupListeners(): void {
const eventMapping = [
{
from: RooCodeEventName.TaskCreated,
to: ExtensionBridgeEventName.TaskCreated,
},
{
from: RooCodeEventName.TaskStarted,
to: ExtensionBridgeEventName.TaskStarted,
},
{
from: RooCodeEventName.TaskCompleted,
to: ExtensionBridgeEventName.TaskCompleted,
},
{
from: RooCodeEventName.TaskAborted,
to: ExtensionBridgeEventName.TaskAborted,
},
{
from: RooCodeEventName.TaskFocused,
to: ExtensionBridgeEventName.TaskFocused,
},
{
from: RooCodeEventName.TaskUnfocused,
to: ExtensionBridgeEventName.TaskUnfocused,
},
{
from: RooCodeEventName.TaskActive,
to: ExtensionBridgeEventName.TaskActive,
},
{
from: RooCodeEventName.TaskInteractive,
to: ExtensionBridgeEventName.TaskInteractive,
},
{
from: RooCodeEventName.TaskResumable,
to: ExtensionBridgeEventName.TaskResumable,
},
{
from: RooCodeEventName.TaskIdle,
to: ExtensionBridgeEventName.TaskIdle,
},
] as const
const addListener =
(type: ExtensionBridgeEventName) =>
async (..._args: unknown[]) => {
this.publishEvent({
type,
instance: this.updateInstance(),
timestamp: Date.now(),
})
}
eventMapping.forEach(({ from, to }) => this.provider.on(from, addListener(to)))
}
private async publishEvent(message: ExtensionBridgeEvent): Promise<boolean> {
if (!this.socket) {
console.error("[ExtensionManager] publishEvent -> socket not available")
return false
}
try {
this.socket.emit(ExtensionSocketEvents.EVENT, message)
console.log(`[ExtensionManager] emit() -> ${ExtensionSocketEvents.EVENT} ${message.type}`, message)
return true
} catch (error) {
console.error(
`[ExtensionManager] emit() failed -> ${ExtensionSocketEvents.EVENT}: ${
error instanceof Error ? error.message : String(error)
}`,
)
return false
}
}
private updateInstance(): ExtensionInstance {
const task = this.provider?.getCurrentTask()
const taskHistory = this.provider?.getRecentTasks() ?? []
this.extensionInstance = {
...this.extensionInstance,
appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties,
gitProperties: this.extensionInstance.gitProperties ?? this.provider.gitProperties,
lastHeartbeat: Date.now(),
task: task
? {
taskId: task.taskId,
taskStatus: task.taskStatus,
...task.metadata,
}
: { taskId: "", taskStatus: TaskStatus.None },
taskAsk: task?.taskAsk,
taskHistory,
}
return this.extensionInstance
}
}

View file

@ -0,0 +1,289 @@
import { io, type Socket } from "socket.io-client"
import { ConnectionState, type RetryConfig } from "@roo-code/types"
export interface SocketConnectionOptions {
url: string
socketOptions: Record<string, unknown>
onConnect?: () => void | Promise<void>
onDisconnect?: (reason: string) => void
onReconnect?: (attemptNumber: number) => void | Promise<void>
onError?: (error: Error) => void
logger?: {
log: (message: string, ...args: unknown[]) => void
error: (message: string, ...args: unknown[]) => void
warn: (message: string, ...args: unknown[]) => void
}
}
export class SocketConnectionManager {
private socket: Socket | null = null
private connectionState: ConnectionState = ConnectionState.DISCONNECTED
private retryAttempt: number = 0
private retryTimeout: NodeJS.Timeout | null = null
private hasConnectedOnce: boolean = false
private readonly retryConfig: RetryConfig = {
maxInitialAttempts: 10,
initialDelay: 1_000,
maxDelay: 15_000,
backoffMultiplier: 2,
}
private readonly CONNECTION_TIMEOUT = 2_000
private readonly options: SocketConnectionOptions
constructor(options: SocketConnectionOptions, retryConfig?: Partial<RetryConfig>) {
this.options = options
if (retryConfig) {
this.retryConfig = { ...this.retryConfig, ...retryConfig }
}
}
public async connect(): Promise<void> {
if (this.connectionState === ConnectionState.CONNECTED) {
console.log(`[SocketConnectionManager] Already connected`)
return
}
if (this.connectionState === ConnectionState.CONNECTING || this.connectionState === ConnectionState.RETRYING) {
console.log(`[SocketConnectionManager] Connection attempt already in progress`)
return
}
// Start connection attempt without blocking.
this.startConnectionAttempt()
}
private async startConnectionAttempt() {
this.retryAttempt = 0
try {
await this.connectWithRetry()
} catch (error) {
console.error(`[SocketConnectionManager] Initial connection attempts failed:`, error)
// If we've never connected successfully, we've exhausted our retry attempts
// The user will need to manually retry or fix the issue
this.connectionState = ConnectionState.FAILED
}
}
private async connectWithRetry(): Promise<void> {
let delay = this.retryConfig.initialDelay
while (this.retryAttempt < this.retryConfig.maxInitialAttempts) {
try {
this.connectionState = this.retryAttempt === 0 ? ConnectionState.CONNECTING : ConnectionState.RETRYING
console.log(
`[SocketConnectionManager] Connection attempt ${this.retryAttempt + 1} / ${this.retryConfig.maxInitialAttempts}`,
)
await this.connectSocket()
console.log(`[SocketConnectionManager] Connected to ${this.options.url}`)
this.connectionState = ConnectionState.CONNECTED
this.retryAttempt = 0
this.clearRetryTimeouts()
if (this.options.onConnect) {
await this.options.onConnect()
}
return
} catch (error) {
this.retryAttempt++
console.error(`[SocketConnectionManager] Connection attempt ${this.retryAttempt} failed:`, error)
if (this.socket) {
this.socket.disconnect()
this.socket = null
}
if (this.retryAttempt >= this.retryConfig.maxInitialAttempts) {
this.connectionState = ConnectionState.FAILED
throw new Error(`Failed to connect after ${this.retryConfig.maxInitialAttempts} attempts`)
}
console.log(`[SocketConnectionManager] Waiting ${delay}ms before retry...`)
await this.delay(delay)
delay = Math.min(delay * this.retryConfig.backoffMultiplier, this.retryConfig.maxDelay)
}
}
}
private async connectSocket(): Promise<void> {
return new Promise((resolve, reject) => {
this.socket = io(this.options.url, this.options.socketOptions)
const connectionTimeout = setTimeout(() => {
console.error(`[SocketConnectionManager] Connection timeout`)
if (this.connectionState !== ConnectionState.CONNECTED) {
this.socket?.disconnect()
reject(new Error("Connection timeout"))
}
}, this.CONNECTION_TIMEOUT)
this.socket.on("connect", async () => {
clearTimeout(connectionTimeout)
const isReconnection = this.hasConnectedOnce
// If this is a reconnection (not the first connect), treat it as a
// reconnect.
// This handles server restarts where 'reconnect' event might not fire.
if (isReconnection) {
console.log(
`[SocketConnectionManager] Treating connect as reconnection (server may have restarted)`,
)
this.connectionState = ConnectionState.CONNECTED
if (this.options.onReconnect) {
// Call onReconnect to re-register instance.
await this.options.onReconnect(0)
}
}
this.hasConnectedOnce = true
resolve()
})
this.socket.on("disconnect", (reason: string) => {
console.log(`[SocketConnectionManager] Disconnected (reason: ${reason})`)
this.connectionState = ConnectionState.DISCONNECTED
if (this.options.onDisconnect) {
this.options.onDisconnect(reason)
}
// Don't attempt to reconnect if we're manually disconnecting.
const isManualDisconnect = reason === "io client disconnect"
if (!isManualDisconnect && this.hasConnectedOnce) {
// After successful initial connection, rely entirely on Socket.IO's
// reconnection.
console.log(`[SocketConnectionManager] Socket.IO will handle reconnection (reason: ${reason})`)
}
})
// Listen for reconnection attempts.
this.socket.on("reconnect_attempt", (attemptNumber: number) => {
console.log(`[SocketConnectionManager] Socket.IO reconnect attempt:`, {
attemptNumber,
})
})
this.socket.on("reconnect", (attemptNumber: number) => {
console.log(`[SocketConnectionManager] Socket reconnected (attempt: ${attemptNumber})`)
this.connectionState = ConnectionState.CONNECTED
if (this.options.onReconnect) {
this.options.onReconnect(attemptNumber)
}
})
this.socket.on("reconnect_error", (error: Error) => {
console.error(`[SocketConnectionManager] Socket.IO reconnect error:`, error)
})
this.socket.on("reconnect_failed", () => {
console.error(`[SocketConnectionManager] Socket.IO reconnection failed after all attempts`)
this.connectionState = ConnectionState.FAILED
// Socket.IO has exhausted its reconnection attempts
// The connection is now permanently failed until manual intervention
})
this.socket.on("error", (error) => {
console.error(`[SocketConnectionManager] Socket error:`, error)
if (this.connectionState !== ConnectionState.CONNECTED) {
clearTimeout(connectionTimeout)
reject(error)
}
if (this.options.onError) {
this.options.onError(error)
}
})
this.socket.on("auth_error", (error) => {
console.error(`[SocketConnectionManager] Authentication error:`, error)
clearTimeout(connectionTimeout)
reject(new Error(error.message || "Authentication failed"))
})
})
}
private delay(ms: number): Promise<void> {
return new Promise((resolve) => {
this.retryTimeout = setTimeout(resolve, ms)
})
}
// 1. Custom retry for initial connection attempts.
// 2. Socket.IO's built-in reconnection after successful initial connection.
private clearRetryTimeouts() {
if (this.retryTimeout) {
clearTimeout(this.retryTimeout)
this.retryTimeout = null
}
}
public async disconnect(): Promise<void> {
console.log(`[SocketConnectionManager] Disconnecting...`)
this.clearRetryTimeouts()
if (this.socket) {
this.socket.removeAllListeners()
this.socket.disconnect()
this.socket = null
}
this.connectionState = ConnectionState.DISCONNECTED
console.log(`[SocketConnectionManager] Disconnected`)
}
public getSocket(): Socket | null {
return this.socket
}
public getConnectionState(): ConnectionState {
return this.connectionState
}
public isConnected(): boolean {
return this.connectionState === ConnectionState.CONNECTED && this.socket?.connected === true
}
public async reconnect(): Promise<void> {
if (this.connectionState === ConnectionState.CONNECTED) {
console.log(`[SocketConnectionManager] Already connected`)
return
}
console.log(`[SocketConnectionManager] Manual reconnection requested`)
this.hasConnectedOnce = false
await this.disconnect()
await this.connect()
}
}

View file

@ -0,0 +1,279 @@
import type { Socket } from "socket.io-client"
import {
type ClineMessage,
type TaskEvents,
type TaskLike,
type TaskBridgeCommand,
type TaskBridgeEvent,
RooCodeEventName,
TaskBridgeEventName,
TaskBridgeCommandName,
TaskSocketEvents,
} from "@roo-code/types"
type TaskEventListener = {
[K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise<void>
}[keyof TaskEvents]
const TASK_EVENT_MAPPING: Record<TaskBridgeEventName, keyof TaskEvents> = {
[TaskBridgeEventName.Message]: RooCodeEventName.Message,
[TaskBridgeEventName.TaskModeSwitched]: RooCodeEventName.TaskModeSwitched,
[TaskBridgeEventName.TaskInteractive]: RooCodeEventName.TaskInteractive,
}
export class TaskManager {
private subscribedTasks: Map<string, TaskLike> = new Map()
private pendingTasks: Map<string, TaskLike> = new Map()
private socket: Socket | null = null
private taskListeners: Map<string, Map<TaskBridgeEventName, TaskEventListener>> = new Map()
constructor() {}
public async onConnect(socket: Socket): Promise<void> {
this.socket = socket
// Rejoin all subscribed tasks.
for (const taskId of this.subscribedTasks.keys()) {
try {
socket.emit(TaskSocketEvents.JOIN, { taskId })
console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
} catch (error) {
console.error(
`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
// Subscribe to any pending tasks.
for (const task of this.pendingTasks.values()) {
await this.subscribeToTask(task, socket)
}
this.pendingTasks.clear()
}
public onDisconnect(): void {
this.socket = null
}
public async onReconnect(socket: Socket): Promise<void> {
this.socket = socket
// Rejoin all subscribed tasks.
for (const taskId of this.subscribedTasks.keys()) {
try {
socket.emit(TaskSocketEvents.JOIN, { taskId })
console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
} catch (error) {
console.error(
`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
}
public async cleanup(socket: Socket | null): Promise<void> {
if (!socket) {
return
}
const unsubscribePromises = []
for (const taskId of this.subscribedTasks.keys()) {
unsubscribePromises.push(this.unsubscribeFromTask(taskId, socket))
}
await Promise.allSettled(unsubscribePromises)
this.subscribedTasks.clear()
this.taskListeners.clear()
this.pendingTasks.clear()
this.socket = null
}
public addPendingTask(task: TaskLike): void {
this.pendingTasks.set(task.taskId, task)
}
public async subscribeToTask(task: TaskLike, socket: Socket): Promise<void> {
const taskId = task.taskId
this.subscribedTasks.set(taskId, task)
this.setupListeners(task)
try {
socket.emit(TaskSocketEvents.JOIN, { taskId })
console.log(`[TaskManager] emit() -> ${TaskSocketEvents.JOIN} ${taskId}`)
} catch (error) {
console.error(
`[TaskManager] emit() failed -> ${TaskSocketEvents.JOIN}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
public async unsubscribeFromTask(taskId: string, socket: Socket): Promise<void> {
const task = this.subscribedTasks.get(taskId)
if (task) {
this.removeListeners(task)
this.subscribedTasks.delete(taskId)
}
try {
socket.emit(TaskSocketEvents.LEAVE, { taskId })
console.log(`[TaskManager] emit() -> ${TaskSocketEvents.LEAVE} ${taskId}`)
} catch (error) {
console.error(
`[TaskManager] emit() failed -> ${TaskSocketEvents.LEAVE}: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
}
public handleTaskCommand(message: TaskBridgeCommand): void {
const task = this.subscribedTasks.get(message.taskId)
if (!task) {
console.error(`[TaskManager#handleTaskCommand] Unable to find task ${message.taskId}`)
return
}
switch (message.type) {
case TaskBridgeCommandName.Message:
console.log(
`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.Message} ${message.taskId} -> submitUserMessage()`,
message,
)
task.submitUserMessage(message.payload.text, message.payload.images)
break
case TaskBridgeCommandName.ApproveAsk:
console.log(
`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.ApproveAsk} ${message.taskId} -> approveAsk()`,
message,
)
task.approveAsk(message.payload)
break
case TaskBridgeCommandName.DenyAsk:
console.log(
`[TaskManager#handleTaskCommand] ${TaskBridgeCommandName.DenyAsk} ${message.taskId} -> denyAsk()`,
message,
)
task.denyAsk(message.payload)
break
}
}
private setupListeners(task: TaskLike): void {
if (this.taskListeners.has(task.taskId)) {
console.warn("[TaskManager] Listeners already exist for task, removing old listeners:", task.taskId)
this.removeListeners(task)
}
const listeners = new Map<TaskBridgeEventName, TaskEventListener>()
const onMessage = ({ action, message }: { action: string; message: ClineMessage }) => {
this.publishEvent({
type: TaskBridgeEventName.Message,
taskId: task.taskId,
action,
message,
})
}
task.on(RooCodeEventName.Message, onMessage)
listeners.set(TaskBridgeEventName.Message, onMessage)
const onTaskModeSwitched = (mode: string) => {
this.publishEvent({
type: TaskBridgeEventName.TaskModeSwitched,
taskId: task.taskId,
mode,
})
}
task.on(RooCodeEventName.TaskModeSwitched, onTaskModeSwitched)
listeners.set(TaskBridgeEventName.TaskModeSwitched, onTaskModeSwitched)
const onTaskInteractive = (_taskId: string) => {
this.publishEvent({
type: TaskBridgeEventName.TaskInteractive,
taskId: task.taskId,
})
}
task.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
listeners.set(TaskBridgeEventName.TaskInteractive, onTaskInteractive)
this.taskListeners.set(task.taskId, listeners)
console.log("[TaskManager] Task listeners setup complete for:", task.taskId)
}
private removeListeners(task: TaskLike): void {
const listeners = this.taskListeners.get(task.taskId)
if (!listeners) {
return
}
console.log("[TaskManager] Removing task listeners for:", task.taskId)
listeners.forEach((listener, eventName) => {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
task.off(TASK_EVENT_MAPPING[eventName], listener as any)
} catch (error) {
console.error(
`[TaskManager] Error removing listener for ${String(eventName)} on task ${task.taskId}:`,
error,
)
}
})
this.taskListeners.delete(task.taskId)
}
private async publishEvent(message: TaskBridgeEvent): Promise<boolean> {
if (!this.socket) {
console.error("[TaskManager] publishEvent -> socket not available")
return false
}
try {
this.socket.emit(TaskSocketEvents.EVENT, message)
if (message.type !== TaskBridgeEventName.Message) {
console.log(
`[TaskManager] emit() -> ${TaskSocketEvents.EVENT} ${message.taskId} ${message.type}`,
message,
)
}
return true
} catch (error) {
console.error(
`[TaskManager] emit() failed -> ${TaskSocketEvents.EVENT}: ${
error instanceof Error ? error.message : String(error)
}`,
)
return false
}
}
}

View file

@ -7,38 +7,43 @@
let vscodeModule: typeof import("vscode") | undefined
/**
* Attempts to dynamically import the `vscode` module.
* Returns undefined if not running in a VSCode extension context.
* Attempts to dynamically import the VS Code module.
* Returns undefined if not running in a VS Code/Cursor extension context.
*/
export async function importVscode(): Promise<typeof import("vscode") | undefined> {
// Check if already loaded
if (vscodeModule) {
return vscodeModule
}
try {
// Method 1: Check if vscode is available in global scope (common in extension hosts).
if (typeof globalThis !== "undefined" && "acquireVsCodeApi" in globalThis) {
// We're in a webview context, vscode module won't be available.
return undefined
}
// Method 2: Try to require the module (works in most extension contexts).
if (typeof require !== "undefined") {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
vscodeModule = require("vscode")
if (vscodeModule) {
console.log("VS Code module loaded from require")
return vscodeModule
}
} catch (error) {
console.error(`Error loading VS Code module: ${error instanceof Error ? error.message : String(error)}`)
console.error("Error loading VS Code module:", error)
// Fall through to dynamic import.
}
}
// Method 3: Dynamic import (original approach, works in VSCode).
vscodeModule = await import("vscode")
console.log("VS Code module loaded from dynamic import")
return vscodeModule
} catch (error) {
console.warn(
`VS Code module not available in this environment: ${error instanceof Error ? error.message : String(error)}`,
)
// Log the original error for debugging.
console.warn("VS Code module not available in this environment:", error)
return undefined
}
}

View file

@ -1,5 +1,5 @@
export * from "./config.js"
export { CloudService } from "./CloudService.js"
export { BridgeOrchestrator } from "./bridge/index.js"
export * from "./CloudAPI.js"
export * from "./CloudService.js"
export * from "./bridge/ExtensionBridgeService.js"

View file

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

View file

@ -7,7 +7,7 @@ import { TaskStatus, taskMetadataSchema } from "./task.js"
import { globalSettingsSchema } from "./global-settings.js"
import { providerSettingsWithIdSchema } from "./provider-settings.js"
import { mcpMarketplaceItemSchema } from "./marketplace.js"
import { clineMessageSchema, queuedMessageSchema, tokenUsageSchema } from "./message.js"
import { clineMessageSchema } from "./message.js"
import { staticAppPropertiesSchema, gitPropertiesSchema } from "./telemetry.js"
/**
@ -359,11 +359,6 @@ export const INSTANCE_TTL_SECONDS = 60
const extensionTaskSchema = z.object({
taskId: z.string(),
taskStatus: z.nativeEnum(TaskStatus),
taskAsk: clineMessageSchema.optional(),
queuedMessages: z.array(queuedMessageSchema).optional(),
parentTaskId: z.string().optional(),
childTaskId: z.string().optional(),
tokenUsage: tokenUsageSchema.optional(),
...taskMetadataSchema.shape,
})
@ -383,10 +378,6 @@ export const extensionInstanceSchema = z.object({
task: extensionTaskSchema,
taskAsk: clineMessageSchema.optional(),
taskHistory: z.array(z.string()),
mode: z.string().optional(),
modes: z.array(z.object({ slug: z.string(), name: z.string() })).optional(),
providerProfile: z.string().optional(),
providerProfiles: z.array(z.object({ name: z.string(), provider: z.string().optional() })).optional(),
})
export type ExtensionInstance = z.infer<typeof extensionInstanceSchema>
@ -407,17 +398,6 @@ export enum ExtensionBridgeEventName {
TaskResumable = RooCodeEventName.TaskResumable,
TaskIdle = RooCodeEventName.TaskIdle,
TaskPaused = RooCodeEventName.TaskPaused,
TaskUnpaused = RooCodeEventName.TaskUnpaused,
TaskSpawned = RooCodeEventName.TaskSpawned,
TaskUserMessage = RooCodeEventName.TaskUserMessage,
TaskTokenUsageUpdated = RooCodeEventName.TaskTokenUsageUpdated,
ModeChanged = RooCodeEventName.ModeChanged,
ProviderProfileChanged = RooCodeEventName.ProviderProfileChanged,
InstanceRegistered = "instance_registered",
InstanceUnregistered = "instance_unregistered",
HeartbeatUpdated = "heartbeat_updated",
@ -474,48 +454,6 @@ export const extensionBridgeEventSchema = z.discriminatedUnion("type", [
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.TaskPaused),
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.TaskUnpaused),
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.TaskSpawned),
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.TaskUserMessage),
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.TaskTokenUsageUpdated),
instance: extensionInstanceSchema,
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.ModeChanged),
instance: extensionInstanceSchema,
mode: z.string(),
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.ProviderProfileChanged),
instance: extensionInstanceSchema,
providerProfile: z.object({ name: z.string(), provider: z.string().optional() }),
timestamp: z.number(),
}),
z.object({
type: z.literal(ExtensionBridgeEventName.InstanceRegistered),
instance: extensionInstanceSchema,
@ -552,8 +490,6 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [
payload: z.object({
text: z.string(),
images: z.array(z.string()).optional(),
mode: z.string().optional(),
providerProfile: z.string().optional(),
}),
timestamp: z.number(),
}),
@ -566,7 +502,9 @@ export const extensionBridgeCommandSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal(ExtensionBridgeCommandName.ResumeTask),
instanceId: z.string(),
payload: z.object({ taskId: z.string() }),
payload: z.object({
taskId: z.string(),
}),
timestamp: z.number(),
}),
])
@ -620,8 +558,6 @@ export const taskBridgeCommandSchema = z.discriminatedUnion("type", [
payload: z.object({
text: z.string(),
images: z.array(z.string()).optional(),
mode: z.string().optional(),
providerProfile: z.string().optional(),
}),
timestamp: z.number(),
}),
@ -651,49 +587,32 @@ export type TaskBridgeCommand = z.infer<typeof taskBridgeCommandSchema>
* ExtensionSocketEvents
*/
export enum ExtensionSocketEvents {
CONNECTED = "extension:connected",
export const ExtensionSocketEvents = {
CONNECTED: "extension:connected",
REGISTER = "extension:register",
UNREGISTER = "extension:unregister",
REGISTER: "extension:register",
UNREGISTER: "extension:unregister",
HEARTBEAT = "extension:heartbeat",
HEARTBEAT: "extension:heartbeat",
EVENT = "extension:event", // event from extension instance
RELAYED_EVENT = "extension:relayed_event", // relay from server
EVENT: "extension:event", // event from extension instance
RELAYED_EVENT: "extension:relayed_event", // relay from server
COMMAND = "extension:command", // command from user
RELAYED_COMMAND = "extension:relayed_command", // relay from server
}
COMMAND: "extension:command", // command from user
RELAYED_COMMAND: "extension:relayed_command", // relay from server
} as const
/**
* TaskSocketEvents
*/
export enum TaskSocketEvents {
JOIN = "task:join",
LEAVE = "task:leave",
export const TaskSocketEvents = {
JOIN: "task:join",
LEAVE: "task:leave",
EVENT = "task:event", // event from extension task
RELAYED_EVENT = "task:relayed_event", // relay from server
EVENT: "task:event", // event from extension task
RELAYED_EVENT: "task:relayed_event", // relay from server
COMMAND = "task:command", // command from user
RELAYED_COMMAND = "task:relayed_command", // relay from server
}
/**
* `emit()` Response Types
*/
export type JoinResponse = {
success: boolean
error?: string
taskId?: string
timestamp?: string
}
export type LeaveResponse = {
success: boolean
taskId?: string
timestamp?: string
}
COMMAND: "task:command", // command from user
RELAYED_COMMAND: "task:relayed_command", // relay from server
} as const

170
pnpm-lock.yaml generated
View file

@ -71,8 +71,8 @@ importers:
specifier: ^4.19.3
version: 4.19.4
turbo:
specifier: ^2.5.3
version: 2.5.4
specifier: ^2.5.6
version: 2.5.6
typescript:
specifier: ^5.4.5
version: 5.8.3
@ -285,8 +285,8 @@ importers:
specifier: ^8.6.0
version: 8.6.0(react@18.3.1)
framer-motion:
specifier: ^12.15.0
version: 12.16.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 12.15.0
version: 12.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
lucide-react:
specifier: ^0.518.0
version: 0.518.0(react@18.3.1)
@ -371,6 +371,46 @@ importers:
specifier: ^3.2.3
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.57)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
packages/cloud:
dependencies:
'@roo-code/types':
specifier: workspace:^
version: link:../types
ioredis:
specifier: ^5.6.1
version: 5.6.1
jwt-decode:
specifier: ^4.0.0
version: 4.0.0
p-wait-for:
specifier: ^5.0.2
version: 5.0.2
socket.io-client:
specifier: ^4.8.1
version: 4.8.1
zod:
specifier: ^3.25.76
version: 3.25.76
devDependencies:
'@roo-code/config-eslint':
specifier: workspace:^
version: link:../config-eslint
'@roo-code/config-typescript':
specifier: workspace:^
version: link:../config-typescript
'@types/node':
specifier: ^24.1.0
version: 24.2.1
'@types/vscode':
specifier: ^1.102.0
version: 1.103.0
globals:
specifier: ^16.3.0
version: 16.3.0
vitest:
specifier: ^3.2.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0)
packages/config-eslint:
devDependencies:
'@eslint/js':
@ -396,7 +436,7 @@ importers:
version: 5.2.0(eslint@9.27.0(jiti@2.4.2))
eslint-plugin-turbo:
specifier: ^2.4.4
version: 2.5.3(eslint@9.27.0(jiti@2.4.2))(turbo@2.5.4)
version: 2.5.3(eslint@9.27.0(jiti@2.4.2))(turbo@2.5.6)
globals:
specifier: ^16.0.0
version: 16.1.0
@ -578,14 +618,14 @@ importers:
specifier: ^1.9.18
version: 1.9.18(zod@3.25.61)
'@modelcontextprotocol/sdk':
specifier: ^1.9.0
specifier: 1.12.0
version: 1.12.0
'@qdrant/js-client-rest':
specifier: ^1.14.0
version: 1.14.0(typescript@5.8.3)
'@roo-code/cloud':
specifier: ^0.29.0
version: 0.29.0
specifier: workspace:^
version: link:../packages/cloud
'@roo-code/ipc':
specifier: workspace:^
version: link:../packages/ipc
@ -595,9 +635,6 @@ importers:
'@roo-code/types':
specifier: workspace:^
version: link:../packages/types
'@types/lodash.debounce':
specifier: ^4.0.9
version: 4.0.9
'@vscode/codicons':
specifier: ^0.0.36
version: 0.0.36
@ -803,6 +840,9 @@ importers:
'@types/glob':
specifier: ^8.1.0
version: 8.1.0
'@types/lodash.debounce':
specifier: ^4.0.9
version: 4.0.9
'@types/mocha':
specifier: ^10.0.10
version: 10.0.10
@ -3346,12 +3386,6 @@ packages:
cpu: [x64]
os: [win32]
'@roo-code/cloud@0.29.0':
resolution: {integrity: sha512-fXN0mdkd5GezpVrCspe6atUkwvSk5D4wF80g+lc8E3aPVqEAozoI97kHNulRChGlBw7UIdd5xxbr1Z8Jtn+S/Q==}
'@roo-code/types@1.63.0':
resolution: {integrity: sha512-pX8ftkDq1CySBbkUTIW9/QEG52ttFT/kl0ID286l0L3W22wpGRUct6PCedNI9kLDM4s5sxaUeZx7b3rUChikkw==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@ -4225,6 +4259,9 @@ packages:
'@types/vscode@1.100.0':
resolution: {integrity: sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==}
'@types/vscode@1.103.0':
resolution: {integrity: sha512-o4hanZAQdNfsKecexq9L3eHICd0AAvdbLk6hA60UzGXbGH/q8b/9xv2RgR7vV3ZcHuyKVq7b37IGd/+gM4Tu+Q==}
'@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@ -6129,8 +6166,8 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
framer-motion@12.16.0:
resolution: {integrity: sha512-xryrmD4jSBQrS2IkMdcTmiS4aSKckbS7kLDCuhUn9110SQKG1w3zlq1RTqCblewg+ZYe+m3sdtzQA6cRwo5g8Q==}
framer-motion@12.15.0:
resolution: {integrity: sha512-XKg/LnKExdLGugZrDILV7jZjI599785lDIJZLxMiiIFidCsy0a4R2ZEf+Izm67zyOuJgQYTHOmodi7igQsw3vg==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
@ -9470,38 +9507,38 @@ packages:
resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==}
engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'}
turbo-darwin-64@2.5.4:
resolution: {integrity: sha512-ah6YnH2dErojhFooxEzmvsoZQTMImaruZhFPfMKPBq8sb+hALRdvBNLqfc8NWlZq576FkfRZ/MSi4SHvVFT9PQ==}
turbo-darwin-64@2.5.6:
resolution: {integrity: sha512-3C1xEdo4aFwMJAPvtlPqz1Sw/+cddWIOmsalHFMrsqqydcptwBfu26WW2cDm3u93bUzMbBJ8k3zNKFqxJ9ei2A==}
cpu: [x64]
os: [darwin]
turbo-darwin-arm64@2.5.4:
resolution: {integrity: sha512-2+Nx6LAyuXw2MdXb7pxqle3MYignLvS7OwtsP9SgtSBaMlnNlxl9BovzqdYAgkUW3AsYiQMJ/wBRb7d+xemM5A==}
turbo-darwin-arm64@2.5.6:
resolution: {integrity: sha512-LyiG+rD7JhMfYwLqB6k3LZQtYn8CQQUePbpA8mF/hMLPAekXdJo1g0bUPw8RZLwQXUIU/3BU7tXENvhSGz5DPA==}
cpu: [arm64]
os: [darwin]
turbo-linux-64@2.5.4:
resolution: {integrity: sha512-5May2kjWbc8w4XxswGAl74GZ5eM4Gr6IiroqdLhXeXyfvWEdm2mFYCSWOzz0/z5cAgqyGidF1jt1qzUR8hTmOA==}
turbo-linux-64@2.5.6:
resolution: {integrity: sha512-GOcUTT0xiT/pSnHL4YD6Yr3HreUhU8pUcGqcI2ksIF9b2/r/kRHwGFcsHgpG3+vtZF/kwsP0MV8FTlTObxsYIA==}
cpu: [x64]
os: [linux]
turbo-linux-arm64@2.5.4:
resolution: {integrity: sha512-/2yqFaS3TbfxV3P5yG2JUI79P7OUQKOUvAnx4MV9Bdz6jqHsHwc9WZPpO4QseQm+NvmgY6ICORnoVPODxGUiJg==}
turbo-linux-arm64@2.5.6:
resolution: {integrity: sha512-10Tm15bruJEA3m0V7iZcnQBpObGBcOgUcO+sY7/2vk1bweW34LMhkWi8svjV9iDF68+KJDThnYDlYE/bc7/zzQ==}
cpu: [arm64]
os: [linux]
turbo-windows-64@2.5.4:
resolution: {integrity: sha512-EQUO4SmaCDhO6zYohxIjJpOKRN3wlfU7jMAj3CgcyTPvQR/UFLEKAYHqJOnJtymbQmiiM/ihX6c6W6Uq0yC7mA==}
turbo-windows-64@2.5.6:
resolution: {integrity: sha512-FyRsVpgaj76It0ludwZsNN40ytHN+17E4PFJyeliBEbxrGTc5BexlXVpufB7XlAaoaZVxbS6KT8RofLfDRyEPg==}
cpu: [x64]
os: [win32]
turbo-windows-arm64@2.5.4:
resolution: {integrity: sha512-oQ8RrK1VS8lrxkLriotFq+PiF7iiGgkZtfLKF4DDKsmdbPo0O9R2mQxm7jHLuXraRCuIQDWMIw6dpcr7Iykf4A==}
turbo-windows-arm64@2.5.6:
resolution: {integrity: sha512-j/tWu8cMeQ7HPpKri6jvKtyXg9K1gRyhdK4tKrrchH8GNHscPX/F71zax58yYtLRWTiK04zNzPcUJuoS0+v/+Q==}
cpu: [arm64]
os: [win32]
turbo@2.5.4:
resolution: {integrity: sha512-kc8ZibdRcuWUG1pbYSBFWqmIjynlD8Lp7IB6U3vIzvOv9VG+6Sp8bzyeBWE3Oi8XV5KsQrznyRTBPvrf99E4mA==}
turbo@2.5.6:
resolution: {integrity: sha512-gxToHmi9oTBNB05UjUsrWf0OyN5ZXtD0apOarC1KIx232Vp3WimRNy3810QzeNSgyD5rsaIDXlxlbnOzlouo+w==}
hasBin: true
turndown@7.2.0:
@ -11424,8 +11461,8 @@ snapshots:
'@modelcontextprotocol/sdk': 1.12.0
google-auth-library: 9.15.1
ws: 8.18.2
zod: 3.25.61
zod-to-json-schema: 3.24.5(zod@3.25.61)
zod: 3.25.76
zod-to-json-schema: 3.24.5(zod@3.25.76)
transitivePeerDependencies:
- bufferutil
- encoding
@ -11684,8 +11721,8 @@ snapshots:
'@lmstudio/lms-isomorphic': 0.4.5
chalk: 4.1.2
jsonschema: 1.5.0
zod: 3.25.61
zod-to-json-schema: 3.24.5(zod@3.25.61)
zod: 3.25.76
zod-to-json-schema: 3.24.5(zod@3.25.76)
transitivePeerDependencies:
- bufferutil
- utf-8-validate
@ -11751,8 +11788,8 @@ snapshots:
express-rate-limit: 7.5.0(express@5.1.0)
pkce-challenge: 5.0.0
raw-body: 3.0.0
zod: 3.25.61
zod-to-json-schema: 3.24.5(zod@3.25.61)
zod: 3.25.76
zod-to-json-schema: 3.24.5(zod@3.25.76)
transitivePeerDependencies:
- supports-color
@ -12732,23 +12769,6 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.40.2':
optional: true
'@roo-code/cloud@0.29.0':
dependencies:
'@roo-code/types': 1.63.0
ioredis: 5.6.1
jwt-decode: 4.0.0
p-wait-for: 5.0.2
socket.io-client: 4.8.1
zod: 3.25.76
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@roo-code/types@1.63.0':
dependencies:
zod: 3.25.76
'@sec-ant/readable-stream@0.4.1': {}
'@sevinf/maybe@0.5.0': {}
@ -13799,6 +13819,8 @@ snapshots:
'@types/vscode@1.100.0': {}
'@types/vscode@1.103.0': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.2.1
@ -15551,11 +15573,11 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
eslint-plugin-turbo@2.5.3(eslint@9.27.0(jiti@2.4.2))(turbo@2.5.4):
eslint-plugin-turbo@2.5.3(eslint@9.27.0(jiti@2.4.2))(turbo@2.5.6):
dependencies:
dotenv: 16.0.3
eslint: 9.27.0(jiti@2.4.2)
turbo: 2.5.4
turbo: 2.5.6
eslint-scope@8.3.0:
dependencies:
@ -16026,7 +16048,7 @@ snapshots:
fraction.js@4.3.7: {}
framer-motion@12.16.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
framer-motion@12.15.0(@emotion/is-prop-valid@1.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
motion-dom: 12.16.0
motion-utils: 12.12.1
@ -19977,32 +19999,32 @@ snapshots:
tunnel@0.0.6: {}
turbo-darwin-64@2.5.4:
turbo-darwin-64@2.5.6:
optional: true
turbo-darwin-arm64@2.5.4:
turbo-darwin-arm64@2.5.6:
optional: true
turbo-linux-64@2.5.4:
turbo-linux-64@2.5.6:
optional: true
turbo-linux-arm64@2.5.4:
turbo-linux-arm64@2.5.6:
optional: true
turbo-windows-64@2.5.4:
turbo-windows-64@2.5.6:
optional: true
turbo-windows-arm64@2.5.4:
turbo-windows-arm64@2.5.6:
optional: true
turbo@2.5.4:
turbo@2.5.6:
optionalDependencies:
turbo-darwin-64: 2.5.4
turbo-darwin-arm64: 2.5.4
turbo-linux-64: 2.5.4
turbo-linux-arm64: 2.5.4
turbo-windows-64: 2.5.4
turbo-windows-arm64: 2.5.4
turbo-darwin-64: 2.5.6
turbo-darwin-arm64: 2.5.6
turbo-linux-64: 2.5.6
turbo-linux-arm64: 2.5.6
turbo-windows-64: 2.5.6
turbo-windows-arm64: 2.5.6
turndown@7.2.0:
dependencies:
@ -20836,6 +20858,10 @@ snapshots:
dependencies:
zod: 3.25.61
zod-to-json-schema@3.24.5(zod@3.25.76):
dependencies:
zod: 3.25.76
zod-to-ts@1.2.0(typescript@5.8.3)(zod@3.25.61):
dependencies:
typescript: 5.8.3

File diff suppressed because it is too large Load diff

View file

@ -12,8 +12,8 @@ try {
console.warn("Failed to load environment variables:", e)
}
import type { CloudUserInfo, AuthState } from "@roo-code/types"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
import type { CloudUserInfo } from "@roo-code/types"
import { CloudService, ExtensionBridgeService } from "@roo-code/cloud"
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
@ -30,6 +30,7 @@ import { CodeIndexManager } from "./services/code-index/manager"
import { MdmService } from "./services/mdm/MdmService"
import { migrateSettings } from "./utils/migrateSettings"
import { autoImportSettings } from "./utils/autoImportSettings"
import { isRemoteControlEnabled } from "./utils/remoteControl"
import { API } from "./extension/api"
import {
@ -53,7 +54,7 @@ let outputChannel: vscode.OutputChannel
let extensionContext: vscode.ExtensionContext
let cloudService: CloudService | undefined
let authStateChangedHandler: ((data: { state: AuthState; previousState: AuthState }) => Promise<void>) | undefined
let authStateChangedHandler: (() => void) | undefined
let settingsUpdatedHandler: (() => void) | undefined
let userInfoHandler: ((data: { userInfo: CloudUserInfo }) => Promise<void>) | undefined
@ -127,50 +128,8 @@ export async function activate(context: vscode.ExtensionContext) {
// Initialize Roo Code Cloud service.
const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebview()
authStateChangedHandler = async (data: { state: AuthState; previousState: AuthState }) => {
postStateListener()
if (data.state === "logged-out") {
try {
await BridgeOrchestrator.disconnect()
cloudLogger("[CloudService] BridgeOrchestrator disconnected on logout")
} catch (error) {
cloudLogger(
`[CloudService] Failed to disconnect BridgeOrchestrator on logout: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
}
settingsUpdatedHandler = async () => {
const userInfo = CloudService.instance.getUserInfo()
if (userInfo && CloudService.instance.cloudAPI) {
try {
const config = await CloudService.instance.cloudAPI.bridgeConfig()
const isCloudAgent =
typeof process.env.ROO_CODE_CLOUD_TOKEN === "string" && process.env.ROO_CODE_CLOUD_TOKEN.length > 0
const remoteControlEnabled = isCloudAgent
? true
: (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false)
await BridgeOrchestrator.connectOrDisconnect(userInfo, remoteControlEnabled, {
...config,
provider,
sessionId: vscode.env.sessionId,
})
} catch (error) {
cloudLogger(
`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on settings change: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
postStateListener()
}
authStateChangedHandler = postStateListener
settingsUpdatedHandler = postStateListener
userInfoHandler = async ({ userInfo }: { userInfo: CloudUserInfo }) => {
postStateListener()
@ -186,18 +145,21 @@ export async function activate(context: vscode.ExtensionContext) {
const isCloudAgent =
typeof process.env.ROO_CODE_CLOUD_TOKEN === "string" && process.env.ROO_CODE_CLOUD_TOKEN.length > 0
const remoteControlEnabled = isCloudAgent
? true
: (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false)
cloudLogger(`[CloudService] isCloudAgent = ${isCloudAgent}, socketBridgeUrl = ${config.socketBridgeUrl}`)
await BridgeOrchestrator.connectOrDisconnect(userInfo, remoteControlEnabled, {
...config,
provider,
sessionId: vscode.env.sessionId,
})
ExtensionBridgeService.handleRemoteControlState(
userInfo,
isCloudAgent ? true : contextProxy.getValue("remoteControlEnabled"),
{
...config,
provider,
sessionId: vscode.env.sessionId,
},
cloudLogger,
)
} catch (error) {
cloudLogger(
`[CloudService] BridgeOrchestrator#connectOrDisconnect failed on user change: ${error instanceof Error ? error.message : String(error)}`,
`[CloudService] Failed to fetch bridgeConfig: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
@ -221,15 +183,6 @@ export async function activate(context: vscode.ExtensionContext) {
// Add to subscriptions for proper cleanup on deactivate.
context.subscriptions.push(cloudService)
// Trigger initial cloud profile sync now that CloudService is ready
try {
await provider.initializeCloudProfileSyncWhenReady()
} catch (error) {
outputChannel.appendLine(
`[CloudService] Failed to initialize cloud profile sync: ${error instanceof Error ? error.message : String(error)}`,
)
}
// Finish initializing the provider.
TelemetryService.instance.setProvider(provider)
@ -380,10 +333,10 @@ export async function deactivate() {
}
}
const bridge = BridgeOrchestrator.getInstance()
const bridgeService = ExtensionBridgeService.getInstance()
if (bridge) {
await bridge.disconnect()
if (bridgeService) {
await bridgeService.disconnect()
}
await McpServerManager.cleanup(extensionContext)

View file

@ -427,13 +427,12 @@
"@google/genai": "^1.0.0",
"@lmstudio/sdk": "^1.1.1",
"@mistralai/mistralai": "^1.9.18",
"@modelcontextprotocol/sdk": "^1.9.0",
"@modelcontextprotocol/sdk": "1.12.0",
"@qdrant/js-client-rest": "^1.14.0",
"@roo-code/cloud": "^0.29.0",
"@roo-code/cloud": "workspace:^",
"@roo-code/ipc": "workspace:^",
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"@types/lodash.debounce": "^4.0.9",
"@vscode/codicons": "^0.0.36",
"async-mutex": "^0.5.0",
"axios": "^1.7.4",
@ -504,6 +503,7 @@
"@types/diff": "^5.2.1",
"@types/diff-match-patch": "^1.0.36",
"@types/glob": "^8.1.0",
"@types/lodash.debounce": "^4.0.9",
"@types/mocha": "^10.0.10",
"@types/node": "20.x",
"@types/node-cache": "^4.1.3",

View file

@ -11,8 +11,10 @@ import type {
TodoItem,
ClineSay,
FileChangeset,
CloudUserInfo,
OrganizationAllowList,
ShareVisibility,
} from "@roo-code/types"
import type { CloudUserInfo, OrganizationAllowList, ShareVisibility } from "@roo-code/cloud"
import { GitCommit } from "../utils/git"

View file

@ -7,7 +7,6 @@ import {
type InstallMarketplaceItemOptions,
type MarketplaceItem,
type ShareVisibility,
type QueuedMessage,
marketplaceItemSchema,
} from "@roo-code/types"
@ -23,8 +22,6 @@ export interface UpdateTodoListPayload {
todos: any[]
}
export type EditQueuedMessagePayload = Pick<QueuedMessage, "id" | "text" | "images">
export interface WebviewMessage {
type:
| "updateTodoList"
@ -177,7 +174,7 @@ export interface WebviewMessage {
| "toggleApiConfigPin"
| "setHistoryPreviewCollapsed"
| "hasOpenedModeSelector"
| "cloudButtonClicked"
| "accountButtonClicked"
| "rooCloudSignIn"
| "rooCloudSignOut"
| "condenseTaskContextRequest"
@ -213,12 +210,6 @@ export interface WebviewMessage {
| "createCommand"
| "insertTextIntoTextarea"
| "showMdmAuthRequiredNotification"
| "imageGenerationSettings"
| "openRouterImageApiKey"
| "openRouterImageGenerationSelectedModel"
| "queueMessage"
| "removeQueuedMessage"
| "editQueuedMessage"
| "viewDiff"
| "acceptFileChange"
| "rejectFileChange"
@ -229,7 +220,7 @@ export interface WebviewMessage {
| "filesChangedBaselineUpdate"
text?: string
editedMessageContent?: string
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
disabled?: boolean
context?: string
dataUri?: string
@ -261,10 +252,8 @@ export interface WebviewMessage {
hasSystemPromptOverride?: boolean
terminalOperation?: "continue" | "abort"
messageTs?: number
restoreCheckpoint?: boolean
historyPreviewCollapsed?: boolean
filters?: { type?: string; search?: string; tags?: string[] }
settings?: any
url?: string // For openExternal
mpItem?: MarketplaceItem
mpInstallOptions?: InstallMarketplaceItemOptions
@ -354,7 +343,6 @@ export type WebViewMessagePayload =
| IndexClearedPayload
| InstallMarketplaceItemWithParametersPayload
| UpdateTodoListPayload
| EditQueuedMessagePayload
// Alias for consistent naming (prefer 'Webview' spelling in new code)
export type WebviewMessagePayload = WebViewMessagePayload

View file

@ -0,0 +1,11 @@
import type { CloudUserInfo } from "@roo-code/types"
/**
* Determines if remote control features should be enabled
* @param cloudUserInfo - User information from cloud service
* @param remoteControlEnabled - User's remote control setting
* @returns true if remote control should be enabled
*/
export function isRemoteControlEnabled(cloudUserInfo?: CloudUserInfo | null, remoteControlEnabled?: boolean): boolean {
return !!(cloudUserInfo?.id && cloudUserInfo.extensionBridgeEnabled && remoteControlEnabled)
}

View file

@ -1,27 +1,26 @@
import { render, screen } from "@/utils/test-utils"
import { CloudView } from "../CloudView"
import { AccountView } from "../AccountView"
// Mock the translation context
vi.mock("@src/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"cloud:title": "Cloud",
"account:title": "Account",
"settings:common.done": "Done",
"cloud:signIn": "Connect to Roo Code Cloud",
"cloud:cloudBenefitsTitle": "Connect to Roo Code Cloud",
"cloud:cloudBenefitSharing": "Share tasks with others",
"cloud:cloudBenefitHistory": "Access your task history",
"cloud:cloudBenefitMetrics": "Get a holistic view of your token consumption",
"cloud:logOut": "Log out",
"cloud:connect": "Connect Now",
"cloud:visitCloudWebsite": "Visit Roo Code Cloud",
"cloud:remoteControl": "Roomote Control",
"cloud:remoteControlDescription":
"account:signIn": "Connect to Roo Code Cloud",
"account:cloudBenefitsTitle": "Connect to Roo Code Cloud",
"account:cloudBenefitSharing": "Share tasks with others",
"account:cloudBenefitHistory": "Access your task history",
"account:cloudBenefitMetrics": "Get a holistic view of your token consumption",
"account:logOut": "Log out",
"account:connect": "Connect Now",
"account:visitCloudWebsite": "Visit Roo Code Cloud",
"account:remoteControl": "Roomote Control",
"account:remoteControlDescription":
"Enable following and interacting with tasks in this workspace with Roo Code Cloud",
"cloud:profilePicture": "Profile picture",
"cloud:cloudUrlPillLabel": "Roo Code Cloud URL: ",
"account:profilePicture": "Profile picture",
}
return translations[key] || key
},
@ -56,10 +55,10 @@ Object.defineProperty(window, "IMAGES_BASE_URI", {
writable: true,
})
describe("CloudView", () => {
describe("AccountView", () => {
it("should display benefits when user is not authenticated", () => {
render(
<CloudView
<AccountView
userInfo={null}
isAuthenticated={false}
cloudApiUrl="https://app.roocode.com"
@ -84,7 +83,7 @@ describe("CloudView", () => {
}
render(
<CloudView
<AccountView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://app.roocode.com"
@ -113,7 +112,7 @@ describe("CloudView", () => {
}
render(
<CloudView
<AccountView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://app.roocode.com"
@ -137,7 +136,7 @@ describe("CloudView", () => {
}
render(
<CloudView
<AccountView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://app.roocode.com"
@ -149,70 +148,4 @@ describe("CloudView", () => {
expect(screen.queryByTestId("remote-control-toggle")).not.toBeInTheDocument()
expect(screen.queryByText("Roomote Control")).not.toBeInTheDocument()
})
it("should not display cloud URL pill when pointing to production", () => {
const mockUserInfo = {
name: "Test User",
email: "test@example.com",
}
render(
<CloudView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://app.roocode.com"
onDone={() => {}}
/>,
)
// Check that the cloud URL pill is NOT displayed for production URL
expect(screen.queryByText(/Roo Code Cloud URL:/)).not.toBeInTheDocument()
})
it("should display cloud URL pill when pointing to non-production environment", () => {
const mockUserInfo = {
name: "Test User",
email: "test@example.com",
}
render(
<CloudView
userInfo={mockUserInfo}
isAuthenticated={true}
cloudApiUrl="https://staging.roocode.com"
onDone={() => {}}
/>,
)
// Check that the cloud URL pill is displayed with the staging URL
expect(screen.getByText(/Roo Code Cloud URL:/)).toBeInTheDocument()
expect(screen.getByText("https://staging.roocode.com")).toBeInTheDocument()
})
it("should display cloud URL pill for non-authenticated users when not pointing to production", () => {
render(
<CloudView
userInfo={null}
isAuthenticated={false}
cloudApiUrl="https://dev.roocode.com"
onDone={() => {}}
/>,
)
// Check that the cloud URL pill is displayed even when not authenticated
expect(screen.getByText(/Roo Code Cloud URL:/)).toBeInTheDocument()
expect(screen.getByText("https://dev.roocode.com")).toBeInTheDocument()
})
it("should not display cloud URL pill when cloudApiUrl is undefined", () => {
const mockUserInfo = {
name: "Test User",
email: "test@example.com",
}
render(<CloudView userInfo={mockUserInfo} isAuthenticated={true} onDone={() => {}} />)
// Check that the cloud URL pill is NOT displayed when cloudApiUrl is undefined
expect(screen.queryByText(/Roo Code Cloud URL:/)).not.toBeInTheDocument()
})
})

View file

@ -1,8 +1,9 @@
import { render, fireEvent } from "@testing-library/react"
import { vi } from "vitest"
import { ImageGenerationSettings } from "../ImageGenerationSettings"
import type { ProviderSettings } from "@roo-code/types"
import { ImageGenerationSettings } from "../ImageGenerationSettings"
// Mock the translation context
vi.mock("@/i18n/TranslationContext", () => ({
useAppTranslation: () => ({