Skip to content

Commit a444813

Browse files
ctejr
authored andcommitted
Improve provider profile management in the external API (#3386)
Co-authored-by: John Richmond <[email protected]>
1 parent 49da2b4 commit a444813

File tree

7 files changed

+254
-124
lines changed

7 files changed

+254
-124
lines changed

src/core/webview/ClineProvider.ts

Lines changed: 111 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import axios from "axios"
99
import pWaitFor from "p-wait-for"
1010
import * as vscode from "vscode"
1111

12-
import type { GlobalState, ProviderName, ProviderSettings, RooCodeSettings } from "../../schemas"
12+
import type { GlobalState, ProviderName, ProviderSettings, RooCodeSettings, ProviderSettingsEntry } from "../../schemas"
1313
import { t } from "../../i18n"
1414
import { setPanel } from "../../activate/registerCommands"
1515
import { requestyDefaultModelId, openRouterDefaultModelId, glamaDefaultModelId } from "../../shared/api"
@@ -246,7 +246,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
246246
return false
247247
}
248248

249-
// check if there is a cline instance in the stack (if this provider has an active task)
249+
// Check if there is a cline instance in the stack (if this provider has an active task)
250250
if (visibleProvider.getCurrentCline()) {
251251
return true
252252
}
@@ -795,39 +795,127 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
795795
await this.postStateToWebview()
796796
}
797797

798+
// Provider Profile Management
799+
800+
getProviderProfileEntries(): ProviderSettingsEntry[] {
801+
return this.contextProxy.getValues().listApiConfigMeta || []
802+
}
803+
804+
getProviderProfileEntry(name: string): ProviderSettingsEntry | undefined {
805+
return this.getProviderProfileEntries().find((profile) => profile.name === name)
806+
}
807+
808+
public hasProviderProfileEntry(name: string): boolean {
809+
return !!this.getProviderProfileEntry(name)
810+
}
811+
812+
async upsertProviderProfile(
813+
name: string,
814+
providerSettings: ProviderSettings,
815+
activate: boolean = true,
816+
): Promise<string | undefined> {
817+
try {
818+
// TODO: Do we need to be calling `activateProfile`? It's not
819+
// clear to me what the source of truth should be; in some cases
820+
// we rely on the `ContextProxy`'s data store and in other cases
821+
// we rely on the `ProviderSettingsManager`'s data store. It might
822+
// be simpler to unify these two.
823+
const id = await this.providerSettingsManager.saveConfig(name, providerSettings)
824+
825+
if (activate) {
826+
const { mode } = await this.getState()
827+
828+
// These promises do the following:
829+
// 1. Adds or updates the list of provider profiles.
830+
// 2. Sets the current provider profile.
831+
// 3. Sets the current mode's provider profile.
832+
// 4. Copies the provider settings to the context.
833+
//
834+
// Note: 1, 2, and 4 can be done in one `ContextProxy` call:
835+
// this.contextProxy.setValues({ ...providerSettings, listApiConfigMeta: ..., currentApiConfigName: ... })
836+
// We should probably switch to that and verify that it works.
837+
// I left the original implementation in just to be safe.
838+
await Promise.all([
839+
this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
840+
this.updateGlobalState("currentApiConfigName", name),
841+
this.providerSettingsManager.setModeConfig(mode, id),
842+
this.contextProxy.setProviderSettings(providerSettings),
843+
])
844+
845+
// Change the provider for the current task.
846+
// TODO: We should rename `buildApiHandler` for clarity (e.g. `getProviderClient`).
847+
const task = this.getCurrentCline()
848+
849+
if (task) {
850+
task.api = buildApiHandler(providerSettings)
851+
}
852+
} else {
853+
await this.updateGlobalState("listApiConfigMeta", await this.providerSettingsManager.listConfig())
854+
}
855+
856+
await this.postStateToWebview()
857+
return id
858+
} catch (error) {
859+
this.log(
860+
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
861+
)
862+
863+
vscode.window.showErrorMessage(t("common:errors.create_api_config"))
864+
return undefined
865+
}
866+
}
867+
868+
async deleteProviderProfile(profileToDelete: ProviderSettingsEntry) {
869+
const globalSettings = this.contextProxy.getValues()
870+
let profileToActivate: string | undefined = globalSettings.currentApiConfigName
871+
872+
if (profileToDelete.name === profileToActivate) {
873+
profileToActivate = this.getProviderProfileEntries().find(({ name }) => name !== profileToDelete.name)?.name
874+
}
875+
876+
if (!profileToActivate) {
877+
throw new Error("You cannot delete the last profile")
878+
}
879+
880+
const entries = this.getProviderProfileEntries().filter(({ name }) => name !== profileToDelete.name)
881+
882+
await this.contextProxy.setValues({
883+
...globalSettings,
884+
currentApiConfigName: profileToActivate,
885+
listApiConfigMeta: entries,
886+
})
887+
888+
await this.postStateToWebview()
889+
}
890+
798891
async activateProviderProfile(args: { name: string } | { id: string }) {
799-
const { name, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
892+
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
800893

894+
// See `upsertProviderProfile` for a description of what this is doing.
801895
await Promise.all([
802896
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
803897
this.contextProxy.setValue("currentApiConfigName", name),
898+
this.contextProxy.setProviderSettings(providerSettings),
804899
])
805900

806-
await this.updateApiConfiguration(providerSettings)
807-
await this.postStateToWebview()
808-
}
809-
810-
async updateApiConfiguration(providerSettings: ProviderSettings) {
811-
// Update mode's default config.
812901
const { mode } = await this.getState()
813902

814-
if (mode) {
815-
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
816-
const listApiConfig = await this.providerSettingsManager.listConfig()
817-
const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
818-
819-
if (config?.id) {
820-
await this.providerSettingsManager.setModeConfig(mode, config.id)
821-
}
903+
if (id) {
904+
await this.providerSettingsManager.setModeConfig(mode, id)
822905
}
823906

824-
await this.contextProxy.setProviderSettings(providerSettings)
907+
// Change the provider for the current task.
908+
const task = this.getCurrentCline()
825909

826-
if (this.getCurrentCline()) {
827-
this.getCurrentCline()!.api = buildApiHandler(providerSettings)
910+
if (task) {
911+
task.api = buildApiHandler(providerSettings)
828912
}
913+
914+
await this.postStateToWebview()
829915
}
830916

917+
// Task Management
918+
831919
async cancelTask() {
832920
const cline = this.getCurrentCline()
833921

@@ -943,7 +1031,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
9431031
openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
9441032
}
9451033

946-
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
1034+
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
9471035
}
9481036

9491037
// Glama
@@ -973,7 +1061,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
9731061
glamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
9741062
}
9751063

976-
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
1064+
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
9771065
}
9781066

