Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
a402f67
chore: merge jbbrown/marketplace squashed
NamesMT May 21, 2025
35c438d
fix: compatibility with latest version
NamesMT May 21, 2025
4c41721
chore: apply org change and re-add contributors
NamesMT May 21, 2025
eecb53b
refactor(marketplace): some UI adjustments (#13)
NamesMT May 22, 2025
779e14a
chore: marketplace rebase to latest fixes
NamesMT May 22, 2025
a96e9b3
fix: use `yaml` instead of `js-yaml`, fix `config-rocket` missing, so…
NamesMT May 22, 2025
7a39087
feat(marketplace): `yaml` migrate for modes, + uninstall UX (remove e…
NamesMT May 22, 2025
a96d65c
fix(marketplace): IMM broken, can't install non-rocket binary
NamesMT May 22, 2025
08399de
chore(marketplace): invoke to reload modes and mcps
NamesMT May 22, 2025
2f7869f
perf(McpHub/reloadMcpServers): ensure no multiple reloads are running…
NamesMT May 22, 2025
45b0e26
feat(i18n): add missing marketplace translations for all languages
Smartsheet-JB-Brown May 23, 2025
7e41d6f
fix knip issue
Smartsheet-JB-Brown May 23, 2025
5433e0a
test: Add mock for kontroll module
Smartsheet-JB-Brown May 23, 2025
1a60034
test: Add mock for execa module
Smartsheet-JB-Brown May 23, 2025
a53b409
fix: Add semver format validation for version field
Smartsheet-JB-Brown May 23, 2025
e17c71c
for now comment out failing marketplace tests after install and UI ch…
Smartsheet-JB-Brown May 24, 2025
55cecbf
For now skip marketplace UI tests that are failing after UI changes w…
Smartsheet-JB-Brown May 24, 2025
2284d7e
chore(marketplace): finishing touches (#14)
elianiva May 24, 2025
2d51d5e
style(marketplace): refactor filter match ui, more accessible install…
elianiva May 25, 2025
47fd3ae
chore: uniform to `roo-rocket` only, remove `config-rocket`
NamesMT May 25, 2025
2cf1218
chore: solve some lint and types errors
NamesMT May 25, 2025
49f7391
chore: remove `kontroll` use existing `lodash.debounce` from `main` head
NamesMT May 25, 2025
7e8ff7e
test(marketplace): resolve some tests
NamesMT May 26, 2025
a688883
Merge main and update marketplace
mrubens Jun 11, 2025
212150a
Merge remote-tracking branch 'origin/main' into marketplace_review
mrubens Jun 11, 2025
967bf34
Ellipsis feedback
mrubens Jun 11, 2025
0545960
Mock onDidChange
mrubens Jun 11, 2025
ff67a33
Update comment
mrubens Jun 11, 2025
7315d7a
Hack to make tests pass, like McpHub
mrubens Jun 11, 2025
d1c0e7f
Make tests pass on Windows
mrubens Jun 11, 2025
4d8c0af
Remove handleCardClick
mrubens Jun 11, 2025
2747403
Add telemetry
mrubens Jun 11, 2025
6782faa
Merge remote-tracking branch 'origin/main' into marketplace_review
mrubens Jun 11, 2025
d1f7c9e
Update packages/telemetry/src/TelemetryService.ts
mrubens Jun 11, 2025
597722f
Fix translation
mrubens Jun 11, 2025
9bfcabb
Better error handling when installing marketplace items
mrubens Jun 11, 2025
9d76f4c
Move power steering to the bottom of experimental settings
mrubens Jun 11, 2025
b83d302
Tweaks to experimental settings copy
mrubens Jun 11, 2025
583cb03
Support prereqs
mrubens Jun 11, 2025
a9d8662
Refresh webview state after deleting MCP
mrubens Jun 11, 2025
f39b520
Merge remote-tracking branch 'origin/main' into marketplace_review
mrubens Jun 11, 2025
e5bda9f
PR feedback
mrubens Jun 11, 2025
c4392c1
Test cleanup
mrubens Jun 11, 2025
8f5a474
More PR feedback
mrubens Jun 11, 2025
42bd37f
Only load marketplace data when in experiment
mrubens Jun 11, 2025
fd9850f
Cleanup
mrubens Jun 11, 2025
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
3 changes: 2 additions & 1 deletion PRIVACY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Roo Code Privacy Policy

**Last Updated: March 7th, 2025**
**Last Updated: June 10th, 2025**

Roo Code respects your privacy and is committed to transparency about how we handle your data. Below is a simple breakdown of where key pieces of data go—and, importantly, where they don’t.

Expand All @@ -11,6 +11,7 @@ Roo Code respects your privacy and is committed to transparency about how we han
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service.
- **API Keys & Credentials**: If you enter an API key (e.g., to connect an AI model), it is stored locally on your device and never sent to us or any third party, except the provider you have chosen.
- **Telemetry (Usage Data)**: We only collect feature usage and error data if you explicitly opt-in. This telemetry is powered by PostHog and helps us understand feature usage to improve Roo Code. This includes your VS Code machine ID and feature usage patterns and exception reports. We do **not** collect personally identifiable information, your code, or AI prompts.
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Code’s backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.

### **How We Use Your Data (If Collected)**

Expand Down
1 change: 1 addition & 0 deletions packages/cloud/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from "./CloudService"
export * from "./Config"
41 changes: 41 additions & 0 deletions packages/telemetry/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,47 @@ export class TelemetryService {
this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId })
}

/**
* Captures a marketplace item installation event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The installation target (project or global)
* @param properties Additional properties like hasParameters, installationMethod
*/
public captureMarketplaceItemInstalled(
itemId: string,
itemType: string,
itemName: string,
target: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
properties?: Record<string, any>,
): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_INSTALLED, {
itemId,
itemType,
itemName,
target,
... (properties || {}),
})
}

