From 4ce37c21d6456db9fd70110fb46dd0767af7f144 Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 3 Sep 2025 11:01:37 -0700 Subject: [PATCH 1/3] Focus the extension when receiving bridge commands --- packages/cloud/src/bridge/BaseChannel.ts | 35 +++++- .../cloud/src/bridge/BridgeOrchestrator.ts | 117 ++++++++++-------- packages/cloud/src/bridge/ExtensionChannel.ts | 19 ++- packages/cloud/src/bridge/TaskChannel.ts | 11 +- .../bridge/__tests__/ExtensionChannel.test.ts | 21 +++- .../src/bridge/__tests__/TaskChannel.test.ts | 12 +- packages/types/npm/package.metadata.json | 2 +- packages/types/src/extension.ts | 7 ++ packages/types/src/index.ts | 1 + src/core/webview/ClineProvider.ts | 2 +- src/core/webview/webviewMessageHandler.ts | 18 ++- src/extension.ts | 22 ++-- src/extension/api.ts | 3 +- src/shared/package.ts | 6 +- 14 files changed, 180 insertions(+), 96 deletions(-) create mode 100644 packages/types/src/extension.ts diff --git a/packages/cloud/src/bridge/BaseChannel.ts b/packages/cloud/src/bridge/BaseChannel.ts index 95db835d1f92..4f9d2feb4de9 100644 --- a/packages/cloud/src/bridge/BaseChannel.ts +++ b/packages/cloud/src/bridge/BaseChannel.ts @@ -1,4 +1,12 @@ import type { Socket } from "socket.io-client" +import * as vscode from "vscode" + +import { ExtensionMetadata } from "@roo-code/types" + +export interface BaseChannelOptions { + instanceId: string + extensionMetadata: ExtensionMetadata +} /** * Abstract base class for communication channels in the bridge system. @@ -11,9 +19,11 @@ import type { Socket } from "socket.io-client" export abstract class BaseChannel { protected socket: Socket | null = null protected readonly instanceId: string + protected readonly extensionMetadata: ExtensionMetadata - constructor(instanceId: string) { - this.instanceId = instanceId + constructor(options: BaseChannelOptions) { + this.instanceId = options.instanceId + this.extensionMetadata = options.extensionMetadata } /** @@ -81,9 +91,26 @@ export abstract class BaseChannel { + // Common functionality: focus the sidebar + await vscode.commands.executeCommand(`${this.extensionMetadata.name}.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. */ - public abstract handleCommand(command: TCommand): Promise + protected abstract handleCommandImplementation(command: TCommand): Promise /** * Handle connection-specific logic. diff --git a/packages/cloud/src/bridge/BridgeOrchestrator.ts b/packages/cloud/src/bridge/BridgeOrchestrator.ts index 69a6f5a57d02..051695cf3fcf 100644 --- a/packages/cloud/src/bridge/BridgeOrchestrator.ts +++ b/packages/cloud/src/bridge/BridgeOrchestrator.ts @@ -6,6 +6,7 @@ import { type CloudUserInfo, type ExtensionBridgeCommand, type TaskBridgeCommand, + type ExtensionMetadata, ConnectionState, ExtensionSocketEvents, TaskSocketEvents, @@ -21,6 +22,7 @@ export interface BridgeOrchestratorOptions { token: string provider: TaskProviderLike sessionId?: string + extensionMetadata: ExtensionMetadata } /** @@ -39,6 +41,7 @@ export class BridgeOrchestrator { private readonly token: string private readonly provider: TaskProviderLike private readonly instanceId: string + private readonly extensionMetadata: ExtensionMetadata // Components private socketTransport: SocketTransport @@ -61,66 +64,69 @@ export class BridgeOrchestrator { public static async connectOrDisconnect( userInfo: CloudUserInfo | null, remoteControlEnabled: boolean | undefined, - options?: BridgeOrchestratorOptions, + options: BridgeOrchestratorOptions, ): Promise { - const isEnabled = BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled) - const instance = BridgeOrchestrator.instance + if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) { + await BridgeOrchestrator.connect(options) + } else { + await BridgeOrchestrator.disconnect() + } + } - if (isEnabled) { - if (!instance) { - if (!options) { - console.error( - `[BridgeOrchestrator#connectOrDisconnect] Cannot connect: options are required for connection`, - ) - return - } - try { - console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`) - 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})`, - ) + public static async connect(options: BridgeOrchestratorOptions) { + const instance = BridgeOrchestrator.instance - 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})`, - ) - } + if (!instance) { + try { + console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`) + 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) { - try { - console.log( - `[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`, - ) + if ( + instance.connectionState === ConnectionState.FAILED || + instance.connectionState === ConnectionState.DISCONNECTED + ) { + console.log( + `[BridgeOrchestrator#connectOrDisconnect] Re-connecting... (state: ${instance.connectionState})`, + ) - await instance.disconnect() - } catch (error) { + instance.reconnect().catch((error) => { console.error( - `[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`, + `[BridgeOrchestrator#connectOrDisconnect] reconnect() failed: ${error instanceof Error ? error.message : String(error)}`, ) - } finally { - BridgeOrchestrator.instance = null - } + }) } else { - console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`) + 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`) } } @@ -146,6 +152,7 @@ export class BridgeOrchestrator { this.token = options.token this.provider = options.provider this.instanceId = options.sessionId || crypto.randomUUID() + this.extensionMetadata = options.extensionMetadata this.socketTransport = new SocketTransport({ url: this.socketBridgeUrl, @@ -166,8 +173,14 @@ export class BridgeOrchestrator { onReconnect: () => this.handleReconnect(), }) - this.extensionChannel = new ExtensionChannel(this.instanceId, this.userId, this.provider) - this.taskChannel = new TaskChannel(this.instanceId) + this.extensionChannel = new ExtensionChannel({ + instanceId: this.instanceId, + userId: this.userId, + provider: this.provider, + extensionMetadata: this.extensionMetadata, + }) + + this.taskChannel = new TaskChannel({ instanceId: this.instanceId, extensionMetadata: this.extensionMetadata }) } private setupSocketListeners() { diff --git a/packages/cloud/src/bridge/ExtensionChannel.ts b/packages/cloud/src/bridge/ExtensionChannel.ts index 735d6c16b1f9..a4fcbbed174c 100644 --- a/packages/cloud/src/bridge/ExtensionChannel.ts +++ b/packages/cloud/src/bridge/ExtensionChannel.ts @@ -14,7 +14,12 @@ import { HEARTBEAT_INTERVAL_MS, } from "@roo-code/types" -import { BaseChannel } from "./BaseChannel.js" +import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js" + +interface ExtensionChannelOptions extends BaseChannelOptions { + userId: string + provider: TaskProviderLike +} /** * Manages the extension-level communication channel. @@ -31,10 +36,11 @@ export class ExtensionChannel extends BaseChannel< private heartbeatInterval: NodeJS.Timeout | null = null private eventListeners: Map void> = new Map() - constructor(instanceId: string, userId: string, provider: TaskProviderLike) { - super(instanceId) - this.userId = userId - this.provider = provider + constructor(options: ExtensionChannelOptions) { + super({ instanceId: options.instanceId, extensionMetadata: options.extensionMetadata }) + + this.userId = options.userId + this.provider = options.provider this.extensionInstance = { instanceId: this.instanceId, @@ -53,11 +59,12 @@ export class ExtensionChannel extends BaseChannel< this.setupListeners() } - public async handleCommand(command: ExtensionBridgeCommand): Promise { + protected async handleCommandImplementation(command: ExtensionBridgeCommand): Promise { if (command.instanceId !== this.instanceId) { console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, { messageInstanceId: command.instanceId, }) + return } diff --git a/packages/cloud/src/bridge/TaskChannel.ts b/packages/cloud/src/bridge/TaskChannel.ts index f974a3e559b9..433e740d4edb 100644 --- a/packages/cloud/src/bridge/TaskChannel.ts +++ b/packages/cloud/src/bridge/TaskChannel.ts @@ -14,7 +14,7 @@ import { TaskSocketEvents, } from "@roo-code/types" -import { BaseChannel } from "./BaseChannel.js" +import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js" type TaskEventListener = { [K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise @@ -26,6 +26,9 @@ type TaskEventMapping = { 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. @@ -69,11 +72,11 @@ export class TaskChannel extends BaseChannel< }, ] as const - constructor(instanceId: string) { - super(instanceId) + constructor(options: TaskChannelOptions) { + super(options) } - public async handleCommand(command: TaskBridgeCommand): Promise { + protected async handleCommandImplementation(command: TaskBridgeCommand): Promise { const task = this.subscribedTasks.get(command.taskId) if (!task) { diff --git a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts index f4d09e51a5ee..f5bb535bfdb5 100644 --- a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts @@ -18,6 +18,13 @@ describe("ExtensionChannel", () => { let extensionChannel: ExtensionChannel const instanceId = "test-instance-123" const userId = "test-user-456" + const extensionMetadata = { + name: "roo-code", + publisher: "Roocode", + version: "1.0.0", + outputChannel: "Roo Code", + sha: undefined, + } // Track registered event listeners const eventListeners = new Map unknown>>() @@ -80,7 +87,12 @@ describe("ExtensionChannel", () => { } as unknown as TaskProviderLike // Create extension channel instance - extensionChannel = new ExtensionChannel(instanceId, userId, mockProvider) + extensionChannel = new ExtensionChannel({ + instanceId, + extensionMetadata, + userId, + provider: mockProvider, + }) }) afterEach(() => { @@ -155,7 +167,12 @@ describe("ExtensionChannel", () => { it("should not have duplicate listeners after multiple channel creations", () => { // Create a second channel with the same provider - const secondChannel = new ExtensionChannel("instance-2", userId, mockProvider) + const secondChannel = new ExtensionChannel({ + instanceId: "instance-2", + extensionMetadata, + userId, + provider: mockProvider, + }) // Each event should have exactly 2 listeners (one from each channel) eventListeners.forEach((listeners) => { diff --git a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts index 4a6aa724684a..ce1dfeab1ded 100644 --- a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts @@ -21,6 +21,13 @@ describe("TaskChannel", () => { let mockTask: TaskLike const instanceId = "test-instance-123" const taskId = "test-task-456" + const extensionMetadata = { + name: "roo-code", + publisher: "Roocode", + version: "1.0.0", + outputChannel: "Roo Code", + sha: undefined, + } beforeEach(() => { // Create mock socket @@ -75,7 +82,10 @@ describe("TaskChannel", () => { } // Create task channel instance - taskChannel = new TaskChannel(instanceId) + taskChannel = new TaskChannel({ + instanceId, + extensionMetadata, + }) }) afterEach(() => { diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 11da65550c62..1b339491e93c 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.69.0", + "version": "1.70.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/extension.ts b/packages/types/src/extension.ts new file mode 100644 index 000000000000..83ac375af42e --- /dev/null +++ b/packages/types/src/extension.ts @@ -0,0 +1,7 @@ +export interface ExtensionMetadata { + publisher: string + name: string + version: string + outputChannel: string + sha: string | undefined +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 38b8c750f7bf..098399e9654a 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,6 +3,7 @@ export * from "./cloud.js" export * from "./codebase-index.js" export * from "./events.js" export * from "./experiment.js" +export * from "./extension.js" export * from "./followup.js" export * from "./global-settings.js" export * from "./history.js" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 15d5fa51a3af..9ebf8b5a4ff8 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2095,7 +2095,6 @@ export class ClineProvider public async remoteControlEnabled(enabled: boolean) { const userInfo = CloudService.instance.getUserInfo() - const config = await CloudService.instance.cloudAPI?.bridgeConfig().catch(() => undefined) if (!config) { @@ -2107,6 +2106,7 @@ export class ClineProvider ...config, provider: this, sessionId: vscode.env.sessionId, + extensionMetadata: Package, }) const bridge = BridgeOrchestrator.getInstance() diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index b9a30708dd57..a495489cc1d6 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -956,13 +956,21 @@ export const webviewMessageHandler = async ( break case "remoteControlEnabled": try { - await CloudService.instance.updateUserSettings({ - extensionBridgeEnabled: message.bool ?? false, - }) + await CloudService.instance.updateUserSettings({ extensionBridgeEnabled: message.bool ?? false }) + } catch (error) { + provider.log( + `CloudService#updateUserSettings failed: ${error instanceof Error ? error.message : String(error)}`, + ) + } + + try { + await provider.remoteControlEnabled(message.bool ?? false) } catch (error) { - provider.log(`Failed to update cloud settings for remote control: ${error}`) + provider.log( + `ClineProvider#remoteControlEnabled failed: ${error instanceof Error ? error.message : String(error)}`, + ) } - await provider.remoteControlEnabled(message.bool ?? false) + await provider.postStateToWebview() break case "refreshAllMcpServers": { diff --git a/src/extension.ts b/src/extension.ts index dec9b8a80dad..4c06b2a0deec 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -131,20 +131,13 @@ export async function activate(context: vscode.ExtensionContext) { authStateChangedHandler = async (data: { state: AuthState; previousState: AuthState }) => { postStateListener() - // Check if user has logged out if (data.state === "logged-out") { try { - // Disconnect the bridge when user logs out - // When userInfo is null and remoteControlEnabled is false, BridgeOrchestrator - // will disconnect. The options parameter is not needed for disconnection. - await BridgeOrchestrator.connectOrDisconnect(null, false) - + await BridgeOrchestrator.disconnect() cloudLogger("[CloudService] BridgeOrchestrator disconnected on logout") } catch (error) { cloudLogger( - `[CloudService] Failed to disconnect BridgeOrchestrator on logout: ${ - error instanceof Error ? error.message : String(error) - }`, + `[CloudService] Failed to disconnect BridgeOrchestrator on logout: ${error instanceof Error ? error.message : String(error)}`, ) } } @@ -152,6 +145,7 @@ export async function activate(context: vscode.ExtensionContext) { settingsUpdatedHandler = async () => { const userInfo = CloudService.instance.getUserInfo() + if (userInfo && CloudService.instance.cloudAPI) { try { const config = await CloudService.instance.cloudAPI.bridgeConfig() @@ -163,16 +157,15 @@ export async function activate(context: vscode.ExtensionContext) { ? true : (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false) - cloudLogger(`[CloudService] Settings updated - remoteControlEnabled = ${remoteControlEnabled}`) - await BridgeOrchestrator.connectOrDisconnect(userInfo, remoteControlEnabled, { ...config, provider, sessionId: vscode.env.sessionId, + extensionMetadata: Package, }) } catch (error) { cloudLogger( - `[CloudService] Failed to update BridgeOrchestrator on settings change: ${error instanceof Error ? error.message : String(error)}`, + `[CloudService] BridgeOrchestrator#connectOrDisconnect failed on settings change: ${error instanceof Error ? error.message : String(error)}`, ) } } @@ -194,8 +187,6 @@ export async function activate(context: vscode.ExtensionContext) { const isCloudAgent = typeof process.env.ROO_CODE_CLOUD_TOKEN === "string" && process.env.ROO_CODE_CLOUD_TOKEN.length > 0 - cloudLogger(`[CloudService] isCloudAgent = ${isCloudAgent}, socketBridgeUrl = ${config.socketBridgeUrl}`) - const remoteControlEnabled = isCloudAgent ? true : (CloudService.instance.getUserSettings()?.settings?.extensionBridgeEnabled ?? false) @@ -204,10 +195,11 @@ export async function activate(context: vscode.ExtensionContext) { ...config, provider, sessionId: vscode.env.sessionId, + extensionMetadata: Package, }) } catch (error) { cloudLogger( - `[CloudService] Failed to fetch bridgeConfig: ${error instanceof Error ? error.message : String(error)}`, + `[CloudService] BridgeOrchestrator#connectOrDisconnect failed on user change: ${error instanceof Error ? error.message : String(error)}`, ) } } diff --git a/src/extension/api.ts b/src/extension/api.ts index 86f2f47aa67f..1820ddee6ca8 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -1,9 +1,10 @@ import { EventEmitter } from "events" -import * as vscode from "vscode" import fs from "fs/promises" import * as path from "path" import * as os from "os" +import * as vscode from "vscode" + import { type RooCodeAPI, type RooCodeSettings, diff --git a/src/shared/package.ts b/src/shared/package.ts index 25588164ecd6..a070b2809a8f 100644 --- a/src/shared/package.ts +++ b/src/shared/package.ts @@ -1,6 +1,4 @@ -/** - * Package - */ +import type { ExtensionMetadata } from "@roo-code/types" import { publisher, name, version } from "../package.json" @@ -10,7 +8,7 @@ import { publisher, name, version } from "../package.json" // The build process still needs to emit a modified package.json for consumption // by VSCode, but that build artifact is not used during the transpile step of // the build, so we still need this override mechanism. -export const Package = { +export const Package: ExtensionMetadata = { publisher, name: process.env.PKG_NAME || name, version: process.env.PKG_VERSION || version, From 71b210c1ea6eb1d8ffd32e3500c7ebd9d0d2cfc3 Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 3 Sep 2025 12:04:01 -0700 Subject: [PATCH 2/3] Simplify and fix tests --- packages/cloud/src/__mocks__/vscode.ts | 4 +++ packages/cloud/src/bridge/BaseChannel.ts | 14 ++++---- .../cloud/src/bridge/BridgeOrchestrator.ts | 15 +++++---- packages/cloud/src/bridge/ExtensionChannel.ts | 9 ++--- .../bridge/__tests__/ExtensionChannel.test.ts | 19 ++++++----- .../src/bridge/__tests__/TaskChannel.test.ts | 33 ++++++++++--------- packages/types/npm/package.metadata.json | 2 +- packages/types/src/extension.ts | 7 ---- packages/types/src/index.ts | 1 - packages/types/src/telemetry.ts | 1 + src/core/webview/ClineProvider.ts | 1 - src/extension.ts | 2 -- src/shared/package.ts | 4 +-- 13 files changed, 54 insertions(+), 58 deletions(-) delete mode 100644 packages/types/src/extension.ts diff --git a/packages/cloud/src/__mocks__/vscode.ts b/packages/cloud/src/__mocks__/vscode.ts index 09384d195ff1..52585437863f 100644 --- a/packages/cloud/src/__mocks__/vscode.ts +++ b/packages/cloud/src/__mocks__/vscode.ts @@ -13,6 +13,10 @@ export const Uri = { parse: vi.fn((uri: string) => ({ toString: () => uri })), } +export const commands = { + executeCommand: vi.fn().mockResolvedValue(undefined), +} + export interface ExtensionContext { secrets: { get: (key: string) => Promise diff --git a/packages/cloud/src/bridge/BaseChannel.ts b/packages/cloud/src/bridge/BaseChannel.ts index 4f9d2feb4de9..e3d21bf2c858 100644 --- a/packages/cloud/src/bridge/BaseChannel.ts +++ b/packages/cloud/src/bridge/BaseChannel.ts @@ -1,11 +1,11 @@ import type { Socket } from "socket.io-client" import * as vscode from "vscode" -import { ExtensionMetadata } from "@roo-code/types" +import { StaticAppProperties } from "@roo-code/types" export interface BaseChannelOptions { instanceId: string - extensionMetadata: ExtensionMetadata + appProperties: StaticAppProperties } /** @@ -19,11 +19,11 @@ export interface BaseChannelOptions { export abstract class BaseChannel { protected socket: Socket | null = null protected readonly instanceId: string - protected readonly extensionMetadata: ExtensionMetadata + protected readonly appProperties: StaticAppProperties constructor(options: BaseChannelOptions) { this.instanceId = options.instanceId - this.extensionMetadata = options.extensionMetadata + this.appProperties = options.appProperties } /** @@ -99,10 +99,10 @@ export abstract class BaseChannel { - // Common functionality: focus the sidebar - await vscode.commands.executeCommand(`${this.extensionMetadata.name}.SidebarProvider.focus`) + // Common functionality: focus the sidebar. + await vscode.commands.executeCommand(`${this.appProperties.appName}.SidebarProvider.focus`) - // Delegate to subclass-specific implementation + // Delegate to subclass-specific implementation. await this.handleCommandImplementation(command) } diff --git a/packages/cloud/src/bridge/BridgeOrchestrator.ts b/packages/cloud/src/bridge/BridgeOrchestrator.ts index 051695cf3fcf..3948fff904fa 100644 --- a/packages/cloud/src/bridge/BridgeOrchestrator.ts +++ b/packages/cloud/src/bridge/BridgeOrchestrator.ts @@ -1,4 +1,5 @@ import crypto from "crypto" +import os from "os" import { type TaskProviderLike, @@ -6,7 +7,7 @@ import { type CloudUserInfo, type ExtensionBridgeCommand, type TaskBridgeCommand, - type ExtensionMetadata, + type StaticAppProperties, ConnectionState, ExtensionSocketEvents, TaskSocketEvents, @@ -22,7 +23,6 @@ export interface BridgeOrchestratorOptions { token: string provider: TaskProviderLike sessionId?: string - extensionMetadata: ExtensionMetadata } /** @@ -41,7 +41,7 @@ export class BridgeOrchestrator { private readonly token: string private readonly provider: TaskProviderLike private readonly instanceId: string - private readonly extensionMetadata: ExtensionMetadata + private readonly appProperties: StaticAppProperties // Components private socketTransport: SocketTransport @@ -152,7 +152,7 @@ export class BridgeOrchestrator { this.token = options.token this.provider = options.provider this.instanceId = options.sessionId || crypto.randomUUID() - this.extensionMetadata = options.extensionMetadata + this.appProperties = { ...options.provider.appProperties, hostname: os.hostname() } this.socketTransport = new SocketTransport({ url: this.socketBridgeUrl, @@ -175,12 +175,15 @@ export class BridgeOrchestrator { this.extensionChannel = new ExtensionChannel({ instanceId: this.instanceId, + appProperties: this.appProperties, userId: this.userId, provider: this.provider, - extensionMetadata: this.extensionMetadata, }) - this.taskChannel = new TaskChannel({ instanceId: this.instanceId, extensionMetadata: this.extensionMetadata }) + this.taskChannel = new TaskChannel({ + instanceId: this.instanceId, + appProperties: this.appProperties, + }) } private setupSocketListeners() { diff --git a/packages/cloud/src/bridge/ExtensionChannel.ts b/packages/cloud/src/bridge/ExtensionChannel.ts index a4fcbbed174c..f84735b3268f 100644 --- a/packages/cloud/src/bridge/ExtensionChannel.ts +++ b/packages/cloud/src/bridge/ExtensionChannel.ts @@ -37,7 +37,7 @@ export class ExtensionChannel extends BaseChannel< private eventListeners: Map void> = new Map() constructor(options: ExtensionChannelOptions) { - super({ instanceId: options.instanceId, extensionMetadata: options.extensionMetadata }) + super({ instanceId: options.instanceId, appProperties: options.appProperties }) this.userId = options.userId this.provider = options.provider @@ -49,10 +49,7 @@ export class ExtensionChannel extends BaseChannel< appProperties: this.provider.appProperties, gitProperties: this.provider.gitProperties, lastHeartbeat: Date.now(), - task: { - taskId: "", - taskStatus: TaskStatus.None, - }, + task: { taskId: "", taskStatus: TaskStatus.None }, taskHistory: [], } @@ -224,8 +221,6 @@ export class ExtensionChannel extends BaseChannel< this.extensionInstance = { ...this.extensionInstance, - appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties, - gitProperties: this.extensionInstance.gitProperties ?? this.provider.gitProperties, lastHeartbeat: Date.now(), task: task ? { diff --git a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts index f5bb535bfdb5..99bd88969ab4 100644 --- a/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts @@ -5,6 +5,7 @@ import type { Socket } from "socket.io-client" import { type TaskProviderLike, type TaskProviderEvents, + type StaticAppProperties, RooCodeEventName, ExtensionBridgeEventName, ExtensionSocketEvents, @@ -18,12 +19,14 @@ describe("ExtensionChannel", () => { let extensionChannel: ExtensionChannel const instanceId = "test-instance-123" const userId = "test-user-456" - const extensionMetadata = { - name: "roo-code", - publisher: "Roocode", - version: "1.0.0", - outputChannel: "Roo Code", - sha: undefined, + + 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 @@ -89,7 +92,7 @@ describe("ExtensionChannel", () => { // Create extension channel instance extensionChannel = new ExtensionChannel({ instanceId, - extensionMetadata, + appProperties, userId, provider: mockProvider, }) @@ -169,7 +172,7 @@ describe("ExtensionChannel", () => { // Create a second channel with the same provider const secondChannel = new ExtensionChannel({ instanceId: "instance-2", - extensionMetadata, + appProperties, userId, provider: mockProvider, }) diff --git a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts index ce1dfeab1ded..1f13da966129 100644 --- a/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts +++ b/packages/cloud/src/bridge/__tests__/TaskChannel.test.ts @@ -6,6 +6,7 @@ import type { Socket } from "socket.io-client" import { type TaskLike, type ClineMessage, + type StaticAppProperties, RooCodeEventName, TaskBridgeEventName, TaskBridgeCommandName, @@ -21,12 +22,14 @@ describe("TaskChannel", () => { let mockTask: TaskLike const instanceId = "test-instance-123" const taskId = "test-task-456" - const extensionMetadata = { - name: "roo-code", - publisher: "Roocode", - version: "1.0.0", - outputChannel: "Roo Code", - sha: undefined, + + const appProperties: StaticAppProperties = { + appName: "roo-code", + appVersion: "1.0.0", + vscodeVersion: "1.0.0", + platform: "darwin", + editorName: "Roo Code", + hostname: "test-host", } beforeEach(() => { @@ -84,7 +87,7 @@ describe("TaskChannel", () => { // Create task channel instance taskChannel = new TaskChannel({ instanceId, - extensionMetadata, + appProperties, }) }) @@ -330,7 +333,7 @@ describe("TaskChannel", () => { channel.subscribedTasks.set(taskId, mockTask) }) - it("should handle Message command", () => { + it("should handle Message command", async () => { const command = { type: TaskBridgeCommandName.Message, taskId, @@ -341,7 +344,7 @@ describe("TaskChannel", () => { }, } - taskChannel.handleCommand(command) + await taskChannel.handleCommand(command) expect(mockTask.submitUserMessage).toHaveBeenCalledWith( command.payload.text, @@ -351,7 +354,7 @@ describe("TaskChannel", () => { ) }) - it("should handle ApproveAsk command", () => { + it("should handle ApproveAsk command", async () => { const command = { type: TaskBridgeCommandName.ApproveAsk, taskId, @@ -361,12 +364,12 @@ describe("TaskChannel", () => { }, } - taskChannel.handleCommand(command) + await taskChannel.handleCommand(command) expect(mockTask.approveAsk).toHaveBeenCalledWith(command.payload) }) - it("should handle DenyAsk command", () => { + it("should handle DenyAsk command", async () => { const command = { type: TaskBridgeCommandName.DenyAsk, taskId, @@ -376,12 +379,12 @@ describe("TaskChannel", () => { }, } - taskChannel.handleCommand(command) + await taskChannel.handleCommand(command) expect(mockTask.denyAsk).toHaveBeenCalledWith(command.payload) }) - it("should log error for unknown task", () => { + it("should log error for unknown task", async () => { const errorSpy = vi.spyOn(console, "error") const command = { @@ -393,7 +396,7 @@ describe("TaskChannel", () => { }, } - taskChannel.handleCommand(command) + await taskChannel.handleCommand(command) expect(errorSpy).toHaveBeenCalledWith(`[TaskChannel] Unable to find task unknown-task`) diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index 1b339491e93c..af0d1bf2cdfa 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -1,6 +1,6 @@ { "name": "@roo-code/types", - "version": "1.70.0", + "version": "1.71.0", "description": "TypeScript type definitions for Roo Code.", "publishConfig": { "access": "public", diff --git a/packages/types/src/extension.ts b/packages/types/src/extension.ts deleted file mode 100644 index 83ac375af42e..000000000000 --- a/packages/types/src/extension.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface ExtensionMetadata { - publisher: string - name: string - version: string - outputChannel: string - sha: string | undefined -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 098399e9654a..38b8c750f7bf 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -3,7 +3,6 @@ export * from "./cloud.js" export * from "./codebase-index.js" export * from "./events.js" export * from "./experiment.js" -export * from "./extension.js" export * from "./followup.js" export * from "./global-settings.js" export * from "./history.js" diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index d872329b93c8..d64827102354 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -78,6 +78,7 @@ export const staticAppPropertiesSchema = z.object({ vscodeVersion: z.string(), platform: z.string(), editorName: z.string(), + hostname: z.string().optional(), }) export type StaticAppProperties = z.infer diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 9ebf8b5a4ff8..bc29e67c0c13 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2106,7 +2106,6 @@ export class ClineProvider ...config, provider: this, sessionId: vscode.env.sessionId, - extensionMetadata: Package, }) const bridge = BridgeOrchestrator.getInstance() diff --git a/src/extension.ts b/src/extension.ts index 4c06b2a0deec..c1f8e0764e50 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -161,7 +161,6 @@ export async function activate(context: vscode.ExtensionContext) { ...config, provider, sessionId: vscode.env.sessionId, - extensionMetadata: Package, }) } catch (error) { cloudLogger( @@ -195,7 +194,6 @@ export async function activate(context: vscode.ExtensionContext) { ...config, provider, sessionId: vscode.env.sessionId, - extensionMetadata: Package, }) } catch (error) { cloudLogger( diff --git a/src/shared/package.ts b/src/shared/package.ts index a070b2809a8f..a5a61da5d227 100644 --- a/src/shared/package.ts +++ b/src/shared/package.ts @@ -1,5 +1,3 @@ -import type { ExtensionMetadata } from "@roo-code/types" - import { publisher, name, version } from "../package.json" // These ENV variables can be defined by ESBuild when building the extension @@ -8,7 +6,7 @@ import { publisher, name, version } from "../package.json" // The build process still needs to emit a modified package.json for consumption // by VSCode, but that build artifact is not used during the transpile step of // the build, so we still need this override mechanism. -export const Package: ExtensionMetadata = { +export const Package = { publisher, name: process.env.PKG_NAME || name, version: process.env.PKG_VERSION || version, From ca86c7b202a8be2c833b34f6c9757e41cb32000c Mon Sep 17 00:00:00 2001 From: cte Date: Wed, 3 Sep 2025 12:22:35 -0700 Subject: [PATCH 3/3] More progress --- packages/cloud/src/bridge/BaseChannel.ts | 5 ++++- packages/cloud/src/bridge/BridgeOrchestrator.ts | 11 ++++++++--- packages/cloud/src/bridge/ExtensionChannel.ts | 10 +++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/cloud/src/bridge/BaseChannel.ts b/packages/cloud/src/bridge/BaseChannel.ts index e3d21bf2c858..90d3ebbe7a1e 100644 --- a/packages/cloud/src/bridge/BaseChannel.ts +++ b/packages/cloud/src/bridge/BaseChannel.ts @@ -1,11 +1,12 @@ import type { Socket } from "socket.io-client" import * as vscode from "vscode" -import { StaticAppProperties } from "@roo-code/types" +import type { StaticAppProperties, GitProperties } from "@roo-code/types" export interface BaseChannelOptions { instanceId: string appProperties: StaticAppProperties + gitProperties?: GitProperties } /** @@ -20,10 +21,12 @@ export abstract class BaseChannel { - // Populate the app and git properties before registering the instance. - await this.provider.getTelemetryProperties() - await this.socketTransport.connect() this.setupSocketListeners() } diff --git a/packages/cloud/src/bridge/ExtensionChannel.ts b/packages/cloud/src/bridge/ExtensionChannel.ts index f84735b3268f..da98f9ac579b 100644 --- a/packages/cloud/src/bridge/ExtensionChannel.ts +++ b/packages/cloud/src/bridge/ExtensionChannel.ts @@ -37,7 +37,11 @@ export class ExtensionChannel extends BaseChannel< private eventListeners: Map void> = new Map() constructor(options: ExtensionChannelOptions) { - super({ instanceId: options.instanceId, appProperties: options.appProperties }) + super({ + instanceId: options.instanceId, + appProperties: options.appProperties, + gitProperties: options.gitProperties, + }) this.userId = options.userId this.provider = options.provider @@ -46,8 +50,8 @@ export class ExtensionChannel extends BaseChannel< instanceId: this.instanceId, userId: this.userId, workspacePath: this.provider.cwd, - appProperties: this.provider.appProperties, - gitProperties: this.provider.gitProperties, + appProperties: this.appProperties, + gitProperties: this.gitProperties, lastHeartbeat: Date.now(), task: { taskId: "", taskStatus: TaskStatus.None }, taskHistory: [],