Skip to content

Commit 280b75d

Browse files
authored
Focus the extension when receiving bridge commands (#7633)
1 parent d789692 commit 280b75d

File tree

14 files changed

+206
-114
lines changed

14 files changed

+206
-114
lines changed

packages/cloud/src/__mocks__/vscode.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ export const Uri = {
1313
parse: vi.fn((uri: string) => ({ toString: () => uri })),
1414
}
1515

16+
export const commands = {
17+
executeCommand: vi.fn().mockResolvedValue(undefined),
18+
}
19+
1620
export interface ExtensionContext {
1721
secrets: {
1822
get: (key: string) => Promise<string | undefined>

packages/cloud/src/bridge/BaseChannel.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
11
import type { Socket } from "socket.io-client"
2+
import * as vscode from "vscode"
3+
4+
import type { StaticAppProperties, GitProperties } from "@roo-code/types"
5+
6+
export interface BaseChannelOptions {
7+
instanceId: string
8+
appProperties: StaticAppProperties
9+
gitProperties?: GitProperties
10+
}
211

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

15-
constructor(instanceId: string) {
16-
this.instanceId = instanceId
26+
constructor(options: BaseChannelOptions) {
27+
this.instanceId = options.instanceId
28+
this.appProperties = options.appProperties
29+
this.gitProperties = options.gitProperties
1730
}
1831

1932
/**
@@ -81,9 +94,26 @@ export abstract class BaseChannel<TCommand = unknown, TEventName extends string
8194
}
8295

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

88118
/**
89119
* Handle connection-specific logic.

packages/cloud/src/bridge/BridgeOrchestrator.ts

Lines changed: 75 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import crypto from "crypto"
2+
import os from "os"
23

34
import {
45
type TaskProviderLike,
56
type TaskLike,
67
type CloudUserInfo,
78
type ExtensionBridgeCommand,
89
type TaskBridgeCommand,
10+
type StaticAppProperties,
11+
type GitProperties,
912
ConnectionState,
1013
ExtensionSocketEvents,
1114
TaskSocketEvents,
@@ -39,6 +42,8 @@ export class BridgeOrchestrator {
3942
private readonly token: string
4043
private readonly provider: TaskProviderLike
4144
private readonly instanceId: string
45+
private readonly appProperties: StaticAppProperties
46+
private readonly gitProperties?: GitProperties
4247

4348
// Components
4449
private socketTransport: SocketTransport
@@ -61,66 +66,72 @@ export class BridgeOrchestrator {
6166
public static async connectOrDisconnect(
6267
userInfo: CloudUserInfo | null,
6368
remoteControlEnabled: boolean | undefined,
64-
options?: BridgeOrchestratorOptions,
69+
options: BridgeOrchestratorOptions,
6570
): Promise<void> {
66-
const isEnabled = BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)
71+
if (BridgeOrchestrator.isEnabled(userInfo, remoteControlEnabled)) {
72+
await BridgeOrchestrator.connect(options)
73+
} else {
74+
await BridgeOrchestrator.disconnect()
75+
}
76+
}
77+
78+
public static async connect(options: BridgeOrchestratorOptions) {
6779
const instance = BridgeOrchestrator.instance
6880

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

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-
}
87+
BridgeOrchestrator.instance = new BridgeOrchestrator(options)
88+
await BridgeOrchestrator.instance.connect()
89+
} catch (error) {
90+
console.error(
91+
`[BridgeOrchestrator#connectOrDisconnect] connect() failed: ${error instanceof Error ? error.message : String(error)}`,
92+
)
10593
}
10694
} else {
107-
if (instance) {
108-
try {
109-
console.log(
110-
`[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`,
111-
)
95+
if (
96+
instance.connectionState === ConnectionState.FAILED ||
97+
instance.connectionState === ConnectionState.DISCONNECTED
98+
) {
99+
console.log(
100+
`[BridgeOrchestrator#connectOrDisconnect] Re-connecting... (state: ${instance.connectionState})`,
101+
)
112102

113-
await instance.disconnect()
114-
} catch (error) {
103+
instance.reconnect().catch((error) => {
115104
console.error(
116-
`[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
105+
`[BridgeOrchestrator#connectOrDisconnect] reconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
117106
)
118-
} finally {
119-
BridgeOrchestrator.instance = null
120-
}
107+
})
121108
} else {
122-
console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`)
109+
console.log(
110+
`[BridgeOrchestrator#connectOrDisconnect] Already connected or connecting (state: ${instance.connectionState})`,
111+
)
112+
}
113+
}
114+
}
115+
116+
public static async disconnect() {
117+
const instance = BridgeOrchestrator.instance
118+
119+
if (instance) {
120+
try {
121+
console.log(
122+
`[BridgeOrchestrator#connectOrDisconnect] Disconnecting... (state: ${instance.connectionState})`,
123+
)
124+
125+
await instance.disconnect()
126+
} catch (error) {
127+
console.error(
128+
`[BridgeOrchestrator#connectOrDisconnect] disconnect() failed: ${error instanceof Error ? error.message : String(error)}`,
129+
)
130+
} finally {
131+
BridgeOrchestrator.instance = null
123132
}
133+
} else {
134+
console.log(`[BridgeOrchestrator#connectOrDisconnect] Already disconnected`)
124135
}
125136
}
126137

@@ -146,6 +157,8 @@ export class BridgeOrchestrator {
146157
this.token = options.token
147158
this.provider = options.provider
148159
this.instanceId = options.sessionId || crypto.randomUUID()
160+
this.appProperties = { ...options.provider.appProperties, hostname: os.hostname() }
161+
this.gitProperties = options.provider.gitProperties
149162

150163
this.socketTransport = new SocketTransport({
151164
url: this.socketBridgeUrl,
@@ -166,8 +179,19 @@ export class BridgeOrchestrator {
166179
onReconnect: () => this.handleReconnect(),
167180
})
168181

169-
this.extensionChannel = new ExtensionChannel(this.instanceId, this.userId, this.provider)
170-
this.taskChannel = new TaskChannel(this.instanceId)
182+
this.extensionChannel = new ExtensionChannel({
183+
instanceId: this.instanceId,
184+
appProperties: this.appProperties,
185+
gitProperties: this.gitProperties,
186+
userId: this.userId,
187+
provider: this.provider,
188+
})
189+
190+
this.taskChannel = new TaskChannel({
191+
instanceId: this.instanceId,
192+
appProperties: this.appProperties,
193+
gitProperties: this.gitProperties,
194+
})
171195
}
172196

173197
private setupSocketListeners() {
@@ -288,9 +312,6 @@ export class BridgeOrchestrator {
288312
}
289313

290314
private async connect(): Promise<void> {
291-
// Populate the app and git properties before registering the instance.
292-
await this.provider.getTelemetryProperties()
293-
294315
await this.socketTransport.connect()
295316
this.setupSocketListeners()
296317
}

packages/cloud/src/bridge/ExtensionChannel.ts

Lines changed: 20 additions & 14 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,33 +36,36 @@ 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({
41+
instanceId: options.instanceId,
42+
appProperties: options.appProperties,
43+
gitProperties: options.gitProperties,
44+
})
45+
46+
this.userId = options.userId
47+
this.provider = options.provider
3848

3949
this.extensionInstance = {
4050
instanceId: this.instanceId,
4151
userId: this.userId,
4252
workspacePath: this.provider.cwd,
43-
appProperties: this.provider.appProperties,
44-
gitProperties: this.provider.gitProperties,
53+
appProperties: this.appProperties,
54+
gitProperties: this.gitProperties,
4555
lastHeartbeat: Date.now(),
46-
task: {
47-
taskId: "",
48-
taskStatus: TaskStatus.None,
49-
},
56+
task: { taskId: "", taskStatus: TaskStatus.None },
5057
taskHistory: [],
5158
}
5259

5360
this.setupListeners()
5461
}
5562

56-
public async handleCommand(command: ExtensionBridgeCommand): Promise<void> {
63+
protected async handleCommandImplementation(command: ExtensionBridgeCommand): Promise<void> {
5764
if (command.instanceId !== this.instanceId) {
5865
console.log(`[ExtensionChannel] command -> instance id mismatch | ${this.instanceId}`, {
5966
messageInstanceId: command.instanceId,
6067
})
68+
6169
return
6270
}
6371

@@ -217,8 +225,6 @@ export class ExtensionChannel extends BaseChannel<
217225

218226
this.extensionInstance = {
219227
...this.extensionInstance,
220-
appProperties: this.extensionInstance.appProperties ?? this.provider.appProperties,
221-
gitProperties: this.extensionInstance.gitProperties ?? this.provider.gitProperties,
222228
lastHeartbeat: Date.now(),
223229
task: task
224230
? {

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) {

0 commit comments

Comments
 (0)