Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/cloud/src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | undefined>
Expand Down
38 changes: 34 additions & 4 deletions packages/cloud/src/bridge/BaseChannel.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
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
}

/**
* Abstract base class for communication channels in the bridge system.
Expand All @@ -11,9 +20,13 @@ import type { Socket } from "socket.io-client"
export abstract class BaseChannel<TCommand = unknown, TEventName extends string = string, TEventData = unknown> {
protected socket: Socket | null = null
protected readonly instanceId: string
protected readonly appProperties: StaticAppProperties
protected readonly gitProperties?: GitProperties

constructor(instanceId: string) {
this.instanceId = instanceId
constructor(options: BaseChannelOptions) {
this.instanceId = options.instanceId
this.appProperties = options.appProperties
this.gitProperties = options.gitProperties
}

/**
Expand Down Expand Up @@ -81,9 +94,26 @@ export abstract class BaseChannel<TCommand = unknown, TEventName extends string
}

/**
* Handle incoming commands - must be implemented by subclasses.
* 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<void> {
// 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.
*/
public abstract handleCommand(command: TCommand): Promise<void>
protected abstract handleCommandImplementation(command: TCommand): Promise<void>

/**
* Handle connection-specific logic.
Expand Down
129 changes: 75 additions & 54 deletions packages/cloud/src/bridge/BridgeOrchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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,
Expand Down Expand Up @@ -39,6 +42,8 @@ export class BridgeOrchestrator {
private readonly token: string
private readonly provider: TaskProviderLike
private readonly instanceId: string
private readonly appProperties: StaticAppProperties
private readonly gitProperties?: GitProperties

// Components
private socketTransport: SocketTransport
Expand All @@ -61,66 +66,72 @@ export class BridgeOrchestrator {
public static async connectOrDisconnect(
userInfo: CloudUserInfo | null,
remoteControlEnabled: boolean | undefined,
options?: BridgeOrchestratorOptions,
options: BridgeOrchestratorOptions,
): Promise<void> {
const isEnabled = BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)
if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) {
await BridgeOrchestrator.connect(options)
} else {
await BridgeOrchestrator.disconnect()
}
}

public static async connect(options: BridgeOrchestratorOptions) {
const instance = BridgeOrchestrator.instance

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})`,
)
if (!instance) {
try {
console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`)
// Populate telemetry properties before registering the instance.
await options.provider.getTelemetryProperties()

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})`,
)
}
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`)
}
}

Expand All @@ -146,6 +157,8 @@ export class BridgeOrchestrator {
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.socketTransport = new SocketTransport({
url: this.socketBridgeUrl,
Expand All @@ -166,8 +179,19 @@ 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,
appProperties: this.appProperties,
gitProperties: this.gitProperties,
userId: this.userId,
provider: this.provider,
})

this.taskChannel = new TaskChannel({
instanceId: this.instanceId,
appProperties: this.appProperties,
gitProperties: this.gitProperties,
})
}

private setupSocketListeners() {
Expand Down Expand Up @@ -288,9 +312,6 @@ export class BridgeOrchestrator {
}

private async connect(): Promise<void> {
// Populate the app and git properties before registering the instance.
await this.provider.getTelemetryProperties()

await this.socketTransport.connect()
this.setupSocketListeners()
}
Expand Down
34 changes: 20 additions & 14 deletions packages/cloud/src/bridge/ExtensionChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -31,33 +36,36 @@ export class ExtensionChannel extends BaseChannel<
private heartbeatInterval: NodeJS.Timeout | null = null
private eventListeners: Map<RooCodeEventName, (...args: unknown[]) => 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,
appProperties: options.appProperties,
gitProperties: options.gitProperties,
})

this.userId = options.userId
this.provider = options.provider

this.extensionInstance = {
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,
},
task: { taskId: "", taskStatus: TaskStatus.None },
taskHistory: [],
}

this.setupListeners()
}

public async handleCommand(command: ExtensionBridgeCommand): Promise<void> {
protected async handleCommandImplementation(command: ExtensionBridgeCommand): Promise<void> {
if (command.instanceId !== this.instanceId) {
console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, {
messageInstanceId: command.instanceId,
})

return
}

Expand Down Expand Up @@ -217,8 +225,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
? {
Expand Down
11 changes: 7 additions & 4 deletions packages/cloud/src/bridge/TaskChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>
Expand All @@ -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.
Expand Down Expand Up @@ -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<void> {
protected async handleCommandImplementation(command: TaskBridgeCommand): Promise<void> {
const task = this.subscribedTasks.get(command.taskId)

if (!task) {
Expand Down
Loading
Loading