Skip to content

Commit 4ce37c2

Browse files
committed
Focus the extension when receiving bridge commands
1 parent 130ce29 commit 4ce37c2

File tree

14 files changed

+180
-96
lines changed

14 files changed

+180
-96
lines changed

packages/cloud/src/bridge/BaseChannel.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import type { Socket } from "socket.io-client"
2+
import * as vscode from "vscode"
3+
4+
import { ExtensionMetadata } from "@roo-code/types"
5+
6+
export interface BaseChannelOptions {
7+
instanceId: string
8+
extensionMetadata: ExtensionMetadata
9+
}
210

311
/**
412
* Abstract base class for communication channels in the bridge system.
@@ -11,9 +19,11 @@ import type { Socket } from "socket.io-client"
1119
export abstract class BaseChannel<TCommand = unknown, TEventName extends string = string, TEventData = unknown> {
1220
protected socket: Socket | null = null
1321
protected readonly instanceId: string
22+
protected readonly extensionMetadata: ExtensionMetadata
1423

15-
constructor(instanceId: string) {
16-
this.instanceId = instanceId
24+
constructor(options: BaseChannelOptions) {
25+
this.instanceId = options.instanceId
26+
this.extensionMetadata = options.extensionMetadata
1727
}
1828

1929
/**
@@ -81,9 +91,26 @@ export abstract class BaseChannel<TCommand = unknown, TEventName extends string
8191
}
8292

8393
/**
84-
* Handle incoming commands - must be implemented by subclasses.
94+
* Handle incoming commands - template method that ensures common functionality
95+
* is executed before subclass-specific logic.
96+
*
97+
* This method should be called by subclasses to handle commands.
98+
* It will execute common functionality and then delegate to the abstract
99+
* handleCommandImplementation method.
100+
*/
101+
public async handleCommand(command: TCommand): Promise<void> {
102+
// Common functionality: focus the sidebar
103+
await vscode.commands.executeCommand(`${this.extensionMetadata.name}.SidebarProvider.focus`)
104+
105+
// Delegate to subclass-specific implementation
106+
await this.handleCommandImplementation(command)
107+
}
108+
109+
/**
110+
* Handle command-specific logic - must be implemented by subclasses.
111+
* This method is called after common functionality has been executed.
85112
*/
86-
public abstract handleCommand(command: TCommand): Promise<void>
113+
protected abstract handleCommandImplementation(command: TCommand): Promise<void>
87114

88115
/**
89116
* Handle connection-specific logic.

packages/cloud/src/bridge/BridgeOrchestrator.ts

Lines changed: 65 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type CloudUserInfo,
77
type ExtensionBridgeCommand,
88
type TaskBridgeCommand,
9+
type ExtensionMetadata,
910
ConnectionState,
1011
ExtensionSocketEvents,
1112
TaskSocketEvents,
@@ -21,6 +22,7 @@ export interface BridgeOrchestratorOptions {
2122
token: string
2223
provider: TaskProviderLike
2324
sessionId?: string
25+
extensionMetadata: ExtensionMetadata
2426
}
2527

2628
/**
@@ -39,6 +41,7 @@ export class BridgeOrchestrator {
3941
private readonly token: string
4042
private readonly provider: TaskProviderLike
4143
private readonly instanceId: string
44+
private readonly extensionMetadata: ExtensionMetadata
4245

4346
// Components
4447
private socketTransport: SocketTransport
@@ -61,66 +64,69 @@ export class BridgeOrchestrator {
6164
public static async connectOrDisconnect(
6265
userInfo: CloudUserInfo | null,
6366
remoteControlEnabled: boolean | undefined,
64-
options?: BridgeOrchestratorOptions,
67+
options: BridgeOrchestratorOptions,
6568
): Promise<void> {
66-
const isEnabled = BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)
67-
const instance = BridgeOrchestrator.instance
69+
if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) {
70+
await BridgeOrchestrator.connect(options)
71+
} else {
72+
await BridgeOrchestrator.disconnect()
73+
}
74+
}
6875

69-
if (isEnabled) {
70-
if (!instance) {
71-
if (!options) {
72-
console.error(
73-
`[BridgeOrchestrator#connectOrDisconnect] Cannot connect: options are required for connection`,
74-
)
75-
return
76-
}
77-
try {
78-
console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`)
79-
BridgeOrchestrator.instance = new BridgeOrchestrator(options)
80-
await BridgeOrchestrator.instance.connect()
81-
} catch (error) {
82-
console.error(
83-
`[BridgeOrchestrator#connectOrDisconnect] connect() failed: ${error instanceof Error ? error.message : String(error)}`,
84-
)
85-
}
86-
} else {
87-
if (
88-
instance.connectionState === ConnectionState.FAILED ||
89-
instance.connectionState === ConnectionState.DISCONNECTED
90-
) {
91-
console.log(
92-
`[BridgeOrchestrator#connectOrDisconnect] Re-connecting... (state: ${instance.connectionState})`,
93-
)
76+
public static async connect(options: BridgeOrchestratorOptions) {
77+
const instance = BridgeOrchestrator.instance
9478

95-
instance.reconnect().catch((error) => {
96-
console.error(
97-
`[BridgeOrchestrator#connectOrDisconnect] reconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
98-
)
99-
})
100-
} else {
101-
console.log(
102-
`[BridgeOrchestrator#connectOrDisconnect] Already connected or connecting (state: ${instance.connectionState})`,
103-
)
104-
}
79+
if (!instance) {
80+
try {
81+
console.log(`[BridgeOrchestrator#connectOrDisconnect] Connecting...`)
82+
BridgeOrchestrator.instance = new BridgeOrchestrator(options)
83+
await BridgeOrchestrator.instance.connect()
84+
} catch (error) {
85+
console.error(
86+
`[BridgeOrchestrator#connectOrDisconnect] connect() failed: ${error instanceof Error ? error.message : String(error)}`,
87+
)
10588
}
10689
} else {
107-
if (instance) {
108-
try {
109-
console.log(
110-
`[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`,
111-
)
90+
if (
91+
instance.connectionState === ConnectionState.FAILED ||
92+
instance.connectionState === ConnectionState.DISCONNECTED
93+
) {
94+
console.log(
95+
`[BridgeOrchestrator#connectOrDisconnect] Re-connecting... (state: ${instance.connectionState})`,
96+
)
11297

113-
await instance.disconnect()
114-
} catch (error) {
98+
instance.reconnect().catch((error) => {
11599
console.error(
116-
`[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
100+
`[BridgeOrchestrator#connectOrDisconnect] reconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
117101
)
118-
} finally {
119-
BridgeOrchestrator.instance = null
120-
}
102+
})
121103
} else {
122-
console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`)
104+
console.log(
105+
`[BridgeOrchestrator#connectOrDisconnect] Already connected or connecting (state: ${instance.connectionState})`,
106+
)
107+
}
108+
}
109+
}
110+
111+
public static async disconnect() {
112+
const instance = BridgeOrchestrator.instance
113+
114+
if (instance) {
115+
try {
116+
console.log(
117+
`[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`,
118+
)
119+
120+
await instance.disconnect()
121+
} catch (error) {
122+
console.error(
123+
`[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
124+
)
125+
} finally {
126+
BridgeOrchestrator.instance = null
123127
}
128+
} else {
129+
console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`)
124130
}
125131
}
126132

@@ -146,6 +152,7 @@ export class BridgeOrchestrator {
146152
this.token = options.token
147153
this.provider = options.provider
148154
this.instanceId = options.sessionId || crypto.randomUUID()
155+
this.extensionMetadata = options.extensionMetadata
149156

150157
this.socketTransport = new SocketTransport({
151158
url: this.socketBridgeUrl,
@@ -166,8 +173,14 @@ export class BridgeOrchestrator {
166173
onReconnect: () => this.handleReconnect(),
167174
})
168175

169-
this.extensionChannel = new ExtensionChannel(this.instanceId, this.userId, this.provider)
170-
this.taskChannel = new TaskChannel(this.instanceId)
176+
this.extensionChannel = new ExtensionChannel({
177+
instanceId: this.instanceId,
178+
userId: this.userId,
179+
provider: this.provider,
180+
extensionMetadata: this.extensionMetadata,
181+
})
182+
183+
this.taskChannel = new TaskChannel({ instanceId: this.instanceId, extensionMetadata: this.extensionMetadata })
171184
}
172185

173186
private setupSocketListeners() {

packages/cloud/src/bridge/ExtensionChannel.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ import {
1414
HEARTBEAT_INTERVAL_MS,
1515
} from "@roo-code/types"
1616

17-
import { BaseChannel } from "./BaseChannel.js"
17+
import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js"
18+
19+
interface ExtensionChannelOptions extends BaseChannelOptions {
20+
userId: string
21+
provider: TaskProviderLike
22+
}
1823

1924
/**
2025
* Manages the extension-level communication channel.
@@ -31,10 +36,11 @@ export class ExtensionChannel extends BaseChannel<
3136
private heartbeatInterval: NodeJS.Timeout | null = null
3237
private eventListeners: Map<RooCodeEventName, (...args: unknown[]) => void> = new Map()
3338

34-
constructor(instanceId: string, userId: string, provider: TaskProviderLike) {
35-
super(instanceId)
36-
this.userId = userId
37-
this.provider = provider
39+
constructor(options: ExtensionChannelOptions) {
40+
super({ instanceId: options.instanceId, extensionMetadata: options.extensionMetadata })
41+
42+
this.userId = options.userId
43+
this.provider = options.provider
3844

3945
this.extensionInstance = {
4046
instanceId: this.instanceId,
@@ -53,11 +59,12 @@ export class ExtensionChannel extends BaseChannel<
5359
this.setupListeners()
5460
}
5561

56-
public async handleCommand(command: ExtensionBridgeCommand): Promise<void> {
62+
protected async handleCommandImplementation(command: ExtensionBridgeCommand): Promise<void> {
5763
if (command.instanceId !== this.instanceId) {
5864
console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, {
5965
messageInstanceId: command.instanceId,
6066
})
67+
6168
return
6269
}
6370

packages/cloud/src/bridge/TaskChannel.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
TaskSocketEvents,
1515
} from "@roo-code/types"
1616

17-
import { BaseChannel } from "./BaseChannel.js"
17+
import { type BaseChannelOptions, BaseChannel } from "./BaseChannel.js"
1818

1919
type TaskEventListener = {
2020
[K in keyof TaskEvents]: (...args: TaskEvents[K]) => void | Promise<void>
@@ -26,6 +26,9 @@ type TaskEventMapping = {
2626
createPayload: (task: TaskLike, ...args: any[]) => any // eslint-disable-line @typescript-eslint/no-explicit-any
2727
}
2828

29+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
30+
interface TaskChannelOptions extends BaseChannelOptions {}
31+
2932
/**
3033
* Manages task-level communication channels.
3134
* Handles task subscriptions, messaging, and task-specific commands.
@@ -69,11 +72,11 @@ export class TaskChannel extends BaseChannel<
6972
},
7073
] as const
7174

72-
constructor(instanceId: string) {
73-
super(instanceId)
75+
constructor(options: TaskChannelOptions) {
76+
super(options)
7477
}
7578

76-
public async handleCommand(command: TaskBridgeCommand): Promise<void> {
79+
protected async handleCommandImplementation(command: TaskBridgeCommand): Promise<void> {
7780
const task = this.subscribedTasks.get(command.taskId)
7881

7982
if (!task) {

packages/cloud/src/bridge/__tests__/ExtensionChannel.test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ describe("ExtensionChannel", () => {
1818
let extensionChannel: ExtensionChannel
1919
const instanceId = "test-instance-123"
2020
const userId = "test-user-456"
21+
const extensionMetadata = {
22+
name: "roo-code",
23+
publisher: "Roocode",
24+
version: "1.0.0",
25+
outputChannel: "Roo Code",
26+
sha: undefined,
27+
}
2128

2229
// Track registered event listeners
2330
const eventListeners = new Map<keyof TaskProviderEvents, Set<(...args: unknown[]) => unknown>>()
@@ -80,7 +87,12 @@ describe("ExtensionChannel", () => {
8087
} as unknown as TaskProviderLike
8188

8289
// Create extension channel instance
83-
extensionChannel = new ExtensionChannel(instanceId, userId, mockProvider)
90+
extensionChannel = new ExtensionChannel({
91+
instanceId,
92+
extensionMetadata,
93+
userId,
94+
provider: mockProvider,
95+
})
8496
})
8597

8698
afterEach(() => {
@@ -155,7 +167,12 @@ describe("ExtensionChannel", () => {
155167

156168
it("should not have duplicate listeners after multiple channel creations", () => {
157169
// Create a second channel with the same provider
158-
const secondChannel = new ExtensionChannel("instance-2", userId, mockProvider)
170+
const secondChannel = new ExtensionChannel({
171+
instanceId: "instance-2",
172+
extensionMetadata,
173+
userId,
174+
provider: mockProvider,
175+
})
159176

160177
// Each event should have exactly 2 listeners (one from each channel)
161178
eventListeners.forEach((listeners) => {

packages/cloud/src/bridge/__tests__/TaskChannel.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ describe("TaskChannel", () => {
2121
let mockTask: TaskLike
2222
const instanceId = "test-instance-123"
2323
const taskId = "test-task-456"
24+
const extensionMetadata = {
25+
name: "roo-code",
26+
publisher: "Roocode",
27+
version: "1.0.0",
28+
outputChannel: "Roo Code",
29+
sha: undefined,
30+
}
2431

2532
beforeEach(() => {
2633
// Create mock socket
@@ -75,7 +82,10 @@ describe("TaskChannel", () => {
7582
}
7683

7784
// Create task channel instance
78-
taskChannel = new TaskChannel(instanceId)
85+
taskChannel = new TaskChannel({
86+
instanceId,
87+
extensionMetadata,
88+
})
7989
})
8090

8191
afterEach(() => {

packages/types/npm/package.metadata.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@roo-code/types",
3-
"version": "1.69.0",
3+
"version": "1.70.0",
44
"description": "TypeScript type definitions for Roo Code.",
55
"publishConfig": {
66
"access": "public",

packages/types/src/extension.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export interface ExtensionMetadata {
2+
publisher: string
3+
name: string
4+
version: string
5+
outputChannel: string
6+
sha: string | undefined
7+
}

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from "./cloud.js"
33
export * from "./codebase-index.js"
44
export * from "./events.js"
55
export * from "./experiment.js"
6+
export * from "./extension.js"
67
export * from "./followup.js"
78
export * from "./global-settings.js"
89
export * from "./history.js"

0 commit comments

Comments
 (0)