mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Move @roo-code/cloud to the Roo-Code repo (#7503)
This commit is contained in:
parent
8d94af5b7a
commit
af00c627b7
20 changed files with 1723 additions and 1022 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
290
packages/cloud/src/bridge/ExtensionBridgeService.ts
Normal file
290
packages/cloud/src/bridge/ExtensionBridgeService.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
297
packages/cloud/src/bridge/ExtensionManager.ts
Normal file
297
packages/cloud/src/bridge/ExtensionManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
289
packages/cloud/src/bridge/SocketConnectionManager.ts
Normal file
289
packages/cloud/src/bridge/SocketConnectionManager.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
279
packages/cloud/src/bridge/TaskManager.ts
Normal file
279
packages/cloud/src/bridge/TaskManager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
170
pnpm-lock.yaml
generated
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
11
src/utils/remoteControl.ts
Normal file
11
src/utils/remoteControl.ts
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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: () => ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue