Skip to content

Commit af8e814

Browse files
committed
Merge branch 'main' into fix/preserve-all-codeindexing-settings
2 parents 8ec860f + 781beeb commit af8e814

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1083
-109
lines changed

packages/types/src/codebase-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const codebaseIndexConfigSchema = z.object({
1212
codebaseIndexEmbedderModelId: z.string().optional(),
1313
codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(),
1414
codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(),
15+
codebaseIndexSearchMinScore: z.number().min(0).max(1).optional(),
1516
})
1617

1718
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>

packages/types/src/followup.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { z } from "zod"
2+
3+
/**
4+
* Interface for follow-up data structure used in follow-up questions
5+
* This represents the data structure for follow-up questions that the LLM can ask
6+
* to gather more information needed to complete a task.
7+
*/
8+
export interface FollowUpData {
9+
/** The question being asked by the LLM */
10+
question?: string
11+
/** Array of suggested answers that the user can select */
12+
suggest?: Array<SuggestionItem>
13+
}
14+
15+
/**
16+
* Interface for a suggestion item with optional mode switching
17+
*/
18+
export interface SuggestionItem {
19+
/** The text of the suggestion */
20+
answer: string
21+
/** Optional mode to switch to when selecting this suggestion */
22+
mode?: string
23+
}
24+
25+
/**
26+
* Zod schema for SuggestionItem
27+
*/
28+
export const suggestionItemSchema = z.object({
29+
answer: z.string(),
30+
mode: z.string().optional(),
31+
})
32+
33+
/**
34+
* Zod schema for FollowUpData
35+
*/
36+
export const followUpDataSchema = z.object({
37+
question: z.string().optional(),
38+
suggest: z.array(suggestionItemSchema).optional(),
39+
})
40+
41+
export type FollowUpDataType = z.infer<typeof followUpDataSchema>

packages/types/src/global-settings.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export const globalSettingsSchema = z.object({
4545
alwaysAllowModeSwitch: z.boolean().optional(),
4646
alwaysAllowSubtasks: z.boolean().optional(),
4747
alwaysAllowExecute: z.boolean().optional(),
48+
alwaysAllowFollowupQuestions: z.boolean().optional(),
49+
followupAutoApproveTimeoutMs: z.number().optional(),
4850
allowedCommands: z.array(z.string()).optional(),
4951
allowedMaxRequests: z.number().nullish(),
5052
autoCondenseContext: z.boolean().optional(),
@@ -190,6 +192,8 @@ export const EVALS_SETTINGS: RooCodeSettings = {
190192
alwaysAllowModeSwitch: true,
191193
alwaysAllowSubtasks: true,
192194
alwaysAllowExecute: true,
195+
alwaysAllowFollowupQuestions: true,
196+
followupAutoApproveTimeoutMs: 0,
193197
allowedCommands: ["*"],
194198

195199
browserToolEnabled: false,

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./api.js"
44
export * from "./codebase-index.js"
55
export * from "./cloud.js"
66
export * from "./experiment.js"
7+
export * from "./followup.js"
78
export * from "./global-settings.js"
89
export * from "./history.js"
910
export * from "./ipc.js"

src/core/webview/ClineProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1411,6 +1411,8 @@ export class ClineProvider
14111411
codebaseIndexConfig,
14121412
codebaseIndexModels,
14131413
profileThresholds,
1414+
alwaysAllowFollowupQuestions,
1415+
followupAutoApproveTimeoutMs,
14141416
} = await this.getState()
14151417

14161418
const telemetryKey = process.env.POSTHOG_API_KEY
@@ -1521,6 +1523,8 @@ export class ClineProvider
15211523
profileThresholds: profileThresholds ?? {},
15221524
cloudApiUrl: getRooCodeApiUrl(),
15231525
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
1526+
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
1527+
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
15241528
}
15251529
}
15261530

