Repository: RooVetGit/Roo-Code
Pull request #2251: Add profile management functions to API
Author: login: gtaylor
Labels: name: Enhancement
description: New feature or request
name: size:L
description: This PR changes 100-499 lines, ignoring generated files.
name: lgtm
description: This PR has been approved by a maintainer
Created: 2025-04-03T05:25:26Z | Merged: 2025-04-03T05:40:07Z

## Context

Continuing the work outlined in the [How to configure Roo from outside of Roo?](https://discord.com/channels/1332146336664915968/1353570725432397926) Discord thread, this PR adds several profile management functions to the Roo extension's exported API: 

* `createProfile(name: string)`
* `getProfiles(): string[]`
* `setActiveProfile(name: string): Promise<void>`
* `getActiveProfile(): string | undefined`
* `deleteProfile(name: string): Promise<void>`

I've tested all of these functions against our WIP Roo Configurator extension.

## Implementation

Most of the changes of substance are in api.ts and build on the recent work to improve the configuration APIs.

I'm using names instead of profile IDs since the settings JSON seems to use them for specifying current active profile.

## How to Test

Create a custom extension that calls these APIs and do something like this:

```
npm run build && code --uninstall-extension rooveterinaryinc.roo-cline && code --install-extension bin/roo-cline-3.11.4.vsix
```

<!-- ELLIPSIS_HIDDEN -->

----

> [!IMPORTANT]
> Adds profile management functions to the API, enabling creation, retrieval, activation, and deletion of profiles, with updates to `RooCodeAPI` interface and `generateId` method.
> 
>   - **API Enhancements**:
>     - Adds profile management functions to `API` class in `api.ts`: `createProfile`, `getProfiles`, `setActiveProfile`, `getActiveProfile`, `deleteProfile`.
>     - Functions handle profile creation, retrieval, activation, and deletion by name.
>   - **Interface Updates**:
>     - Updates `RooCodeAPI` interface in `interface.ts` and `roo-code.d.ts` to include new profile management functions.
>   - **Miscellaneous**:
>     - Changes `generateId` method in `ProviderSettingsManager.ts` to public to support profile ID generation.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis" src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=RooVetGit%2FRoo-Code&utm_source=github&utm_medium=referral)<sup> for d2997399d2a063735cf4fe74873399fea75d47b2. It will automatically update as commits are pushed.</sup>

<!-- ELLIPSIS_HIDDEN -->


Diff:
diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts
index 9729219a2de..a6153e636c0 100644
--- a/src/core/config/ProviderSettingsManager.ts
+++ b/src/core/config/ProviderSettingsManager.ts
@@ -40,7 +40,7 @@ export class ProviderSettingsManager {
 		this.initialize().catch(console.error)
 	}
 
-	private generateId() {
+	public generateId() {
 		return Math.random().toString(36).substring(2, 15)
 	}
 
diff --git a/src/exports/api.ts b/src/exports/api.ts
index 609eda3b6d4..b1061cb56ae 100644
--- a/src/exports/api.ts
+++ b/src/exports/api.ts
@@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
+		}
+
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		if (profiles.some((profile) => profile.name === name)) {
+			throw new Error(`A profile with the name "${name}" already exists`)
+		}
+
+		// Generate unique ID and create profile
+		const id = this.sidebarProvider.providerSettingsManager.generateId()
+		const newProfile = {
+			id,
+			name: name.trim(),
+			apiProvider: "openai" as const, // Type assertion for better type safety
+		}
+
+		// Update configuration with new profile
+		await this.setConfiguration({
+			...currentSettings,
+			listApiConfigMeta: [...profiles, newProfile],
+		})
+		return id
+	}
+
+	public getProfiles(): string[] {
+		const profiles = this.getConfiguration().listApiConfigMeta || []
+		return profiles.map((profile) => profile.name)
+	}
+
+	public async setActiveProfile(name: string): Promise<void> {
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		const profile = profiles.find((p) => p.name === name)
+		if (!profile) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		await this.setConfiguration({
+			...currentSettings,
+			currentApiConfigName: profile.name,
+		})
+	}
+
+	public getActiveProfile(): string | undefined {
+		return this.getConfiguration().currentApiConfigName
+	}
+
+	public async deleteProfile(name: string): Promise<void> {
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+		const targetIndex = profiles.findIndex((p) => p.name === name)
+		if (targetIndex === -1) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		const profileToDelete = profiles[targetIndex]
+		profiles.splice(targetIndex, 1)
+
+		// If we're deleting the active profile, clear the currentApiConfigName
+		const newSettings: RooCodeSettings = {
+			...currentSettings,
+			listApiConfigMeta: profiles,
+			currentApiConfigName:
+				currentSettings.currentApiConfigName === profileToDelete.name
+					? undefined
+					: currentSettings.currentApiConfigName,
+		}
+		await this.setConfiguration(newSettings)
+	}
+
 	public isReady() {
 		return this.sidebarProvider.viewLaunched
 	}
