Skip to content

Commit fc46cf2

Browse files
jrroomote-agent
authored andcommitted
separate task sync roomote control (RooCodeInc#7799)
* feat: separate Task Sync and Roomote Control settings - Add new taskSyncEnabled setting to control task content syncing - Keep remoteControlEnabled for Roomote Control functionality - Task Sync controls whether task content is sent to cloud - Roomote Control controls whether cloud can send instructions back - Roomote Control now depends on Task Sync being enabled - Usage metrics (tokens, cost) always reported regardless of settings - Update UI with two separate toggles and clear descriptions - Add info text explaining usage metrics are always reported * feat: add missing translations for Task Sync and Roomote Control settings - Added taskSync, taskSyncDescription, remoteControlRequiresTaskSync, and usageMetricsAlwaysReported keys to all non-English cloud.json files - Updated cloudBenefit keys to match English structure - Ensured all languages have consistent translation keys for the new Task Sync and Roomote Control features * Cloud: cleanup taskSyncEnabled additions * fix: correct indentation localization files --------- Co-authored-by: Roo Code <[email protected]>
1 parent 4355b40 commit fc46cf2

38 files changed

+786
-283
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,11 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
248248
return this.settingsService!.updateUserSettings(settings)
249249
}
250250

251+
public isTaskSyncEnabled(): boolean {
252+
this.ensureInitialized()
253+
return this.settingsService!.isTaskSyncEnabled()
254+
}
255+
251256
// TelemetryClient
252257

253258
public captureEvent(event: TelemetryEvent): void {

packages/cloud/src/CloudSettingsService.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,21 @@ export class CloudSettingsService extends EventEmitter<SettingsServiceEvents> im
266266
}
267267
}
268268

269+
public isTaskSyncEnabled(): boolean {
270+
// Org settings take precedence
271+
if (this.authService.getStoredOrganizationId()) {
272+
return this.settings?.cloudSettings?.recordTaskMessages ?? false
273+
}
274+
275+
// User settings default to true if unspecified
276+
const userSettings = this.userSettings
277+
if (userSettings) {
278+
return userSettings.settings.taskSyncEnabled ?? true
279+
}
280+
281+
return false
282+
}
283+
269284
private async removeSettings(): Promise<void> {
270285
this.settings = undefined
271286
this.userSettings = undefined

packages/cloud/src/StaticSettingsService.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export class StaticSettingsService implements SettingsService {
5151
},
5252
settings: {
5353
extensionBridgeEnabled: true,
54+
taskSyncEnabled: true,
5455
},
5556
version: 1,
5657
}
@@ -65,13 +66,19 @@ export class StaticSettingsService implements SettingsService {
6566
public getUserSettingsConfig(): UserSettingsConfig {
6667
return {
6768
extensionBridgeEnabled: true,
69+
taskSyncEnabled: true,
6870
}
6971
}
7072

7173
public async updateUserSettings(_settings: Partial<UserSettingsConfig>): Promise<boolean> {
7274
throw new Error("User settings updates are not supported in static mode")
7375
}
7476

77+
public isTaskSyncEnabled(): boolean {
78+
// Static settings always enable task sync
79+
return true
80+
}
81+
7582
public dispose(): void {
7683
// No resources to clean up for static settings.
7784
}

packages/cloud/src/TelemetryClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,9 +233,9 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
233233
return false
234234
}
235235

236-
// Only record message telemetry if a cloud account is present and explicitly configured to record messages
236+
// Only record message telemetry if task sync is enabled
237237
if (eventName === TelemetryEventName.TASK_MESSAGE) {
238-
return this.settingsService.getSettings()?.cloudSettings?.recordTaskMessages || false
238+
return this.settingsService.isTaskSyncEnabled()
239239
}
240240

241241
// Other telemetry types are capturable at this point

