Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export const globalSettingsSchema = z.object({
customSupportPrompts: customSupportPromptsSchema.optional(),
enhancementApiConfigId: z.string().optional(),
includeTaskHistoryInEnhance: z.boolean().optional(),
commitMessageApiConfigId: z.string().optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
/**
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode-extension-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ export type ExtensionState = Pick<
| "customModePrompts"
| "customSupportPrompts"
| "enhancementApiConfigId"
| "commitMessageApiConfigId"
| "customCondensingPrompt"
| "codebaseIndexConfig"
| "codebaseIndexModels"
Expand Down
3 changes: 3 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2461,6 +2461,7 @@ export class ClineProvider
customModePrompts,
customSupportPrompts,
enhancementApiConfigId,
commitMessageApiConfigId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we cover set and unset commitMessageApiConfigId values through both state-return paths? This would catch a future omission that makes the saved selector revert after a webview refresh.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

four tests covering set and unset across both getState() and getStateToPostToWebview(), in the ClineProvider spec next to the existing state coverage

autoApprovalEnabled,
customModes,
experiments,
Expand Down Expand Up @@ -2619,6 +2620,7 @@ export class ClineProvider
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId,
commitMessageApiConfigId,
autoApprovalEnabled: autoApprovalEnabled ?? false,
customModes,
experiments: experiments ?? experimentDefault,
Expand Down Expand Up @@ -2852,6 +2854,7 @@ export class ClineProvider
customModePrompts: stateValues.customModePrompts ?? {},
customSupportPrompts: stateValues.customSupportPrompts ?? {},
enhancementApiConfigId: stateValues.enhancementApiConfigId,
commitMessageApiConfigId: stateValues.commitMessageApiConfigId,
experiments: stateValues.experiments ?? experimentDefault,
autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
customModes,
Expand Down
41 changes: 41 additions & 0 deletions src/core/webview/__tests__/ClineProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1226,6 +1226,47 @@ describe("ClineProvider", () => {
})
})

describe("commit message model selection is included in state", () => {
// Both paths matter: the webview reads the posted state to show the current selection, and
// the generator reads getState() to pick a profile. Dropping either one makes a saved
// selection look like it reverted.
it("getStateToPostToWebview returns the saved commitMessageApiConfigId", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("commitMessageApiConfigId", "config-2")

const state = await provider.getStateToPostToWebview()

expect(state.commitMessageApiConfigId).toBe("config-2")
})

it("getStateToPostToWebview leaves commitMessageApiConfigId unset when no profile is chosen", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("commitMessageApiConfigId", undefined)

const state = await provider.getStateToPostToWebview()

expect(state.commitMessageApiConfigId).toBeUndefined()
})

it("getState returns the saved commitMessageApiConfigId", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("commitMessageApiConfigId", "config-2")

const state = await provider.getState()

expect(state.commitMessageApiConfigId).toBe("config-2")
})

it("getState leaves commitMessageApiConfigId unset when no profile is chosen", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("commitMessageApiConfigId", undefined)

const state = await provider.getState()

expect(state.commitMessageApiConfigId).toBeUndefined()
})
})

it("getStateToPostToWebview passes through defined diffFuzzyThreshold value", async () => {
await provider.resolveWebviewView(mockWebviewView)
await provider.contextProxy.setValue("diffFuzzyThreshold", 0.5)
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ca/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/de/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"update_support_prompt": "Failed to update support prompt",
"reset_support_prompt": "Failed to reset support prompt",
"enhance_prompt": "Failed to enhance prompt",
"commit_message_empty_response": "The model returned an empty commit message.",
"get_system_prompt": "Failed to get system prompt",
"search_commits": "Failed to search commits",
"save_api_config": "Failed to save api configuration",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/fr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/hi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/id/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/it/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ja/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ko/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/nl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pl/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/ru/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/tr/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/vi/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/zh-CN/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/i18n/locales/zh-TW/common.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions src/services/commit-message/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import type { ProviderSettings } from "@roo-code/types"

import { getCommitMessageSettings } from "../config"
import type { ClineProvider } from "../../../core/webview/ClineProvider"

describe("getCommitMessageSettings", () => {
const apiConfiguration: ProviderSettings = { apiProvider: "openai", apiKey: "key", apiModelId: "gpt-4" }

const listApiConfigMeta = [
{ id: "config1", name: "Config 1" },
{ id: "config2", name: "Config 2" },
]

const commitProfile = {
name: "Commit Config",
apiProvider: "anthropic" as const,
apiKey: "commit-key",
apiModelId: "claude-3",
}

let getProfile: ReturnType<typeof vi.fn>

// `ClineProvider` is a large concrete class, and constructing one would drag in the extension
// host. This reads the two members the function actually touches, so the double assertion is
// the narrowest way to stand in for it - widening to `unknown` first because the stub is not
// structurally assignable to the full class.
const makeProvider = (commitMessageApiConfigId?: string) =>
({
getState: vi.fn().mockResolvedValue({
apiConfiguration,
listApiConfigMeta,
customSupportPrompts: { COMMIT_MESSAGE: "custom" },
commitMessageApiConfigId,
}),
providerSettingsManager: { getProfile },
}) as unknown as ClineProvider

beforeEach(() => {
vi.clearAllMocks()
getProfile = vi.fn().mockResolvedValue(commitProfile)
})

it("uses the active configuration when no dedicated profile is chosen", async () => {
const settings = await getCommitMessageSettings(makeProvider())

expect(settings.apiConfiguration).toBe(apiConfiguration)
expect(getProfile).not.toHaveBeenCalled()
})

it("uses the dedicated profile when one is configured", async () => {
const settings = await getCommitMessageSettings(makeProvider("config2"))

expect(getProfile).toHaveBeenCalledWith({ id: "config2" })
expect(settings.apiConfiguration).toEqual({
apiProvider: "anthropic",
apiKey: "commit-key",
apiModelId: "claude-3",
})
})

it("carries the customized prompt through", async () => {
const settings = await getCommitMessageSettings(makeProvider())

expect(settings.customSupportPrompts).toEqual({ COMMIT_MESSAGE: "custom" })
})

it("falls back when the saved id is not in the known profiles", async () => {
const settings = await getCommitMessageSettings(makeProvider("deleted-config"))

expect(getProfile).not.toHaveBeenCalled()
expect(settings.apiConfiguration).toBe(apiConfiguration)
})

// The metadata check is not enough on its own: a profile can be deleted between reading the
// state and looking it up, and stale metadata points at profiles that are already gone.
it("falls back when the profile disappears between the state read and the lookup", async () => {
getProfile = vi.fn().mockRejectedValue(new Error("Profile not found"))

const settings = await getCommitMessageSettings(makeProvider("config2"))

expect(getProfile).toHaveBeenCalledWith({ id: "config2" })
expect(settings.apiConfiguration).toBe(apiConfiguration)
})

it("falls back when the saved profile has no provider configured", async () => {
getProfile = vi.fn().mockResolvedValue({ name: "Empty Config" })

const settings = await getCommitMessageSettings(makeProvider("config2"))

expect(settings.apiConfiguration).toBe(apiConfiguration)
})
})
Loading
Loading