Skip to content

Commit 1d714c8

Browse files
ctemrubens
andauthored
Extension bridge (RooCodeInc#6677)
Co-authored-by: Matt Rubens <[email protected]>
1 parent 7ca4901 commit 1d714c8

32 files changed

+404
-86
lines changed

pnpm-lock.yaml

Lines changed: 15 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/task/Task.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
isBlockingAsk,
3333
} from "@roo-code/types"
3434
import { TelemetryService } from "@roo-code/telemetry"
35-
import { CloudService } from "@roo-code/cloud"
35+
import { CloudService, TaskBridgeService } from "@roo-code/cloud"
3636

3737
// api
3838
import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
@@ -118,6 +118,7 @@ export type TaskOptions = {
118118
parentTask?: Task
119119
taskNumber?: number
120120
onCreated?: (task: Task) => void
121+
enableTaskBridge?: boolean
121122
}
122123

123124
export class Task extends EventEmitter<TaskEvents> implements TaskLike {
@@ -237,6 +238,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
237238
checkpointService?: RepoPerTaskCheckpointService
238239
checkpointServiceInitializing = false
239240

241+
// Task Bridge
242+
taskBridgeService?: TaskBridgeService
243+
240244
// Streaming
241245
isWaitingForFirstChunk = false
242246
isStreaming = false
@@ -268,6 +272,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
268272
parentTask,
269273
taskNumber = -1,
270274
onCreated,
275+
enableTaskBridge = false,
271276
}: TaskOptions) {
272277
super()
273278

@@ -345,6 +350,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
345350

346351
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
347352

353+
// Initialize TaskBridgeService only if enabled
354+
if (enableTaskBridge) {
355+
this.taskBridgeService = TaskBridgeService.getInstance()
356+
}
357+
348358
onCreated?.(this)
349359

350360
if (startTask) {
@@ -931,6 +941,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
931941
// Start / Abort / Resume
932942

933943
private async startTask(task?: string, images?: string[]): Promise<void> {
944+
if (this.taskBridgeService) {
945+
await this.taskBridgeService.initialize()
946+
await this.taskBridgeService.subscribeToTask(this)
947+
}
948+
934949
// `conversationHistory` (for API) and `clineMessages` (for webview)
935950
// need to be in sync.
936951
// If the extension process were killed, then on restart the
@@ -982,6 +997,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
982997
}
983998

984999
private async resumeTaskFromHistory() {
1000+
if (this.taskBridgeService) {
1001+
await this.taskBridgeService.initialize()
1002+
await this.taskBridgeService.subscribeToTask(this)
1003+
}
1004+
9851005
const modifiedClineMessages = await this.getSavedClineMessages()
9861006

9871007
// Remove any resume messages that may have been added before
@@ -1227,6 +1247,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
12271247
this.pauseInterval = undefined
12281248
}
12291249

1250+
// Unsubscribe from TaskBridge service.
1251+
if (this.taskBridgeService) {
1252+
this.taskBridgeService
1253+
.unsubscribeFromTask(this.taskId)
1254+
.catch((error) => console.error("Error unsubscribing from task bridge:", error))
1255+
}
1256+
12301257
// Release any terminals associated with this task.
12311258
try {
12321259
// Release any terminals associated with this task.

src/core/webview/ClineProvider.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {
1717
type ProviderSettings,
1818
type RooCodeSettings,
1919
type ProviderSettingsEntry,
20-
type ProviderSettingsWithId,
2120
type TelemetryProperties,
2221
type TelemetryPropertiesProvider,
2322
type CodeActionId,
@@ -66,6 +65,7 @@ import { fileExistsAtPath } from "../../utils/fs"
6665
import { setTtsEnabled, setTtsSpeed } from "../../utils/tts"
6766
import { getWorkspaceGitInfo } from "../../utils/git"
6867
import { getWorkspacePath } from "../../utils/path"
68+
import { isRemoteControlEnabled } from "../../utils/remoteControl"
6969

7070
import { setPanel } from "../../activate/registerCommands"
7171

@@ -111,6 +111,8 @@ export class ClineProvider
111111
protected mcpHub?: McpHub // Change from private to protected
112112
private marketplaceManager: MarketplaceManager
113113
private mdmService?: MdmService
114+
private taskCreationCallback: (task: Task) => void
115+
private taskEventListeners: WeakMap<Task, Array<() => void>> = new WeakMap()
114116

115117
public isViewLaunched = false
116118
public settingsImportedAt?: number
@@ -162,6 +164,40 @@ export class ClineProvider
162164

163165
this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
164166

167+
this.taskCreationCallback = (instance: Task) => {
168+
this.emit(RooCodeEventName.TaskCreated, instance)
169+
170+
// Create named listener functions so we can remove them later.
171+
const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
172+
const onTaskCompleted = (taskId: string, tokenUsage: any, toolUsage: any) =>
173+
this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
174+
const onTaskAborted = () => this.emit(RooCodeEventName.TaskAborted, instance.taskId)
175+
const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
176+
const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
177+
const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
178+
const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId)
179+
180+
// Attach the listeners.
181+
instance.on(RooCodeEventName.TaskStarted, onTaskStarted)
182+
instance.on(RooCodeEventName.TaskCompleted, onTaskCompleted)
183+
instance.on(RooCodeEventName.TaskAborted, onTaskAborted)
184+
instance.on(RooCodeEventName.TaskFocused, onTaskFocused)
185+
instance.on(RooCodeEventName.TaskUnfocused, onTaskUnfocused)
186+
instance.on(RooCodeEventName.TaskActive, onTaskActive)
187+
instance.on(RooCodeEventName.TaskIdle, onTaskIdle)
188+
189+
// Store the cleanup functions for later removal.
190+
this.taskEventListeners.set(instance, [
191+
() => instance.off(RooCodeEventName.TaskStarted, onTaskStarted),
192+
() => instance.off(RooCodeEventName.TaskCompleted, onTaskCompleted),
193+
() => instance.off(RooCodeEventName.TaskAborted, onTaskAborted),
194+
() => instance.off(RooCodeEventName.TaskFocused, onTaskFocused),
195+
() => instance.off(RooCodeEventName.TaskUnfocused, onTaskUnfocused),
196+
() => instance.off(RooCodeEventName.TaskActive, onTaskActive),
197+
() => instance.off(RooCodeEventName.TaskIdle, onTaskIdle),
198+
])
199+
}
200+
165201
// Initialize Roo Code Cloud profile sync.
166202
this.initializeCloudProfileSync().catch((error) => {
167203
this.log(`Failed to initialize cloud profile sync: ${error}`)
@@ -297,6 +333,14 @@ export class ClineProvider
297333

298334
task.emit(RooCodeEventName.TaskUnfocused)
299335

336+
// Remove event listeners before clearing the reference.
337+
const cleanupFunctions = this.taskEventListeners.get(task)
338+
339+
if (cleanupFunctions) {
340+
cleanupFunctions.forEach((cleanup) => cleanup())
341+
this.taskEventListeners.delete(task)
342+
}
343+
300344
// Make sure no reference kept, once promises end it will be
301345
// garbage collected.
302346
task = undefined
@@ -654,12 +698,17 @@ export class ClineProvider
654698
enableCheckpoints,
655699
fuzzyMatchThreshold,
656700
experiments,
701+
cloudUserInfo,
702+
remoteControlEnabled,
657703
} = await this.getState()
658704

659705
if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
660706
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
661707
}
662708

709+
// Determine if TaskBridge should be enabled
710+
const enableTaskBridge = isRemoteControlEnabled(cloudUserInfo, remoteControlEnabled)
711+
663712
const task = new Task({
664713
provider: this,
665714
apiConfiguration,
@@ -673,7 +722,8 @@ export class ClineProvider
673722
rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined,
674723
parentTask,
675724
taskNumber: this.clineStack.length + 1,
676-
onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance),
725+
onCreated: this.taskCreationCallback,
726+
enableTaskBridge,
677727
...options,
678728
})
679729

@@ -738,8 +788,13 @@ export class ClineProvider
738788
enableCheckpoints,
739789
fuzzyMatchThreshold,
740790
experiments,
791+
cloudUserInfo,
792+
remoteControlEnabled,
741793
} = await this.getState()
742794

795+
// Determine if TaskBridge should be enabled
796+
const enableTaskBridge = isRemoteControlEnabled(cloudUserInfo, remoteControlEnabled)
797+
743798
const task = new Task({
744799
provider: this,
745800
apiConfiguration,
@@ -752,7 +807,8 @@ export class ClineProvider
752807
rootTask: historyItem.rootTask,
753808
parentTask: historyItem.parentTask,
754809
taskNumber: historyItem.number,
755-
onCreated: (instance) => this.emit(RooCodeEventName.TaskCreated, instance),
810+
onCreated: this.taskCreationCallback,
811+
enableTaskBridge,
756812
})
757813

758814
await this.addClineToStack(task)
@@ -1631,6 +1687,7 @@ export class ClineProvider
16311687
includeDiagnosticMessages,
16321688
maxDiagnosticMessages,
16331689
includeTaskHistoryInEnhance,
1690+
remoteControlEnabled,
16341691
} = await this.getState()
16351692