packages/cloud/src/__tests__/CloudService.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ describe("CloudService", () => {
5959
initialize: ReturnType<typeof vi.fn>
6060
getSettings: ReturnType<typeof vi.fn>
6161
getAllowList: ReturnType<typeof vi.fn>
62+
isTaskSyncEnabled: ReturnType<typeof vi.fn>
6263
dispose: ReturnType<typeof vi.fn>
6364
on: ReturnType<typeof vi.fn>
6465
off: ReturnType<typeof vi.fn>
@@ -130,6 +131,7 @@ describe("CloudService", () => {
130131
initialize: vi.fn(),
131132
getSettings: vi.fn(),
132133
getAllowList: vi.fn(),
134+
isTaskSyncEnabled: vi.fn().mockReturnValue(true),
133135
dispose: vi.fn(),
134136
on: vi.fn(),
135137
off: vi.fn(),
@@ -343,6 +345,12 @@ describe("CloudService", () => {
343345
cloudService.getAllowList()
344346
expect(mockSettingsService.getAllowList).toHaveBeenCalled()
345347
})
348+
349+
it("should delegate isTaskSyncEnabled to SettingsService", () => {
350+
const result = cloudService.isTaskSyncEnabled()
351+
expect(mockSettingsService.isTaskSyncEnabled).toHaveBeenCalled()
352+
expect(result).toBe(true)
353+
})
346354
})
347355

348356
describe("error handling", () => {

packages/cloud/src/__tests__/CloudSettingsService.test.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe("CloudSettingsService", () => {
2020
getSessionToken: ReturnType<typeof vi.fn>
2121
hasActiveSession: ReturnType<typeof vi.fn>
2222
on: ReturnType<typeof vi.fn>
23+
getStoredOrganizationId: ReturnType<typeof vi.fn>
2324
}
2425
let mockRefreshTimer: {
2526
start: ReturnType<typeof vi.fn>
@@ -63,6 +64,7 @@ describe("CloudSettingsService", () => {
6364
getSessionToken: vi.fn(),
6465
hasActiveSession: vi.fn().mockReturnValue(false),
6566
on: vi.fn(),
67+
getStoredOrganizationId: vi.fn().mockReturnValue(null),
6668
}
6769

6870
mockRefreshTimer = {
@@ -532,4 +534,191 @@ describe("CloudSettingsService", () => {
532534
expect(mockContext.globalState.update).toHaveBeenCalledWith("user-settings", undefined)
533535
})
534536
})
537+
538+
describe("isTaskSyncEnabled", () => {
539+
beforeEach(async () => {
540+
await cloudSettingsService.initialize()
541+
})
542+
543+
it("should return true when org recordTaskMessages is true", () => {
544+
// Set up mock settings with org recordTaskMessages = true
545+
const mockSettings = {
546+
version: 1,
547+
cloudSettings: {
548+
recordTaskMessages: true,
549+
},
550+
defaultSettings: {},
551+
allowList: { allowAll: true, providers: {} },
552+
}
553+
554+
// Mock that user has organization ID (indicating org settings should be used)
555+
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
556+
557+
// Use reflection to set private settings
558+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
559+
560+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
561+
})
562+
563+
it("should return false when org recordTaskMessages is false", () => {
564+
// Set up mock settings with org recordTaskMessages = false
565+
const mockSettings = {
566+
version: 1,
567+
cloudSettings: {
568+
recordTaskMessages: false,
569+
},
570+
defaultSettings: {},
571+
allowList: { allowAll: true, providers: {} },
572+
}
573+
574+
// Mock that user has organization ID (indicating org settings should be used)
575+
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
576+
577+
// Use reflection to set private settings
578+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
579+
580+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
581+
})
582+
583+
it("should fall back to user taskSyncEnabled when org recordTaskMessages is undefined", () => {
584+
// Set up mock settings with org recordTaskMessages undefined
585+
const mockSettings = {
586+
version: 1,
587+
cloudSettings: {},
588+
defaultSettings: {},
589+
allowList: { allowAll: true, providers: {} },
590+
}
591+
592+
const mockUserSettings = {
593+
version: 1,
594+
features: {},
595+
settings: {
596+
taskSyncEnabled: true,
597+
},
598+
}
599+
600+
// Mock that user has no organization ID (indicating user settings should be used)
601+
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
602+
603+
// Use reflection to set private settings
604+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
605+
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
606+
mockUserSettings
607+
608+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
609+
})
610+
611+
it("should return false when user taskSyncEnabled is false", () => {
612+
// Set up mock settings with org recordTaskMessages undefined
613+
const mockSettings = {
614+
version: 1,
615+
cloudSettings: {},
616+
defaultSettings: {},
617+
allowList: { allowAll: true, providers: {} },
618+
}
619+
620+
const mockUserSettings = {
621+
version: 1,
622+
features: {},
623+
settings: {
624+
taskSyncEnabled: false,
625+
},
626+
}
627+
628+
// Mock that user has no organization ID (indicating user settings should be used)
629+
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
630+
631+
// Use reflection to set private settings
632+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
633+
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
634+
mockUserSettings
635+
636+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
637+
})
638+
639+
it("should return true when user taskSyncEnabled is undefined (default)", () => {
640+
// Set up mock settings with org recordTaskMessages undefined
641+
const mockSettings = {
642+
version: 1,
643+
cloudSettings: {},
644+
defaultSettings: {},
645+
allowList: { allowAll: true, providers: {} },
646+
}
647+
648+
const mockUserSettings = {
649+
version: 1,
650+
features: {},
651+
settings: {},
652+
}
653+
654+
// Mock that user has no organization ID (indicating user settings should be used)
655+
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
656+
657+
// Use reflection to set private settings
658+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
659+
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
660+
mockUserSettings
661+
662+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(true)
663+
})
664+
665+
it("should return false when no settings are available", () => {
666+
// Mock that user has no organization ID
667+
mockAuthService.getStoredOrganizationId.mockReturnValue(null)
668+
669+
// Clear both settings
670+
;(cloudSettingsService as unknown as { settings: undefined }).settings = undefined
671+
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
672+
673+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
674+
})
675+
676+
it("should return false when only org settings are available but cloudSettings is undefined", () => {
677+
const mockSettings = {
678+
version: 1,
679+
defaultSettings: {},
680+
allowList: { allowAll: true, providers: {} },
681+
}
682+
683+
// Mock that user has organization ID (indicating org settings should be used)
684+
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
685+
686+
// Use reflection to set private settings
687+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
688+
;(cloudSettingsService as unknown as { userSettings: undefined }).userSettings = undefined
689+
690+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
691+
})
692+
693+
it("should prioritize org settings over user settings", () => {
694+
// Set up conflicting settings: org = false, user = true
695+
const mockSettings = {
696+
version: 1,
697+
cloudSettings: {
698+
recordTaskMessages: false,
699+
},
700+
defaultSettings: {},
701+
allowList: { allowAll: true, providers: {} },
702+
}
703+
704+
const mockUserSettings = {
705+
version: 1,
706+
features: {},
707+
settings: {
708+
taskSyncEnabled: true,
709+
},
710+
}
711+
712+
// Mock that user has organization ID (indicating org settings should be used)
713+
mockAuthService.getStoredOrganizationId.mockReturnValue("org-123")
714+
715+
// Use reflection to set private settings
716+
;(cloudSettingsService as unknown as { settings: typeof mockSettings }).settings = mockSettings
717+
;(cloudSettingsService as unknown as { userSettings: typeof mockUserSettings }).userSettings =
718+
mockUserSettings
719+
720+
// Should return false (org setting takes precedence)
721+
expect(cloudSettingsService.isTaskSyncEnabled()).toBe(false)
722+
})
723+
})
535724
})

packages/cloud/src/__tests__/StaticSettingsService.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,5 +98,28 @@ describe("StaticSettingsService", () => {
9898

9999
expect(mockLog).not.toHaveBeenCalled()
100100
})
101+
102+
describe("isTaskSyncEnabled", () => {
103+
it("should always return true", () => {
104+
const service = new StaticSettingsService(validBase64)
105+
expect(service.isTaskSyncEnabled()).toBe(true)
106+
})
107+
108+
it("should return true regardless of settings content", () => {
109+
// Create settings with different content
110+
const differentSettings = {
111+
version: 2,
112+
cloudSettings: {
113+
recordTaskMessages: false,
114+
},
115+
defaultSettings: {},
116+
allowList: { allowAll: false, providers: {} },
117+
}
118+
const differentBase64 = Buffer.from(JSON.stringify(differentSettings)).toString("base64")
119+
120+
const service = new StaticSettingsService(differentBase64)
121+
expect(service.isTaskSyncEnabled()).toBe(true)
122+
})
123+
})
101124
})
102125
})

0 commit comments

Comments
 (0)