CodeIndexPopover.tsx 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287
  1. import React, { useState, useEffect, useMemo, useCallback, useRef } from "react"
  2. import { Trans } from "react-i18next"
  3. import { z } from "zod"
  4. import {
  5. VSCodeButton,
  6. VSCodeTextField,
  7. VSCodeDropdown,
  8. VSCodeOption,
  9. VSCodeLink,
  10. VSCodeCheckbox,
  11. } from "@vscode/webview-ui-toolkit/react"
  12. import * as ProgressPrimitive from "@radix-ui/react-progress"
  13. import { vscode } from "@src/utils/vscode"
  14. import { useExtensionState } from "@src/context/ExtensionStateContext"
  15. import { useAppTranslation } from "@src/i18n/TranslationContext"
  16. import { buildDocLink } from "@src/utils/docLinks"
  17. import { cn } from "@src/lib/utils"
  18. import {
  19. Select,
  20. SelectContent,
  21. SelectItem,
  22. SelectTrigger,
  23. SelectValue,
  24. AlertDialog,
  25. AlertDialogAction,
  26. AlertDialogCancel,
  27. AlertDialogContent,
  28. AlertDialogDescription,
  29. AlertDialogFooter,
  30. AlertDialogHeader,
  31. AlertDialogTitle,
  32. AlertDialogTrigger,
  33. Popover,
  34. PopoverContent,
  35. PopoverTrigger,
  36. Slider,
  37. StandardTooltip,
  38. } from "@src/components/ui"
  39. import { AlertTriangle } from "lucide-react"
  40. import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
  41. import { useEscapeKey } from "@src/hooks/useEscapeKey"
  42. import type { EmbedderProvider } from "@roo/embeddingModels"
  43. import type { IndexingStatus } from "@roo/ExtensionMessage"
  44. import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
  45. // Default URLs for providers
  46. const DEFAULT_QDRANT_URL = "http://localhost:6333"
  47. const DEFAULT_OLLAMA_URL = "http://localhost:11434"
  48. interface CodeIndexPopoverProps {
  49. children: React.ReactNode
  50. indexingStatus: IndexingStatus
  51. }
  52. interface LocalCodeIndexSettings {
  53. // Global state settings
  54. codebaseIndexEnabled: boolean
  55. codebaseIndexQdrantUrl: string
  56. codebaseIndexEmbedderProvider: EmbedderProvider
  57. codebaseIndexEmbedderBaseUrl?: string
  58. codebaseIndexEmbedderModelId: string
  59. codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers
  60. codebaseIndexSearchMaxResults?: number
  61. codebaseIndexSearchMinScore?: number
  62. // Secret settings (start empty, will be loaded separately)
  63. codeIndexOpenAiKey?: string
  64. codeIndexQdrantApiKey?: string
  65. codebaseIndexOpenAiCompatibleBaseUrl?: string
  66. codebaseIndexOpenAiCompatibleApiKey?: string
  67. codebaseIndexGeminiApiKey?: string
  68. codebaseIndexMistralApiKey?: string
  69. }
  70. // Validation schema for codebase index settings
  71. const createValidationSchema = (provider: EmbedderProvider, t: any) => {
  72. const baseSchema = z.object({
  73. codebaseIndexEnabled: z.boolean(),
  74. codebaseIndexQdrantUrl: z
  75. .string()
  76. .min(1, t("settings:codeIndex.validation.qdrantUrlRequired"))
  77. .url(t("settings:codeIndex.validation.invalidQdrantUrl")),
  78. codeIndexQdrantApiKey: z.string().optional(),
  79. })
  80. switch (provider) {
  81. case "openai":
  82. return baseSchema.extend({
  83. codeIndexOpenAiKey: z.string().min(1, t("settings:codeIndex.validation.openaiApiKeyRequired")),
  84. codebaseIndexEmbedderModelId: z
  85. .string()
  86. .min(1, t("settings:codeIndex.validation.modelSelectionRequired")),
  87. })
  88. case "ollama":
  89. return baseSchema.extend({
  90. codebaseIndexEmbedderBaseUrl: z
  91. .string()
  92. .min(1, t("settings:codeIndex.validation.ollamaBaseUrlRequired"))
  93. .url(t("settings:codeIndex.validation.invalidOllamaUrl")),
  94. codebaseIndexEmbedderModelId: z.string().min(1, t("settings:codeIndex.validation.modelIdRequired")),
  95. codebaseIndexEmbedderModelDimension: z
  96. .number()
  97. .min(1, t("settings:codeIndex.validation.modelDimensionRequired"))
  98. .optional(),
  99. })
  100. case "openai-compatible":
  101. return baseSchema.extend({
  102. codebaseIndexOpenAiCompatibleBaseUrl: z
  103. .string()
  104. .min(1, t("settings:codeIndex.validation.baseUrlRequired"))
  105. .url(t("settings:codeIndex.validation.invalidBaseUrl")),
  106. codebaseIndexOpenAiCompatibleApiKey: z
  107. .string()
  108. .min(1, t("settings:codeIndex.validation.apiKeyRequired")),
  109. codebaseIndexEmbedderModelId: z.string().min(1, t("settings:codeIndex.validation.modelIdRequired")),
  110. codebaseIndexEmbedderModelDimension: z
  111. .number()
  112. .min(1, t("settings:codeIndex.validation.modelDimensionRequired")),
  113. })
  114. case "gemini":
  115. return baseSchema.extend({
  116. codebaseIndexGeminiApiKey: z.string().min(1, t("settings:codeIndex.validation.geminiApiKeyRequired")),
  117. codebaseIndexEmbedderModelId: z
  118. .string()
  119. .min(1, t("settings:codeIndex.validation.modelSelectionRequired")),
  120. })
  121. case "mistral":
  122. return baseSchema.extend({
  123. codebaseIndexMistralApiKey: z.string().min(1, t("settings:codeIndex.validation.mistralApiKeyRequired")),
  124. codebaseIndexEmbedderModelId: z
  125. .string()
  126. .min(1, t("settings:codeIndex.validation.modelSelectionRequired")),
  127. })
  128. default:
  129. return baseSchema
  130. }
  131. }
  132. export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
  133. children,
  134. indexingStatus: externalIndexingStatus,
  135. }) => {
  136. const SECRET_PLACEHOLDER = "••••••••••••••••"
  137. const { t } = useAppTranslation()
  138. const { codebaseIndexConfig, codebaseIndexModels, cwd } = useExtensionState()
  139. const [open, setOpen] = useState(false)
  140. const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
  141. const [isSetupSettingsOpen, setIsSetupSettingsOpen] = useState(false)
  142. const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>(externalIndexingStatus)
  143. const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
  144. const [saveError, setSaveError] = useState<string | null>(null)
  145. // Form validation state
  146. const [formErrors, setFormErrors] = useState<Record<string, string>>({})
  147. // Discard changes dialog state
  148. const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
  149. const confirmDialogHandler = useRef<(() => void) | null>(null)
  150. // Default settings template
  151. const getDefaultSettings = (): LocalCodeIndexSettings => ({
  152. codebaseIndexEnabled: true,
  153. codebaseIndexQdrantUrl: "",
  154. codebaseIndexEmbedderProvider: "openai",
  155. codebaseIndexEmbedderBaseUrl: "",
  156. codebaseIndexEmbedderModelId: "",
  157. codebaseIndexEmbedderModelDimension: undefined,
  158. codebaseIndexSearchMaxResults: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  159. codebaseIndexSearchMinScore: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  160. codeIndexOpenAiKey: "",
  161. codeIndexQdrantApiKey: "",
  162. codebaseIndexOpenAiCompatibleBaseUrl: "",
  163. codebaseIndexOpenAiCompatibleApiKey: "",
  164. codebaseIndexGeminiApiKey: "",
  165. codebaseIndexMistralApiKey: "",
  166. })
  167. // Initial settings state - stores the settings when popover opens
  168. const [initialSettings, setInitialSettings] = useState<LocalCodeIndexSettings>(getDefaultSettings())
  169. // Current settings state - tracks user changes
  170. const [currentSettings, setCurrentSettings] = useState<LocalCodeIndexSettings>(getDefaultSettings())
  171. // Update indexing status from parent
  172. useEffect(() => {
  173. setIndexingStatus(externalIndexingStatus)
  174. }, [externalIndexingStatus])
  175. // Initialize settings from global state
  176. useEffect(() => {
  177. if (codebaseIndexConfig) {
  178. const settings = {
  179. codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true,
  180. codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "",
  181. codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai",
  182. codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
  183. codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "",
  184. codebaseIndexEmbedderModelDimension:
  185. codebaseIndexConfig.codebaseIndexEmbedderModelDimension || undefined,
  186. codebaseIndexSearchMaxResults:
  187. codebaseIndexConfig.codebaseIndexSearchMaxResults ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  188. codebaseIndexSearchMinScore:
  189. codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  190. codeIndexOpenAiKey: "",
  191. codeIndexQdrantApiKey: "",
  192. codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "",
  193. codebaseIndexOpenAiCompatibleApiKey: "",
  194. codebaseIndexGeminiApiKey: "",
  195. codebaseIndexMistralApiKey: "",
  196. }
  197. setInitialSettings(settings)
  198. setCurrentSettings(settings)
  199. // Request secret status to check if secrets exist
  200. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  201. }
  202. }, [codebaseIndexConfig])
  203. // Request initial indexing status
  204. useEffect(() => {
  205. if (open) {
  206. vscode.postMessage({ type: "requestIndexingStatus" })
  207. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  208. }
  209. const handleMessage = (event: MessageEvent) => {
  210. if (event.data.type === "workspaceUpdated") {
  211. // When workspace changes, request updated indexing status
  212. if (open) {
  213. vscode.postMessage({ type: "requestIndexingStatus" })
  214. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  215. }
  216. }
  217. }
  218. window.addEventListener("message", handleMessage)
  219. return () => window.removeEventListener("message", handleMessage)
  220. }, [open])
  221. // Use a ref to capture current settings for the save handler
  222. const currentSettingsRef = useRef(currentSettings)
  223. currentSettingsRef.current = currentSettings
  224. // Listen for indexing status updates and save responses
  225. useEffect(() => {
  226. const handleMessage = (event: MessageEvent<any>) => {
  227. if (event.data.type === "indexingStatusUpdate") {
  228. if (!event.data.values.workspacePath || event.data.values.workspacePath === cwd) {
  229. setIndexingStatus({
  230. systemStatus: event.data.values.systemStatus,
  231. message: event.data.values.message || "",
  232. processedItems: event.data.values.processedItems,
  233. totalItems: event.data.values.totalItems,
  234. currentItemUnit: event.data.values.currentItemUnit || "items",
  235. })
  236. }
  237. } else if (event.data.type === "codeIndexSettingsSaved") {
  238. if (event.data.success) {
  239. setSaveStatus("saved")
  240. // Update initial settings to match current settings after successful save
  241. // This ensures hasUnsavedChanges becomes false
  242. const savedSettings = { ...currentSettingsRef.current }
  243. setInitialSettings(savedSettings)
  244. // Also update current settings to maintain consistency
  245. setCurrentSettings(savedSettings)
  246. // Request secret status to ensure we have the latest state
  247. // This is important to maintain placeholder display after save
  248. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  249. setSaveStatus("idle")
  250. } else {
  251. setSaveStatus("error")
  252. setSaveError(event.data.error || t("settings:codeIndex.saveError"))
  253. // Clear error message after 5 seconds
  254. setSaveStatus("idle")
  255. setSaveError(null)
  256. }
  257. }
  258. }
  259. window.addEventListener("message", handleMessage)
  260. return () => window.removeEventListener("message", handleMessage)
  261. }, [t, cwd])
  262. // Listen for secret status
  263. useEffect(() => {
  264. const handleMessage = (event: MessageEvent) => {
  265. if (event.data.type === "codeIndexSecretStatus") {
  266. // Update settings to show placeholders for existing secrets
  267. const secretStatus = event.data.values
  268. // Update both current and initial settings based on what secrets exist
  269. const updateWithSecrets = (prev: LocalCodeIndexSettings): LocalCodeIndexSettings => {
  270. const updated = { ...prev }
  271. // Only update to placeholder if the field is currently empty or already a placeholder
  272. // This preserves user input when they're actively editing
  273. if (!prev.codeIndexOpenAiKey || prev.codeIndexOpenAiKey === SECRET_PLACEHOLDER) {
  274. updated.codeIndexOpenAiKey = secretStatus.hasOpenAiKey ? SECRET_PLACEHOLDER : ""
  275. }
  276. if (!prev.codeIndexQdrantApiKey || prev.codeIndexQdrantApiKey === SECRET_PLACEHOLDER) {
  277. updated.codeIndexQdrantApiKey = secretStatus.hasQdrantApiKey ? SECRET_PLACEHOLDER : ""
  278. }
  279. if (
  280. !prev.codebaseIndexOpenAiCompatibleApiKey ||
  281. prev.codebaseIndexOpenAiCompatibleApiKey === SECRET_PLACEHOLDER
  282. ) {
  283. updated.codebaseIndexOpenAiCompatibleApiKey = secretStatus.hasOpenAiCompatibleApiKey
  284. ? SECRET_PLACEHOLDER
  285. : ""
  286. }
  287. if (!prev.codebaseIndexGeminiApiKey || prev.codebaseIndexGeminiApiKey === SECRET_PLACEHOLDER) {
  288. updated.codebaseIndexGeminiApiKey = secretStatus.hasGeminiApiKey ? SECRET_PLACEHOLDER : ""
  289. }
  290. if (!prev.codebaseIndexMistralApiKey || prev.codebaseIndexMistralApiKey === SECRET_PLACEHOLDER) {
  291. updated.codebaseIndexMistralApiKey = secretStatus.hasMistralApiKey ? SECRET_PLACEHOLDER : ""
  292. }
  293. return updated
  294. }
  295. // Only update settings if we're not in the middle of saving
  296. // After save is complete (saved status), we still want to update to maintain consistency
  297. if (saveStatus === "idle" || saveStatus === "saved") {
  298. setCurrentSettings(updateWithSecrets)
  299. setInitialSettings(updateWithSecrets)
  300. }
  301. }
  302. }
  303. window.addEventListener("message", handleMessage)
  304. return () => window.removeEventListener("message", handleMessage)
  305. }, [saveStatus])
  306. // Generic comparison function that detects changes between initial and current settings
  307. const hasUnsavedChanges = useMemo(() => {
  308. // Get all keys from both objects to handle any field
  309. const allKeys = [...Object.keys(initialSettings), ...Object.keys(currentSettings)] as Array<
  310. keyof LocalCodeIndexSettings
  311. >
  312. // Use a Set to ensure unique keys
  313. const uniqueKeys = Array.from(new Set(allKeys))
  314. for (const key of uniqueKeys) {
  315. const currentValue = currentSettings[key]
  316. const initialValue = initialSettings[key]
  317. // For secret fields, check if the value has been modified from placeholder
  318. if (currentValue === SECRET_PLACEHOLDER) {
  319. // If it's still showing placeholder, no change
  320. continue
  321. }
  322. // Compare values - handles all types including undefined
  323. if (currentValue !== initialValue) {
  324. return true
  325. }
  326. }
  327. return false
  328. }, [currentSettings, initialSettings])
  329. const updateSetting = (key: keyof LocalCodeIndexSettings, value: any) => {
  330. setCurrentSettings((prev) => ({ ...prev, [key]: value }))
  331. // Clear validation error for this field when user starts typing
  332. if (formErrors[key]) {
  333. setFormErrors((prev) => {
  334. const newErrors = { ...prev }
  335. delete newErrors[key]
  336. return newErrors
  337. })
  338. }
  339. }
  340. // Validation function
  341. const validateSettings = (): boolean => {
  342. const schema = createValidationSchema(currentSettings.codebaseIndexEmbedderProvider, t)
  343. // Prepare data for validation
  344. const dataToValidate: any = {}
  345. for (const [key, value] of Object.entries(currentSettings)) {
  346. // For secret fields with placeholder values, treat them as valid (they exist in backend)
  347. if (value === SECRET_PLACEHOLDER) {
  348. // Add a dummy value that will pass validation for these fields
  349. if (
  350. key === "codeIndexOpenAiKey" ||
  351. key === "codebaseIndexOpenAiCompatibleApiKey" ||
  352. key === "codebaseIndexGeminiApiKey" ||
  353. key === "codebaseIndexMistralApiKey"
  354. ) {
  355. dataToValidate[key] = "placeholder-valid"
  356. }
  357. } else {
  358. dataToValidate[key] = value
  359. }
  360. }
  361. try {
  362. // Validate using the schema
  363. schema.parse(dataToValidate)
  364. setFormErrors({})
  365. return true
  366. } catch (error) {
  367. if (error instanceof z.ZodError) {
  368. const errors: Record<string, string> = {}
  369. error.errors.forEach((err) => {
  370. if (err.path[0]) {
  371. errors[err.path[0] as string] = err.message
  372. }
  373. })
  374. setFormErrors(errors)
  375. }
  376. return false
  377. }
  378. }
  379. // Discard changes functionality
  380. const checkUnsavedChanges = useCallback(
  381. (then: () => void) => {
  382. if (hasUnsavedChanges) {
  383. confirmDialogHandler.current = then
  384. setDiscardDialogShow(true)
  385. } else {
  386. then()
  387. }
  388. },
  389. [hasUnsavedChanges],
  390. )
  391. const onConfirmDialogResult = useCallback(
  392. (confirm: boolean) => {
  393. if (confirm) {
  394. // Discard changes: Reset to initial settings
  395. setCurrentSettings(initialSettings)
  396. setFormErrors({}) // Clear any validation errors
  397. confirmDialogHandler.current?.() // Execute the pending action (e.g., close popover)
  398. }
  399. setDiscardDialogShow(false)
  400. },
  401. [initialSettings],
  402. )
  403. // Handle popover close with unsaved changes check
  404. const handlePopoverClose = useCallback(() => {
  405. checkUnsavedChanges(() => {
  406. setOpen(false)
  407. })
  408. }, [checkUnsavedChanges])
  409. // Use the shared ESC key handler hook - respects unsaved changes logic
  410. useEscapeKey(open, handlePopoverClose)
  411. const handleSaveSettings = () => {
  412. // Validate settings before saving
  413. if (!validateSettings()) {
  414. return
  415. }
  416. setSaveStatus("saving")
  417. setSaveError(null)
  418. // Prepare settings to save
  419. const settingsToSave: any = {}
  420. // Iterate through all current settings
  421. for (const [key, value] of Object.entries(currentSettings)) {
  422. // For secret fields with placeholder, don't send the placeholder
  423. // but also don't send an empty string - just skip the field
  424. // This tells the backend to keep the existing secret
  425. if (value === SECRET_PLACEHOLDER) {
  426. // Skip sending placeholder values - backend will preserve existing secrets
  427. continue
  428. }
  429. // Include all other fields, including empty strings (which clear secrets)
  430. settingsToSave[key] = value
  431. }
  432. // Always include codebaseIndexEnabled to ensure it's persisted
  433. settingsToSave.codebaseIndexEnabled = currentSettings.codebaseIndexEnabled
  434. // Save settings to backend
  435. vscode.postMessage({
  436. type: "saveCodeIndexSettingsAtomic",
  437. codeIndexSettings: settingsToSave,
  438. })
  439. }
  440. const progressPercentage = useMemo(
  441. () =>
  442. indexingStatus.totalItems > 0
  443. ? Math.round((indexingStatus.processedItems / indexingStatus.totalItems) * 100)
  444. : 0,
  445. [indexingStatus.processedItems, indexingStatus.totalItems],
  446. )
  447. const transformStyleString = `translateX(-${100 - progressPercentage}%)`
  448. const getAvailableModels = () => {
  449. if (!codebaseIndexModels) return []
  450. const models = codebaseIndexModels[currentSettings.codebaseIndexEmbedderProvider]
  451. return models ? Object.keys(models) : []
  452. }
  453. const portalContainer = useRooPortal("roo-portal")
  454. return (
  455. <>
  456. <Popover
  457. open={open}
  458. onOpenChange={(newOpen) => {
  459. if (!newOpen) {
  460. // User is trying to close the popover
  461. handlePopoverClose()
  462. } else {
  463. setOpen(newOpen)
  464. }
  465. }}>
  466. <PopoverTrigger asChild>{children}</PopoverTrigger>
  467. <PopoverContent
  468. className="w-[calc(100vw-32px)] max-w-[450px] max-h-[80vh] overflow-y-auto p-0"
  469. align="end"
  470. alignOffset={0}
  471. side="bottom"
  472. sideOffset={5}
  473. collisionPadding={16}
  474. avoidCollisions={true}
  475. container={portalContainer}>
  476. <div className="p-3 border-b border-vscode-dropdown-border cursor-default">
  477. <div className="flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full">
  478. <h4 className="m-0 pb-2 flex-1">{t("settings:codeIndex.title")}</h4>
  479. </div>
  480. <p className="my-0 pr-4 text-sm w-full">
  481. <Trans i18nKey="settings:codeIndex.description">
  482. <VSCodeLink
  483. href={buildDocLink("features/experimental/codebase-indexing", "settings")}
  484. style={{ display: "inline" }}
  485. />
  486. </Trans>
  487. </p>
  488. </div>
  489. <div className="p-4">
  490. {/* Enable/Disable Toggle */}
  491. <div className="mb-4">
  492. <div className="flex items-center gap-2">
  493. <VSCodeCheckbox
  494. checked={currentSettings.codebaseIndexEnabled}
  495. onChange={(e: any) => updateSetting("codebaseIndexEnabled", e.target.checked)}>
  496. <span className="font-medium">{t("settings:codeIndex.enableLabel")}</span>
  497. </VSCodeCheckbox>
  498. <StandardTooltip content={t("settings:codeIndex.enableDescription")}>
  499. <span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
  500. </StandardTooltip>
  501. </div>
  502. </div>
  503. {/* Status Section */}
  504. <div className="space-y-2">
  505. <h4 className="text-sm font-medium">{t("settings:codeIndex.statusTitle")}</h4>
  506. <div className="text-sm text-vscode-descriptionForeground">
  507. <span
  508. className={cn("inline-block w-3 h-3 rounded-full mr-2", {
  509. "bg-gray-400": indexingStatus.systemStatus === "Standby",
  510. "bg-yellow-500 animate-pulse": indexingStatus.systemStatus === "Indexing",
  511. "bg-green-500": indexingStatus.systemStatus === "Indexed",
  512. "bg-red-500": indexingStatus.systemStatus === "Error",
  513. })}
  514. />
  515. {t(`settings:codeIndex.indexingStatuses.${indexingStatus.systemStatus.toLowerCase()}`)}
  516. {indexingStatus.message ? ` - ${indexingStatus.message}` : ""}
  517. </div>
  518. {indexingStatus.systemStatus === "Indexing" && (
  519. <div className="mt-2">
  520. <ProgressPrimitive.Root
  521. className="relative h-2 w-full overflow-hidden rounded-full bg-secondary"
  522. value={progressPercentage}>
  523. <ProgressPrimitive.Indicator
  524. className="h-full w-full flex-1 bg-primary transition-transform duration-300 ease-in-out"
  525. style={{
  526. transform: transformStyleString,
  527. }}
  528. />
  529. </ProgressPrimitive.Root>
  530. </div>
  531. )}
  532. </div>
  533. {/* Setup Settings Disclosure */}
  534. <div className="mt-4">
  535. <button
  536. onClick={() => setIsSetupSettingsOpen(!isSetupSettingsOpen)}
  537. className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
  538. aria-expanded={isSetupSettingsOpen}>
  539. <span
  540. className={`codicon codicon-${isSetupSettingsOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
  541. <span className="text-base font-semibold">
  542. {t("settings:codeIndex.setupConfigLabel")}
  543. </span>
  544. </button>
  545. {isSetupSettingsOpen && (
  546. <div className="mt-4 space-y-4">
  547. {/* Embedder Provider Section */}
  548. <div className="space-y-2">
  549. <label className="text-sm font-medium">
  550. {t("settings:codeIndex.embedderProviderLabel")}
  551. </label>
  552. <Select
  553. value={currentSettings.codebaseIndexEmbedderProvider}
  554. onValueChange={(value: EmbedderProvider) => {
  555. updateSetting("codebaseIndexEmbedderProvider", value)
  556. // Clear model selection when switching providers
  557. updateSetting("codebaseIndexEmbedderModelId", "")
  558. }}>
  559. <SelectTrigger className="w-full">
  560. <SelectValue />
  561. </SelectTrigger>
  562. <SelectContent>
  563. <SelectItem value="openai">
  564. {t("settings:codeIndex.openaiProvider")}
  565. </SelectItem>
  566. <SelectItem value="ollama">
  567. {t("settings:codeIndex.ollamaProvider")}
  568. </SelectItem>
  569. <SelectItem value="openai-compatible">
  570. {t("settings:codeIndex.openaiCompatibleProvider")}
  571. </SelectItem>
  572. <SelectItem value="gemini">
  573. {t("settings:codeIndex.geminiProvider")}
  574. </SelectItem>
  575. <SelectItem value="mistral">
  576. {t("settings:codeIndex.mistralProvider")}
  577. </SelectItem>
  578. </SelectContent>
  579. </Select>
  580. </div>
  581. {/* Provider-specific settings */}
  582. {currentSettings.codebaseIndexEmbedderProvider === "openai" && (
  583. <>
  584. <div className="space-y-2">
  585. <label className="text-sm font-medium">
  586. {t("settings:codeIndex.openAiKeyLabel")}
  587. </label>
  588. <VSCodeTextField
  589. type="password"
  590. value={currentSettings.codeIndexOpenAiKey || ""}
  591. onInput={(e: any) =>
  592. updateSetting("codeIndexOpenAiKey", e.target.value)
  593. }
  594. placeholder={t("settings:codeIndex.openAiKeyPlaceholder")}
  595. className={cn("w-full", {
  596. "border-red-500": formErrors.codeIndexOpenAiKey,
  597. })}
  598. />
  599. {formErrors.codeIndexOpenAiKey && (
  600. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  601. {formErrors.codeIndexOpenAiKey}
  602. </p>
  603. )}
  604. </div>
  605. <div className="space-y-2">
  606. <label className="text-sm font-medium">
  607. {t("settings:codeIndex.modelLabel")}
  608. </label>
  609. <VSCodeDropdown
  610. value={currentSettings.codebaseIndexEmbedderModelId}
  611. onChange={(e: any) =>
  612. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  613. }
  614. className={cn("w-full", {
  615. "border-red-500": formErrors.codebaseIndexEmbedderModelId,
  616. })}>
  617. <VSCodeOption value="" className="p-2">
  618. {t("settings:codeIndex.selectModel")}
  619. </VSCodeOption>
  620. {getAvailableModels().map((modelId) => {
  621. const model =
  622. codebaseIndexModels?.[
  623. currentSettings.codebaseIndexEmbedderProvider
  624. ]?.[modelId]
  625. return (
  626. <VSCodeOption key={modelId} value={modelId} className="p-2">
  627. {modelId}{" "}
  628. {model
  629. ? t("settings:codeIndex.modelDimensions", {
  630. dimension: model.dimension,
  631. })
  632. : ""}
  633. </VSCodeOption>
  634. )
  635. })}
  636. </VSCodeDropdown>
  637. {formErrors.codebaseIndexEmbedderModelId && (
  638. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  639. {formErrors.codebaseIndexEmbedderModelId}
  640. </p>
  641. )}
  642. </div>
  643. </>
  644. )}
  645. {currentSettings.codebaseIndexEmbedderProvider === "ollama" && (
  646. <>
  647. <div className="space-y-2">
  648. <label className="text-sm font-medium">
  649. {t("settings:codeIndex.ollamaBaseUrlLabel")}
  650. </label>
  651. <VSCodeTextField
  652. value={currentSettings.codebaseIndexEmbedderBaseUrl || ""}
  653. onInput={(e: any) =>
  654. updateSetting("codebaseIndexEmbedderBaseUrl", e.target.value)
  655. }
  656. onBlur={(e: any) => {
  657. // Set default Ollama URL if field is empty
  658. if (!e.target.value.trim()) {
  659. e.target.value = DEFAULT_OLLAMA_URL
  660. updateSetting(
  661. "codebaseIndexEmbedderBaseUrl",
  662. DEFAULT_OLLAMA_URL,
  663. )
  664. }
  665. }}
  666. placeholder={t("settings:codeIndex.ollamaUrlPlaceholder")}
  667. className={cn("w-full", {
  668. "border-red-500": formErrors.codebaseIndexEmbedderBaseUrl,
  669. })}
  670. />
  671. {formErrors.codebaseIndexEmbedderBaseUrl && (
  672. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  673. {formErrors.codebaseIndexEmbedderBaseUrl}
  674. </p>
  675. )}
  676. </div>
  677. <div className="space-y-2">
  678. <label className="text-sm font-medium">
  679. {t("settings:codeIndex.modelLabel")}
  680. </label>
  681. <VSCodeTextField
  682. value={currentSettings.codebaseIndexEmbedderModelId || ""}
  683. onInput={(e: any) =>
  684. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  685. }
  686. placeholder={t("settings:codeIndex.modelPlaceholder")}
  687. className={cn("w-full", {
  688. "border-red-500": formErrors.codebaseIndexEmbedderModelId,
  689. })}
  690. />
  691. {formErrors.codebaseIndexEmbedderModelId && (
  692. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  693. {formErrors.codebaseIndexEmbedderModelId}
  694. </p>
  695. )}
  696. </div>
  697. <div className="space-y-2">
  698. <label className="text-sm font-medium">
  699. {t("settings:codeIndex.modelDimensionLabel")}
  700. </label>
  701. <VSCodeTextField
  702. value={
  703. currentSettings.codebaseIndexEmbedderModelDimension?.toString() ||
  704. ""
  705. }
  706. onInput={(e: any) => {
  707. const value = e.target.value
  708. ? parseInt(e.target.value, 10) || undefined
  709. : undefined
  710. updateSetting("codebaseIndexEmbedderModelDimension", value)
  711. }}
  712. placeholder={t("settings:codeIndex.modelDimensionPlaceholder")}
  713. className={cn("w-full", {
  714. "border-red-500":
  715. formErrors.codebaseIndexEmbedderModelDimension,
  716. })}
  717. />
  718. {formErrors.codebaseIndexEmbedderModelDimension && (
  719. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  720. {formErrors.codebaseIndexEmbedderModelDimension}
  721. </p>
  722. )}
  723. </div>
  724. </>
  725. )}
  726. {currentSettings.codebaseIndexEmbedderProvider === "openai-compatible" && (
  727. <>
  728. <div className="space-y-2">
  729. <label className="text-sm font-medium">
  730. {t("settings:codeIndex.openAiCompatibleBaseUrlLabel")}
  731. </label>
  732. <VSCodeTextField
  733. value={currentSettings.codebaseIndexOpenAiCompatibleBaseUrl || ""}
  734. onInput={(e: any) =>
  735. updateSetting(
  736. "codebaseIndexOpenAiCompatibleBaseUrl",
  737. e.target.value,
  738. )
  739. }
  740. placeholder={t(
  741. "settings:codeIndex.openAiCompatibleBaseUrlPlaceholder",
  742. )}
  743. className={cn("w-full", {
  744. "border-red-500":
  745. formErrors.codebaseIndexOpenAiCompatibleBaseUrl,
  746. })}
  747. />
  748. {formErrors.codebaseIndexOpenAiCompatibleBaseUrl && (
  749. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  750. {formErrors.codebaseIndexOpenAiCompatibleBaseUrl}
  751. </p>
  752. )}
  753. </div>
  754. <div className="space-y-2">
  755. <label className="text-sm font-medium">
  756. {t("settings:codeIndex.openAiCompatibleApiKeyLabel")}
  757. </label>
  758. <VSCodeTextField
  759. type="password"
  760. value={currentSettings.codebaseIndexOpenAiCompatibleApiKey || ""}
  761. onInput={(e: any) =>
  762. updateSetting(
  763. "codebaseIndexOpenAiCompatibleApiKey",
  764. e.target.value,
  765. )
  766. }
  767. placeholder={t(
  768. "settings:codeIndex.openAiCompatibleApiKeyPlaceholder",
  769. )}
  770. className={cn("w-full", {
  771. "border-red-500":
  772. formErrors.codebaseIndexOpenAiCompatibleApiKey,
  773. })}
  774. />
  775. {formErrors.codebaseIndexOpenAiCompatibleApiKey && (
  776. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  777. {formErrors.codebaseIndexOpenAiCompatibleApiKey}
  778. </p>
  779. )}
  780. </div>
  781. <div className="space-y-2">
  782. <label className="text-sm font-medium">
  783. {t("settings:codeIndex.modelLabel")}
  784. </label>
  785. <VSCodeTextField
  786. value={currentSettings.codebaseIndexEmbedderModelId || ""}
  787. onInput={(e: any) =>
  788. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  789. }
  790. placeholder={t("settings:codeIndex.modelPlaceholder")}
  791. className={cn("w-full", {
  792. "border-red-500": formErrors.codebaseIndexEmbedderModelId,
  793. })}
  794. />
  795. {formErrors.codebaseIndexEmbedderModelId && (
  796. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  797. {formErrors.codebaseIndexEmbedderModelId}
  798. </p>
  799. )}
  800. </div>
  801. <div className="space-y-2">
  802. <label className="text-sm font-medium">
  803. {t("settings:codeIndex.modelDimensionLabel")}
  804. </label>
  805. <VSCodeTextField
  806. value={
  807. currentSettings.codebaseIndexEmbedderModelDimension?.toString() ||
  808. ""
  809. }
  810. onInput={(e: any) => {
  811. const value = e.target.value
  812. ? parseInt(e.target.value, 10) || undefined
  813. : undefined
  814. updateSetting("codebaseIndexEmbedderModelDimension", value)
  815. }}
  816. placeholder={t("settings:codeIndex.modelDimensionPlaceholder")}
  817. className={cn("w-full", {
  818. "border-red-500":
  819. formErrors.codebaseIndexEmbedderModelDimension,
  820. })}
  821. />
  822. {formErrors.codebaseIndexEmbedderModelDimension && (
  823. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  824. {formErrors.codebaseIndexEmbedderModelDimension}
  825. </p>
  826. )}
  827. </div>
  828. </>
  829. )}
  830. {currentSettings.codebaseIndexEmbedderProvider === "gemini" && (
  831. <>
  832. <div className="space-y-2">
  833. <label className="text-sm font-medium">
  834. {t("settings:codeIndex.geminiApiKeyLabel")}
  835. </label>
  836. <VSCodeTextField
  837. type="password"
  838. value={currentSettings.codebaseIndexGeminiApiKey || ""}
  839. onInput={(e: any) =>
  840. updateSetting("codebaseIndexGeminiApiKey", e.target.value)
  841. }
  842. placeholder={t("settings:codeIndex.geminiApiKeyPlaceholder")}
  843. className={cn("w-full", {
  844. "border-red-500": formErrors.codebaseIndexGeminiApiKey,
  845. })}
  846. />
  847. {formErrors.codebaseIndexGeminiApiKey && (
  848. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  849. {formErrors.codebaseIndexGeminiApiKey}
  850. </p>
  851. )}
  852. </div>
  853. <div className="space-y-2">
  854. <label className="text-sm font-medium">
  855. {t("settings:codeIndex.modelLabel")}
  856. </label>
  857. <VSCodeDropdown
  858. value={currentSettings.codebaseIndexEmbedderModelId}
  859. onChange={(e: any) =>
  860. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  861. }
  862. className={cn("w-full", {
  863. "border-red-500": formErrors.codebaseIndexEmbedderModelId,
  864. })}>
  865. <VSCodeOption value="" className="p-2">
  866. {t("settings:codeIndex.selectModel")}
  867. </VSCodeOption>
  868. {getAvailableModels().map((modelId) => {
  869. const model =
  870. codebaseIndexModels?.[
  871. currentSettings.codebaseIndexEmbedderProvider
  872. ]?.[modelId]
  873. return (
  874. <VSCodeOption key={modelId} value={modelId} className="p-2">
  875. {modelId}{" "}
  876. {model
  877. ? t("settings:codeIndex.modelDimensions", {
  878. dimension: model.dimension,
  879. })
  880. : ""}
  881. </VSCodeOption>
  882. )
  883. })}
  884. </VSCodeDropdown>
  885. {formErrors.codebaseIndexEmbedderModelId && (
  886. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  887. {formErrors.codebaseIndexEmbedderModelId}
  888. </p>
  889. )}
  890. </div>
  891. </>
  892. )}
  893. {currentSettings.codebaseIndexEmbedderProvider === "mistral" && (
  894. <>
  895. <div className="space-y-2">
  896. <label className="text-sm font-medium">
  897. {t("settings:codeIndex.mistralApiKeyLabel")}
  898. </label>
  899. <VSCodeTextField
  900. type="password"
  901. value={currentSettings.codebaseIndexMistralApiKey || ""}
  902. onInput={(e: any) =>
  903. updateSetting("codebaseIndexMistralApiKey", e.target.value)
  904. }
  905. placeholder={t("settings:codeIndex.mistralApiKeyPlaceholder")}
  906. className={cn("w-full", {
  907. "border-red-500": formErrors.codebaseIndexMistralApiKey,
  908. })}
  909. />
  910. {formErrors.codebaseIndexMistralApiKey && (
  911. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  912. {formErrors.codebaseIndexMistralApiKey}
  913. </p>
  914. )}
  915. </div>
  916. <div className="space-y-2">
  917. <label className="text-sm font-medium">
  918. {t("settings:codeIndex.modelLabel")}
  919. </label>
  920. <VSCodeDropdown
  921. value={currentSettings.codebaseIndexEmbedderModelId}
  922. onChange={(e: any) =>
  923. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  924. }
  925. className={cn("w-full", {
  926. "border-red-500": formErrors.codebaseIndexEmbedderModelId,
  927. })}>
  928. <VSCodeOption value="" className="p-2">
  929. {t("settings:codeIndex.selectModel")}
  930. </VSCodeOption>
  931. {getAvailableModels().map((modelId) => {
  932. const model =
  933. codebaseIndexModels?.[
  934. currentSettings.codebaseIndexEmbedderProvider
  935. ]?.[modelId]
  936. return (
  937. <VSCodeOption key={modelId} value={modelId} className="p-2">
  938. {modelId}{" "}
  939. {model
  940. ? t("settings:codeIndex.modelDimensions", {
  941. dimension: model.dimension,
  942. })
  943. : ""}
  944. </VSCodeOption>
  945. )
  946. })}
  947. </VSCodeDropdown>
  948. {formErrors.codebaseIndexEmbedderModelId && (
  949. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  950. {formErrors.codebaseIndexEmbedderModelId}
  951. </p>
  952. )}
  953. </div>
  954. </>
  955. )}
  956. {/* Qdrant Settings */}
  957. <div className="space-y-2">
  958. <label className="text-sm font-medium">
  959. {t("settings:codeIndex.qdrantUrlLabel")}
  960. </label>
  961. <VSCodeTextField
  962. value={currentSettings.codebaseIndexQdrantUrl || ""}
  963. onInput={(e: any) =>
  964. updateSetting("codebaseIndexQdrantUrl", e.target.value)
  965. }
  966. onBlur={(e: any) => {
  967. // Set default Qdrant URL if field is empty
  968. if (!e.target.value.trim()) {
  969. currentSettings.codebaseIndexQdrantUrl = DEFAULT_QDRANT_URL
  970. updateSetting("codebaseIndexQdrantUrl", DEFAULT_QDRANT_URL)
  971. }
  972. }}
  973. placeholder={t("settings:codeIndex.qdrantUrlPlaceholder")}
  974. className={cn("w-full", {
  975. "border-red-500": formErrors.codebaseIndexQdrantUrl,
  976. })}
  977. />
  978. {formErrors.codebaseIndexQdrantUrl && (
  979. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  980. {formErrors.codebaseIndexQdrantUrl}
  981. </p>
  982. )}
  983. </div>
  984. <div className="space-y-2">
  985. <label className="text-sm font-medium">
  986. {t("settings:codeIndex.qdrantApiKeyLabel")}
  987. </label>
  988. <VSCodeTextField
  989. type="password"
  990. value={currentSettings.codeIndexQdrantApiKey || ""}
  991. onInput={(e: any) => updateSetting("codeIndexQdrantApiKey", e.target.value)}
  992. placeholder={t("settings:codeIndex.qdrantApiKeyPlaceholder")}
  993. className={cn("w-full", {
  994. "border-red-500": formErrors.codeIndexQdrantApiKey,
  995. })}
  996. />
  997. {formErrors.codeIndexQdrantApiKey && (
  998. <p className="text-xs text-vscode-errorForeground mt-1 mb-0">
  999. {formErrors.codeIndexQdrantApiKey}
  1000. </p>
  1001. )}
  1002. </div>
  1003. </div>
  1004. )}
  1005. </div>
  1006. {/* Advanced Settings Disclosure */}
  1007. <div className="mt-4">
  1008. <button
  1009. onClick={() => setIsAdvancedSettingsOpen(!isAdvancedSettingsOpen)}
  1010. className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
  1011. aria-expanded={isAdvancedSettingsOpen}>
  1012. <span
  1013. className={`codicon codicon-${isAdvancedSettingsOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
  1014. <span className="text-base font-semibold">
  1015. {t("settings:codeIndex.advancedConfigLabel")}
  1016. </span>
  1017. </button>
  1018. {isAdvancedSettingsOpen && (
  1019. <div className="mt-4 space-y-4">
  1020. {/* Search Score Threshold Slider */}
  1021. <div className="space-y-2">
  1022. <div className="flex items-center gap-2">
  1023. <label className="text-sm font-medium">
  1024. {t("settings:codeIndex.searchMinScoreLabel")}
  1025. </label>
  1026. <StandardTooltip
  1027. content={t("settings:codeIndex.searchMinScoreDescription")}>
  1028. <span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
  1029. </StandardTooltip>
  1030. </div>
  1031. <div className="flex items-center gap-2">
  1032. <Slider
  1033. min={CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_SCORE}
  1034. max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_SCORE}
  1035. step={CODEBASE_INDEX_DEFAULTS.SEARCH_SCORE_STEP}
  1036. value={[
  1037. currentSettings.codebaseIndexSearchMinScore ??
  1038. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  1039. ]}
  1040. onValueChange={(values) =>
  1041. updateSetting("codebaseIndexSearchMinScore", values[0])
  1042. }
  1043. className="flex-1"
  1044. data-testid="search-min-score-slider"
  1045. />
  1046. <span className="w-12 text-center">
  1047. {(
  1048. currentSettings.codebaseIndexSearchMinScore ??
  1049. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE
  1050. ).toFixed(2)}
  1051. </span>
  1052. <VSCodeButton
  1053. appearance="icon"
  1054. title={t("settings:codeIndex.resetToDefault")}
  1055. onClick={() =>
  1056. updateSetting(
  1057. "codebaseIndexSearchMinScore",
  1058. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  1059. )
  1060. }>
  1061. <span className="codicon codicon-discard" />
  1062. </VSCodeButton>
  1063. </div>
  1064. </div>
  1065. {/* Maximum Search Results Slider */}
  1066. <div className="space-y-2">
  1067. <div className="flex items-center gap-2">
  1068. <label className="text-sm font-medium">
  1069. {t("settings:codeIndex.searchMaxResultsLabel")}
  1070. </label>
  1071. <StandardTooltip
  1072. content={t("settings:codeIndex.searchMaxResultsDescription")}>
  1073. <span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
  1074. </StandardTooltip>
  1075. </div>
  1076. <div className="flex items-center gap-2">
  1077. <Slider
  1078. min={CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS}
  1079. max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS}
  1080. step={CODEBASE_INDEX_DEFAULTS.SEARCH_RESULTS_STEP}
  1081. value={[
  1082. currentSettings.codebaseIndexSearchMaxResults ??
  1083. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  1084. ]}
  1085. onValueChange={(values) =>
  1086. updateSetting("codebaseIndexSearchMaxResults", values[0])
  1087. }
  1088. className="flex-1"
  1089. data-testid="search-max-results-slider"
  1090. />
  1091. <span className="w-12 text-center">
  1092. {currentSettings.codebaseIndexSearchMaxResults ??
  1093. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS}
  1094. </span>
  1095. <VSCodeButton
  1096. appearance="icon"
  1097. title={t("settings:codeIndex.resetToDefault")}
  1098. onClick={() =>
  1099. updateSetting(
  1100. "codebaseIndexSearchMaxResults",
  1101. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  1102. )
  1103. }>
  1104. <span className="codicon codicon-discard" />
  1105. </VSCodeButton>
  1106. </div>
  1107. </div>
  1108. </div>
  1109. )}
  1110. </div>
  1111. {/* Action Buttons */}
  1112. <div className="flex items-center justify-between gap-2 pt-6">
  1113. <div className="flex gap-2">
  1114. {currentSettings.codebaseIndexEnabled &&
  1115. (indexingStatus.systemStatus === "Error" ||
  1116. indexingStatus.systemStatus === "Standby") && (
  1117. <VSCodeButton
  1118. onClick={() => vscode.postMessage({ type: "startIndexing" })}
  1119. disabled={saveStatus === "saving" || hasUnsavedChanges}>
  1120. {t("settings:codeIndex.startIndexingButton")}
  1121. </VSCodeButton>
  1122. )}
  1123. {currentSettings.codebaseIndexEnabled &&
  1124. (indexingStatus.systemStatus === "Indexed" ||
  1125. indexingStatus.systemStatus === "Error") && (
  1126. <AlertDialog>
  1127. <AlertDialogTrigger asChild>
  1128. <VSCodeButton appearance="secondary">
  1129. {t("settings:codeIndex.clearIndexDataButton")}
  1130. </VSCodeButton>
  1131. </AlertDialogTrigger>
  1132. <AlertDialogContent>
  1133. <AlertDialogHeader>
  1134. <AlertDialogTitle>
  1135. {t("settings:codeIndex.clearDataDialog.title")}
  1136. </AlertDialogTitle>
  1137. <AlertDialogDescription>
  1138. {t("settings:codeIndex.clearDataDialog.description")}
  1139. </AlertDialogDescription>
  1140. </AlertDialogHeader>
  1141. <AlertDialogFooter>
  1142. <AlertDialogCancel>
  1143. {t("settings:codeIndex.clearDataDialog.cancelButton")}
  1144. </AlertDialogCancel>
  1145. <AlertDialogAction
  1146. onClick={() => vscode.postMessage({ type: "clearIndexData" })}>
  1147. {t("settings:codeIndex.clearDataDialog.confirmButton")}
  1148. </AlertDialogAction>
  1149. </AlertDialogFooter>
  1150. </AlertDialogContent>
  1151. </AlertDialog>
  1152. )}
  1153. </div>
  1154. <VSCodeButton
  1155. onClick={handleSaveSettings}
  1156. disabled={!hasUnsavedChanges || saveStatus === "saving"}>
  1157. {saveStatus === "saving"
  1158. ? t("settings:codeIndex.saving")
  1159. : t("settings:codeIndex.saveSettings")}
  1160. </VSCodeButton>
  1161. </div>
  1162. {/* Save Status Messages */}
  1163. {saveStatus === "error" && (
  1164. <div className="mt-2">
  1165. <span className="text-sm text-vscode-errorForeground block">
  1166. {saveError || t("settings:codeIndex.saveError")}
  1167. </span>
  1168. </div>
  1169. )}
  1170. </div>
  1171. </PopoverContent>
  1172. </Popover>
  1173. {/* Discard Changes Dialog */}
  1174. <AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
  1175. <AlertDialogContent>
  1176. <AlertDialogHeader>
  1177. <AlertDialogTitle className="flex items-center gap-2">
  1178. <AlertTriangle className="w-5 h-5 text-yellow-500" />
  1179. {t("settings:unsavedChangesDialog.title")}
  1180. </AlertDialogTitle>
  1181. <AlertDialogDescription>
  1182. {t("settings:unsavedChangesDialog.description")}
  1183. </AlertDialogDescription>
  1184. </AlertDialogHeader>
  1185. <AlertDialogFooter>
  1186. <AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>
  1187. {t("settings:unsavedChangesDialog.cancelButton")}
  1188. </AlertDialogCancel>
  1189. <AlertDialogAction onClick={() => onConfirmDialogResult(true)}>
  1190. {t("settings:unsavedChangesDialog.discardButton")}
  1191. </AlertDialogAction>
  1192. </AlertDialogFooter>
  1193. </AlertDialogContent>
  1194. </AlertDialog>
  1195. </>
  1196. )
  1197. }