diff --git a/src/exports/interface.ts b/src/exports/interface.ts
index 6e0eb8adec4..f30cc3a09e9 100644
--- a/src/exports/interface.ts
+++ b/src/exports/interface.ts
@@ -72,6 +72,39 @@ export interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 */
 	setConfiguration(values: RooCodeSettings): Promise<void>
 
+	/**
+	 * Creates a new API configuration profile
+	 * @param name The name of the profile
+	 * @returns The ID of the created profile
+	 */
+	createProfile(name: string): Promise<string>
+
+	/**
+	 * Returns a list of all configured profile names
+	 * @returns Array of profile names
+	 */
+	getProfiles(): string[]
+
+	/**
+	 * Changes the active API configuration profile
+	 * @param name The name of the profile to activate
+	 * @throws Error if the profile does not exist
+	 */
+	setActiveProfile(name: string): Promise<void>
+
+	/**
+	 * Returns the name of the currently active profile
+	 * @returns The profile name, or undefined if no profile is active
+	 */
+	getActiveProfile(): string | undefined
+
+	/**
+	 * Deletes a profile by name
+	 * @param name The name of the profile to delete
+	 * @throws Error if the profile does not exist
+	 */
+	deleteProfile(name: string): Promise<void>
+
 	/**
 	 * Returns true if the API is ready to use.
 	 */
diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts
index c75db2ab2fc..238dff137c1 100644
--- a/src/exports/roo-code.d.ts
+++ b/src/exports/roo-code.d.ts
@@ -572,6 +572,34 @@ interface RooCodeAPI extends EventEmitter<RooCodeEvents> {
 	 * @param values An object containing key-value pairs to set.
 	 */
 	setConfiguration(values: RooCodeSettings): Promise<void>
+	/**
+	 * Creates a new API configuration profile
+	 * @param name The name of the profile
+	 * @returns The ID of the created profile
+	 */
+	createProfile(name: string): Promise<string>
+	/**
+	 * Returns a list of all configured profile names
+	 * @returns Array of profile names
+	 */
+	getProfiles(): string[]
+	/**
+	 * Changes the active API configuration profile
+	 * @param name The name of the profile to activate
+	 * @throws Error if the profile does not exist
+	 */
+	setActiveProfile(name: string): Promise<void>
+	/**
+	 * Returns the name of the currently active profile
+	 * @returns The profile name, or undefined if no profile is active
+	 */
+	getActiveProfile(): string | undefined
+	/**
+	 * Deletes a profile by name
+	 * @param name The name of the profile to delete
+	 * @throws Error if the profile does not exist
+	 */
+	deleteProfile(name: string): Promise<void>
 	/**
 	 * Returns true if the API is ready to use.
 	 */


Reviews:
user: login: ellipsis-dev[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T05:27:36Z
user: login: mrubens
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T05:30:51Z
user: login: mrubens
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T05:32:10Z
user: login: mrubens
body: Looks good to me. What do you think @cte?
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T05:32:51Z
user: login: cte
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-03T05:36:34Z
user: login: cte
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-03T05:38:17Z

Review comments:
diff_hunk: @@ -40,7 +40,7 @@ export class ProviderSettingsManager {
 		this.initialize().catch(console.error)
 	}
 
-	private generateId() {
+	public generateId() {
path: src/core/config/ProviderSettingsManager.ts
user: login: ellipsis-dev[bot]
body: Exposing `generateId()` as public may allow external misuse. Consider documenting its intended use and, if IDs are security-sensitive, using a more robust method (e.g., `crypto.randomUUID()`).
created_at: 2025-04-03T05:27:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
path: src/exports/api.ts
user: login: ellipsis-dev[bot]
body: Error messages in `createProfile` are user-facing. Consider internationalizing these strings (e.g., using `i18next`) to adhere to our standards.
```suggestion
			throw new Error(i18next.t('error.profileNameEmpty'))
```
created_at: 2025-04-03T05:27:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
path: src/exports/api.ts
user: login: ellipsis-dev[bot]
body: When checking for existing profiles, an error is thrown using a literal string. Please internationalize this message as well.
```suggestion
			throw new Error(i18n.t('error.profileNameEmpty'))
```
created_at: 2025-04-03T05:27:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
+		}
+
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		if (profiles.some((profile) => profile.name === name)) {
+			throw new Error(`A profile with the name "${name}" already exists`)
+		}
+
+		// Generate unique ID and create profile
+		const id = this.sidebarProvider.providerSettingsManager.generateId()
+		const newProfile = {
+			id,
+			name: name.trim(),
+			apiProvider: "openai" as const, // Type assertion for better type safety
+		}
+
+		// Update configuration with new profile
+		await this.setConfiguration({
+			...currentSettings,
+			listApiConfigMeta: [...profiles, newProfile],
+		})
+		return id
+	}
+
+	public getProfiles(): string[] {
+		const profiles = this.getConfiguration().listApiConfigMeta || []
+		return profiles.map((profile) => profile.name)
+	}
+
+	public async setActiveProfile(name: string): Promise<void> {
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		const profile = profiles.find((p) => p.name === name)
+		if (!profile) {
+			throw new Error(`Profile with name "${name}" does not exist`)
path: src/exports/api.ts
user: login: ellipsis-dev[bot]
body: In `setActiveProfile`, the error message is hard-coded. Consider using internationalized strings for consistency with our development standards.
created_at: 2025-04-03T05:27:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
+		}
+
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		if (profiles.some((profile) => profile.name === name)) {
+			throw new Error(`A profile with the name "${name}" already exists`)
+		}
+
+		// Generate unique ID and create profile
+		const id = this.sidebarProvider.providerSettingsManager.generateId()
+		const newProfile = {
+			id,
+			name: name.trim(),
+			apiProvider: "openai" as const, // Type assertion for better type safety
+		}
+
+		// Update configuration with new profile
+		await this.setConfiguration({
+			...currentSettings,
+			listApiConfigMeta: [...profiles, newProfile],
+		})
+		return id
+	}
+
+	public getProfiles(): string[] {
+		const profiles = this.getConfiguration().listApiConfigMeta || []
+		return profiles.map((profile) => profile.name)
+	}
+
+	public async setActiveProfile(name: string): Promise<void> {
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		const profile = profiles.find((p) => p.name === name)
+		if (!profile) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		await this.setConfiguration({
+			...currentSettings,
+			currentApiConfigName: profile.name,
+		})
+	}
+
+	public getActiveProfile(): string | undefined {
+		return this.getConfiguration().currentApiConfigName
+	}
+
+	public async deleteProfile(name: string): Promise<void> {
path: src/exports/api.ts
user: login: ellipsis-dev[bot]
body: The `deleteProfile` method throws user-facing errors and resets the active profile without fallback. Consider internationalizing the error and, if appropriate, selecting a different active profile rather than setting it to undefined.
created_at: 2025-04-03T05:27:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
path: src/exports/api.ts
user: login: mrubens
body: Not sure we need to worry about this for the API
created_at: 2025-04-03T05:30:51Z
author_association: COLLABORATOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
+		}
+
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		if (profiles.some((profile) => profile.name === name)) {
+			throw new Error(`A profile with the name "${name}" already exists`)
+		}
+
+		// Generate unique ID and create profile
+		const id = this.sidebarProvider.providerSettingsManager.generateId()
+		const newProfile = {
+			id,
+			name: name.trim(),
+			apiProvider: "openai" as const, // Type assertion for better type safety
+		}
+
+		// Update configuration with new profile
+		await this.setConfiguration({
+			...currentSettings,
+			listApiConfigMeta: [...profiles, newProfile],
+		})
+		return id
+	}
+
+	public getProfiles(): string[] {
+		const profiles = this.getConfiguration().listApiConfigMeta || []
+		return profiles.map((profile) => profile.name)
+	}
+
+	public async setActiveProfile(name: string): Promise<void> {
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		const profile = profiles.find((p) => p.name === name)
+		if (!profile) {
+			throw new Error(`Profile with name "${name}" does not exist`)
+		}
+
+		await this.setConfiguration({
+			...currentSettings,
+			currentApiConfigName: profile.name,
+		})
+	}
+
+	public getActiveProfile(): string | undefined {
+		return this.getConfiguration().currentApiConfigName
+	}
+
+	public async deleteProfile(name: string): Promise<void> {
path: src/exports/api.ts
user: login: mrubens
body: In the API case is it better to have no active profile after deleting than to switch to another one like we do in the extension?
created_at: 2025-04-03T05:32:10Z
author_association: COLLABORATOR
diff_hunk: @@ -163,6 +163,82 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
 		await this.sidebarProvider.postStateToWebview()
 	}
 
+	public async createProfile(name: string): Promise<string> {
+		// Input validation
+		if (!name || !name.trim()) {
+			throw new Error("Profile name cannot be empty")
+		}
+
+		const currentSettings = this.getConfiguration()
+		const profiles = currentSettings.listApiConfigMeta || []
+
+		if (profiles.some((profile) => profile.name === name)) {
path: src/exports/api.ts
user: login: cte
body: Not a huge deal, but we might want to improve / expand the `ProviderSettingsManager` interface and just proxy these calls through to that object. It already has a few things you could use like fetching a profile by name or id. In general the current implementation seems fine though.
created_at: 2025-04-03T05:36:34Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-03T05:25:29Z
body: ###  ⚠️  No Changeset found

Latest commit: d2997399d2a063735cf4fe74873399fea75d47b2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/gtaylor/Roo-Code/new/profile-mgmt?filename=.changeset/ninety-schools-exercise.md&value=---%0A%22roo-cline%22%3A%20patch%0A---%0A%0AAdd%20profile%20management%20functions%20to%20API%0A)


author_association: NONE
user: login: cte
created_at: 2025-04-03T05:38:04Z
body: > Looks good to me. What do you think @cte?

Left one comment about pushing more of the logic into `ProviderSettingsManager`, but we can clean that up later.
author_association: COLLABORATOR