@@ -1601,6 +1605,8 @@ export class ClineProvider
16011605
alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
16021606
alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
16031607
alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
1608+
alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
1609+
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 60000,
16041610
allowedMaxRequests: stateValues.allowedMaxRequests,
16051611
autoCondenseContext: stateValues.autoCondenseContext ?? true,
16061612
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,6 +1100,14 @@ export const webviewMessageHandler = async (
11001100
await updateGlobalState("maxWorkspaceFiles", fileCount)
11011101
await provider.postStateToWebview()
11021102
break
1103+
case "alwaysAllowFollowupQuestions":
1104+
await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false)
1105+
await provider.postStateToWebview()
1106+
break
1107+
case "followupAutoApproveTimeoutMs":
1108+
await updateGlobalState("followupAutoApproveTimeoutMs", message.value)
1109+
await provider.postStateToWebview()
1110+
break
11031111
case "browserToolEnabled":
11041112
await updateGlobalState("browserToolEnabled", message.bool ?? true)
11051113
await provider.postStateToWebview()

src/services/code-index/__tests__/config-manager.spec.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,153 @@ describe("CodeIndexConfigManager", () => {
713713
const result = await configManager.loadConfiguration()
714714
expect(result.requiresRestart).toBe(false)
715715
})
716+
717+
describe("currentSearchMinScore priority system", () => {
718+
it("should return user-configured score when set", async () => {
719+
mockContextProxy.getGlobalState.mockReturnValue({
720+
codebaseIndexEnabled: true,
721+
codebaseIndexQdrantUrl: "http://qdrant.local",
722+
codebaseIndexEmbedderProvider: "openai",
723+
codebaseIndexEmbedderModelId: "text-embedding-3-small",
724+
codebaseIndexSearchMinScore: 0.8, // User setting
725+
})
726+
mockContextProxy.getSecret.mockImplementation((key: string) => {
727+
if (key === "codeIndexOpenAiKey") return "test-key"
728+
return undefined
729+
})
730+
731+
await configManager.loadConfiguration()
732+
expect(configManager.currentSearchMinScore).toBe(0.8)
733+
})
734+
735+
it("should fall back to model-specific threshold when user setting is undefined", async () => {
736+
mockContextProxy.getGlobalState.mockReturnValue({
737+
codebaseIndexEnabled: true,
738+
codebaseIndexQdrantUrl: "http://qdrant.local",
739+
codebaseIndexEmbedderProvider: "ollama",
740+
codebaseIndexEmbedderModelId: "nomic-embed-code",
741+
// No codebaseIndexSearchMinScore - user hasn't configured it
742+
})
743+
744+
await configManager.loadConfiguration()
745+
// nomic-embed-code has a specific threshold of 0.15
746+
expect(configManager.currentSearchMinScore).toBe(0.15)
747+
})
748+
749+
it("should fall back to default SEARCH_MIN_SCORE when neither user setting nor model threshold exists", async () => {
750+
mockContextProxy.getGlobalState.mockReturnValue({
751+
codebaseIndexEnabled: true,
752+
codebaseIndexQdrantUrl: "http://qdrant.local",
753+
codebaseIndexEmbedderProvider: "openai",
754+
codebaseIndexEmbedderModelId: "unknown-model", // Model not in profiles
755+
// No codebaseIndexSearchMinScore
756+
})
757+
mockContextProxy.getSecret.mockImplementation((key: string) => {
758+
if (key === "codeIndexOpenAiKey") return "test-key"
759+
return undefined
760+
})
761+
762+
await configManager.loadConfiguration()
763+
// Should fall back to default SEARCH_MIN_SCORE (0.4)
764+
expect(configManager.currentSearchMinScore).toBe(0.4)
765+
})
766+
767+
it("should respect user setting of 0 (edge case)", async () => {
768+
mockContextProxy.getGlobalState.mockReturnValue({
769+
codebaseIndexEnabled: true,
770+
codebaseIndexQdrantUrl: "http://qdrant.local",
771+
codebaseIndexEmbedderProvider: "ollama",
772+
codebaseIndexEmbedderModelId: "nomic-embed-code",
773+
codebaseIndexSearchMinScore: 0, // User explicitly sets 0
774+
})
775+
776+
await configManager.loadConfiguration()
777+
// Should return 0, not fall back to model threshold (0.15)
778+
expect(configManager.currentSearchMinScore).toBe(0)
779+
})
780+
781+
it("should use model-specific threshold with openai-compatible provider", async () => {
782+
mockContextProxy.getGlobalState.mockImplementation((key: string) => {
783+
if (key === "codebaseIndexConfig") {
784+
return {
785+
codebaseIndexEnabled: true,
786+
codebaseIndexQdrantUrl: "http://qdrant.local",
787+
codebaseIndexEmbedderProvider: "openai-compatible",
788+
codebaseIndexEmbedderModelId: "nomic-embed-code",
789+
// No codebaseIndexSearchMinScore
790+
}
791+
}
792+
if (key === "codebaseIndexOpenAiCompatibleBaseUrl") return "https://api.example.com/v1"
793+
return undefined
794+
})
795+
mockContextProxy.getSecret.mockImplementation((key: string) => {
796+
if (key === "codebaseIndexOpenAiCompatibleApiKey") return "test-api-key"
797+
return undefined
798+
})
799+
800+
await configManager.loadConfiguration()
801+
// openai-compatible provider also has nomic-embed-code with 0.15 threshold
802+
expect(configManager.currentSearchMinScore).toBe(0.15)
803+
})
804+
805+
it("should use default model ID when modelId is not specified", async () => {
806+
mockContextProxy.getGlobalState.mockReturnValue({
807+
codebaseIndexEnabled: true,
808+
codebaseIndexQdrantUrl: "http://qdrant.local",
809+
codebaseIndexEmbedderProvider: "openai",
810+
// No modelId specified
811+
// No codebaseIndexSearchMinScore
812+
})
813+
mockContextProxy.getSecret.mockImplementation((key: string) => {
814+
if (key === "codeIndexOpenAiKey") return "test-key"
815+
return undefined
816+
})
817+
818+
await configManager.loadConfiguration()
819+
// Should use default model (text-embedding-3-small) threshold (0.4)
820+
expect(configManager.currentSearchMinScore).toBe(0.4)
821+
})
822+
823+
it("should handle priority correctly: user > model > default", async () => {
824+
// Test 1: User setting takes precedence
825+
mockContextProxy.getGlobalState.mockReturnValue({
826+
codebaseIndexEnabled: true,
827+
codebaseIndexQdrantUrl: "http://qdrant.local",
828+
codebaseIndexEmbedderProvider: "ollama",
829+
codebaseIndexEmbedderModelId: "nomic-embed-code", // Has 0.15 threshold
830+
codebaseIndexSearchMinScore: 0.9, // User overrides
831+
})
832+
833+
await configManager.loadConfiguration()
834+
expect(configManager.currentSearchMinScore).toBe(0.9) // User setting wins
835+
836+
// Test 2: Model threshold when no user setting
837+
mockContextProxy.getGlobalState.mockReturnValue({
838+
codebaseIndexEnabled: true,
839+
codebaseIndexQdrantUrl: "http://qdrant.local",
840+
codebaseIndexEmbedderProvider: "ollama",
841+
codebaseIndexEmbedderModelId: "nomic-embed-code",
842+
// No user setting
843+
})
844+
845+
const newManager = new CodeIndexConfigManager(mockContextProxy)
846+
await newManager.loadConfiguration()
847+
expect(newManager.currentSearchMinScore).toBe(0.15) // Model threshold
848+
849+
// Test 3: Default when neither exists
850+
mockContextProxy.getGlobalState.mockReturnValue({
851+
codebaseIndexEnabled: true,
852+
codebaseIndexQdrantUrl: "http://qdrant.local",
853+
codebaseIndexEmbedderProvider: "openai",
854+
codebaseIndexEmbedderModelId: "custom-unknown-model",
855+
// No user setting, unknown model
856+
})
857+
858+
const anotherManager = new CodeIndexConfigManager(mockContextProxy)
859+
await anotherManager.loadConfiguration()
860+
expect(anotherManager.currentSearchMinScore).toBe(0.4) // Default
861+
})
862+
})
716863
})
717864