/**
* Captures a marketplace item removal event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The removal target (project or global)
*/
public captureMarketplaceItemRemoved(itemId: string, itemType: string, itemName: string, target: string): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_REMOVED, {
itemId,
itemType,
itemName,
target,
})
}

/**
* Captures a title button click event
* @param button The button that was clicked
Expand Down
8 changes: 7 additions & 1 deletion packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId
*/

export const experimentIds = ["powerSteering", "concurrentFileReads", "disableCompletionCommand"] as const
export const experimentIds = [
"powerSteering",
"marketplace",
"concurrentFileReads",
"disableCompletionCommand",
] as const

export const experimentIdsSchema = z.enum(experimentIds)

Expand All @@ -18,6 +23,7 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>

export const experimentsSchema = z.object({
powerSteering: z.boolean(),
marketplace: z.boolean(),
concurrentFileReads: z.boolean(),
disableCompletionCommand: z.boolean(),
})
Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export enum TelemetryEventName {

AUTHENTICATION_INITIATED = "Authentication Initiated",

MARKETPLACE_ITEM_INSTALLED = "Marketplace Item Installed",
MARKETPLACE_ITEM_REMOVED = "Marketplace Item Removed",

SCHEMA_VALIDATION_ERROR = "Schema Validation Error",
DIFF_APPLICATION_ERROR = "Diff Application Error",
SHELL_INTEGRATION_ERROR = "Shell Integration Error",
Expand Down Expand Up @@ -106,6 +109,8 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [
TelemetryEventName.PROMPT_ENHANCED,
TelemetryEventName.TITLE_BUTTON_CLICKED,
TelemetryEventName.AUTHENTICATION_INITIATED,
TelemetryEventName.MARKETPLACE_ITEM_INSTALLED,
TelemetryEventName.MARKETPLACE_ITEM_REMOVED,
TelemetryEventName.SCHEMA_VALIDATION_ERROR,
TelemetryEventName.DIFF_APPLICATION_ERROR,
TelemetryEventName.SHELL_INTEGRATION_ERROR,
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const commandIds = [
"promptsButtonClicked",
"mcpButtonClicked",
"historyButtonClicked",
"marketplaceButtonClicked",
"popoutButtonClicked",
"accountButtonClicked",
"settingsButtonClicked",
Expand Down
1 change: 1 addition & 0 deletions src/__mocks__/vscode.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const vscode = {
onDidSaveTextDocument: jest.fn(),
createFileSystemWatcher: jest.fn().mockReturnValue({
onDidCreate: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidChange: jest.fn().mockReturnValue({ dispose: jest.fn() }),
onDidDelete: jest.fn().mockReturnValue({ dispose: jest.fn() }),
dispose: jest.fn(),
}),
Expand Down
5 changes: 5 additions & 0 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt

visibleProvider.postMessageToWebview({ type: "action", action: "historyButtonClicked" })
},
marketplaceButtonClicked: () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)
if (!visibleProvider) return
visibleProvider.postMessageToWebview({ type: "action", action: "marketplaceButtonClicked" })
},
showHumanRelayDialog: (params: { requestId: string; promptText: string }) => {
const panel = getPanel()

Expand Down
120 changes: 75 additions & 45 deletions src/core/config/CustomModesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
import { logger } from "../../utils/logging"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"

const ROOMODES_FILENAME = ".roomodes"

Expand All @@ -26,8 +27,9 @@ export class CustomModesManager {
private readonly context: vscode.ExtensionContext,
private readonly onUpdate: () => Promise<void>,
) {
// TODO: We really shouldn't have async methods in the constructor.
this.watchCustomModesFiles()
this.watchCustomModesFiles().catch((error) => {
console.error("[CustomModesManager] Failed to setup file watchers:", error)
})
}

private async queueWrite(operation: () => Promise<void>): Promise<void> {
Expand Down Expand Up @@ -117,7 +119,7 @@ export class CustomModesManager {
}

public async getCustomModesFilePath(): Promise<string> {
const settingsDir = await this.ensureSettingsDirectoryExists()
const settingsDir = await ensureSettingsDirectoryExists(this.context)
const filePath = path.join(settingsDir, GlobalFileNames.customModes)
const fileExists = await fileExistsAtPath(filePath)

Expand All @@ -129,64 +131,98 @@ export class CustomModesManager {
}

private async watchCustomModesFiles(): Promise<void> {
// Skip if test environment is detected
if (process.env.NODE_ENV === "test" || process.env.JEST_WORKER_ID !== undefined) {
return
}

const settingsPath = await this.getCustomModesFilePath()

// Watch settings file
this.disposables.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (arePathsEqual(document.uri.fsPath, settingsPath)) {
const content = await fs.readFile(settingsPath, "utf-8")
const settingsWatcher = vscode.workspace.createFileSystemWatcher(settingsPath)

const errorMessage =
"Invalid custom modes format. Please ensure your settings follow the correct YAML format."
const handleSettingsChange = async () => {
try {
// Ensure that the settings file exists (especially important for delete events)
await this.getCustomModesFilePath()
const content = await fs.readFile(settingsPath, "utf-8")

let config: any
const errorMessage =
"Invalid custom modes format. Please ensure your settings follow the correct YAML format."

try {
config = yaml.parse(content)
} catch (error) {
console.error(error)
vscode.window.showErrorMessage(errorMessage)
return
}
let config: any

const result = customModesSettingsSchema.safeParse(config)
try {
config = yaml.parse(content)
} catch (error) {
console.error(error)
vscode.window.showErrorMessage(errorMessage)
return
}

if (!result.success) {
vscode.window.showErrorMessage(errorMessage)
return
}
const result = customModesSettingsSchema.safeParse(config)

if (!result.success) {
vscode.window.showErrorMessage(errorMessage)
return
}

// Get modes from .roomodes if it exists (takes precedence)
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
// Get modes from .roomodes if it exists (takes precedence)
const roomodesPath = await this.getWorkspaceRoomodes()
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []

// Merge modes from both sources (.roomodes takes precedence)
const mergedModes = await this.mergeCustomModes(roomodesModes, result.data.customModes)
// Merge modes from both sources (.roomodes takes precedence)
const mergedModes = await this.mergeCustomModes(roomodesModes, result.data.customModes)
await this.context.globalState.update("customModes", mergedModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling settings file change:`, error)
}
}

this.disposables.push(settingsWatcher.onDidChange(handleSettingsChange))
this.disposables.push(settingsWatcher.onDidCreate(handleSettingsChange))
this.disposables.push(settingsWatcher.onDidDelete(handleSettingsChange))
this.disposables.push(settingsWatcher)

// Watch .roomodes file - watch the path even if it doesn't exist yet
const workspaceFolders = vscode.workspace.workspaceFolders
if (workspaceFolders && workspaceFolders.length > 0) {
const workspaceRoot = getWorkspacePath()
const roomodesPath = path.join(workspaceRoot, ROOMODES_FILENAME)
const roomodesWatcher = vscode.workspace.createFileSystemWatcher(roomodesPath)

const handleRoomodesChange = async () => {
try {
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = await this.loadModesFromFile(roomodesPath)
// .roomodes takes precedence
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling .roomodes file change:`, error)
}
}),
)

// Watch .roomodes file if it exists
const roomodesPath = await this.getWorkspaceRoomodes()
}

if (roomodesPath) {
this.disposables.push(roomodesWatcher.onDidChange(handleRoomodesChange))
this.disposables.push(roomodesWatcher.onDidCreate(handleRoomodesChange))
this.disposables.push(
vscode.workspace.onDidSaveTextDocument(async (document) => {
if (arePathsEqual(document.uri.fsPath, roomodesPath)) {
roomodesWatcher.onDidDelete(async () => {
// When .roomodes is deleted, refresh with only settings modes
try {
const settingsModes = await this.loadModesFromFile(settingsPath)
const roomodesModes = await this.loadModesFromFile(roomodesPath)
// .roomodes takes precedence
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
await this.context.globalState.update("customModes", mergedModes)
await this.context.globalState.update("customModes", settingsModes)
this.clearCache()
await this.onUpdate()
} catch (error) {
console.error(`[CustomModesManager] Error handling .roomodes file deletion:`, error)
}
}),
)
this.disposables.push(roomodesWatcher)
}
}

Expand Down Expand Up @@ -362,12 +398,6 @@ export class CustomModesManager {
}
}

private async ensureSettingsDirectoryExists(): Promise<string> {
const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
await fs.mkdir(settingsDir, { recursive: true })
return settingsDir
}

public async resetCustomModes(): Promise<void> {
try {
const filePath = await this.getCustomModesFilePath()
Expand Down
Loading
Loading