From dc40ba1ee3e48e1645dd248495b7649d6d7fc035 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 6 Feb 2026 20:17:36 +0000 Subject: [PATCH] feat: remove Roomote Control from extension Remove all Roomote Control (remote control) functionality: - Remove BridgeOrchestrator and entire bridge directory from @roo-code/cloud - Remove remoteControlEnabled, featureRoomoteControlEnabled from extension state - Remove extensionBridgeEnabled from CloudUserInfo and user settings - Remove roomoteControlEnabled from organization/user feature schemas - Remove enableBridge from Task and ClineProvider - Remove remote control toggle from CloudView UI - Remove remoteControlEnabled message handler - Remove extension bridge disconnect on logout/deactivate - Update CloudTaskButton to show for all logged-in users - Remove remote control translation strings from all locales - Update all related tests CLO-765 --- packages/cloud/src/StaticSettingsService.ts | 12 +- packages/cloud/src/StaticTokenAuthService.ts | 1 - packages/cloud/src/WebAuthService.ts | 16 - .../CloudSettingsService.parsing.test.ts | 8 +- .../__tests__/StaticTokenAuthService.spec.ts | 19 +- .../src/__tests__/WebAuthService.spec.ts | 5 - packages/cloud/src/bridge/BaseChannel.ts | 142 ------ .../cloud/src/bridge/BridgeOrchestrator.ts | 355 --------------- packages/cloud/src/bridge/ExtensionChannel.ts | 282 ------------ packages/cloud/src/bridge/SocketTransport.ts | 281 ------------ packages/cloud/src/bridge/TaskChannel.ts | 241 ----------- .../bridge/__tests__/ExtensionChannel.test.ts | 402 ----------------- .../src/bridge/__tests__/TaskChannel.test.ts | 407 ------------------ packages/cloud/src/bridge/index.ts | 6 - packages/cloud/src/index.ts | 2 - packages/types/src/__tests__/cloud.test.ts | 90 +--- packages/types/src/cloud.ts | 10 +- packages/types/src/vscode-extension-host.ts | 3 - src/__tests__/extension.spec.ts | 81 +--- src/__tests__/single-open-invariant.spec.ts | 2 - src/core/task/Task.ts | 38 +- .../task/__tests__/grounding-sources.test.ts | 3 - .../__tests__/reasoning-preservation.test.ts | 3 - src/core/webview/ClineProvider.ts | 100 +---- .../ClineProvider.apiHandlerRebuild.spec.ts | 3 - .../ClineProvider.flicker-free-cancel.spec.ts | 3 - .../webview/__tests__/ClineProvider.spec.ts | 5 - .../ClineProvider.sticky-mode.spec.ts | 3 - .../ClineProvider.sticky-profile.spec.ts | 3 - .../ClineProvider.taskHistory.spec.ts | 3 - src/core/webview/webviewMessageHandler.ts | 15 - src/extension.ts | 43 +- .../src/components/chat/CloudTaskButton.tsx | 2 +- .../chat/__tests__/CloudTaskButton.spec.tsx | 2 - webview-ui/src/components/cloud/CloudView.tsx | 44 +- .../cloud/__tests__/CloudView.spec.tsx | 84 ---- .../src/context/ExtensionStateContext.tsx | 11 - .../__tests__/ExtensionStateContext.spec.tsx | 2 - webview-ui/src/i18n/locales/ca/cloud.json | 3 - webview-ui/src/i18n/locales/de/cloud.json | 3 - webview-ui/src/i18n/locales/en/cloud.json | 3 - webview-ui/src/i18n/locales/es/cloud.json | 3 - webview-ui/src/i18n/locales/fr/cloud.json | 3 - webview-ui/src/i18n/locales/hi/cloud.json | 3 - webview-ui/src/i18n/locales/id/cloud.json | 3 - webview-ui/src/i18n/locales/it/cloud.json | 3 - webview-ui/src/i18n/locales/ja/cloud.json | 3 - webview-ui/src/i18n/locales/ko/cloud.json | 3 - webview-ui/src/i18n/locales/nl/cloud.json | 3 - webview-ui/src/i18n/locales/pl/cloud.json | 3 - webview-ui/src/i18n/locales/pt-BR/cloud.json | 3 - webview-ui/src/i18n/locales/ru/cloud.json | 3 - webview-ui/src/i18n/locales/tr/cloud.json | 3 - webview-ui/src/i18n/locales/vi/cloud.json | 3 - webview-ui/src/i18n/locales/zh-CN/cloud.json | 3 - webview-ui/src/i18n/locales/zh-TW/cloud.json | 3 - 56 files changed, 25 insertions(+), 2761 deletions(-) delete mode 100644 packages/cloud/src/bridge/BaseChannel.ts delete mode 100644 packages/cloud/src/bridge/BridgeOrchestrator.ts delete mode 100644 packages/cloud/src/bridge/ExtensionChannel.ts delete mode 100644 packages/cloud/src/bridge/SocketTransport.ts delete mode 100644 packages/cloud/src/bridge/TaskChannel.ts delete mode 100644 packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts delete mode 100644 packages/cloud/src/bridge/__tests__/TaskChannel.test.ts delete mode 100644 packages/cloud/src/bridge/index.ts diff --git a/packages/cloud/src/StaticSettingsService.ts b/packages/cloud/src/StaticSettingsService.ts index 492a0a8d4b6..2365208f9d7 100644 --- a/packages/cloud/src/StaticSettingsService.ts +++ b/packages/cloud/src/StaticSettingsService.ts @@ -42,15 +42,12 @@ export class StaticSettingsService implements SettingsService { } /** - * Returns static user settings with roomoteControlEnabled and extensionBridgeEnabled as true + * Returns static user settings with task sync enabled */ public getUserSettings(): UserSettingsData | undefined { return { - features: { - roomoteControlEnabled: true, - }, + features: {}, settings: { - extensionBridgeEnabled: true, taskSyncEnabled: true, }, version: 1, @@ -58,14 +55,11 @@ export class StaticSettingsService implements SettingsService { } public getUserFeatures(): UserFeatures { - return { - roomoteControlEnabled: true, - } + return {} } public getUserSettingsConfig(): UserSettingsConfig { return { - extensionBridgeEnabled: true, taskSyncEnabled: true, } } diff --git a/packages/cloud/src/StaticTokenAuthService.ts b/packages/cloud/src/StaticTokenAuthService.ts index 2ff7b75f0e0..c2450b0c22a 100644 --- a/packages/cloud/src/StaticTokenAuthService.ts +++ b/packages/cloud/src/StaticTokenAuthService.ts @@ -30,7 +30,6 @@ export class StaticTokenAuthService extends EventEmitter impl this.userInfo = { id: payload?.r?.u || payload?.sub || undefined, organizationId: payload?.r?.o || undefined, - extensionBridgeEnabled: true, } } diff --git a/packages/cloud/src/WebAuthService.ts b/packages/cloud/src/WebAuthService.ts index 69ad28e8ecd..8f51bad236c 100644 --- a/packages/cloud/src/WebAuthService.ts +++ b/packages/cloud/src/WebAuthService.ts @@ -625,8 +625,6 @@ export class WebAuthService extends EventEmitter implements A )?.email_address } - let extensionBridgeEnabled = true - // Fetch organization info if user is in organization context try { const storedOrgId = this.getStoredOrganizationId() @@ -641,8 +639,6 @@ export class WebAuthService extends EventEmitter implements A if (userMembership) { this.setUserOrganizationInfo(userInfo, userMembership) - extensionBridgeEnabled = await this.isExtensionBridgeEnabledForOrganization(storedOrgId) - this.log("[auth] User in organization context:", { id: userMembership.organization.id, name: userMembership.organization.name, @@ -662,10 +658,6 @@ export class WebAuthService extends EventEmitter implements A if (primaryOrgMembership) { this.setUserOrganizationInfo(userInfo, primaryOrgMembership) - extensionBridgeEnabled = await this.isExtensionBridgeEnabledForOrganization( - primaryOrgMembership.organization.id, - ) - this.log("[auth] Legacy credentials: Found organization membership:", { id: primaryOrgMembership.organization.id, name: primaryOrgMembership.organization.name, @@ -680,9 +672,6 @@ export class WebAuthService extends EventEmitter implements A // Don't throw - organization info is optional } - // Set the extension bridge enabled flag - userInfo.extensionBridgeEnabled = extensionBridgeEnabled - return userInfo } @@ -754,11 +743,6 @@ export class WebAuthService extends EventEmitter implements A } } - private async isExtensionBridgeEnabledForOrganization(organizationId: string): Promise { - const orgMetadata = await this.getOrganizationMetadata(organizationId) - return orgMetadata?.public_metadata?.extension_bridge_enabled === true - } - private async clerkLogout(credentials: AuthCredentials): Promise { const formData = new URLSearchParams() formData.append("_is_native", "1") diff --git a/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts b/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts index 8d69303c380..b0486b971c4 100644 --- a/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts +++ b/packages/cloud/src/__tests__/CloudSettingsService.parsing.test.ts @@ -105,12 +105,8 @@ describe("CloudSettingsService - Response Parsing", () => { }, }, user: { - features: { - roomoteControlEnabled: true, - }, - settings: { - extensionBridgeEnabled: true, - }, + features: {}, + settings: {}, version: 1, }, } diff --git a/packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts b/packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts index a3756082eac..c37f2a7df9e 100644 --- a/packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts +++ b/packages/cloud/src/__tests__/StaticTokenAuthService.spec.ts @@ -89,7 +89,6 @@ describe("StaticTokenAuthService", () => { const userInfo = serviceWithJWT.getUserInfo() expect(userInfo?.id).toBe("user_2xmBhejNeDTwanM8CgIOnMgVxzC") expect(userInfo?.organizationId).toBe("org_123abc") - expect(userInfo?.extensionBridgeEnabled).toBe(true) }) it("should parse job token without orgId (null orgId case)", () => { @@ -98,7 +97,6 @@ describe("StaticTokenAuthService", () => { const userInfo = serviceWithJWT.getUserInfo() expect(userInfo?.id).toBe("user_2xmBhejNeDTwanM8CgIOnMgVxzC") expect(userInfo?.organizationId).toBeUndefined() - expect(userInfo?.extensionBridgeEnabled).toBe(true) }) it("should parse auth token and extract userId from r.u", () => { @@ -107,7 +105,6 @@ describe("StaticTokenAuthService", () => { const userInfo = serviceWithAuthToken.getUserInfo() expect(userInfo?.id).toBe("user_123") expect(userInfo?.organizationId).toBeUndefined() - expect(userInfo?.extensionBridgeEnabled).toBe(true) }) it("should handle legacy JWT format with sub field", () => { @@ -116,7 +113,6 @@ describe("StaticTokenAuthService", () => { const userInfo = serviceWithLegacyJWT.getUserInfo() expect(userInfo?.id).toBe("user_123") expect(userInfo?.organizationId).toBeUndefined() - expect(userInfo?.extensionBridgeEnabled).toBe(true) }) it("should handle invalid JWT gracefully", () => { @@ -125,7 +121,6 @@ describe("StaticTokenAuthService", () => { const userInfo = serviceWithInvalidJWT.getUserInfo() expect(userInfo?.id).toBeUndefined() expect(userInfo?.organizationId).toBeUndefined() - expect(userInfo?.extensionBridgeEnabled).toBe(true) expect(mockLog).toHaveBeenCalledWith("[auth] Failed to parse JWT:", expect.any(Error)) }) @@ -183,9 +178,7 @@ describe("StaticTokenAuthService", () => { authService.broadcast() expect(spy).toHaveBeenCalledWith({ - userInfo: expect.objectContaining({ - extensionBridgeEnabled: true, - }), + userInfo: expect.objectContaining({}), }) }) @@ -199,7 +192,6 @@ describe("StaticTokenAuthService", () => { expect(spy).toHaveBeenCalledWith({ userInfo: { - extensionBridgeEnabled: true, id: "user_2xmBhejNeDTwanM8CgIOnMgVxzC", organizationId: "org_123abc", }, @@ -220,10 +212,9 @@ describe("StaticTokenAuthService", () => { }) describe("getUserInfo", () => { - it("should return object with extensionBridgeEnabled flag", () => { + it("should return user info object", () => { const userInfo = authService.getUserInfo() - expect(userInfo).toHaveProperty("extensionBridgeEnabled") - expect(userInfo?.extensionBridgeEnabled).toBe(true) + expect(userInfo).toBeDefined() }) }) @@ -305,9 +296,7 @@ describe("StaticTokenAuthService", () => { }) expect(userInfoSpy).toHaveBeenCalledWith({ - userInfo: expect.objectContaining({ - extensionBridgeEnabled: true, - }), + userInfo: expect.objectContaining({}), }) }) }) diff --git a/packages/cloud/src/__tests__/WebAuthService.spec.ts b/packages/cloud/src/__tests__/WebAuthService.spec.ts index 3398e3f2a3a..aa406e400d7 100644 --- a/packages/cloud/src/__tests__/WebAuthService.spec.ts +++ b/packages/cloud/src/__tests__/WebAuthService.spec.ts @@ -636,7 +636,6 @@ describe("WebAuthService", () => { name: "John Doe", email: "john@example.com", picture: "https://example.com/avatar.jpg", - extensionBridgeEnabled: true, }, }) }) @@ -801,7 +800,6 @@ describe("WebAuthService", () => { name: "Jane Smith", email: "jane@example.com", picture: "https://example.com/jane.jpg", - extensionBridgeEnabled: true, }) }) @@ -869,7 +867,6 @@ describe("WebAuthService", () => { name: "Jane Smith", email: "jane@example.com", picture: "https://example.com/jane.jpg", - extensionBridgeEnabled: false, organizationId: "org_1", organizationName: "Org 1", organizationRole: "member", @@ -920,7 +917,6 @@ describe("WebAuthService", () => { name: "John Doe", email: undefined, picture: undefined, - extensionBridgeEnabled: true, }) }) }) @@ -1045,7 +1041,6 @@ describe("WebAuthService", () => { name: "Test User", email: undefined, picture: undefined, - extensionBridgeEnabled: true, }, }) }) diff --git a/packages/cloud/src/bridge/BaseChannel.ts b/packages/cloud/src/bridge/BaseChannel.ts deleted file mode 100644 index 1b2615b24c3..00000000000 --- a/packages/cloud/src/bridge/BaseChannel.ts +++ /dev/null @@ -1,142 +0,0 @@ -import type { Socket } from "socket.io-client" -import * as vscode from "vscode" - -import type { StaticAppProperties, GitProperties } from "@roo-code/types" - -export interface BaseChannelOptions { - instanceId: string - appProperties: StaticAppProperties - gitProperties?: GitProperties - isCloudAgent: boolean -} - -/** - * Abstract base class for communication channels in the bridge system. - * Provides common functionality for bidirectional communication between - * the VSCode extension and web application. - * - * @template TCommand - Type of commands this channel can receive. - * @template TEvent - Type of events this channel can publish. - */ -export abstract class BaseChannel { - protected socket: Socket | null = null - protected readonly instanceId: string - protected readonly appProperties: StaticAppProperties - protected readonly gitProperties?: GitProperties - protected readonly isCloudAgent: boolean - - constructor(options: BaseChannelOptions) { - this.instanceId = options.instanceId - this.appProperties = options.appProperties - this.gitProperties = options.gitProperties - this.isCloudAgent = options.isCloudAgent - } - - /** - * Called when socket connects. - */ - public async onConnect(socket: Socket): Promise { - this.socket = socket - await this.handleConnect(socket) - } - - /** - * Called when socket disconnects. - */ - public onDisconnect(): void { - this.socket = null - this.handleDisconnect() - } - - /** - * Called when socket reconnects. - */ - public async onReconnect(socket: Socket): Promise { - this.socket = socket - await this.handleReconnect(socket) - } - - /** - * Cleanup resources. - */ - public async cleanup(socket: Socket | null): Promise { - if (socket) { - await this.handleCleanup(socket) - } - - this.socket = null - } - - /** - * Emit a socket event with error handling. - */ - protected publish( - eventName: TEventName, - data: TEventData, - callback?: (params: Params) => void, - ): boolean { - if (!this.socket) { - console.error(`[${this.constructor.name}#emit] socket not available for ${eventName}`) - return false - } - - try { - // console.log(`[${this.constructor.name}#emit] emit() -> ${eventName}`, data) - this.socket.emit(eventName, data, callback) - - return true - } catch (error) { - console.error( - `[${this.constructor.name}#emit] emit() failed -> ${eventName}: ${ - error instanceof Error ? error.message : String(error) - }`, - ) - - return false - } - } - - /** - * Handle incoming commands - template method that ensures common functionality - * is executed before subclass-specific logic. - * - * This method should be called by subclasses to handle commands. - * It will execute common functionality and then delegate to the abstract - * handleCommandImplementation method. - */ - public async handleCommand(command: TCommand): Promise { - // Common functionality: focus the sidebar. - await vscode.commands.executeCommand(`${this.appProperties.appName}.SidebarProvider.focus`) - - // Delegate to subclass-specific implementation. - await this.handleCommandImplementation(command) - } - - /** - * Handle command-specific logic - must be implemented by subclasses. - * This method is called after common functionality has been executed. - */ - protected abstract handleCommandImplementation(command: TCommand): Promise - - /** - * Handle connection-specific logic. - */ - protected abstract handleConnect(socket: Socket): Promise - - /** - * Handle disconnection-specific logic. - */ - protected handleDisconnect(): void { - // Default implementation - can be overridden. - } - - /** - * Handle reconnection-specific logic. - */ - protected abstract handleReconnect(socket: Socket): Promise - - /** - * Handle cleanup-specific logic. - */ - protected abstract handleCleanup(socket: Socket): Promise -} diff --git a/packages/cloud/src/bridge/BridgeOrchestrator.ts b/packages/cloud/src/bridge/BridgeOrchestrator.ts deleted file mode 100644 index 16ad0244f08..00000000000 --- a/packages/cloud/src/bridge/BridgeOrchestrator.ts +++ /dev/null @@ -1,355 +0,0 @@ -import crypto from "crypto" -import os from "os" - -import { - type TaskProviderLike, - type TaskLike, - type CloudUserInfo, - type ExtensionBridgeCommand, - type TaskBridgeCommand, - type StaticAppProperties, - type GitProperties, - ConnectionState, - ExtensionSocketEvents, - TaskSocketEvents, -} from "@roo-code/types" - -import { SocketTransport } from "./SocketTransport.js" -import { ExtensionChannel } from "./ExtensionChannel.js" -import { TaskChannel } from "./TaskChannel.js" - -export interface BridgeOrchestratorOptions { - userId: string - socketBridgeUrl: string - token: string - provider: TaskProviderLike - sessionId: string - isCloudAgent: boolean -} - -/** - * Central orchestrator for the extension bridge system. - * Coordinates communication between the VSCode extension and web application - * through WebSocket connections and manages extension/task channels. - */ -export class BridgeOrchestrator { - private static instance: BridgeOrchestrator | null = null - - private static pendingTask: TaskLike | null = null - - // Core - private readonly userId: string - private readonly socketBridgeUrl: string - private readonly token: string - private readonly provider: TaskProviderLike - private readonly instanceId: string - private readonly appProperties: StaticAppProperties - private readonly gitProperties?: GitProperties - private readonly isCloudAgent?: boolean - - // Components - private socketTransport: SocketTransport - private extensionChannel: ExtensionChannel - private taskChannel: TaskChannel - - // Reconnection - private readonly MAX_RECONNECT_ATTEMPTS = Infinity - private readonly RECONNECT_DELAY = 1_000 - private readonly RECONNECT_DELAY_MAX = 30_000 - - public static getInstance(): BridgeOrchestrator | null { - return BridgeOrchestrator.instance - } - - public static isEnabled(user: CloudUserInfo | null, remoteControlEnabled: boolean): boolean { - // Always disabled if signed out. - if (!user) { - return false - } - - // Disabled by the user's organization? - if (!user.extensionBridgeEnabled) { - return false - } - - // Disabled by the user? - if (!remoteControlEnabled) { - return false - } - - return true - } - - public static async connectOrDisconnect( - userInfo: CloudUserInfo, - remoteControlEnabled: boolean, - options: BridgeOrchestratorOptions, - ): Promise { - if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) { - await BridgeOrchestrator.connect(options) - } else { - await BridgeOrchestrator.disconnect() - } - } - - public static async connect(options: BridgeOrchestratorOptions) { - const instance = BridgeOrchestrator.instance - - if (!instance) { - try { - console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`) - - // Populate telemetry properties before registering the instance. - await options.provider.getTelemetryProperties() - - BridgeOrchestrator.instance = new BridgeOrchestrator(options) - await BridgeOrchestrator.instance.connect() - } catch (error) { - console.error( - `[BridgeOrchestrator#connectOrDisconnect] connect() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } else { - if ( - instance.connectionState === ConnectionState.FAILED || - instance.connectionState === ConnectionState.DISCONNECTED - ) { - console.log( - `[BridgeOrchestrator#connectOrDisconnect] Re-connecting... (state: ${instance.connectionState})`, - ) - - instance.reconnect().catch((error) => { - console.error( - `[BridgeOrchestrator#connectOrDisconnect] reconnect() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - }) - } else { - console.log( - `[BridgeOrchestrator#connectOrDisconnect] Already connected or connecting (state: ${instance.connectionState})`, - ) - } - } - } - - public static async disconnect() { - const instance = BridgeOrchestrator.instance - - if (instance) { - try { - console.log( - `[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`, - ) - - await instance.disconnect() - } catch (error) { - console.error( - `[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } finally { - BridgeOrchestrator.instance = null - } - } else { - console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`) - } - } - - /** - * @TODO: What if subtasks also get spawned? We'd probably want deferred - * subscriptions for those too. - */ - public static async subscribeToTask(task: TaskLike): Promise { - const instance = BridgeOrchestrator.instance - - if (instance && instance.socketTransport.isConnected()) { - console.log(`[BridgeOrchestrator#subscribeToTask] Subscribing to task ${task.taskId}`) - await instance.subscribeToTask(task) - } else { - console.log(`[BridgeOrchestrator#subscribeToTask] Deferring subscription for task ${task.taskId}`) - BridgeOrchestrator.pendingTask = task - } - } - - private constructor(options: BridgeOrchestratorOptions) { - this.userId = options.userId - this.socketBridgeUrl = options.socketBridgeUrl - this.token = options.token - this.provider = options.provider - this.instanceId = options.sessionId || crypto.randomUUID() - this.appProperties = { ...options.provider.appProperties, hostname: os.hostname() } - this.gitProperties = options.provider.gitProperties - this.isCloudAgent = options.isCloudAgent - - this.socketTransport = new SocketTransport({ - 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.extensionChannel = new ExtensionChannel({ - instanceId: this.instanceId, - appProperties: this.appProperties, - gitProperties: this.gitProperties, - userId: this.userId, - provider: this.provider, - isCloudAgent: this.isCloudAgent, - }) - - this.taskChannel = new TaskChannel({ - instanceId: this.instanceId, - appProperties: this.appProperties, - gitProperties: this.gitProperties, - isCloudAgent: this.isCloudAgent, - }) - } - - private setupSocketListeners() { - const socket = this.socketTransport.getSocket() - - if (!socket) { - console.error("[BridgeOrchestrator] 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( - `[BridgeOrchestrator] on(${ExtensionSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.instanceId}`, - ) - - this.extensionChannel?.handleCommand(message) - }) - - socket.on(TaskSocketEvents.RELAYED_COMMAND, (message: TaskBridgeCommand) => { - console.log( - `[BridgeOrchestrator] on(${TaskSocketEvents.RELAYED_COMMAND}) -> ${message.type} for ${message.taskId}`, - ) - - this.taskChannel.handleCommand(message) - }) - } - - private async handleConnect() { - const socket = this.socketTransport.getSocket() - - if (!socket) { - console.error("[BridgeOrchestrator#handleConnect] Socket not available") - return - } - - await this.extensionChannel.onConnect(socket) - await this.taskChannel.onConnect(socket) - - if (BridgeOrchestrator.pendingTask) { - console.log( - `[BridgeOrchestrator#handleConnect] Subscribing to task ${BridgeOrchestrator.pendingTask.taskId}`, - ) - - try { - await this.subscribeToTask(BridgeOrchestrator.pendingTask) - BridgeOrchestrator.pendingTask = null - } catch (error) { - console.error( - `[BridgeOrchestrator#handleConnect] subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - } - - private handleDisconnect() { - this.extensionChannel.onDisconnect() - this.taskChannel.onDisconnect() - } - - private async handleReconnect() { - const socket = this.socketTransport.getSocket() - - if (!socket) { - console.error("[BridgeOrchestrator] 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.extensionChannel.onReconnect(socket) - await this.taskChannel.onReconnect(socket) - } - - // Task API - - public async subscribeToTask(task: TaskLike): Promise { - const socket = this.socketTransport.getSocket() - - if (!socket || !this.socketTransport.isConnected()) { - console.warn("[BridgeOrchestrator] Cannot subscribe to task: not connected. Will retry when connected.") - this.taskChannel.addPendingTask(task) - - if ( - this.connectionState === ConnectionState.DISCONNECTED || - this.connectionState === ConnectionState.FAILED - ) { - await this.connect() - } - - return - } - - await this.taskChannel.subscribeToTask(task, socket) - } - - public async unsubscribeFromTask(taskId: string): Promise { - const socket = this.socketTransport.getSocket() - - if (!socket) { - return - } - - await this.taskChannel.unsubscribeFromTask(taskId, socket) - } - - // Shared API - - public get connectionState(): ConnectionState { - return this.socketTransport.getConnectionState() - } - - private async connect(): Promise { - await this.socketTransport.connect() - this.setupSocketListeners() - } - - public async disconnect(): Promise { - await this.extensionChannel.cleanup(this.socketTransport.getSocket()) - await this.taskChannel.cleanup(this.socketTransport.getSocket()) - await this.socketTransport.disconnect() - BridgeOrchestrator.instance = null - BridgeOrchestrator.pendingTask = null - } - - public async reconnect(): Promise { - await this.socketTransport.reconnect() - - // After a manual reconnect, we have a new socket instance - // so we need to set up listeners again. - this.setupSocketListeners() - } -} diff --git a/packages/cloud/src/bridge/ExtensionChannel.ts b/packages/cloud/src/bridge/ExtensionChannel.ts deleted file mode 100644 index 26fce96228e..00000000000 --- a/packages/cloud/src/bridge/ExtensionChannel.ts +++ /dev/null @@ -1,282 +0,0 @@ -import type { Socket } from "socket.io-client" - -import { - type TaskProviderLike, - type TaskProviderEvents, - type ExtensionInstance, - type ExtensionBridgeCommand, - type ExtensionBridgeEvent, - RooCodeEventName, - TaskStatus, - ExtensionBridgeCommandName, - ExtensionBridgeEventName, - ExtensionSocketEvents, - HEARTBEAT_INTERVAL_MS, -} from "@roo-code/types" - -import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js" - -interface ExtensionChannelOptions extends BaseChannelOptions { - userId: string - provider: TaskProviderLike -} - -/** - * Manages the extension-level communication channel. - * Handles extension registration, heartbeat, and extension-specific commands. - */ -export class ExtensionChannel extends BaseChannel< - ExtensionBridgeCommand, - ExtensionSocketEvents, - ExtensionBridgeEvent | ExtensionInstance -> { - private userId: string - private provider: TaskProviderLike - private extensionInstance: ExtensionInstance - private heartbeatInterval: NodeJS.Timeout | null = null - private eventListeners: Map void> = new Map() - - constructor(options: ExtensionChannelOptions) { - super({ - instanceId: options.instanceId, - appProperties: options.appProperties, - gitProperties: options.gitProperties, - isCloudAgent: options.isCloudAgent, - }) - - this.userId = options.userId - this.provider = options.provider - - this.extensionInstance = { - instanceId: this.instanceId, - userId: this.userId, - workspacePath: this.provider.cwd, - appProperties: this.appProperties, - gitProperties: this.gitProperties, - lastHeartbeat: Date.now(), - task: { taskId: "", taskStatus: TaskStatus.None }, - taskHistory: [], - isCloudAgent: this.isCloudAgent, - } - - this.setupListeners() - } - - protected async handleCommandImplementation(command: ExtensionBridgeCommand): Promise { - if (command.instanceId !== this.instanceId) { - console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, { - messageInstanceId: command.instanceId, - }) - - return - } - - switch (command.type) { - case ExtensionBridgeCommandName.StartTask: { - console.log(`[ExtensionChannel] command -> createTask() | ${command.instanceId}`, { - text: command.payload.text?.substring(0, 100) + "...", - hasImages: !!command.payload.images, - mode: command.payload.mode, - providerProfile: command.payload.providerProfile, - }) - - this.provider.createTask( - command.payload.text, - command.payload.images, - undefined, // parentTask - undefined, // options - { mode: command.payload.mode, currentApiConfigName: command.payload.providerProfile }, - ) - - break - } - case ExtensionBridgeCommandName.StopTask: { - const instance = await this.updateInstance() - - if (instance.task.taskStatus === TaskStatus.Running) { - console.log(`[ExtensionChannel] command -> cancelTask() | ${command.instanceId}`) - this.provider.cancelTask() - this.provider.postStateToWebview() - } else if (instance.task.taskId) { - console.log(`[ExtensionChannel] command -> clearTask() | ${command.instanceId}`) - this.provider.clearTask() - this.provider.postStateToWebview() - } - - break - } - case ExtensionBridgeCommandName.ResumeTask: { - console.log(`[ExtensionChannel] command -> resumeTask() | ${command.instanceId}`, { - taskId: command.payload.taskId, - }) - - this.provider.resumeTask(command.payload.taskId) - this.provider.postStateToWebview() - break - } - } - } - - protected async handleConnect(socket: Socket): Promise { - await this.registerInstance(socket) - this.startHeartbeat(socket) - } - - protected async handleReconnect(socket: Socket): Promise { - await this.registerInstance(socket) - this.startHeartbeat(socket) - } - - protected override handleDisconnect(): void { - this.stopHeartbeat() - } - - protected async handleCleanup(socket: Socket): Promise { - this.stopHeartbeat() - this.cleanupListeners() - await this.unregisterInstance(socket) - } - - private async registerInstance(_socket: Socket): Promise { - const instance = await this.updateInstance() - await this.publish(ExtensionSocketEvents.REGISTER, instance) - } - - private async unregisterInstance(_socket: Socket): Promise { - const instance = await this.updateInstance() - await this.publish(ExtensionSocketEvents.UNREGISTER, instance) - } - - private startHeartbeat(socket: Socket): void { - this.stopHeartbeat() - - this.heartbeatInterval = setInterval(async () => { - const instance = await this.updateInstance() - - try { - socket.emit(ExtensionSocketEvents.HEARTBEAT, instance) - // Heartbeat is too frequent to log - } catch (error) { - console.error( - `[ExtensionChannel] 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 }, - { from: RooCodeEventName.TaskPaused, to: ExtensionBridgeEventName.TaskPaused }, - { from: RooCodeEventName.TaskUnpaused, to: ExtensionBridgeEventName.TaskUnpaused }, - { from: RooCodeEventName.TaskSpawned, to: ExtensionBridgeEventName.TaskSpawned }, - { from: RooCodeEventName.TaskDelegated, to: ExtensionBridgeEventName.TaskDelegated }, - { from: RooCodeEventName.TaskDelegationCompleted, to: ExtensionBridgeEventName.TaskDelegationCompleted }, - { from: RooCodeEventName.TaskDelegationResumed, to: ExtensionBridgeEventName.TaskDelegationResumed }, - { from: RooCodeEventName.TaskUserMessage, to: ExtensionBridgeEventName.TaskUserMessage }, - { from: RooCodeEventName.TaskTokenUsageUpdated, to: ExtensionBridgeEventName.TaskTokenUsageUpdated }, - ] as const - - eventMapping.forEach(({ from, to }) => { - // Create and store the listener function for cleanup. - const listener = async (...args: unknown[]) => { - const baseEvent: { - type: ExtensionBridgeEventName - instance: ExtensionInstance - timestamp: number - } = { - type: to, - instance: await this.updateInstance(), - timestamp: Date.now(), - } - - let eventToPublish: ExtensionBridgeEvent - - // Add payload for delegation events while avoiding `any` - if (to === ExtensionBridgeEventName.TaskDelegationCompleted) { - const [parentTaskId, childTaskId, summary] = args as [string, string, string] - eventToPublish = { - ...(baseEvent as unknown as ExtensionBridgeEvent), - payload: { parentTaskId, childTaskId, summary }, - } as unknown as ExtensionBridgeEvent - } else if (to === ExtensionBridgeEventName.TaskDelegationResumed) { - const [parentTaskId, childTaskId] = args as [string, string] - eventToPublish = { - ...(baseEvent as unknown as ExtensionBridgeEvent), - payload: { parentTaskId, childTaskId }, - } as unknown as ExtensionBridgeEvent - } else { - eventToPublish = baseEvent as unknown as ExtensionBridgeEvent - } - - this.publish(ExtensionSocketEvents.EVENT, eventToPublish) - } - - this.eventListeners.set(from, listener) - this.provider.on(from, listener) - }) - } - - private cleanupListeners(): void { - this.eventListeners.forEach((listener, eventName) => { - // Cast is safe because we only store valid event names from eventMapping. - this.provider.off(eventName as keyof TaskProviderEvents, listener) - }) - - this.eventListeners.clear() - } - - private async updateInstance(): Promise { - const task = this.provider?.getCurrentTask() - const taskHistory = this.provider?.getRecentTasks() ?? [] - - const mode = await this.provider?.getMode() - const modes = (await this.provider?.getModes()) ?? [] - - const providerProfile = await this.provider?.getProviderProfile() - const providerProfiles = (await this.provider?.getProviderProfiles()) ?? [] - - this.extensionInstance = { - ...this.extensionInstance, - lastHeartbeat: Date.now(), - task: task - ? { - taskId: task.taskId, - parentTaskId: task.parentTaskId, - childTaskId: task.childTaskId, - taskStatus: task.taskStatus, - taskAsk: task?.taskAsk, - queuedMessages: task.queuedMessages, - tokenUsage: task.tokenUsage, - ...task.metadata, - } - : { taskId: "", taskStatus: TaskStatus.None }, - taskAsk: task?.taskAsk, - taskHistory, - mode, - providerProfile, - modes, - providerProfiles, - } - - return this.extensionInstance - } -} diff --git a/packages/cloud/src/bridge/SocketTransport.ts b/packages/cloud/src/bridge/SocketTransport.ts deleted file mode 100644 index 2df3cf95eba..00000000000 --- a/packages/cloud/src/bridge/SocketTransport.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { io, type Socket, type SocketOptions, type ManagerOptions } from "socket.io-client" - -import { ConnectionState, type RetryConfig } from "@roo-code/types" - -export interface SocketTransportOptions { - url: string - socketOptions: Partial - onConnect?: () => void | Promise - onDisconnect?: (reason: string) => void - onReconnect?: () => void | Promise - logger?: { - log: (message: string, ...args: unknown[]) => void - error: (message: string, ...args: unknown[]) => void - warn: (message: string, ...args: unknown[]) => void - } -} - -/** - * Manages the WebSocket transport layer for the bridge system. - * Handles connection lifecycle, retries, and reconnection logic. - */ -export class SocketTransport { - private socket: Socket | null = null - private connectionState: ConnectionState = ConnectionState.DISCONNECTED - private retryTimeout: NodeJS.Timeout | null = null - private isPreviouslyConnected: boolean = false - - private readonly retryConfig: RetryConfig = { - maxInitialAttempts: Infinity, - initialDelay: 1_000, - maxDelay: 15_000, - backoffMultiplier: 2, - } - - private readonly CONNECTION_TIMEOUT = 2_000 - private readonly options: SocketTransportOptions - - constructor(options: SocketTransportOptions, retryConfig?: Partial) { - this.options = options - - if (retryConfig) { - this.retryConfig = { ...this.retryConfig, ...retryConfig } - } - } - - // This is the initial connnect attempt. We need to implement our own - // infinite retry mechanism since Socket.io's automatic reconnection only - // kicks in after a successful initial connection. - public async connect(): Promise { - if (this.connectionState === ConnectionState.CONNECTED) { - console.log(`[SocketTransport#connect] Already connected`) - return - } - - if (this.connectionState === ConnectionState.CONNECTING || this.connectionState === ConnectionState.RETRYING) { - console.log(`[SocketTransport#connect] Already in progress`) - return - } - - let attempt = 0 - let delay = this.retryConfig.initialDelay - - while (attempt < this.retryConfig.maxInitialAttempts) { - console.log(`[SocketTransport#connect] attempt = ${attempt + 1}, delay = ${delay}ms`) - this.connectionState = attempt === 0 ? ConnectionState.CONNECTING : ConnectionState.RETRYING - - try { - await this._connect() - break - } catch (_error) { - attempt++ - - if (this.socket) { - this.socket.disconnect() - this.socket = null - } - - const promise = new Promise((resolve) => { - this.retryTimeout = setTimeout(resolve, delay) - }) - - await promise - - delay = Math.min(delay * this.retryConfig.backoffMultiplier, this.retryConfig.maxDelay) - } - } - - if (this.retryTimeout) { - clearTimeout(this.retryTimeout) - this.retryTimeout = null - } - - if (this.socket?.connected) { - console.log(`[SocketTransport#connect] connected - ${this.options.url}`) - } else { - // Since we have infinite retries this should never happen. - this.connectionState = ConnectionState.FAILED - console.error(`[SocketTransport#connect] Giving up`) - } - } - - private async _connect(): Promise { - return new Promise((resolve, reject) => { - this.socket = io(this.options.url, this.options.socketOptions) - - let connectionTimeout: NodeJS.Timeout | null = setTimeout(() => { - console.error(`[SocketTransport#_connect] failed to connect after ${this.CONNECTION_TIMEOUT}ms`) - - if (this.connectionState !== ConnectionState.CONNECTED) { - this.socket?.disconnect() - reject(new Error("Connection timeout")) - } - }, this.CONNECTION_TIMEOUT) - - // https://socket.io/docs/v4/client-api/#event-connect - this.socket.on("connect", async () => { - console.log( - `[SocketTransport#_connect] on(connect): isPreviouslyConnected = ${this.isPreviouslyConnected}`, - ) - - if (connectionTimeout) { - clearTimeout(connectionTimeout) - connectionTimeout = null - } - - this.connectionState = ConnectionState.CONNECTED - - if (this.isPreviouslyConnected) { - if (this.options.onReconnect) { - await this.options.onReconnect() - } - } else { - if (this.options.onConnect) { - await this.options.onConnect() - } - } - - this.isPreviouslyConnected = true - resolve() - }) - - // https://socket.io/docs/v4/client-api/#event-connect_error - this.socket.on("connect_error", (error) => { - if (connectionTimeout && this.connectionState !== ConnectionState.CONNECTED) { - console.error(`[SocketTransport] on(connect_error): ${error.message}`) - clearTimeout(connectionTimeout) - connectionTimeout = null - reject(error) - } - }) - - // https://socket.io/docs/v4/client-api/#event-disconnect - this.socket.on("disconnect", (reason, details) => { - console.log( - `[SocketTransport#_connect] on(disconnect) (reason: ${reason}, details: ${JSON.stringify(details)})`, - ) - 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.isPreviouslyConnected) { - // After successful initial connection, rely entirely on - // Socket.IO's reconnection logic. - console.log("[SocketTransport#_connect] will attempt to reconnect") - } else { - console.log("[SocketTransport#_connect] will *NOT* attempt to reconnect") - } - }) - - // https://socket.io/docs/v4/client-api/#event-error - // Fired upon a connection error. - this.socket.io.on("error", (error) => { - // Connection error. - if (connectionTimeout && this.connectionState !== ConnectionState.CONNECTED) { - console.error(`[SocketTransport#_connect] on(error): ${error.message}`) - clearTimeout(connectionTimeout) - connectionTimeout = null - reject(error) - } - - // Post-connection error. - if (this.connectionState === ConnectionState.CONNECTED) { - console.error(`[SocketTransport#_connect] on(error): ${error.message}`) - } - }) - - // https://socket.io/docs/v4/client-api/#event-reconnect - // Fired upon a successful reconnection. - this.socket.io.on("reconnect", (attempt) => { - console.log(`[SocketTransport#_connect] on(reconnect) - ${attempt}`) - this.connectionState = ConnectionState.CONNECTED - - if (this.options.onReconnect) { - this.options.onReconnect() - } - }) - - // https://socket.io/docs/v4/client-api/#event-reconnect_attempt - // Fired upon an attempt to reconnect. - this.socket.io.on("reconnect_attempt", (attempt) => { - console.log(`[SocketTransport#_connect] on(reconnect_attempt) - ${attempt}`) - }) - - // https://socket.io/docs/v4/client-api/#event-reconnect_error - // Fired upon a reconnection attempt error. - this.socket.io.on("reconnect_error", (error) => { - console.error(`[SocketTransport#_connect] on(reconnect_error): ${error.message}`) - }) - - // https://socket.io/docs/v4/client-api/#event-reconnect_failed - // Fired when couldn't reconnect within `reconnectionAttempts`. - // Since we use infinite retries, this should never fire. - this.socket.io.on("reconnect_failed", () => { - console.error(`[SocketTransport#_connect] on(reconnect_failed) - giving up`) - this.connectionState = ConnectionState.FAILED - }) - - // This is a custom event fired by the server. - this.socket.on("auth_error", (error) => { - console.error( - `[SocketTransport#_connect] on(auth_error): ${error instanceof Error ? error.message : String(error)}`, - ) - - if (connectionTimeout && this.connectionState !== ConnectionState.CONNECTED) { - clearTimeout(connectionTimeout) - connectionTimeout = null - reject(new Error(error.message || "Authentication failed")) - } - }) - }) - } - - public async disconnect(): Promise { - console.log(`[SocketTransport#disconnect] Disconnecting...`) - - if (this.retryTimeout) { - clearTimeout(this.retryTimeout) - this.retryTimeout = null - } - - if (this.socket) { - this.socket.removeAllListeners() - this.socket.io.removeAllListeners() - this.socket.disconnect() - this.socket = null - } - - this.connectionState = ConnectionState.DISCONNECTED - console.log(`[SocketTransport#disconnect] 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 { - console.log(`[SocketTransport#reconnect] Manually reconnecting...`) - - if (this.connectionState === ConnectionState.CONNECTED) { - console.log(`[SocketTransport#reconnect] Already connected`) - return - } - - this.isPreviouslyConnected = false - await this.disconnect() - await this.connect() - } -} diff --git a/packages/cloud/src/bridge/TaskChannel.ts b/packages/cloud/src/bridge/TaskChannel.ts deleted file mode 100644 index 433e740d4ed..00000000000 --- a/packages/cloud/src/bridge/TaskChannel.ts +++ /dev/null @@ -1,241 +0,0 @@ -import type { Socket } from "socket.io-client" - -import { - type ClineMessage, - type TaskEvents, - type TaskLike, - type TaskBridgeCommand, - type TaskBridgeEvent, - type JoinResponse, - type LeaveResponse, - RooCodeEventName, - TaskBridgeEventName, - TaskBridgeCommandName, - TaskSocketEvents, -} from "@roo-code/types" - -import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js" - -type TaskEventListener = { - [K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise -}[keyof TaskEvents] - -type TaskEventMapping = { - from: keyof TaskEvents - to: TaskBridgeEventName - createPayload: (task: TaskLike, ...args: any[]) => any // eslint-disable-line @typescript-eslint/no-explicit-any -} - -// eslint-disable-next-line @typescript-eslint/no-empty-object-type -interface TaskChannelOptions extends BaseChannelOptions {} - -/** - * Manages task-level communication channels. - * Handles task subscriptions, messaging, and task-specific commands. - */ -export class TaskChannel extends BaseChannel< - TaskBridgeCommand, - TaskSocketEvents, - TaskBridgeEvent | { taskId: string } -> { - private subscribedTasks: Map = new Map() - private pendingTasks: Map = new Map() - private taskListeners: Map> = new Map() - - private readonly eventMapping: readonly TaskEventMapping[] = [ - { - from: RooCodeEventName.Message, - to: TaskBridgeEventName.Message, - createPayload: (task: TaskLike, data: { action: string; message: ClineMessage }) => ({ - type: TaskBridgeEventName.Message, - taskId: task.taskId, - action: data.action, - message: data.message, - }), - }, - { - from: RooCodeEventName.TaskModeSwitched, - to: TaskBridgeEventName.TaskModeSwitched, - createPayload: (task: TaskLike, mode: string) => ({ - type: TaskBridgeEventName.TaskModeSwitched, - taskId: task.taskId, - mode, - }), - }, - { - from: RooCodeEventName.TaskInteractive, - to: TaskBridgeEventName.TaskInteractive, - createPayload: (task: TaskLike, _taskId: string) => ({ - type: TaskBridgeEventName.TaskInteractive, - taskId: task.taskId, - }), - }, - ] as const - - constructor(options: TaskChannelOptions) { - super(options) - } - - protected async handleCommandImplementation(command: TaskBridgeCommand): Promise { - const task = this.subscribedTasks.get(command.taskId) - - if (!task) { - console.error(`[TaskChannel] Unable to find task ${command.taskId}`) - return - } - - switch (command.type) { - case TaskBridgeCommandName.Message: - console.log( - `[TaskChannel] ${TaskBridgeCommandName.Message} ${command.taskId} -> submitUserMessage()`, - command, - ) - - await task.submitUserMessage( - command.payload.text, - command.payload.images, - command.payload.mode, - command.payload.providerProfile, - ) - - break - - case TaskBridgeCommandName.ApproveAsk: - console.log( - `[TaskChannel] ${TaskBridgeCommandName.ApproveAsk} ${command.taskId} -> approveAsk()`, - command, - ) - - task.approveAsk(command.payload) - break - - case TaskBridgeCommandName.DenyAsk: - console.log(`[TaskChannel] ${TaskBridgeCommandName.DenyAsk} ${command.taskId} -> denyAsk()`, command) - task.denyAsk(command.payload) - break - } - } - - protected async handleConnect(socket: Socket): Promise { - // Rejoin all subscribed tasks. - for (const taskId of this.subscribedTasks.keys()) { - await this.publish(TaskSocketEvents.JOIN, { taskId }) - } - - // Subscribe to any pending tasks. - for (const task of this.pendingTasks.values()) { - await this.subscribeToTask(task, socket) - } - - this.pendingTasks.clear() - } - - protected async handleReconnect(_socket: Socket): Promise { - // Rejoin all subscribed tasks. - for (const taskId of this.subscribedTasks.keys()) { - await this.publish(TaskSocketEvents.JOIN, { taskId }) - } - } - - protected async handleCleanup(socket: Socket): Promise { - 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() - } - - /** - * Add a task to the pending queue (will be subscribed when connected). - */ - public addPendingTask(task: TaskLike): void { - this.pendingTasks.set(task.taskId, task) - } - - public async subscribeToTask(task: TaskLike, _socket: Socket): Promise { - const taskId = task.taskId - - await this.publish(TaskSocketEvents.JOIN, { taskId }, (response: JoinResponse) => { - if (response.success) { - console.log(`[TaskChannel#subscribeToTask] subscribed to ${taskId}`) - this.subscribedTasks.set(taskId, task) - this.setupTaskListeners(task) - } else { - console.error(`[TaskChannel#subscribeToTask] failed to subscribe to ${taskId}: ${response.error}`) - } - }) - } - - public async unsubscribeFromTask(taskId: string, _socket: Socket): Promise { - const task = this.subscribedTasks.get(taskId) - - if (!task) { - return - } - - await this.publish(TaskSocketEvents.LEAVE, { taskId }, (response: LeaveResponse) => { - if (response.success) { - console.log(`[TaskChannel#unsubscribeFromTask] unsubscribed from ${taskId}`) - } else { - console.error(`[TaskChannel#unsubscribeFromTask] failed to unsubscribe from ${taskId}`) - } - - // If we failed to unsubscribe then something is probably wrong and - // we should still discard this task from `subscribedTasks`. - this.removeTaskListeners(task) - this.subscribedTasks.delete(taskId) - }) - } - - private setupTaskListeners(task: TaskLike): void { - if (this.taskListeners.has(task.taskId)) { - console.warn(`[TaskChannel] Listeners already exist for task, removing old listeners for ${task.taskId}`) - this.removeTaskListeners(task) - } - - const listeners = new Map() - - this.eventMapping.forEach(({ from, to, createPayload }) => { - const listener = (...args: unknown[]) => { - const payload = createPayload(task, ...args) - this.publish(TaskSocketEvents.EVENT, payload) - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - task.on(from, listener as any) - listeners.set(to, listener) - }) - - this.taskListeners.set(task.taskId, listeners) - } - - private removeTaskListeners(task: TaskLike): void { - const listeners = this.taskListeners.get(task.taskId) - - if (!listeners) { - return - } - - this.eventMapping.forEach(({ from, to }) => { - const listener = listeners.get(to) - if (listener) { - try { - task.off(from, listener as any) // eslint-disable-line @typescript-eslint/no-explicit-any - } catch (error) { - console.error( - `[TaskChannel] task.off(${from}) failed for task ${task.taskId}: ${ - error instanceof Error ? error.message : String(error) - }`, - ) - } - } - }) - - this.taskListeners.delete(task.taskId) - } -} diff --git a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts deleted file mode 100644 index 188e2cc0294..00000000000 --- a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts +++ /dev/null @@ -1,402 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { Socket } from "socket.io-client" - -import { - type TaskProviderLike, - type TaskProviderEvents, - type StaticAppProperties, - RooCodeEventName, - ExtensionBridgeEventName, - ExtensionSocketEvents, -} from "@roo-code/types" - -import { ExtensionChannel } from "../ExtensionChannel.js" - -describe("ExtensionChannel", () => { - let mockSocket: Socket - let mockProvider: TaskProviderLike - let extensionChannel: ExtensionChannel - const instanceId = "test-instance-123" - const userId = "test-user-456" - - const appProperties: StaticAppProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.0.0", - platform: "darwin", - editorName: "Roo Code", - hostname: "test-host", - } - - // Track registered event listeners - const eventListeners = new Map unknown>>() - - beforeEach(() => { - // Reset the event listeners tracker - eventListeners.clear() - - // Create mock socket - mockSocket = { - emit: vi.fn(), - on: vi.fn(), - off: vi.fn(), - disconnect: vi.fn(), - } as unknown as Socket - - // Create mock provider with event listener tracking - mockProvider = { - cwd: "/test/workspace", - appProperties: { - version: "1.0.0", - extensionVersion: "1.0.0", - }, - gitProperties: undefined, - getCurrentTask: vi.fn().mockReturnValue(undefined), - getCurrentTaskStack: vi.fn().mockReturnValue([]), - getRecentTasks: vi.fn().mockReturnValue([]), - createTask: vi.fn(), - cancelTask: vi.fn(), - clearTask: vi.fn(), - resumeTask: vi.fn(), - getState: vi.fn(), - postStateToWebview: vi.fn(), - postMessageToWebview: vi.fn(), - getTelemetryProperties: vi.fn(), - getMode: vi.fn().mockResolvedValue("code"), - getModes: vi.fn().mockResolvedValue([ - { slug: "code", name: "Code", description: "Code mode" }, - { slug: "architect", name: "Architect", description: "Architect mode" }, - ]), - getProviderProfile: vi.fn().mockResolvedValue("default"), - getProviderProfiles: vi.fn().mockResolvedValue([{ name: "default", description: "Default profile" }]), - on: vi.fn((event: keyof TaskProviderEvents, listener: (...args: unknown[]) => unknown) => { - if (!eventListeners.has(event)) { - eventListeners.set(event, new Set()) - } - eventListeners.get(event)!.add(listener) - return mockProvider - }), - off: vi.fn((event: keyof TaskProviderEvents, listener: (...args: unknown[]) => unknown) => { - const listeners = eventListeners.get(event) - if (listeners) { - listeners.delete(listener) - if (listeners.size === 0) { - eventListeners.delete(event) - } - } - return mockProvider - }), - } as unknown as TaskProviderLike - - // Create extension channel instance - extensionChannel = new ExtensionChannel({ - instanceId, - appProperties, - userId, - provider: mockProvider, - isCloudAgent: false, - }) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("Event Listener Management", () => { - it("should register event listeners on initialization", () => { - // Verify that listeners were registered for all expected events - const expectedEvents: RooCodeEventName[] = [ - RooCodeEventName.TaskCreated, - RooCodeEventName.TaskStarted, - RooCodeEventName.TaskCompleted, - RooCodeEventName.TaskAborted, - RooCodeEventName.TaskFocused, - RooCodeEventName.TaskUnfocused, - RooCodeEventName.TaskActive, - RooCodeEventName.TaskInteractive, - RooCodeEventName.TaskResumable, - RooCodeEventName.TaskIdle, - RooCodeEventName.TaskPaused, - RooCodeEventName.TaskUnpaused, - RooCodeEventName.TaskSpawned, - RooCodeEventName.TaskDelegated, - RooCodeEventName.TaskDelegationCompleted, - RooCodeEventName.TaskDelegationResumed, - - RooCodeEventName.TaskUserMessage, - RooCodeEventName.TaskTokenUsageUpdated, - ] - - // Check that on() was called for each event - expect(mockProvider.on).toHaveBeenCalledTimes(expectedEvents.length) - - // Verify each event was registered - expectedEvents.forEach((eventName) => { - expect(mockProvider.on).toHaveBeenCalledWith(eventName, expect.any(Function)) - }) - - // Verify listeners are tracked in our Map - expect(eventListeners.size).toBe(expectedEvents.length) - }) - - it("should remove all event listeners during cleanup", async () => { - // Verify initial state - listeners are registered - const initialListenerCount = eventListeners.size - expect(initialListenerCount).toBeGreaterThan(0) - - // Get the count of listeners for each event before cleanup - const listenerCountsBefore = new Map() - eventListeners.forEach((listeners, event) => { - listenerCountsBefore.set(event, listeners.size) - }) - - // Perform cleanup - await extensionChannel.cleanup(mockSocket) - - // Verify that off() was called for each registered event - expect(mockProvider.off).toHaveBeenCalledTimes(initialListenerCount) - - // Verify all listeners were removed from our tracking Map - expect(eventListeners.size).toBe(0) - - // Verify that the same listener functions that were added were removed - const onCalls = (mockProvider.on as any).mock.calls - const offCalls = (mockProvider.off as any).mock.calls - - // Each on() call should have a corresponding off() call with the same listener - onCalls.forEach(([eventName, listener]: [keyof TaskProviderEvents, any]) => { - const hasMatchingOff = offCalls.some( - ([offEvent, offListener]: [keyof TaskProviderEvents, any]) => - offEvent === eventName && offListener === listener, - ) - expect(hasMatchingOff).toBe(true) - }) - }) - - it("should not have duplicate listeners after multiple channel creations", () => { - // Create a second channel with the same provider - const secondChannel = new ExtensionChannel({ - instanceId: "instance-2", - appProperties, - userId, - provider: mockProvider, - isCloudAgent: false, - }) - - // Each event should have exactly 2 listeners (one from each channel) - eventListeners.forEach((listeners) => { - expect(listeners.size).toBe(2) - }) - - // Clean up the first channel - extensionChannel.cleanup(mockSocket) - - // Each event should now have exactly 1 listener (from the second channel) - eventListeners.forEach((listeners) => { - expect(listeners.size).toBe(1) - }) - - // Clean up the second channel - secondChannel.cleanup(mockSocket) - - // All listeners should be removed - expect(eventListeners.size).toBe(0) - }) - - it("should handle cleanup even if called multiple times", async () => { - // First cleanup - await extensionChannel.cleanup(mockSocket) - const firstOffCallCount = (mockProvider.off as any).mock.calls.length - - // Second cleanup (should be safe to call again) - await extensionChannel.cleanup(mockSocket) - const secondOffCallCount = (mockProvider.off as any).mock.calls.length - - // The second cleanup shouldn't try to remove listeners again - // since the internal Map was cleared - expect(secondOffCallCount).toBe(firstOffCallCount) - }) - - it("should properly forward events to socket when listeners are triggered", async () => { - // Connect the socket to enable publishing - await extensionChannel.onConnect(mockSocket) - - // Clear the mock calls from the connection (which emits a register event) - ;(mockSocket.emit as any).mockClear() - - // Get a listener that was registered for TaskStarted - const taskStartedListeners = eventListeners.get(RooCodeEventName.TaskStarted) - expect(taskStartedListeners).toBeDefined() - expect(taskStartedListeners!.size).toBe(1) - - // Trigger the listener - const listener = Array.from(taskStartedListeners!)[0] - if (listener) { - await listener("test-task-id") - } - - // Verify the event was published to the socket - expect(mockSocket.emit).toHaveBeenCalledWith( - ExtensionSocketEvents.EVENT, - expect.objectContaining({ - type: ExtensionBridgeEventName.TaskStarted, - instance: expect.objectContaining({ - instanceId, - userId, - }), - timestamp: expect.any(Number), - }), - undefined, - ) - }) - - it("should forward delegation events to socket", async () => { - await extensionChannel.onConnect(mockSocket) - ;(mockSocket.emit as any).mockClear() - - const delegatedListeners = eventListeners.get(RooCodeEventName.TaskDelegated) - expect(delegatedListeners).toBeDefined() - expect(delegatedListeners!.size).toBe(1) - - const listener = Array.from(delegatedListeners!)[0] - if (listener) { - await (listener as any)("parent-id", "child-id") - } - - expect(mockSocket.emit).toHaveBeenCalledWith( - ExtensionSocketEvents.EVENT, - expect.objectContaining({ - type: ExtensionBridgeEventName.TaskDelegated, - instance: expect.any(Object), - timestamp: expect.any(Number), - }), - undefined, - ) - }) - - it("should forward TaskDelegationCompleted with correct payload", async () => { - await extensionChannel.onConnect(mockSocket) - ;(mockSocket.emit as any).mockClear() - - const completedListeners = eventListeners.get(RooCodeEventName.TaskDelegationCompleted) - expect(completedListeners).toBeDefined() - - const listener = Array.from(completedListeners!)[0] - if (listener) { - await (listener as any)("parent-1", "child-1", "Summary text") - } - - expect(mockSocket.emit).toHaveBeenCalledWith( - ExtensionSocketEvents.EVENT, - expect.objectContaining({ - type: ExtensionBridgeEventName.TaskDelegationCompleted, - instance: expect.any(Object), - timestamp: expect.any(Number), - payload: expect.objectContaining({ - parentTaskId: "parent-1", - childTaskId: "child-1", - summary: "Summary text", - }), - }), - undefined, - ) - }) - - it("should forward TaskDelegationResumed with correct payload", async () => { - await extensionChannel.onConnect(mockSocket) - ;(mockSocket.emit as any).mockClear() - - const resumedListeners = eventListeners.get(RooCodeEventName.TaskDelegationResumed) - expect(resumedListeners).toBeDefined() - - const listener = Array.from(resumedListeners!)[0] - if (listener) { - await (listener as any)("parent-2", "child-2") - } - - expect(mockSocket.emit).toHaveBeenCalledWith( - ExtensionSocketEvents.EVENT, - expect.objectContaining({ - type: ExtensionBridgeEventName.TaskDelegationResumed, - instance: expect.any(Object), - timestamp: expect.any(Number), - payload: expect.objectContaining({ - parentTaskId: "parent-2", - childTaskId: "child-2", - }), - }), - undefined, - ) - }) - - it("should propagate all three delegation events in order", async () => { - await extensionChannel.onConnect(mockSocket) - ;(mockSocket.emit as any).mockClear() - - // Trigger TaskDelegated - const delegatedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegated)!)[0] - await (delegatedListener as any)("p1", "c1") - - // Trigger TaskDelegationCompleted - const completedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegationCompleted)!)[0] - await (completedListener as any)("p1", "c1", "result") - - // Trigger TaskDelegationResumed - const resumedListener = Array.from(eventListeners.get(RooCodeEventName.TaskDelegationResumed)!)[0] - await (resumedListener as any)("p1", "c1") - - // Verify all three events were emitted - const emittedEvents = (mockSocket.emit as any).mock.calls.map((call: any[]) => call[1]?.type) - expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegated) - expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegationCompleted) - expect(emittedEvents).toContain(ExtensionBridgeEventName.TaskDelegationResumed) - - // Verify correct order: Delegated → Completed → Resumed - const delegatedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegated) - const completedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegationCompleted) - const resumedIdx = emittedEvents.indexOf(ExtensionBridgeEventName.TaskDelegationResumed) - - expect(delegatedIdx).toBeLessThan(completedIdx) - expect(completedIdx).toBeLessThan(resumedIdx) - }) - }) - - describe("Memory Leak Prevention", () => { - it("should not accumulate listeners over multiple connect/disconnect cycles", async () => { - // Simulate multiple connect/disconnect cycles - for (let i = 0; i < 5; i++) { - await extensionChannel.onConnect(mockSocket) - extensionChannel.onDisconnect() - } - - // Listeners should still be the same count (not accumulated) - expect(eventListeners.size).toBe(18) - - // Each event should have exactly 1 listener - eventListeners.forEach((listeners) => { - expect(listeners.size).toBe(1) - }) - }) - - it("should properly clean up heartbeat interval", async () => { - // Spy on setInterval and clearInterval - const setIntervalSpy = vi.spyOn(global, "setInterval") - const clearIntervalSpy = vi.spyOn(global, "clearInterval") - - // Connect to start heartbeat - await extensionChannel.onConnect(mockSocket) - expect(setIntervalSpy).toHaveBeenCalled() - - // Get the interval ID - const intervalId = setIntervalSpy.mock.results[0]?.value - - // Cleanup should stop the heartbeat - await extensionChannel.cleanup(mockSocket) - expect(clearIntervalSpy).toHaveBeenCalledWith(intervalId) - - setIntervalSpy.mockRestore() - clearIntervalSpy.mockRestore() - }) - }) -}) diff --git a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts deleted file mode 100644 index f4f15266077..00000000000 --- a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts +++ /dev/null @@ -1,407 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-function-type */ -/* eslint-disable @typescript-eslint/no-explicit-any */ - -import type { Socket } from "socket.io-client" - -import { - type TaskLike, - type ClineMessage, - type StaticAppProperties, - RooCodeEventName, - TaskBridgeEventName, - TaskBridgeCommandName, - TaskSocketEvents, - TaskStatus, -} from "@roo-code/types" - -import { TaskChannel } from "../TaskChannel.js" - -describe("TaskChannel", () => { - let mockSocket: Socket - let taskChannel: TaskChannel - let mockTask: TaskLike - const instanceId = "test-instance-123" - const taskId = "test-task-456" - - const appProperties: StaticAppProperties = { - appName: "roo-code", - appVersion: "1.0.0", - vscodeVersion: "1.0.0", - platform: "darwin", - editorName: "Roo Code", - hostname: "test-host", - } - - beforeEach(() => { - // Create mock socket - mockSocket = { - emit: vi.fn(), - on: vi.fn(), - off: vi.fn(), - disconnect: vi.fn(), - } as unknown as Socket - - // Create mock task with event emitter functionality - const listeners = new Map unknown>>() - mockTask = { - taskId, - taskStatus: TaskStatus.Running, - taskAsk: undefined, - metadata: {}, - on: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => { - if (!listeners.has(event)) { - listeners.set(event, new Set()) - } - listeners.get(event)!.add(listener) - return mockTask - }), - off: vi.fn((event: string, listener: (...args: unknown[]) => unknown) => { - const eventListeners = listeners.get(event) - if (eventListeners) { - eventListeners.delete(listener) - if (eventListeners.size === 0) { - listeners.delete(event) - } - } - return mockTask - }), - approveAsk: vi.fn(), - denyAsk: vi.fn(), - submitUserMessage: vi.fn(), - abortTask: vi.fn(), - // Helper to trigger events in tests - _triggerEvent: (event: string, ...args: any[]) => { - const eventListeners = listeners.get(event) - if (eventListeners) { - eventListeners.forEach((listener) => listener(...args)) - } - }, - _getListenerCount: (event: string) => { - return listeners.get(event)?.size || 0 - }, - } as unknown as TaskLike & { - _triggerEvent: (event: string, ...args: any[]) => void - _getListenerCount: (event: string) => number - } - - // Create task channel instance - taskChannel = new TaskChannel({ - instanceId, - appProperties, - isCloudAgent: false, - }) - }) - - afterEach(() => { - vi.clearAllMocks() - }) - - describe("Event Mapping Refactoring", () => { - it("should use the unified event mapping approach", () => { - // Access the private eventMapping through type assertion - const channel = taskChannel as any - - // Verify eventMapping exists and has the correct structure - expect(channel.eventMapping).toBeDefined() - expect(Array.isArray(channel.eventMapping)).toBe(true) - expect(channel.eventMapping.length).toBe(3) - - // Verify each mapping has the required properties - channel.eventMapping.forEach((mapping: any) => { - expect(mapping).toHaveProperty("from") - expect(mapping).toHaveProperty("to") - expect(mapping).toHaveProperty("createPayload") - expect(typeof mapping.createPayload).toBe("function") - }) - - // Verify specific mappings - expect(channel.eventMapping[0].from).toBe(RooCodeEventName.Message) - expect(channel.eventMapping[0].to).toBe(TaskBridgeEventName.Message) - - expect(channel.eventMapping[1].from).toBe(RooCodeEventName.TaskModeSwitched) - expect(channel.eventMapping[1].to).toBe(TaskBridgeEventName.TaskModeSwitched) - - expect(channel.eventMapping[2].from).toBe(RooCodeEventName.TaskInteractive) - expect(channel.eventMapping[2].to).toBe(TaskBridgeEventName.TaskInteractive) - }) - - it("should setup listeners using the event mapping", async () => { - // Mock the publish method to simulate successful subscription - const channel = taskChannel as any - channel.publish = vi.fn((event: string, data: any, callback?: Function) => { - if (event === TaskSocketEvents.JOIN && callback) { - // Simulate successful join response - callback({ success: true }) - } - return true - }) - - // Connect and subscribe to task - await taskChannel.onConnect(mockSocket) - await channel.subscribeToTask(mockTask, mockSocket) - - // Wait for async operations - await new Promise((resolve) => setTimeout(resolve, 0)) - - // Verify listeners were registered for all mapped events - const task = mockTask as any - expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1) - }) - - it("should correctly transform Message event payloads", async () => { - // Setup channel with task - const channel = taskChannel as any - let publishCalls: any[] = [] - - channel.publish = vi.fn((event: string, data: any, callback?: Function) => { - publishCalls.push({ event, data }) - - if (event === TaskSocketEvents.JOIN && callback) { - callback({ success: true }) - } - - return true - }) - - await taskChannel.onConnect(mockSocket) - await channel.subscribeToTask(mockTask, mockSocket) - await new Promise((resolve) => setTimeout(resolve, 0)) - - // Clear previous calls - publishCalls = [] - - // Trigger Message event - const messageData = { - action: "test-action", - message: { type: "say", text: "Hello" } as ClineMessage, - } - - ;(mockTask as any)._triggerEvent(RooCodeEventName.Message, messageData) - - // Verify the event was published with correct payload - expect(publishCalls.length).toBe(1) - expect(publishCalls[0]).toEqual({ - event: TaskSocketEvents.EVENT, - data: { - type: TaskBridgeEventName.Message, - taskId: taskId, - action: messageData.action, - message: messageData.message, - }, - }) - }) - - it("should correctly transform TaskModeSwitched event payloads", async () => { - // Setup channel with task - const channel = taskChannel as any - let publishCalls: any[] = [] - - channel.publish = vi.fn((event: string, data: any, callback?: Function) => { - publishCalls.push({ event, data }) - - if (event === TaskSocketEvents.JOIN && callback) { - callback({ success: true }) - } - - return true - }) - - await taskChannel.onConnect(mockSocket) - await channel.subscribeToTask(mockTask, mockSocket) - await new Promise((resolve) => setTimeout(resolve, 0)) - - // Clear previous calls - publishCalls = [] - - // Trigger TaskModeSwitched event - const mode = "architect" - ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskModeSwitched, mode) - - // Verify the event was published with correct payload - expect(publishCalls.length).toBe(1) - expect(publishCalls[0]).toEqual({ - event: TaskSocketEvents.EVENT, - data: { - type: TaskBridgeEventName.TaskModeSwitched, - taskId: taskId, - mode: mode, - }, - }) - }) - - it("should correctly transform TaskInteractive event payloads", async () => { - // Setup channel with task - const channel = taskChannel as any - let publishCalls: any[] = [] - - channel.publish = vi.fn((event: string, data: any, callback?: Function) => { - publishCalls.push({ event, data }) - if (event === TaskSocketEvents.JOIN && callback) { - callback({ success: true }) - } - return true - }) - - await taskChannel.onConnect(mockSocket) - await channel.subscribeToTask(mockTask, mockSocket) - await new Promise((resolve) => setTimeout(resolve, 0)) - - // Clear previous calls - publishCalls = [] - - // Trigger TaskInteractive event - ;(mockTask as any)._triggerEvent(RooCodeEventName.TaskInteractive, taskId) - - // Verify the event was published with correct payload - expect(publishCalls.length).toBe(1) - expect(publishCalls[0]).toEqual({ - event: TaskSocketEvents.EVENT, - data: { - type: TaskBridgeEventName.TaskInteractive, - taskId: taskId, - }, - }) - }) - - it("should properly clean up listeners using event mapping", async () => { - // Setup channel with task - const channel = taskChannel as any - - channel.publish = vi.fn((event: string, data: any, callback?: Function) => { - if (event === TaskSocketEvents.JOIN && callback) { - callback({ success: true }) - } - if (event === TaskSocketEvents.LEAVE && callback) { - callback({ success: true }) - } - return true - }) - - await taskChannel.onConnect(mockSocket) - await channel.subscribeToTask(mockTask, mockSocket) - await new Promise((resolve) => setTimeout(resolve, 0)) - - // Verify listeners are registered - const task = mockTask as any - expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1) - - // Clean up - await taskChannel.cleanup(mockSocket) - - // Verify all listeners were removed - expect(task._getListenerCount(RooCodeEventName.Message)).toBe(0) - expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(0) - expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(0) - }) - - it("should handle duplicate listener prevention", async () => { - // Setup channel with task - await taskChannel.onConnect(mockSocket) - - // Subscribe to the same task twice - const channel = taskChannel as any - channel.subscribedTasks.set(taskId, mockTask) - channel.setupTaskListeners(mockTask) - - // Try to setup listeners again (should remove old ones first) - const warnSpy = vi.spyOn(console, "warn") - channel.setupTaskListeners(mockTask) - - // Verify warning was logged - expect(warnSpy).toHaveBeenCalledWith( - `[TaskChannel] Listeners already exist for task, removing old listeners for ${taskId}`, - ) - - // Verify only one set of listeners exists - const task = mockTask as any - expect(task._getListenerCount(RooCodeEventName.Message)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskModeSwitched)).toBe(1) - expect(task._getListenerCount(RooCodeEventName.TaskInteractive)).toBe(1) - - warnSpy.mockRestore() - }) - }) - - describe("Command Handling", () => { - beforeEach(async () => { - // Setup channel with a subscribed task - await taskChannel.onConnect(mockSocket) - const channel = taskChannel as any - channel.subscribedTasks.set(taskId, mockTask) - }) - - it("should handle Message command", async () => { - const command = { - type: TaskBridgeCommandName.Message, - taskId, - timestamp: Date.now(), - payload: { - text: "Hello, world!", - images: ["image1.png"], - }, - } - - await taskChannel.handleCommand(command) - - expect(mockTask.submitUserMessage).toHaveBeenCalledWith( - command.payload.text, - command.payload.images, - undefined, - undefined, - ) - }) - - it("should handle ApproveAsk command", async () => { - const command = { - type: TaskBridgeCommandName.ApproveAsk, - taskId, - timestamp: Date.now(), - payload: { - text: "Approved", - }, - } - - await taskChannel.handleCommand(command) - - expect(mockTask.approveAsk).toHaveBeenCalledWith(command.payload) - }) - - it("should handle DenyAsk command", async () => { - const command = { - type: TaskBridgeCommandName.DenyAsk, - taskId, - timestamp: Date.now(), - payload: { - text: "Denied", - }, - } - - await taskChannel.handleCommand(command) - - expect(mockTask.denyAsk).toHaveBeenCalledWith(command.payload) - }) - - it("should log error for unknown task", async () => { - const errorSpy = vi.spyOn(console, "error") - - const command = { - type: TaskBridgeCommandName.Message, - taskId: "unknown-task", - timestamp: Date.now(), - payload: { - text: "Hello", - }, - } - - await taskChannel.handleCommand(command) - - expect(errorSpy).toHaveBeenCalledWith(`[TaskChannel] Unable to find task unknown-task`) - - errorSpy.mockRestore() - }) - }) -}) diff --git a/packages/cloud/src/bridge/index.ts b/packages/cloud/src/bridge/index.ts deleted file mode 100644 index 94873c09fdf..00000000000 --- a/packages/cloud/src/bridge/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { type BridgeOrchestratorOptions, BridgeOrchestrator } from "./BridgeOrchestrator.js" -export { type SocketTransportOptions, SocketTransport } from "./SocketTransport.js" - -export { BaseChannel } from "./BaseChannel.js" -export { ExtensionChannel } from "./ExtensionChannel.js" -export { TaskChannel } from "./TaskChannel.js" diff --git a/packages/cloud/src/index.ts b/packages/cloud/src/index.ts index 65b92ebc13c..8792176fee0 100644 --- a/packages/cloud/src/index.ts +++ b/packages/cloud/src/index.ts @@ -2,7 +2,5 @@ export * from "./config.js" export { CloudService } from "./CloudService.js" -export { BridgeOrchestrator } from "./bridge/index.js" - export { RetryQueue } from "./retry-queue/index.js" export type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./retry-queue/index.js" diff --git a/packages/types/src/__tests__/cloud.test.ts b/packages/types/src/__tests__/cloud.test.ts index 7a6cebd8a51..a8aa2755b2e 100644 --- a/packages/types/src/__tests__/cloud.test.ts +++ b/packages/types/src/__tests__/cloud.test.ts @@ -19,43 +19,9 @@ describe("organizationFeaturesSchema", () => { expect(result.data).toEqual({}) }) - it("should validate with roomoteControlEnabled as true", () => { - const input = { roomoteControlEnabled: true } - const result = organizationFeaturesSchema.safeParse(input) - expect(result.success).toBe(true) - expect(result.data).toEqual(input) - }) - - it("should validate with roomoteControlEnabled as false", () => { - const input = { roomoteControlEnabled: false } - const result = organizationFeaturesSchema.safeParse(input) - expect(result.success).toBe(true) - expect(result.data).toEqual(input) - }) - - it("should reject non-boolean roomoteControlEnabled", () => { - const input = { roomoteControlEnabled: "true" } - const result = organizationFeaturesSchema.safeParse(input) - expect(result.success).toBe(false) - }) - - it("should allow additional properties (for future extensibility)", () => { - const input = { roomoteControlEnabled: true, futureProperty: "test" } - const result = organizationFeaturesSchema.safeParse(input) - expect(result.success).toBe(true) - expect(result.data?.roomoteControlEnabled).toBe(true) - // Note: Additional properties are stripped by Zod, which is expected behavior - }) - it("should have correct TypeScript type", () => { - // Type-only test to ensure TypeScript compilation - const features: OrganizationFeatures = { - roomoteControlEnabled: true, - } - expect(features.roomoteControlEnabled).toBe(true) - const emptyFeatures: OrganizationFeatures = {} - expect(emptyFeatures.roomoteControlEnabled).toBeUndefined() + expect(emptyFeatures).toEqual({}) }) }) @@ -85,43 +51,7 @@ describe("organizationSettingsSchema with features", () => { expect(result.data?.features).toEqual({}) }) - it("should validate with features.roomoteControlEnabled as true", () => { - const input = { - ...validBaseSettings, - features: { - roomoteControlEnabled: true, - }, - } - const result = organizationSettingsSchema.safeParse(input) - expect(result.success).toBe(true) - expect(result.data?.features?.roomoteControlEnabled).toBe(true) - }) - - it("should validate with features.roomoteControlEnabled as false", () => { - const input = { - ...validBaseSettings, - features: { - roomoteControlEnabled: false, - }, - } - const result = organizationSettingsSchema.safeParse(input) - expect(result.success).toBe(true) - expect(result.data?.features?.roomoteControlEnabled).toBe(false) - }) - - it("should reject invalid features object", () => { - const input = { - ...validBaseSettings, - features: { - roomoteControlEnabled: "invalid", - }, - } - const result = organizationSettingsSchema.safeParse(input) - expect(result.success).toBe(false) - }) - it("should have correct TypeScript type for features", () => { - // Type-only test to ensure TypeScript compilation const settings: OrganizationSettings = { version: 1, defaultSettings: {}, @@ -129,11 +59,9 @@ describe("organizationSettingsSchema with features", () => { allowAll: true, providers: {}, }, - features: { - roomoteControlEnabled: true, - }, + features: {}, } - expect(settings.features?.roomoteControlEnabled).toBe(true) + expect(settings.features).toEqual({}) const settingsWithoutFeatures: OrganizationSettings = { version: 1, @@ -163,9 +91,7 @@ describe("organizationSettingsSchema with features", () => { }, }, }, - features: { - roomoteControlEnabled: true, - }, + features: {}, hiddenMcps: ["test-mcp"], hideMarketplaceMcps: true, mcps: [], @@ -413,7 +339,6 @@ describe("organizationCloudSettingsSchema with llmEnhancedFeaturesEnabled", () = describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => { it("should validate without llmEnhancedFeaturesEnabled property", () => { const input = { - extensionBridgeEnabled: true, taskSyncEnabled: true, } const result = userSettingsConfigSchema.safeParse(input) @@ -423,7 +348,6 @@ describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => { it("should validate with llmEnhancedFeaturesEnabled as true", () => { const input = { - extensionBridgeEnabled: true, taskSyncEnabled: true, llmEnhancedFeaturesEnabled: true, } @@ -434,7 +358,6 @@ describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => { it("should validate with llmEnhancedFeaturesEnabled as false", () => { const input = { - extensionBridgeEnabled: true, taskSyncEnabled: true, llmEnhancedFeaturesEnabled: false, } @@ -454,15 +377,12 @@ describe("userSettingsConfigSchema with llmEnhancedFeaturesEnabled", () => { it("should have correct TypeScript type", () => { // Type-only test to ensure TypeScript compilation const settings: UserSettingsConfig = { - extensionBridgeEnabled: true, taskSyncEnabled: true, llmEnhancedFeaturesEnabled: true, } expect(settings.llmEnhancedFeaturesEnabled).toBe(true) - const settingsWithoutLlmFeatures: UserSettingsConfig = { - extensionBridgeEnabled: false, - } + const settingsWithoutLlmFeatures: UserSettingsConfig = {} expect(settingsWithoutLlmFeatures.llmEnhancedFeaturesEnabled).toBeUndefined() }) diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 206a5647b3e..f81845c782a 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -41,7 +41,6 @@ export interface CloudUserInfo { organizationName?: string organizationRole?: string organizationImageUrl?: string - extensionBridgeEnabled?: boolean } /** @@ -142,9 +141,7 @@ export type OrganizationCloudSettings = z.infer @@ -170,14 +167,11 @@ export type OrganizationSettings = z.infer * User Settings Schemas */ -export const userFeaturesSchema = z.object({ - roomoteControlEnabled: z.boolean().optional(), -}) +export const userFeaturesSchema = z.object({}) export type UserFeatures = z.infer export const userSettingsConfigSchema = z.object({ - extensionBridgeEnabled: z.boolean().optional(), taskSyncEnabled: z.boolean().optional(), llmEnhancedFeaturesEnabled: z.boolean().optional(), }) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index fa2f04c0e5d..26216b3cc41 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -399,9 +399,7 @@ export type ExtensionState = Pick< mcpServers?: McpServer[] hasSystemPromptOverride?: boolean mdmCompliant?: boolean - remoteControlEnabled: boolean taskSyncEnabled: boolean - featureRoomoteControlEnabled: boolean openAiCodexIsAuthenticated?: boolean debug?: boolean } @@ -498,7 +496,6 @@ export interface WebviewMessage { | "deleteMessageConfirm" | "submitEditedMessage" | "editMessageConfirm" - | "remoteControlEnabled" | "taskSyncEnabled" | "searchCommits" | "setApiConfigPassword" diff --git a/src/__tests__/extension.spec.ts b/src/__tests__/extension.spec.ts index 0bdbb26d462..96a9009ccf2 100644 --- a/src/__tests__/extension.spec.ts +++ b/src/__tests__/extension.spec.ts @@ -51,8 +51,6 @@ vi.mock("fs", () => ({ existsSync: vi.fn().mockReturnValue(false), })) -const mockBridgeOrchestratorDisconnect = vi.fn().mockResolvedValue(undefined) - const mockCloudServiceInstance = { off: vi.fn(), on: vi.fn(), @@ -71,9 +69,6 @@ vi.mock("@roo-code/cloud", () => ({ return mockCloudServiceInstance }, }, - BridgeOrchestrator: { - disconnect: mockBridgeOrchestratorDisconnect, - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) @@ -181,19 +176,13 @@ vi.mock("../i18n", () => ({ t: vi.fn((key) => key), })) -// Mock ClineProvider - remoteControlEnabled must call BridgeOrchestrator.disconnect for the test +// Mock ClineProvider vi.mock("../core/webview/ClineProvider", async () => { - const { BridgeOrchestrator } = await import("@roo-code/cloud") const mockInstance = { resolveWebviewView: vi.fn(), postMessageToWebview: vi.fn(), postStateToWebview: vi.fn(), getState: vi.fn().mockResolvedValue({}), - remoteControlEnabled: vi.fn().mockImplementation(async (enabled: boolean) => { - if (!enabled) { - await BridgeOrchestrator.disconnect() - } - }), initializeCloudProfileSyncWhenReady: vi.fn().mockResolvedValue(undefined), providerSettingsManager: {}, contextProxy: { getGlobalState: vi.fn() }, @@ -229,7 +218,6 @@ describe("extension.ts", () => { beforeEach(() => { vi.clearAllMocks() - mockBridgeOrchestratorDisconnect.mockClear() mockContext = { extensionPath: "/test/path", @@ -273,73 +261,6 @@ describe("extension.ts", () => { expect(dotenvx.config).toHaveBeenCalledTimes(1) }) - test("authStateChangedHandler calls BridgeOrchestrator.disconnect when logged-out event fires", async () => { - const { CloudService, BridgeOrchestrator } = await import("@roo-code/cloud") - - // Capture the auth state changed handler. - vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => { - if (handlers?.["auth-state-changed"]) { - authStateChangedHandler = handlers["auth-state-changed"] - } - - return { - off: vi.fn(), - on: vi.fn(), - telemetryClient: null, - hasActiveSession: vi.fn().mockReturnValue(false), - authService: null, - } as any - }) - - // Activate the extension. - const { activate } = await import("../extension") - await activate(mockContext) - - // Verify handler was registered. - expect(authStateChangedHandler).toBeDefined() - - // Trigger logout. - await authStateChangedHandler!({ - state: "logged-out" as AuthState, - previousState: "logged-in" as AuthState, - }) - - // Verify BridgeOrchestrator.disconnect was called - expect(mockBridgeOrchestratorDisconnect).toHaveBeenCalled() - }) - - test("authStateChangedHandler does not call BridgeOrchestrator.disconnect for other states", async () => { - const { CloudService } = await import("@roo-code/cloud") - - // Capture the auth state changed handler. - vi.mocked(CloudService.createInstance).mockImplementation(async (_context, _logger, handlers) => { - if (handlers?.["auth-state-changed"]) { - authStateChangedHandler = handlers["auth-state-changed"] - } - - return { - off: vi.fn(), - on: vi.fn(), - telemetryClient: null, - hasActiveSession: vi.fn().mockReturnValue(false), - authService: null, - } as any - }) - - // Activate the extension. - const { activate } = await import("../extension") - await activate(mockContext) - - // Trigger login. - await authStateChangedHandler!({ - state: "logged-in" as AuthState, - previousState: "logged-out" as AuthState, - }) - - // Verify BridgeOrchestrator.disconnect was NOT called. - expect(mockBridgeOrchestratorDisconnect).not.toHaveBeenCalled() - }) - describe("Roo model cache refresh on auth state change (ROO-202)", () => { beforeEach(() => { vi.resetModules() diff --git a/src/__tests__/single-open-invariant.spec.ts b/src/__tests__/single-open-invariant.spec.ts index 7fac886030f..b7e1b99d7c4 100644 --- a/src/__tests__/single-open-invariant.spec.ts +++ b/src/__tests__/single-open-invariant.spec.ts @@ -13,7 +13,6 @@ vi.mock("../core/task/Task", () => { public parentTask?: any public apiConfiguration: any public rootTask?: any - public enableBridge?: boolean constructor(opts: any) { this.taskId = opts.historyItem?.id ?? `task-${Math.random().toString(36).slice(2, 8)}` this.parentTask = opts.parentTask @@ -49,7 +48,6 @@ describe("Single-open-task invariant", () => { enableCheckpoints: true, checkpointTimeout: 60, cloudUserInfo: null, - remoteControlEnabled: false, }), removeClineFromStack, addClineToStack, diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d5e9aa0cfb6..8d3599b3cde 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -54,7 +54,7 @@ import { countEnabledMcpTools, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService, BridgeOrchestrator } from "@roo-code/cloud" +import { CloudService } from "@roo-code/cloud" // api import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api" @@ -144,7 +144,6 @@ export interface TaskOptions extends CreateTaskOptions { apiConfiguration: ProviderSettings enableCheckpoints?: boolean checkpointTimeout?: number - enableBridge?: boolean consecutiveMistakeLimit?: number task?: string images?: string[] @@ -337,9 +336,6 @@ export class Task extends EventEmitter implements TaskLike { checkpointService?: RepoPerTaskCheckpointService checkpointServiceInitializing = false - // Task Bridge - enableBridge: boolean - // Message Queue Service public readonly messageQueueService: MessageQueueService private messageQueueStateChangedHandler: (() => void) | undefined @@ -551,7 +547,6 @@ export class Task extends EventEmitter implements TaskLike { apiConfiguration, enableCheckpoints = true, checkpointTimeout = DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, - enableBridge = false, consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, task, images, @@ -645,7 +640,6 @@ export class Task extends EventEmitter implements TaskLike { this.diffViewProvider = new DiffViewProvider(this.cwd, this) this.enableCheckpoints = enableCheckpoints this.checkpointTimeout = checkpointTimeout - this.enableBridge = enableBridge this.parentTask = parentTask this.taskNumber = taskNumber @@ -2032,16 +2026,6 @@ export class Task extends EventEmitter implements TaskLike { private async startTask(task?: string, images?: string[]): Promise { try { - if (this.enableBridge) { - try { - await BridgeOrchestrator.subscribeToTask(this) - } catch (error) { - console.error( - `[Task#startTask] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - // `conversationHistory` (for API) and `clineMessages` (for webview) // need to be in sync. // If the extension process were killed, then on restart the @@ -2105,16 +2089,6 @@ export class Task extends EventEmitter implements TaskLike { } private async resumeTaskFromHistory() { - if (this.enableBridge) { - try { - await BridgeOrchestrator.subscribeToTask(this) - } catch (error) { - console.error( - `[Task#resumeTaskFromHistory] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - const modifiedClineMessages = await this.getSavedClineMessages() // Remove any resume messages that may have been added before. @@ -2424,16 +2398,6 @@ export class Task extends EventEmitter implements TaskLike { console.error("Error removing event listeners:", error) } - if (this.enableBridge) { - BridgeOrchestrator.getInstance() - ?.unsubscribeFromTask(this.taskId) - .catch((error) => - console.error( - `[Task#dispose] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}`, - ), - ) - } - // Release any terminals associated with this task. try { // Release any terminals associated with this task. diff --git a/src/core/task/__tests__/grounding-sources.test.ts b/src/core/task/__tests__/grounding-sources.test.ts index f6874a581e4..e80452fec5f 100644 --- a/src/core/task/__tests__/grounding-sources.test.ts +++ b/src/core/task/__tests__/grounding-sources.test.ts @@ -80,9 +80,6 @@ vi.mock("@roo-code/cloud", () => ({ CloudService: { isEnabled: () => false, }, - BridgeOrchestrator: { - subscribeToTask: vi.fn(), - }, })) // Mock delay to prevent actual delays diff --git a/src/core/task/__tests__/reasoning-preservation.test.ts b/src/core/task/__tests__/reasoning-preservation.test.ts index 2a3978e9111..ee06c22a1a8 100644 --- a/src/core/task/__tests__/reasoning-preservation.test.ts +++ b/src/core/task/__tests__/reasoning-preservation.test.ts @@ -80,9 +80,6 @@ vi.mock("@roo-code/cloud", () => ({ CloudService: { isEnabled: () => false, }, - BridgeOrchestrator: { - subscribeToTask: vi.fn(), - }, })) // Mock delay to prevent actual delays diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 84cc76825f7..20954fc4ec8 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -48,7 +48,7 @@ import { } from "@roo-code/types" import { aggregateTaskCostsRecursive, type AggregatedCosts } from "./aggregateTaskCosts" import { TelemetryService } from "@roo-code/telemetry" -import { CloudService, BridgeOrchestrator, getRooCodeApiUrl } from "@roo-code/cloud" +import { CloudService, getRooCodeApiUrl } from "@roo-code/cloud" import { Package } from "../../shared/package" import { findLast } from "../../shared/array" @@ -986,7 +986,6 @@ export class ClineProvider workspacePath: historyItem.workspace, onCreated: this.taskCreationCallback, startTask: options?.startTask ?? true, - enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, taskSyncEnabled), // Preserve the status from the history item to avoid overwriting it when the task saves messages initialStatus: historyItem.status, }) @@ -2065,11 +2064,9 @@ export class ClineProvider includeCurrentCost, maxGitStatusFiles, taskSyncEnabled, - remoteControlEnabled, imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, - featureRoomoteControlEnabled, isBrowserSessionActive, } = await this.getState() @@ -2227,11 +2224,9 @@ export class ClineProvider includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, taskSyncEnabled, - remoteControlEnabled, imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, - featureRoomoteControlEnabled, openAiCodexIsAuthenticated: await (async () => { try { const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth") @@ -2459,32 +2454,9 @@ export class ClineProvider includeCurrentCost: stateValues.includeCurrentCost ?? true, maxGitStatusFiles: stateValues.maxGitStatusFiles ?? 0, taskSyncEnabled, - remoteControlEnabled: (() => { - try { - const cloudSettings = CloudService.instance.getUserSettings() - return cloudSettings?.settings?.extensionBridgeEnabled ?? false - } catch (error) { - console.error( - `[getState] failed to get remote control setting from cloud: ${error instanceof Error ? error.message : String(error)}`, - ) - return false - } - })(), imageGenerationProvider: stateValues.imageGenerationProvider, openRouterImageApiKey: stateValues.openRouterImageApiKey, openRouterImageGenerationSelectedModel: stateValues.openRouterImageGenerationSelectedModel, - featureRoomoteControlEnabled: (() => { - try { - const userSettings = CloudService.instance.getUserSettings() - const hasOrganization = cloudUserInfo?.organizationId != null - return hasOrganization || (userSettings?.features?.roomoteControlEnabled ?? false) - } catch (error) { - console.error( - `[getState] failed to get featureRoomoteControlEnabled: ${error instanceof Error ? error.message : String(error)}`, - ) - return false - } - })(), } } @@ -2656,64 +2628,6 @@ export class ClineProvider return true } - public async remoteControlEnabled(enabled: boolean) { - if (!enabled) { - await BridgeOrchestrator.disconnect() - return - } - - const userInfo = CloudService.instance.getUserInfo() - - if (!userInfo) { - this.log("[ClineProvider#remoteControlEnabled] Failed to get user info, disconnecting") - await BridgeOrchestrator.disconnect() - return - } - - const config = await CloudService.instance.cloudAPI?.bridgeConfig().catch(() => undefined) - - if (!config) { - this.log("[ClineProvider#remoteControlEnabled] Failed to get bridge config") - return - } - - await BridgeOrchestrator.connectOrDisconnect(userInfo, enabled, { - ...config, - provider: this, - sessionId: vscode.env.sessionId, - isCloudAgent: CloudService.instance.isCloudAgent, - }) - - const bridge = BridgeOrchestrator.getInstance() - - if (bridge) { - const currentTask = this.getCurrentTask() - - if (currentTask && !currentTask.enableBridge) { - try { - currentTask.enableBridge = true - await BridgeOrchestrator.subscribeToTask(currentTask) - } catch (error) { - const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}` - this.log(message) - console.error(message) - } - } - } else { - for (const task of this.clineStack) { - if (task.enableBridge) { - try { - await BridgeOrchestrator.getInstance()?.unsubscribeFromTask(task.taskId) - } catch (error) { - const message = `[ClineProvider#remoteControlEnabled] BridgeOrchestrator#unsubscribeFromTask() failed: ${error instanceof Error ? error.message : String(error)}` - this.log(message) - console.error(message) - } - } - } - } - } - /** * Gets the CodeIndexManager for the current active workspace * @returns CodeIndexManager instance for the current workspace or the default one @@ -2869,15 +2783,8 @@ export class ClineProvider } } - const { - apiConfiguration, - organizationAllowList, - enableCheckpoints, - checkpointTimeout, - experiments, - cloudUserInfo, - remoteControlEnabled, - } = await this.getState() + const { apiConfiguration, organizationAllowList, enableCheckpoints, checkpointTimeout, experiments } = + await this.getState() // Single-open-task invariant: always enforce for user-initiated top-level tasks if (!parentTask) { @@ -2905,7 +2812,6 @@ export class ClineProvider parentTask, taskNumber: this.clineStack.length + 1, onCreated: this.taskCreationCallback, - enableBridge: BridgeOrchestrator.isEnabled(cloudUserInfo, remoteControlEnabled), initialTodos: options.initialTodos, ...options, }) diff --git a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts index 04f5d577929..3e18a0a0a43 100644 --- a/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.apiHandlerRebuild.spec.ts @@ -129,9 +129,6 @@ vi.mock("@roo-code/cloud", () => ({ } }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) diff --git a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts index 8533865031e..4bb01347a3d 100644 --- a/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.flicker-free-cancel.spec.ts @@ -78,9 +78,6 @@ vi.mock("@roo-code/cloud", () => ({ isAuthenticated: vi.fn().mockReturnValue(false), }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://api.roo-code.com"), })) diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index b65b137597c..43a00f2d569 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -330,9 +330,6 @@ vi.mock("@roo-code/cloud", () => ({ } }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) @@ -582,9 +579,7 @@ describe("ClineProvider", () => { diagnosticsEnabled: true, openRouterImageApiKey: undefined, openRouterImageGenerationSelectedModel: undefined, - remoteControlEnabled: false, taskSyncEnabled: false, - featureRoomoteControlEnabled: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, } diff --git a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts index 27aab0b7da2..fe7d9d46eaa 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts @@ -112,9 +112,6 @@ vi.mock("@roo-code/cloud", () => ({ } }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) diff --git a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts index 80b14746a76..b6a1472f0d9 100644 --- a/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts @@ -114,9 +114,6 @@ vi.mock("@roo-code/cloud", () => ({ } }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) diff --git a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts index f5e6afa7f06..fbf5e9159eb 100644 --- a/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.taskHistory.spec.ts @@ -233,9 +233,6 @@ vi.mock("@roo-code/cloud", () => ({ } }, }, - BridgeOrchestrator: { - isEnabled: vi.fn().mockReturnValue(false), - }, getRooCodeApiUrl: vi.fn().mockReturnValue("https://app.roocode.com"), })) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 75f1ce0ff4a..d880be58e7b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1457,25 +1457,10 @@ export const webviewMessageHandler = async ( } break } - case "remoteControlEnabled": - try { - await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false }) - } catch (error) { - provider.log( - `CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - break - case "taskSyncEnabled": const enabled = message.bool ?? false const updatedSettings: Partial = { taskSyncEnabled: enabled } - // If disabling task sync, also disable remote control. - if (!enabled) { - updatedSettings.extensionBridgeEnabled = false - } - try { await CloudService.instance.updateUserSettings(updatedSettings) } catch (error) { diff --git a/src/extension.ts b/src/extension.ts index 262bf623378..569f7aa627d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -18,7 +18,7 @@ if (fs.existsSync(envPath)) { } import type { CloudUserInfo, AuthState } from "@roo-code/types" -import { CloudService, BridgeOrchestrator } from "@roo-code/cloud" +import { CloudService } from "@roo-code/cloud" import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry" import { customToolRegistry } from "@roo-code/core" @@ -200,16 +200,6 @@ export async function activate(context: vscode.ExtensionContext) { authStateChangedHandler = async (data: { state: AuthState; previousState: AuthState }) => { postStateListener() - if (data.state === "logged-out") { - try { - await provider.remoteControlEnabled(false) - } catch (error) { - cloudLogger( - `[authStateChangedHandler] remoteControlEnabled(false) failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - // Handle Roo models cache based on auth state (ROO-202) const handleRooModelsCache = async () => { try { @@ -265,36 +255,11 @@ export async function activate(context: vscode.ExtensionContext) { } settingsUpdatedHandler = async () => { - const userInfo = CloudService.instance.getUserInfo() - - if (userInfo && CloudService.instance.cloudAPI) { - try { - provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled()) - } catch (error) { - cloudLogger( - `[settingsUpdatedHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } - } - postStateListener() } userInfoHandler = async ({ userInfo }: { userInfo: CloudUserInfo }) => { postStateListener() - - if (!CloudService.instance.cloudAPI) { - cloudLogger("[userInfoHandler] CloudAPI is not initialized") - return - } - - try { - provider.remoteControlEnabled(CloudService.instance.isTaskSyncEnabled()) - } catch (error) { - cloudLogger( - `[userInfoHandler] remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`, - ) - } } cloudService = await CloudService.createInstance(context, cloudLogger, { @@ -481,12 +446,6 @@ export async function deactivate() { } } - const bridge = BridgeOrchestrator.getInstance() - - if (bridge) { - await bridge.disconnect() - } - await McpServerManager.cleanup(extensionContext) TelemetryService.instance.shutdown() TerminalRegistry.cleanup() diff --git a/webview-ui/src/components/chat/CloudTaskButton.tsx b/webview-ui/src/components/chat/CloudTaskButton.tsx index 672bf020bb1..effe57c6d74 100644 --- a/webview-ui/src/components/chat/CloudTaskButton.tsx +++ b/webview-ui/src/components/chat/CloudTaskButton.tsx @@ -78,7 +78,7 @@ export const CloudTaskButton = ({ item, disabled = false }: CloudTaskButtonProps } }, [dialogOpen, canvasElement, generateQRCode]) - if (!cloudUserInfo?.extensionBridgeEnabled || !item?.id) { + if (!cloudUserInfo || !item?.id) { return null } diff --git a/webview-ui/src/components/chat/__tests__/CloudTaskButton.spec.tsx b/webview-ui/src/components/chat/__tests__/CloudTaskButton.spec.tsx index fc2b9f025ec..87382434f5d 100644 --- a/webview-ui/src/components/chat/__tests__/CloudTaskButton.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/CloudTaskButton.spec.tsx @@ -68,7 +68,6 @@ describe("CloudTaskButton", () => { cloudUserInfo: { id: "test-user", email: "test@example.com", - extensionBridgeEnabled: true, }, cloudApiUrl: "https://app.roocode.com", } as any) @@ -87,7 +86,6 @@ describe("CloudTaskButton", () => { cloudUserInfo: { id: "test-user", email: "test@example.com", - extensionBridgeEnabled: false, }, cloudApiUrl: "https://app.roocode.com", } as any) diff --git a/webview-ui/src/components/cloud/CloudView.tsx b/webview-ui/src/components/cloud/CloudView.tsx index e8ed9e163c2..997997ccd0c 100644 --- a/webview-ui/src/components/cloud/CloudView.tsx +++ b/webview-ui/src/components/cloud/CloudView.tsx @@ -9,7 +9,7 @@ import { vscode } from "@src/utils/vscode" import { telemetryClient } from "@src/utils/TelemetryClient" import { ToggleSwitch } from "@/components/ui/toggle-switch" import { renderCloudBenefitsContent } from "./CloudUpsellDialog" -import { ArrowRight, CircleAlert, Info, Lock, TriangleAlert } from "lucide-react" +import { ArrowRight, Info, Lock, TriangleAlert } from "lucide-react" import { cn } from "@/lib/utils" import { Tab, TabContent } from "../common/Tab" import { Button } from "@/components/ui/button" @@ -28,13 +28,7 @@ type CloudViewProps = { export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, organizations = [] }: CloudViewProps) => { const { t } = useAppTranslation() - const { - remoteControlEnabled, - setRemoteControlEnabled, - taskSyncEnabled, - setTaskSyncEnabled, - featureRoomoteControlEnabled, - } = useExtensionState() + const { taskSyncEnabled, setTaskSyncEnabled } = useExtensionState() const wasAuthenticatedRef = useRef(false) const timeoutRef = useRef(null) const manualUrlInputRef = useRef(null) @@ -144,12 +138,6 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, organization } } - const handleRemoteControlToggle = () => { - const newValue = !remoteControlEnabled - setRemoteControlEnabled(newValue) - vscode.postMessage({ type: "remoteControlEnabled", bool: newValue }) - } - const handleTaskSyncToggle = () => { const newValue = !taskSyncEnabled setTaskSyncEnabled(newValue) @@ -219,34 +207,6 @@ export const CloudView = ({ userInfo, isAuthenticated, cloudApiUrl, organization
{t("cloud:taskSyncDescription")}
- - {/* Remote Control Toggle - Only shown when both extensionBridgeEnabled and featureRoomoteControlEnabled are true */} - {userInfo?.extensionBridgeEnabled && featureRoomoteControlEnabled && ( - <> -
- - - {t("cloud:remoteControl")} - -
-
- {t("cloud:remoteControlDescription")} - {!taskSyncEnabled && ( -
- - {t("cloud:remoteControlRequiresTaskSync")} -
- )} -
- - )}
diff --git a/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx b/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx index 87f5da9c651..120579e7327 100644 --- a/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx +++ b/webview-ui/src/components/cloud/__tests__/CloudView.spec.tsx @@ -23,10 +23,6 @@ vi.mock("@src/i18n/TranslationContext", () => ({ "cloud:taskSync": "Task sync", "cloud:taskSyncDescription": "Sync your tasks for viewing and sharing on Roo Code Cloud", "cloud:taskSyncManagedByOrganization": "Task sync is managed by your organization", - "cloud:remoteControl": "Roomote Control", - "cloud:remoteControlDescription": - "Enable following and interacting with tasks in this workspace with Roo Code Cloud", - "cloud:remoteControlRequiresTaskSync": "Task sync must be enabled to use Roomote Control", "cloud:usageMetricsAlwaysReported": "Model usage info is always reported when logged in", "cloud:profilePicture": "Profile picture", "cloud:cloudUrlPillLabel": "Roo Code Cloud URL: ", @@ -52,12 +48,8 @@ vi.mock("@src/utils/TelemetryClient", () => ({ // Mock the extension state context const mockExtensionState = { - remoteControlEnabled: false, - setRemoteControlEnabled: vi.fn(), taskSyncEnabled: true, setTaskSyncEnabled: vi.fn(), - featureRoomoteControlEnabled: true, // Default to true for tests - setFeatureRoomoteControlEnabled: vi.fn(), } vi.mock("@src/context/ExtensionStateContext", () => ({ @@ -116,82 +108,6 @@ describe("CloudView", () => { expect(screen.getByText("test@example.com")).toBeInTheDocument() }) - it("should display remote control toggle when user has extension bridge enabled and roomote control enabled", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - extensionBridgeEnabled: true, - } - - render() - - // Check that the remote control toggle is displayed - expect(screen.getByTestId("remote-control-toggle")).toBeInTheDocument() - expect(screen.getByText("Roomote Control")).toBeInTheDocument() - expect( - screen.getByText("Enable following and interacting with tasks in this workspace with Roo Code Cloud"), - ).toBeInTheDocument() - }) - - it("should not display remote control toggle when user does not have extension bridge enabled", () => { - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - extensionBridgeEnabled: false, - } - - render() - - // Check that the remote control toggle is NOT displayed - expect(screen.queryByTestId("remote-control-toggle")).not.toBeInTheDocument() - expect(screen.queryByText("Roomote Control")).not.toBeInTheDocument() - }) - - it("should not display remote control toggle when roomote control is disabled", () => { - // Temporarily override the mock for this specific test - const originalFeatureRoomoteControlEnabled = mockExtensionState.featureRoomoteControlEnabled - mockExtensionState.featureRoomoteControlEnabled = false - - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - extensionBridgeEnabled: true, // Bridge enabled but roomote control disabled - } - - render() - - // Check that the remote control toggle is NOT displayed - expect(screen.queryByTestId("remote-control-toggle")).not.toBeInTheDocument() - expect(screen.queryByText("Roomote Control")).not.toBeInTheDocument() - - // Restore the original value - mockExtensionState.featureRoomoteControlEnabled = originalFeatureRoomoteControlEnabled - }) - - it("should display remote control toggle for organization users (simulating backend logic)", () => { - // This test simulates what the ClineProvider would do: - // Organization users are treated as having featureRoomoteControlEnabled true - const originalFeatureRoomoteControlEnabled = mockExtensionState.featureRoomoteControlEnabled - mockExtensionState.featureRoomoteControlEnabled = true // Simulating ClineProvider logic for org users - - const mockUserInfo = { - name: "Test User", - email: "test@example.com", - organizationId: "org-123", // User is in an organization - extensionBridgeEnabled: true, - } - - render() - - // Check that the remote control toggle IS displayed for organization users - // (The ClineProvider would set featureRoomoteControlEnabled to true for org users) - expect(screen.getByTestId("remote-control-toggle")).toBeInTheDocument() - expect(screen.getByText("Roomote Control")).toBeInTheDocument() - - // Restore the original value - mockExtensionState.featureRoomoteControlEnabled = originalFeatureRoomoteControlEnabled - }) - it("should not display cloud URL pill when pointing to production", () => { const mockUserInfo = { name: "Test User", diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 47110d08751..4fd000cd449 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -101,12 +101,8 @@ export interface ExtensionStateContextType extends ExtensionState { setTerminalOutputPreviewSize: (value: "small" | "medium" | "large") => void mcpEnabled: boolean setMcpEnabled: (value: boolean) => void - remoteControlEnabled: boolean - setRemoteControlEnabled: (value: boolean) => void taskSyncEnabled: boolean setTaskSyncEnabled: (value: boolean) => void - featureRoomoteControlEnabled: boolean - setFeatureRoomoteControlEnabled: (value: boolean) => void setCurrentApiConfigName: (value: string) => void setListApiConfigMeta: (value: ProviderSettingsEntry[]) => void mode: Mode @@ -208,9 +204,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode screenshotQuality: 75, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, - remoteControlEnabled: false, taskSyncEnabled: false, - featureRoomoteControlEnabled: false, currentApiConfigName: "default", listApiConfigMeta: [], mode: defaultModeSlug, @@ -501,9 +495,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode profileThresholds: state.profileThresholds ?? {}, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs, - remoteControlEnabled: state.remoteControlEnabled ?? false, taskSyncEnabled: state.taskSyncEnabled, - featureRoomoteControlEnabled: state.featureRoomoteControlEnabled ?? false, setExperimentEnabled: (id, enabled) => setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })), setApiConfiguration, @@ -545,10 +537,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, terminalShellIntegrationDisabled: value })), setTerminalZdotdir: (value) => setState((prevState) => ({ ...prevState, terminalZdotdir: value })), setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })), - setRemoteControlEnabled: (value) => setState((prevState) => ({ ...prevState, remoteControlEnabled: value })), setTaskSyncEnabled: (value) => setState((prevState) => ({ ...prevState, taskSyncEnabled: value }) as any), - setFeatureRoomoteControlEnabled: (value) => - setState((prevState) => ({ ...prevState, featureRoomoteControlEnabled: value })), setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })), setListApiConfigMeta, setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })), diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index bfc60f1ace4..c0c5485be2d 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -213,9 +213,7 @@ describe("mergeExtensionState", () => { hasOpenedModeSelector: false, // Add the new required property maxImageFileSize: 5, maxTotalImageSize: 20, - remoteControlEnabled: false, taskSyncEnabled: false, - featureRoomoteControlEnabled: false, isBrowserSessionActive: false, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, // Add the checkpoint timeout property } diff --git a/webview-ui/src/i18n/locales/ca/cloud.json b/webview-ui/src/i18n/locales/ca/cloud.json index 077cdf42fa3..7c0eeff82b0 100644 --- a/webview-ui/src/i18n/locales/ca/cloud.json +++ b/webview-ui/src/i18n/locales/ca/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visita Roo Code Cloud", "taskSync": "Sincronització de tasques", "taskSyncDescription": "Sincronitza les teves tasques per veure-les i compartir-les a Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Permet controlar tasques des de Roo Code Cloud", - "remoteControlRequiresTaskSync": "La sincronització de tasques ha d'estar habilitada per utilitzar Roomote Control", "taskSyncManagedByOrganization": "La sincronització de tasques la gestiona la teva organització", "usageMetricsAlwaysReported": "La informació d'ús del model sempre es reporta quan s'ha iniciat sessió", "cloudUrlPillLabel": "URL de Roo Code Cloud", diff --git a/webview-ui/src/i18n/locales/de/cloud.json b/webview-ui/src/i18n/locales/de/cloud.json index 70e480cbc4e..7ac165b4972 100644 --- a/webview-ui/src/i18n/locales/de/cloud.json +++ b/webview-ui/src/i18n/locales/de/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Roo Code Cloud besuchen", "taskSync": "Aufgabensynchronisierung", "taskSyncDescription": "Synchronisiere deine Aufgaben zum Anzeigen und Teilen in Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Ermöglicht die Steuerung von Aufgaben über Roo Code Cloud", - "remoteControlRequiresTaskSync": "Die Aufgabensynchronisierung muss aktiviert sein, um Roomote Control zu verwenden", "taskSyncManagedByOrganization": "Die Aufgabensynchronisierung wird von deiner Organisation verwaltet", "usageMetricsAlwaysReported": "Modellnutzungsinformationen werden bei Anmeldung immer gemeldet", "authWaiting": "Warte auf Abschluss der Authentifizierung...", diff --git a/webview-ui/src/i18n/locales/en/cloud.json b/webview-ui/src/i18n/locales/en/cloud.json index 4ba0d5f62d2..5f52afdc6a2 100644 --- a/webview-ui/src/i18n/locales/en/cloud.json +++ b/webview-ui/src/i18n/locales/en/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visit Roo Code Cloud", "taskSync": "Task sync", "taskSyncDescription": "Sync your tasks for viewing and sharing on Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Allow controlling tasks from Roo Code Cloud", - "remoteControlRequiresTaskSync": "Task sync must be enabled to use Roomote Control", "taskSyncManagedByOrganization": "Task sync is managed by your organization", "usageMetricsAlwaysReported": "Model usage info is always reported when logged in", "cloudUrlPillLabel": "Roo Code Cloud URL", diff --git a/webview-ui/src/i18n/locales/es/cloud.json b/webview-ui/src/i18n/locales/es/cloud.json index 1281cccb419..eb0cddc8b98 100644 --- a/webview-ui/src/i18n/locales/es/cloud.json +++ b/webview-ui/src/i18n/locales/es/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visitar Roo Code Cloud", "taskSync": "Sincronización de tareas", "taskSyncDescription": "Sincroniza tus tareas para verlas y compartirlas en Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Permite controlar tareas desde Roo Code Cloud", - "remoteControlRequiresTaskSync": "La sincronización de tareas debe estar habilitada para usar Roomote Control", "taskSyncManagedByOrganization": "La sincronización de tareas es gestionada por tu organización", "usageMetricsAlwaysReported": "La información de uso del modelo siempre se reporta cuando se ha iniciado sesión", "authWaiting": "Esperando que se complete la autenticación...", diff --git a/webview-ui/src/i18n/locales/fr/cloud.json b/webview-ui/src/i18n/locales/fr/cloud.json index 8e031aaad8a..ac8a8d15378 100644 --- a/webview-ui/src/i18n/locales/fr/cloud.json +++ b/webview-ui/src/i18n/locales/fr/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visiter Roo Code Cloud", "taskSync": "Synchronisation des tâches", "taskSyncDescription": "Synchronisez vos tâches pour les visualiser et les partager sur Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Permet de contrôler les tâches depuis Roo Code Cloud", - "remoteControlRequiresTaskSync": "La synchronisation des tâches doit être activée pour utiliser Roomote Control", "taskSyncManagedByOrganization": "La synchronisation des tâches est gérée par votre organisation", "usageMetricsAlwaysReported": "Les informations d'utilisation du modèle sont toujours signalées lors de la connexion", "authWaiting": "En attente de la fin de l'authentification...", diff --git a/webview-ui/src/i18n/locales/hi/cloud.json b/webview-ui/src/i18n/locales/hi/cloud.json index ecf6e1f1010..c573284e1db 100644 --- a/webview-ui/src/i18n/locales/hi/cloud.json +++ b/webview-ui/src/i18n/locales/hi/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Roo Code Cloud पर जाएं", "taskSync": "कार्य सिंक", "taskSyncDescription": "Roo Code Cloud पर देखने और साझा करने के लिए अपने कार्यों को सिंक करें", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Roo Code Cloud से कार्यों को नियंत्रित करने की अनुमति दें", - "remoteControlRequiresTaskSync": "Roomote Control का उपयोग करने के लिए कार्य सिंक सक्षम होना चाहिए", "taskSyncManagedByOrganization": "कार्य सिंक आपके संगठन द्वारा प्रबंधित किया जाता है", "usageMetricsAlwaysReported": "लॉग इन होने पर मॉडल उपयोग जानकारी हमेशा रिपोर्ट की जाती है", "authWaiting": "प्रमाणीकरण पूरा होने की प्रतीक्षा कर रहे हैं...", diff --git a/webview-ui/src/i18n/locales/id/cloud.json b/webview-ui/src/i18n/locales/id/cloud.json index 3e3e293f973..5e8000c7b8c 100644 --- a/webview-ui/src/i18n/locales/id/cloud.json +++ b/webview-ui/src/i18n/locales/id/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Kunjungi Roo Code Cloud", "taskSync": "Sinkronisasi tugas", "taskSyncDescription": "Sinkronkan tugas Anda untuk melihat dan berbagi di Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Izinkan mengontrol tugas dari Roo Code Cloud", - "remoteControlRequiresTaskSync": "Sinkronisasi tugas harus diaktifkan untuk menggunakan Roomote Control", "taskSyncManagedByOrganization": "Sinkronisasi tugas dikelola oleh organisasi Anda", "usageMetricsAlwaysReported": "Informasi penggunaan model selalu dilaporkan saat masuk", "authWaiting": "Menunggu autentikasi selesai...", diff --git a/webview-ui/src/i18n/locales/it/cloud.json b/webview-ui/src/i18n/locales/it/cloud.json index cd575786d5c..b0c0dceec20 100644 --- a/webview-ui/src/i18n/locales/it/cloud.json +++ b/webview-ui/src/i18n/locales/it/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visita Roo Code Cloud", "taskSync": "Sincronizzazione attività", "taskSyncDescription": "Sincronizza le tue attività per visualizzarle e condividerle su Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Consenti il controllo delle attività da Roo Code Cloud", - "remoteControlRequiresTaskSync": "La sincronizzazione delle attività deve essere abilitata per utilizzare Roomote Control", "taskSyncManagedByOrganization": "La sincronizzazione delle attività è gestita dalla tua organizzazione", "usageMetricsAlwaysReported": "Le informazioni sull'utilizzo del modello vengono sempre segnalate quando si è connessi", "authWaiting": "In attesa del completamento dell'autenticazione...", diff --git a/webview-ui/src/i18n/locales/ja/cloud.json b/webview-ui/src/i18n/locales/ja/cloud.json index d25f61b78c8..6474bd8fe00 100644 --- a/webview-ui/src/i18n/locales/ja/cloud.json +++ b/webview-ui/src/i18n/locales/ja/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Roo Code Cloudを訪問", "taskSync": "タスク同期", "taskSyncDescription": "Roo Code Cloudでタスクを表示・共有するために同期", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Roo Code Cloudからタスクを制御できるようにする", - "remoteControlRequiresTaskSync": "Roomote Controlを使用するにはタスク同期を有効にする必要があります", "taskSyncManagedByOrganization": "タスク同期は組織によって管理されます", "usageMetricsAlwaysReported": "ログイン時にはモデル使用情報が常に報告されます", "authWaiting": "認証完了をお待ちください...", diff --git a/webview-ui/src/i18n/locales/ko/cloud.json b/webview-ui/src/i18n/locales/ko/cloud.json index 6aba5f58ba2..c0731e3bf4e 100644 --- a/webview-ui/src/i18n/locales/ko/cloud.json +++ b/webview-ui/src/i18n/locales/ko/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Roo Code Cloud 방문", "taskSync": "작업 동기화", "taskSyncDescription": "Roo Code Cloud에서 보고 공유할 수 있도록 작업을 동기화", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Roo Code Cloud에서 작업을 제어할 수 있도록 허용", - "remoteControlRequiresTaskSync": "Roomote Control을 사용하려면 작업 동기화가 활성화되어야 합니다", "taskSyncManagedByOrganization": "작업 동기화는 조직에서 관리합니다", "usageMetricsAlwaysReported": "로그인 시 모델 사용 정보가 항상 보고됩니다", "authWaiting": "인증 완료를 기다리는 중...", diff --git a/webview-ui/src/i18n/locales/nl/cloud.json b/webview-ui/src/i18n/locales/nl/cloud.json index dad073fdc2f..5a4a5c4db28 100644 --- a/webview-ui/src/i18n/locales/nl/cloud.json +++ b/webview-ui/src/i18n/locales/nl/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Bezoek Roo Code Cloud", "taskSync": "Taaksynchronisatie", "taskSyncDescription": "Synchroniseer je taken om ze te bekijken en delen op Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Sta toe taken te besturen vanuit Roo Code Cloud", - "remoteControlRequiresTaskSync": "Taaksynchronisatie moet ingeschakeld zijn om Roomote Control te gebruiken", "taskSyncManagedByOrganization": "Taaksynchronisatie wordt beheerd door uw organisatie", "usageMetricsAlwaysReported": "Modelgebruiksinformatie wordt altijd gerapporteerd wanneer ingelogd", "authWaiting": "Wachten tot authenticatie voltooid is...", diff --git a/webview-ui/src/i18n/locales/pl/cloud.json b/webview-ui/src/i18n/locales/pl/cloud.json index 827eaa70ef7..3a7eac8cdba 100644 --- a/webview-ui/src/i18n/locales/pl/cloud.json +++ b/webview-ui/src/i18n/locales/pl/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Odwiedź Roo Code Cloud", "taskSync": "Synchronizacja zadań", "taskSyncDescription": "Synchronizuj swoje zadania, aby przeglądać i udostępniać je w Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Pozwól kontrolować zadania z Roo Code Cloud", - "remoteControlRequiresTaskSync": "Synchronizacja zadań musi być włączona, aby używać Roomote Control", "taskSyncManagedByOrganization": "Synchronizacja zadań jest zarządzana przez Twoją organizację", "usageMetricsAlwaysReported": "Informacje o użyciu modelu są zawsze raportowane po zalogowaniu", "authWaiting": "Oczekiwanie na zakończenie uwierzytelniania...", diff --git a/webview-ui/src/i18n/locales/pt-BR/cloud.json b/webview-ui/src/i18n/locales/pt-BR/cloud.json index 1876f1a6669..08ee2f205f0 100644 --- a/webview-ui/src/i18n/locales/pt-BR/cloud.json +++ b/webview-ui/src/i18n/locales/pt-BR/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Visitar Roo Code Cloud", "taskSync": "Sincronização de tarefas", "taskSyncDescription": "Sincronize suas tarefas para visualizar e compartilhar no Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Permite controlar tarefas a partir do Roo Code Cloud", - "remoteControlRequiresTaskSync": "A sincronização de tarefas deve estar habilitada para usar o Roomote Control", "taskSyncManagedByOrganization": "A sincronização de tarefas é gerenciada pela sua organização", "usageMetricsAlwaysReported": "As informações de uso do modelo são sempre reportadas quando conectado", "authWaiting": "Aguardando conclusão da autenticação...", diff --git a/webview-ui/src/i18n/locales/ru/cloud.json b/webview-ui/src/i18n/locales/ru/cloud.json index 438a1a90fd6..20aa6d45069 100644 --- a/webview-ui/src/i18n/locales/ru/cloud.json +++ b/webview-ui/src/i18n/locales/ru/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Посетить Roo Code Cloud", "taskSync": "Синхронизация задач", "taskSyncDescription": "Синхронизируйте свои задачи для просмотра и обмена в Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Разрешить управление задачами из Roo Code Cloud", - "remoteControlRequiresTaskSync": "Для использования Roomote Control должна быть включена синхронизация задач", "taskSyncManagedByOrganization": "Синхронизация задач управляется вашей организацией", "usageMetricsAlwaysReported": "Информация об использовании модели всегда сообщается при входе в систему", "authWaiting": "Ожидание завершения аутентификации...", diff --git a/webview-ui/src/i18n/locales/tr/cloud.json b/webview-ui/src/i18n/locales/tr/cloud.json index 24e767a6c37..8c14c744a7c 100644 --- a/webview-ui/src/i18n/locales/tr/cloud.json +++ b/webview-ui/src/i18n/locales/tr/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Roo Code Cloud'u ziyaret et", "taskSync": "Görev senkronizasyonu", "taskSyncDescription": "Görevlerinizi Roo Code Cloud'da görüntülemek ve paylaşmak için senkronize edin", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Roo Code Cloud'dan görevleri kontrol etmeye izin ver", - "remoteControlRequiresTaskSync": "Roomote Control'ü kullanmak için görev senkronizasyonu etkinleştirilmelidir", "taskSyncManagedByOrganization": "Görev senkronizasyonu kuruluşunuz tarafından yönetilir", "usageMetricsAlwaysReported": "Oturum açıldığında model kullanım bilgileri her zaman raporlanır", "authWaiting": "Kimlik doğrulama tamamlanması bekleniyor...", diff --git a/webview-ui/src/i18n/locales/vi/cloud.json b/webview-ui/src/i18n/locales/vi/cloud.json index e9c4dee7202..45dd092b750 100644 --- a/webview-ui/src/i18n/locales/vi/cloud.json +++ b/webview-ui/src/i18n/locales/vi/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "Truy cập Roo Code Cloud", "taskSync": "Đồng bộ tác vụ", "taskSyncDescription": "Đồng bộ tác vụ của bạn để xem và chia sẻ trên Roo Code Cloud", - "remoteControl": "Roomote Control", - "remoteControlDescription": "Cho phép điều khiển tác vụ từ Roo Code Cloud", - "remoteControlRequiresTaskSync": "Đồng bộ tác vụ phải được bật để sử dụng Roomote Control", "taskSyncManagedByOrganization": "Việc đồng bộ hóa công việc được quản lý bởi tổ chức của bạn", "usageMetricsAlwaysReported": "Thông tin sử dụng mô hình luôn được báo cáo khi đăng nhập", "authWaiting": "Đang chờ hoàn tất xác thực...", diff --git a/webview-ui/src/i18n/locales/zh-CN/cloud.json b/webview-ui/src/i18n/locales/zh-CN/cloud.json index 29b4f98e4c6..560218c56f4 100644 --- a/webview-ui/src/i18n/locales/zh-CN/cloud.json +++ b/webview-ui/src/i18n/locales/zh-CN/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "访问 Roo Code Cloud", "taskSync": "任务同步", "taskSyncDescription": "同步您的任务以在 Roo Code Cloud 上查看和共享", - "remoteControl": "Roomote Control", - "remoteControlDescription": "允许从 Roo Code Cloud 控制任务", - "remoteControlRequiresTaskSync": "必须启用任务同步才能使用 Roomote Control", "taskSyncManagedByOrganization": "任务同步由您的组织管理", "usageMetricsAlwaysReported": "登录时始终报告模型使用信息", "authWaiting": "等待身份验证完成...", diff --git a/webview-ui/src/i18n/locales/zh-TW/cloud.json b/webview-ui/src/i18n/locales/zh-TW/cloud.json index 1ade83d3e34..032c18ed915 100644 --- a/webview-ui/src/i18n/locales/zh-TW/cloud.json +++ b/webview-ui/src/i18n/locales/zh-TW/cloud.json @@ -15,9 +15,6 @@ "visitCloudWebsite": "造訪 Roo Code Cloud", "taskSync": "任務同步", "taskSyncDescription": "同步工作以在 Roo Code Cloud 上檢視和分享", - "remoteControl": "Roomote Control", - "remoteControlDescription": "允許從 Roo Code Cloud 控制工作", - "remoteControlRequiresTaskSync": "必須啟用任務同步才能使用 Roomote Control", "taskSyncManagedByOrganization": "工作同步由您的組織管理", "usageMetricsAlwaysReported": "登入時會一律回報模型使用資訊", "authWaiting": "等待瀏覽器驗證完成...",