718865
describe("empty/missing API key handling", () => {

src/services/code-index/config-manager.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ContextProxy } from "../../core/config/ContextProxy"
33
import { EmbedderProvider } from "./interfaces/manager"
44
import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
55
import { SEARCH_MIN_SCORE } from "./constants"
6-
import { getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
6+
import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels"
77

88
/**
99
* Manages configuration state and validation for the code indexing feature.
@@ -34,12 +34,12 @@ export class CodeIndexConfigManager {
3434
const codebaseIndexConfig = this.contextProxy.getGlobalState("codebaseIndexConfig") ?? {
3535
codebaseIndexEnabled: false,
3636
codebaseIndexQdrantUrl: "http://localhost:6333",
37-
codebaseIndexSearchMinScore: 0.4,
3837
codebaseIndexEmbedderProvider: "openai",
3938
codebaseIndexEmbedderBaseUrl: "",
4039
codebaseIndexEmbedderModelId: "",
4140
codebaseIndexOpenAiCompatibleBaseUrl: "",
4241
codebaseIndexOpenAiCompatibleModelDimension: undefined,
42+
codebaseIndexSearchMinScore: undefined,
4343
}
4444

4545
const {
@@ -50,6 +50,7 @@ export class CodeIndexConfigManager {
5050
codebaseIndexEmbedderModelId,
5151
codebaseIndexOpenAiCompatibleBaseUrl,
5252
codebaseIndexOpenAiCompatibleModelDimension,
53+
codebaseIndexSearchMinScore,
5354
} = codebaseIndexConfig
5455

5556
const openAiKey = this.contextProxy.getSecret("codeIndexOpenAiKey") ?? ""
@@ -63,8 +64,8 @@ export class CodeIndexConfigManager {
6364
this.isEnabled = codebaseIndexEnabled || false
6465
this.qdrantUrl = codebaseIndexQdrantUrl
6566
this.qdrantApiKey = qdrantApiKey ?? ""
67+
this.searchMinScore = codebaseIndexSearchMinScore
6668
this.openAiOptions = { openAiNativeApiKey: openAiKey }
67-
this.searchMinScore = SEARCH_MIN_SCORE
6869

6970
// Set embedder provider with support for openai-compatible
7071
if (codebaseIndexEmbedderProvider === "ollama") {
@@ -142,7 +143,7 @@ export class CodeIndexConfigManager {
142143
openAiCompatibleOptions: this.openAiCompatibleOptions,
143144
qdrantUrl: this.qdrantUrl,
144145
qdrantApiKey: this.qdrantApiKey,
145-
searchMinScore: this.searchMinScore,
146+
searchMinScore: this.currentSearchMinScore,
146147
},
147148
requiresRestart,
148149
}
@@ -297,7 +298,7 @@ export class CodeIndexConfigManager {
297298
openAiCompatibleOptions: this.openAiCompatibleOptions,
298299
qdrantUrl: this.qdrantUrl,
299300
qdrantApiKey: this.qdrantApiKey,
300-
searchMinScore: this.searchMinScore,
301+
searchMinScore: this.currentSearchMinScore,
301302
}
302303
}
303304

@@ -340,9 +341,18 @@ export class CodeIndexConfigManager {
340341
}
341342

342343
/**
343-
* Gets the configured minimum search score.
344+
* Gets the configured minimum search score based on user setting, model-specific threshold, or fallback.
345+
* Priority: 1) User setting, 2) Model-specific threshold, 3) Default SEARCH_MIN_SCORE constant.
344346
*/
345-
public get currentSearchMinScore(): number | undefined {
346-
return this.searchMinScore
347+
public get currentSearchMinScore(): number {
348+
// First check if user has configured a custom score threshold
349+
if (this.searchMinScore !== undefined) {
350+
return this.searchMinScore
351+
}
352+
353+
// Fall back to model-specific threshold
354+
const currentModelId = this.modelId ?? getDefaultModelId(this.embedderProvider)
355+
const modelSpecificThreshold = getModelScoreThreshold(this.embedderProvider, currentModelId)
356+
return modelSpecificThreshold ?? SEARCH_MIN_SCORE
347357
}
348358
}

0 commit comments

Comments
 (0)