16361693
const telemetryKey = process.env.POSTHOG_API_KEY
@@ -1758,6 +1815,7 @@ export class ClineProvider
17581815
includeDiagnosticMessages: includeDiagnosticMessages ?? true,
17591816
maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
17601817
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? false,
1818+
remoteControlEnabled: remoteControlEnabled ?? false,
17611819
}
17621820
}
17631821

@@ -1945,6 +2003,8 @@ export class ClineProvider
19452003
maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
19462004
// Add includeTaskHistoryInEnhance setting
19472005
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? false,
2006+
// Add remoteControlEnabled setting
2007+
remoteControlEnabled: stateValues.remoteControlEnabled ?? false,
19482008
}
19492009
}
19502010

@@ -2057,6 +2117,55 @@ export class ClineProvider
20572117
return true
20582118
}
20592119

2120+
/**
2121+
* Handle remote control enabled/disabled state changes
2122+
* Manages ExtensionBridgeService and TaskBridgeService lifecycle
2123+
*/
2124+
public async handleRemoteControlToggle(enabled: boolean): Promise<void> {
2125+
const {
2126+
CloudService: CloudServiceImport,
2127+
ExtensionBridgeService,
2128+
TaskBridgeService,
2129+
} = await import("@roo-code/cloud")
2130+
const userInfo = CloudServiceImport.instance.getUserInfo()
2131+
2132+
// Handle ExtensionBridgeService using static method
2133+
await ExtensionBridgeService.handleRemoteControlState(userInfo, enabled, this, (message: string) =>
2134+
this.log(message),
2135+
)
2136+
2137+
if (isRemoteControlEnabled(userInfo, enabled)) {
2138+
// Set up TaskBridgeService for the currently active task if one exists
2139+
const currentTask = this.getCurrentCline()
2140+
if (currentTask && !currentTask.taskBridgeService) {
2141+
try {
2142+
currentTask.taskBridgeService = TaskBridgeService.getInstance()
2143+
await currentTask.taskBridgeService.subscribeToTask(currentTask)
2144+
this.log(`[TaskBridgeService] Subscribed current task ${currentTask.taskId} to TaskBridge`)
2145+
} catch (error) {
2146+
const message = `[TaskBridgeService#subscribeToTask] ${error instanceof Error ? error.message : String(error)}`
2147+
this.log(message)
2148+
console.error(message)
2149+
}
2150+
}
2151+
} else {
2152+
// Disconnect TaskBridgeService for all tasks in the stack
2153+
for (const task of this.clineStack) {
2154+
if (task.taskBridgeService) {
2155+
try {
2156+
await task.taskBridgeService.unsubscribeFromTask(task.taskId)
2157+
task.taskBridgeService = undefined
2158+
this.log(`[TaskBridgeService] Unsubscribed task ${task.taskId} from TaskBridge`)
2159+
} catch (error) {
2160+
const message = `[TaskBridgeService#unsubscribeFromTask] for task ${task.taskId}: ${error instanceof Error ? error.message : String(error)}`
2161+
this.log(message)
2162+
console.error(message)
2163+
}
2164+
}
2165+
}
2166+
}
2167+
}
2168+
20602169
/**
20612170
* Returns properties to be included in every telemetry event
20622171
* This method is called by the telemetry service to get context information

src/core/webview/webviewMessageHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -906,6 +906,11 @@ export const webviewMessageHandler = async (
906906
await updateGlobalState("enableMcpServerCreation", message.bool ?? true)
907907
await provider.postStateToWebview()
908908
break
909+
case "remoteControlEnabled":
910+
await updateGlobalState("remoteControlEnabled", message.bool ?? false)
911+
await provider.handleRemoteControlToggle(message.bool ?? false)
912+
await provider.postStateToWebview()
913+
break
909914
case "refreshAllMcpServers": {
910915
const mcpHub = provider.getMcpHub()
911916
if (mcpHub) {

0 commit comments

Comments
 (0)