9791067
// Requesty
@@ -988,29 +1076,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
9881076
requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
9891077
}
9901078

991-
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
992-
}
993-
994-
// Save configuration
995-
996-
async upsertApiConfiguration(configName: string, apiConfiguration: ProviderSettings) {
997-
try {
998-
await this.providerSettingsManager.saveConfig(configName, apiConfiguration)
999-
const listApiConfig = await this.providerSettingsManager.listConfig()
1000-
1001-
await Promise.all([
1002-
this.updateGlobalState("listApiConfigMeta", listApiConfig),
1003-
this.updateApiConfiguration(apiConfiguration),
1004-
this.updateGlobalState("currentApiConfigName", configName),
1005-
])
1006-
1007-
await this.postStateToWebview()
1008-
} catch (error) {
1009-
this.log(
1010-
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1011-
)
1012-
vscode.window.showErrorMessage(t("common:errors.create_api_config"))
1013-
}
1079+
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
10141080
}
10151081

10161082
// Task history

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -612,7 +612,7 @@ describe("ClineProvider", () => {
612612
expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
613613
})
614614

615-
test("saves current config when switching to mode without config", async () => {
615+
it("saves current config when switching to mode without config", async () => {
616616
await provider.resolveWebviewView(mockWebviewView)
617617
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
618618

@@ -633,15 +633,15 @@ describe("ClineProvider", () => {
633633
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
634634
})
635635

636-
test("saves config as default for current mode when loading config", async () => {
636+
it("saves config as default for current mode when loading config", async () => {
637637
await provider.resolveWebviewView(mockWebviewView)
638638
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
639639

640+
const profile: ProviderSettingsEntry = { apiProvider: "anthropic", id: "new-id", name: "new-config" }
641+
640642
;(provider as any).providerSettingsManager = {
641-
activateProfile: jest
642-
.fn()
643-
.mockResolvedValue({ config: { apiProvider: "anthropic", id: "new-id" }, name: "new-config" }),
644-
listConfig: jest.fn().mockResolvedValue([{ name: "new-config", id: "new-id", apiProvider: "anthropic" }]),
643+
activateProfile: jest.fn().mockResolvedValue(profile),
644+
listConfig: jest.fn().mockResolvedValue([profile]),
645645
setModeConfig: jest.fn(),
646646
getModeConfigId: jest.fn().mockResolvedValue(undefined),
647647
} as any
@@ -656,18 +656,19 @@ describe("ClineProvider", () => {
656656
expect(provider.providerSettingsManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
657657
})
658658

659-
test("load API configuration by ID works and updates mode config", async () => {
659+
it("load API configuration by ID works and updates mode config", async () => {
660660
await provider.resolveWebviewView(mockWebviewView)
661661
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
662662

663+
const profile: ProviderSettingsEntry = {
664+
name: "config-by-id",
665+
id: "config-id-123",
666+
apiProvider: "anthropic",
667+
}
668+
663669
;(provider as any).providerSettingsManager = {
664-
activateProfile: jest.fn().mockResolvedValue({
665-
config: { apiProvider: "anthropic", id: "config-id-123" },
666-
name: "config-by-id",
667-
}),
668-
listConfig: jest
669-
.fn()
670-
.mockResolvedValue([{ name: "config-by-id", id: "config-id-123", apiProvider: "anthropic" }]),
670+
activateProfile: jest.fn().mockResolvedValue(profile),
671+
listConfig: jest.fn().mockResolvedValue([profile]),
671672
setModeConfig: jest.fn(),
672673
getModeConfigId: jest.fn().mockResolvedValue(undefined),
673674
} as any
@@ -881,7 +882,7 @@ describe("ClineProvider", () => {
881882
})
882883
})
883884

884-
test("saves mode config when updating API configuration", async () => {
885+
it("saves mode config when updating API configuration", async () => {
885886
// Setup mock context with mode and config name
886887
mockContext = {
887888
...mockContext,
@@ -907,12 +908,14 @@ describe("ClineProvider", () => {
907908

908909
;(provider as any).providerSettingsManager = {
909910
listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
911+
saveConfig: jest.fn().mockResolvedValue("test-id"),
910912
setModeConfig: jest.fn(),
911913
} as any
912914

913915
// Update API configuration
914916
await messageHandler({
915-
type: "apiConfiguration",
917+
type: "upsertApiConfiguration",
918+
text: "test-config",
916919
apiConfiguration: { apiProvider: "anthropic" },
917920
})
918921

@@ -1675,14 +1678,11 @@ describe("ClineProvider", () => {
16751678
currentApiConfigName: "test-config",
16761679
} as any)
16771680

1678-
// Trigger updateApiConfiguration
1681+
// Trigger upsertApiConfiguration
16791682
await messageHandler({
16801683
type: "upsertApiConfiguration",
16811684
text: "test-config",
1682-
apiConfiguration: {
1683-
apiProvider: "anthropic",
1684-
apiKey: "test-key",
1685-
},
1685+
apiConfiguration: { apiProvider: "anthropic", apiKey: "test-key" },
16861686
})
16871687

16881688
// Verify error was logged and user was notified

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,6 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
126126
// task. This essentially creates a fresh slate for the new task.
127127
await provider.initClineWithTask(message.text, message.images)
128128
break
129-
case "apiConfiguration":
130-
if (message.apiConfiguration) {
131-
await provider.updateApiConfiguration(message.apiConfiguration)
132-
}
133-
await provider.postStateToWebview()
134-
break
135129
case "customInstructions":
136130
await provider.updateCustomInstructions(message.text)
137131
break
@@ -1068,7 +1062,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
10681062
break
10691063
case "upsertApiConfiguration":
10701064
if (message.text && message.apiConfiguration) {
1071-
await provider.upsertApiConfiguration(message.text, message.apiConfiguration)
1065+
await provider.upsertProviderProfile(message.text, message.apiConfiguration)
10721066
}
10731067
break
10741068
case "renameApiConfiguration":

0 commit comments

Comments
 (0)