Skip to content

Commit f4abc87

Browse files
committed
Exclusion mechanism
1 parent 07ab2c1 commit f4abc87

File tree

5 files changed

+228
-1
lines changed

5 files changed

+228
-1
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// npx vitest run src/hooks/__tests__/useSettingsSearch.exclusions.spec.ts
2+
3+
import { renderHook } from "@testing-library/react"
4+
5+
import { useSettingsSearch } from "../useSettingsSearch"
6+
7+
// Mock react-i18next
8+
vi.mock("react-i18next", () => ({
9+
useTranslation: () => ({
10+
t: (key: string) => key,
11+
}),
12+
}))
13+
14+
// Mock settings data to include excluded paths and normal settings
15+
vi.mock("@/i18n/locales/en/settings.json", () => ({
16+
default: {
17+
modelInfo: {
18+
inputPrice: "Input price",
19+
outputPrice: "Output price",
20+
},
21+
validation: {
22+
apiKey: "You must provide a valid API key",
23+
},
24+
browser: {
25+
enable: {
26+
label: "Enable browser tool",
27+
description: "Allows Roo to use a browser",
28+
},
29+
},
30+
},
31+
}))
32+
33+
describe("useSettingsSearch - exclusions", () => {
34+
it("does not return excluded modelInfo entries", () => {
35+
const { result } = renderHook(() => useSettingsSearch("price"))
36+
37+
const modelInfoResults = result.current.filter((r) => r.id.startsWith("modelInfo."))
38+
expect(modelInfoResults).toHaveLength(0)
39+
})
40+
41+
it("does not return excluded validation entries", () => {
42+
const { result } = renderHook(() => useSettingsSearch("api key"))
43+
44+
const validationResults = result.current.filter((r) => r.id.startsWith("validation."))
45+
expect(validationResults).toHaveLength(0)
46+
})
47+
48+
it("still returns actionable settings", () => {
49+
const { result } = renderHook(() => useSettingsSearch("browser"))
50+
51+
const browserResult = result.current.find((r) => r.id === "browser.enable")
52+
expect(browserResult).toBeDefined()
53+
})
54+
})

webview-ui/src/utils/__tests__/parseSettingsI18nKeys.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
import { parseSettingsI18nKeys, type SectionName, sectionNames } from "../parseSettingsI18nKeys"
22

33
describe("parseSettingsI18nKeys", () => {
4+
it("should exclude display-only and helper entries", () => {
5+
const translations = {
6+
modelInfo: {
7+
inputPrice: "Input price",
8+
},
9+
validation: {
10+
apiKey: "You must provide an API key",
11+
},
12+
placeholders: {
13+
apiKey: "Enter API Key",
14+
},
15+
browser: {
16+
enable: {
17+
label: "Enable browser tool",
18+
},
19+
},
20+
}
21+
22+
const results = parseSettingsI18nKeys(translations)
23+
24+
// Excluded categories
25+
expect(results.find((r) => r.id.startsWith("modelInfo."))).toBeUndefined()
26+
expect(results.find((r) => r.id.startsWith("validation."))).toBeUndefined()
27+
expect(results.find((r) => r.id.startsWith("placeholders."))).toBeUndefined()
28+
29+
// Included actionable setting
30+
expect(results.find((r) => r.id === "browser.enable")).toBeDefined()
31+
})
32+
433
describe("basic parsing functionality", () => {
534
it("should parse settings with label property", () => {
635
const translations = {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, it, expect } from "vitest"
2+
3+
import { getExclusionReason, shouldExcludeFromSearch } from "../settingsSearchExclusions"
4+
5+
describe("settingsSearchExclusions", () => {
6+
describe("shouldExcludeFromSearch", () => {
7+
it("excludes modelInfo display fields", () => {
8+
expect(shouldExcludeFromSearch("modelInfo.inputPrice")).toBe(true)
9+
expect(shouldExcludeFromSearch("modelInfo.outputPrice")).toBe(true)
10+
expect(shouldExcludeFromSearch("modelInfo.contextWindow")).toBe(true)
11+
})
12+
13+
it("excludes validation messages", () => {
14+
expect(shouldExcludeFromSearch("validation.apiKey")).toBe(true)
15+
expect(shouldExcludeFromSearch("validation.modelId")).toBe(true)
16+
})
17+
18+
it("excludes placeholders", () => {
19+
expect(shouldExcludeFromSearch("placeholders.apiKey")).toBe(true)
20+
expect(shouldExcludeFromSearch("placeholders.baseUrl")).toBe(true)
21+
})
22+
23+
it("excludes custom model pricing", () => {
24+
expect(shouldExcludeFromSearch("providers.customModel.pricing.input")).toBe(true)
25+
expect(shouldExcludeFromSearch("providers.customModel.pricing.output")).toBe(true)
26+
})
27+
28+
it("excludes service tier display-only entries", () => {
29+
expect(shouldExcludeFromSearch("serviceTier.columns.tier")).toBe(true)
30+
expect(shouldExcludeFromSearch("serviceTier.pricingTableTitle")).toBe(true)
31+
})
32+
33+
it("does not exclude actionable settings", () => {
34+
expect(shouldExcludeFromSearch("browser.enable")).toBe(false)
35+
expect(shouldExcludeFromSearch("providers.apiProvider")).toBe(false)
36+
expect(shouldExcludeFromSearch("terminal.outputLineLimit")).toBe(false)
37+
})
38+
39+
it("does not exclude settings that merely contain keywords", () => {
40+
expect(shouldExcludeFromSearch("providers.enablePromptCaching")).toBe(false)
41+
})
42+
})
43+
44+
describe("getExclusionReason", () => {
45+
it("returns reason for excluded ids", () => {
46+
const reason = getExclusionReason("modelInfo.inputPrice")
47+
expect(reason).toBeDefined()
48+
expect(reason?.length).toBeGreaterThan(0)
49+
})
50+
51+
it("returns undefined for included ids", () => {
52+
expect(getExclusionReason("browser.enable")).toBeUndefined()
53+
})
54+
})
55+
})

webview-ui/src/utils/parseSettingsI18nKeys.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { shouldExcludeFromSearch } from "./settingsSearchExclusions"
2+
13
/**
24
* Utility for parsing i18n translation structure to extract searchable settings information.
35
*
@@ -384,5 +386,7 @@ export function parseSettingsI18nKeys(
384386
}
385387
}
386388

387-
return results
389+
const filteredResults = results.filter((setting) => !shouldExcludeFromSearch(setting.id))
390+
391+
return filteredResults
388392
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
export interface ExclusionRule {
2+
/** Pattern to match against a parsed setting id */
3+
pattern: string | RegExp
4+
/** Human-readable reason for exclusion */
5+
reason: string
6+
/** Example ids matched by this rule */
7+
examples?: string[]
8+
}
9+
10+
const SEARCH_EXCLUSIONS: ExclusionRule[] = [
11+
{
12+
pattern: /^modelInfo\./,
13+
reason: "Model information is display-only and not configurable",
14+
examples: ["modelInfo.inputPrice", "modelInfo.outputPrice", "modelInfo.contextWindow"],
15+
},
16+
{
17+
pattern: /^providers\.customModel\.pricing\./,
18+
reason: "Custom model pricing fields are display-focused and should not clutter search",
19+
examples: ["providers.customModel.pricing.input", "providers.customModel.pricing.output"],
20+
},
21+
{
22+
pattern: /^validation\./,
23+
reason: "Validation messages are error text, not settings",
24+
examples: ["validation.apiKey", "validation.modelId"],
25+
},
26+
{
27+
pattern: /^placeholders\./,
28+
reason: "Placeholder text is helper content, not a setting",
29+
examples: ["placeholders.apiKey", "placeholders.baseUrl"],
30+
},
31+
{
32+
pattern: /^defaults\./,
33+
reason: "Default value descriptions are informational only",
34+
examples: ["defaults.ollamaUrl", "defaults.lmStudioUrl"],
35+
},
36+
{
37+
pattern: /^labels\./,
38+
reason: "Generic labels are helper text, not settings",
39+
examples: ["labels.customArn", "labels.useCustomArn"],
40+
},
41+
{
42+
pattern: /^thinkingBudget\./,
43+
reason: "Thinking budget entries are display-only",
44+
examples: ["thinkingBudget.maxTokens", "thinkingBudget.maxThinkingTokens"],
45+
},
46+
{
47+
pattern: /^serviceTier\.columns\./,
48+
reason: "Service tier column headers are display-only",
49+
examples: ["serviceTier.columns.tier", "serviceTier.columns.input"],
50+
},
51+
{
52+
pattern: /^serviceTier\.pricingTableTitle$/,
53+
reason: "Service tier table title is display-only",
54+
examples: ["serviceTier.pricingTableTitle"],
55+
},
56+
{
57+
pattern: /^modelPicker\.simplifiedExplanation$/,
58+
reason: "Model picker helper text is informational",
59+
examples: ["modelPicker.simplifiedExplanation"],
60+
},
61+
{
62+
pattern: /^modelInfo\.gemini\.(freeRequests|pricingDetails|billingEstimate)$/,
63+
reason: "Gemini pricing notes are display-only",
64+
examples: ["modelInfo.gemini.freeRequests", "modelInfo.gemini.pricingDetails"],
65+
},
66+
]
67+
68+
function matchesRule(settingId: string, rule: ExclusionRule): boolean {
69+
if (typeof rule.pattern === "string") {
70+
return settingId === rule.pattern
71+
}
72+
73+
return rule.pattern.test(settingId)
74+
}
75+
76+
export function shouldExcludeFromSearch(settingId: string): boolean {
77+
return SEARCH_EXCLUSIONS.some((rule) => matchesRule(settingId, rule))
78+
}
79+
80+
export function getExclusionReason(settingId: string): string | undefined {
81+
const rule = SEARCH_EXCLUSIONS.find((candidate) => matchesRule(settingId, candidate))
82+
return rule?.reason
83+
}
84+
85+
export { SEARCH_EXCLUSIONS }

0 commit comments

Comments
 (0)