CodeIndexPopover.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. import React, { useState, useEffect, useMemo } from "react"
  2. import { Trans } from "react-i18next"
  3. import {
  4. VSCodeButton,
  5. VSCodeTextField,
  6. VSCodeDropdown,
  7. VSCodeOption,
  8. VSCodeLink,
  9. } from "@vscode/webview-ui-toolkit/react"
  10. import * as ProgressPrimitive from "@radix-ui/react-progress"
  11. import { vscode } from "@src/utils/vscode"
  12. import { useExtensionState } from "@src/context/ExtensionStateContext"
  13. import { useAppTranslation } from "@src/i18n/TranslationContext"
  14. import { buildDocLink } from "@src/utils/docLinks"
  15. import { cn } from "@src/lib/utils"
  16. import {
  17. Select,
  18. SelectContent,
  19. SelectItem,
  20. SelectTrigger,
  21. SelectValue,
  22. AlertDialog,
  23. AlertDialogAction,
  24. AlertDialogCancel,
  25. AlertDialogContent,
  26. AlertDialogDescription,
  27. AlertDialogFooter,
  28. AlertDialogHeader,
  29. AlertDialogTitle,
  30. AlertDialogTrigger,
  31. Popover,
  32. PopoverContent,
  33. PopoverTrigger,
  34. Slider,
  35. StandardTooltip,
  36. } from "@src/components/ui"
  37. import { useRooPortal } from "@src/components/ui/hooks/useRooPortal"
  38. import type { EmbedderProvider } from "@roo/embeddingModels"
  39. import type { IndexingStatus } from "@roo/ExtensionMessage"
  40. import { CODEBASE_INDEX_DEFAULTS } from "@roo-code/types"
  41. interface CodeIndexPopoverProps {
  42. children: React.ReactNode
  43. indexingStatus: IndexingStatus
  44. }
  45. interface LocalCodeIndexSettings {
  46. // Global state settings
  47. codebaseIndexEnabled: boolean
  48. codebaseIndexQdrantUrl: string
  49. codebaseIndexEmbedderProvider: EmbedderProvider
  50. codebaseIndexEmbedderBaseUrl?: string
  51. codebaseIndexEmbedderModelId: string
  52. codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers
  53. codebaseIndexSearchMaxResults?: number
  54. codebaseIndexSearchMinScore?: number
  55. // Secret settings (start empty, will be loaded separately)
  56. codeIndexOpenAiKey?: string
  57. codeIndexQdrantApiKey?: string
  58. codebaseIndexOpenAiCompatibleBaseUrl?: string
  59. codebaseIndexOpenAiCompatibleApiKey?: string
  60. codebaseIndexGeminiApiKey?: string
  61. }
  62. export const CodeIndexPopover: React.FC<CodeIndexPopoverProps> = ({
  63. children,
  64. indexingStatus: externalIndexingStatus,
  65. }) => {
  66. const SECRET_PLACEHOLDER = "••••••••••••••••"
  67. const { t } = useAppTranslation()
  68. const { codebaseIndexConfig, codebaseIndexModels } = useExtensionState()
  69. const [open, setOpen] = useState(false)
  70. const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false)
  71. const [isSetupSettingsOpen, setIsSetupSettingsOpen] = useState(false)
  72. const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>(externalIndexingStatus)
  73. const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle")
  74. const [saveError, setSaveError] = useState<string | null>(null)
  75. // Default settings template
  76. const getDefaultSettings = (): LocalCodeIndexSettings => ({
  77. codebaseIndexEnabled: true,
  78. codebaseIndexQdrantUrl: "",
  79. codebaseIndexEmbedderProvider: "openai",
  80. codebaseIndexEmbedderBaseUrl: "",
  81. codebaseIndexEmbedderModelId: "",
  82. codebaseIndexEmbedderModelDimension: undefined,
  83. codebaseIndexSearchMaxResults: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  84. codebaseIndexSearchMinScore: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  85. codeIndexOpenAiKey: "",
  86. codeIndexQdrantApiKey: "",
  87. codebaseIndexOpenAiCompatibleBaseUrl: "",
  88. codebaseIndexOpenAiCompatibleApiKey: "",
  89. codebaseIndexGeminiApiKey: "",
  90. })
  91. // Initial settings state - stores the settings when popover opens
  92. const [initialSettings, setInitialSettings] = useState<LocalCodeIndexSettings>(getDefaultSettings())
  93. // Current settings state - tracks user changes
  94. const [currentSettings, setCurrentSettings] = useState<LocalCodeIndexSettings>(getDefaultSettings())
  95. // Update indexing status from parent
  96. useEffect(() => {
  97. setIndexingStatus(externalIndexingStatus)
  98. }, [externalIndexingStatus])
  99. // Initialize settings from global state
  100. useEffect(() => {
  101. if (codebaseIndexConfig) {
  102. const settings = {
  103. codebaseIndexEnabled: codebaseIndexConfig.codebaseIndexEnabled ?? true,
  104. codebaseIndexQdrantUrl: codebaseIndexConfig.codebaseIndexQdrantUrl || "",
  105. codebaseIndexEmbedderProvider: codebaseIndexConfig.codebaseIndexEmbedderProvider || "openai",
  106. codebaseIndexEmbedderBaseUrl: codebaseIndexConfig.codebaseIndexEmbedderBaseUrl || "",
  107. codebaseIndexEmbedderModelId: codebaseIndexConfig.codebaseIndexEmbedderModelId || "",
  108. codebaseIndexEmbedderModelDimension:
  109. codebaseIndexConfig.codebaseIndexEmbedderModelDimension || undefined,
  110. codebaseIndexSearchMaxResults:
  111. codebaseIndexConfig.codebaseIndexSearchMaxResults ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  112. codebaseIndexSearchMinScore:
  113. codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  114. codeIndexOpenAiKey: "",
  115. codeIndexQdrantApiKey: "",
  116. codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "",
  117. codebaseIndexOpenAiCompatibleApiKey: "",
  118. codebaseIndexGeminiApiKey: "",
  119. }
  120. setInitialSettings(settings)
  121. setCurrentSettings(settings)
  122. // Request secret status to check if secrets exist
  123. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  124. }
  125. }, [codebaseIndexConfig])
  126. // Request initial indexing status
  127. useEffect(() => {
  128. if (open) {
  129. vscode.postMessage({ type: "requestIndexingStatus" })
  130. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  131. }
  132. }, [open])
  133. // Listen for indexing status updates and save responses
  134. useEffect(() => {
  135. const handleMessage = (event: MessageEvent<any>) => {
  136. if (event.data.type === "indexingStatusUpdate") {
  137. setIndexingStatus({
  138. systemStatus: event.data.values.systemStatus,
  139. message: event.data.values.message || "",
  140. processedItems: event.data.values.processedItems,
  141. totalItems: event.data.values.totalItems,
  142. currentItemUnit: event.data.values.currentItemUnit || "items",
  143. })
  144. } else if (event.data.type === "codeIndexSettingsSaved") {
  145. if (event.data.success) {
  146. setSaveStatus("saved")
  147. // Don't update initial settings here - wait for the secret status response
  148. // Request updated secret status after save
  149. vscode.postMessage({ type: "requestCodeIndexSecretStatus" })
  150. // Reset status after 3 seconds
  151. setTimeout(() => {
  152. setSaveStatus("idle")
  153. }, 3000)
  154. } else {
  155. setSaveStatus("error")
  156. setSaveError(event.data.error || t("settings:codeIndex.saveError"))
  157. // Clear error message after 5 seconds
  158. setTimeout(() => {
  159. setSaveStatus("idle")
  160. setSaveError(null)
  161. }, 5000)
  162. }
  163. }
  164. }
  165. window.addEventListener("message", handleMessage)
  166. return () => window.removeEventListener("message", handleMessage)
  167. }, [t])
  168. // Listen for secret status
  169. useEffect(() => {
  170. const handleMessage = (event: MessageEvent) => {
  171. if (event.data.type === "codeIndexSecretStatus") {
  172. // Update settings to show placeholders for existing secrets
  173. const secretStatus = event.data.values
  174. // Update both current and initial settings based on what secrets exist
  175. const updateWithSecrets = (prev: LocalCodeIndexSettings): LocalCodeIndexSettings => {
  176. const updated = { ...prev }
  177. // Only update to placeholder if the field is currently empty or already a placeholder
  178. // This preserves user input when they're actively editing
  179. if (!prev.codeIndexOpenAiKey || prev.codeIndexOpenAiKey === SECRET_PLACEHOLDER) {
  180. updated.codeIndexOpenAiKey = secretStatus.hasOpenAiKey ? SECRET_PLACEHOLDER : ""
  181. }
  182. if (!prev.codeIndexQdrantApiKey || prev.codeIndexQdrantApiKey === SECRET_PLACEHOLDER) {
  183. updated.codeIndexQdrantApiKey = secretStatus.hasQdrantApiKey ? SECRET_PLACEHOLDER : ""
  184. }
  185. if (
  186. !prev.codebaseIndexOpenAiCompatibleApiKey ||
  187. prev.codebaseIndexOpenAiCompatibleApiKey === SECRET_PLACEHOLDER
  188. ) {
  189. updated.codebaseIndexOpenAiCompatibleApiKey = secretStatus.hasOpenAiCompatibleApiKey
  190. ? SECRET_PLACEHOLDER
  191. : ""
  192. }
  193. if (!prev.codebaseIndexGeminiApiKey || prev.codebaseIndexGeminiApiKey === SECRET_PLACEHOLDER) {
  194. updated.codebaseIndexGeminiApiKey = secretStatus.hasGeminiApiKey ? SECRET_PLACEHOLDER : ""
  195. }
  196. return updated
  197. }
  198. setCurrentSettings(updateWithSecrets)
  199. setInitialSettings(updateWithSecrets)
  200. }
  201. }
  202. window.addEventListener("message", handleMessage)
  203. return () => window.removeEventListener("message", handleMessage)
  204. }, [])
  205. // Generic comparison function that detects changes between initial and current settings
  206. const hasUnsavedChanges = useMemo(() => {
  207. // Get all keys from both objects to handle any field
  208. const allKeys = [...Object.keys(initialSettings), ...Object.keys(currentSettings)] as Array<
  209. keyof LocalCodeIndexSettings
  210. >
  211. // Use a Set to ensure unique keys
  212. const uniqueKeys = Array.from(new Set(allKeys))
  213. for (const key of uniqueKeys) {
  214. const currentValue = currentSettings[key]
  215. const initialValue = initialSettings[key]
  216. // For secret fields, check if the value has been modified from placeholder
  217. if (currentValue === SECRET_PLACEHOLDER) {
  218. // If it's still showing placeholder, no change
  219. continue
  220. }
  221. // Compare values - handles all types including undefined
  222. if (currentValue !== initialValue) {
  223. return true
  224. }
  225. }
  226. return false
  227. }, [currentSettings, initialSettings])
  228. const updateSetting = (key: keyof LocalCodeIndexSettings, value: any) => {
  229. setCurrentSettings((prev) => ({ ...prev, [key]: value }))
  230. }
  231. const handleSaveSettings = () => {
  232. setSaveStatus("saving")
  233. setSaveError(null)
  234. // Prepare settings to save - include all fields except secrets with placeholder values
  235. const settingsToSave: any = {}
  236. // Iterate through all current settings
  237. for (const [key, value] of Object.entries(currentSettings)) {
  238. // Skip secret fields that still have placeholder value
  239. if (value === SECRET_PLACEHOLDER) {
  240. continue
  241. }
  242. // Include all other fields
  243. settingsToSave[key] = value
  244. }
  245. // Save settings to backend
  246. vscode.postMessage({
  247. type: "saveCodeIndexSettingsAtomic",
  248. codeIndexSettings: settingsToSave,
  249. })
  250. }
  251. const progressPercentage = useMemo(
  252. () =>
  253. indexingStatus.totalItems > 0
  254. ? Math.round((indexingStatus.processedItems / indexingStatus.totalItems) * 100)
  255. : 0,
  256. [indexingStatus.processedItems, indexingStatus.totalItems],
  257. )
  258. const transformStyleString = `translateX(-${100 - progressPercentage}%)`
  259. const getAvailableModels = () => {
  260. if (!codebaseIndexModels) return []
  261. const models = codebaseIndexModels[currentSettings.codebaseIndexEmbedderProvider]
  262. return models ? Object.keys(models) : []
  263. }
  264. const portalContainer = useRooPortal("roo-portal")
  265. return (
  266. <Popover open={open} onOpenChange={setOpen}>
  267. <PopoverTrigger asChild>{children}</PopoverTrigger>
  268. <PopoverContent
  269. className="w-[calc(100vw-32px)] max-w-[450px] max-h-[80vh] overflow-y-auto p-0"
  270. align="end"
  271. alignOffset={0}
  272. side="bottom"
  273. sideOffset={5}
  274. collisionPadding={16}
  275. avoidCollisions={true}
  276. container={portalContainer}>
  277. <div className="p-3 border-b border-vscode-dropdown-border cursor-default">
  278. <div className="flex flex-row items-center gap-1 p-0 mt-0 mb-1 w-full">
  279. <h4 className="m-0 pb-2 flex-1">{t("settings:codeIndex.title")}</h4>
  280. </div>
  281. <p className="my-0 pr-4 text-sm w-full">
  282. <Trans i18nKey="settings:codeIndex.description">
  283. <VSCodeLink
  284. href={buildDocLink("features/experimental/codebase-indexing", "settings")}
  285. style={{ display: "inline" }}
  286. />
  287. </Trans>
  288. </p>
  289. </div>
  290. <div className="p-4">
  291. {/* Status Section */}
  292. <div className="space-y-2">
  293. <h4 className="text-sm font-medium">{t("settings:codeIndex.statusTitle")}</h4>
  294. <div className="text-sm text-vscode-descriptionForeground">
  295. <span
  296. className={cn("inline-block w-3 h-3 rounded-full mr-2", {
  297. "bg-gray-400": indexingStatus.systemStatus === "Standby",
  298. "bg-yellow-500 animate-pulse": indexingStatus.systemStatus === "Indexing",
  299. "bg-green-500": indexingStatus.systemStatus === "Indexed",
  300. "bg-red-500": indexingStatus.systemStatus === "Error",
  301. })}
  302. />
  303. {t(`settings:codeIndex.indexingStatuses.${indexingStatus.systemStatus.toLowerCase()}`)}
  304. {indexingStatus.message ? ` - ${indexingStatus.message}` : ""}
  305. </div>
  306. {indexingStatus.systemStatus === "Indexing" && (
  307. <div className="mt-2">
  308. <ProgressPrimitive.Root
  309. className="relative h-2 w-full overflow-hidden rounded-full bg-secondary"
  310. value={progressPercentage}>
  311. <ProgressPrimitive.Indicator
  312. className="h-full w-full flex-1 bg-primary transition-transform duration-300 ease-in-out"
  313. style={{
  314. transform: transformStyleString,
  315. }}
  316. />
  317. </ProgressPrimitive.Root>
  318. </div>
  319. )}
  320. </div>
  321. {/* Setup Settings Disclosure */}
  322. <div className="mt-4">
  323. <button
  324. onClick={() => setIsSetupSettingsOpen(!isSetupSettingsOpen)}
  325. className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
  326. aria-expanded={isSetupSettingsOpen}>
  327. <span
  328. className={`codicon codicon-${isSetupSettingsOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
  329. <span className="text-base font-semibold">{t("settings:codeIndex.setupConfigLabel")}</span>
  330. </button>
  331. {isSetupSettingsOpen && (
  332. <div className="mt-4 space-y-4">
  333. {/* Embedder Provider Section */}
  334. <div className="space-y-2">
  335. <label className="text-sm font-medium">
  336. {t("settings:codeIndex.embedderProviderLabel")}
  337. </label>
  338. <Select
  339. value={currentSettings.codebaseIndexEmbedderProvider}
  340. onValueChange={(value: EmbedderProvider) =>
  341. updateSetting("codebaseIndexEmbedderProvider", value)
  342. }>
  343. <SelectTrigger className="w-full">
  344. <SelectValue />
  345. </SelectTrigger>
  346. <SelectContent>
  347. <SelectItem value="openai">
  348. {t("settings:codeIndex.openaiProvider")}
  349. </SelectItem>
  350. <SelectItem value="ollama">
  351. {t("settings:codeIndex.ollamaProvider")}
  352. </SelectItem>
  353. <SelectItem value="openai-compatible">
  354. {t("settings:codeIndex.openaiCompatibleProvider")}
  355. </SelectItem>
  356. <SelectItem value="gemini">
  357. {t("settings:codeIndex.geminiProvider")}
  358. </SelectItem>
  359. </SelectContent>
  360. </Select>
  361. </div>
  362. {/* Provider-specific settings */}
  363. {currentSettings.codebaseIndexEmbedderProvider === "openai" && (
  364. <>
  365. <div className="space-y-2">
  366. <label className="text-sm font-medium">
  367. {t("settings:codeIndex.openAiKeyLabel")}
  368. </label>
  369. <VSCodeTextField
  370. type="password"
  371. value={currentSettings.codeIndexOpenAiKey || ""}
  372. onInput={(e: any) =>
  373. updateSetting("codeIndexOpenAiKey", e.target.value)
  374. }
  375. placeholder={t("settings:codeIndex.openAiKeyPlaceholder")}
  376. className="w-full"
  377. />
  378. </div>
  379. <div className="space-y-2">
  380. <label className="text-sm font-medium">
  381. {t("settings:codeIndex.modelLabel")}
  382. </label>
  383. <VSCodeDropdown
  384. value={currentSettings.codebaseIndexEmbedderModelId}
  385. onChange={(e: any) =>
  386. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  387. }
  388. className="w-full">
  389. <VSCodeOption value="">
  390. {t("settings:codeIndex.selectModel")}
  391. </VSCodeOption>
  392. {getAvailableModels().map((modelId) => {
  393. const model =
  394. codebaseIndexModels?.[
  395. currentSettings.codebaseIndexEmbedderProvider
  396. ]?.[modelId]
  397. return (
  398. <VSCodeOption key={modelId} value={modelId}>
  399. {modelId}{" "}
  400. {model
  401. ? t("settings:codeIndex.modelDimensions", {
  402. dimension: model.dimension,
  403. })
  404. : ""}
  405. </VSCodeOption>
  406. )
  407. })}
  408. </VSCodeDropdown>
  409. </div>
  410. </>
  411. )}
  412. {currentSettings.codebaseIndexEmbedderProvider === "ollama" && (
  413. <>
  414. <div className="space-y-2">
  415. <label className="text-sm font-medium">
  416. {t("settings:codeIndex.ollamaBaseUrlLabel")}
  417. </label>
  418. <VSCodeTextField
  419. value={currentSettings.codebaseIndexEmbedderBaseUrl || ""}
  420. onInput={(e: any) =>
  421. updateSetting("codebaseIndexEmbedderBaseUrl", e.target.value)
  422. }
  423. placeholder={t("settings:codeIndex.ollamaUrlPlaceholder")}
  424. className="w-full"
  425. />
  426. </div>
  427. <div className="space-y-2">
  428. <label className="text-sm font-medium">
  429. {t("settings:codeIndex.modelLabel")}
  430. </label>
  431. <VSCodeDropdown
  432. value={currentSettings.codebaseIndexEmbedderModelId}
  433. onChange={(e: any) =>
  434. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  435. }
  436. className="w-full">
  437. <VSCodeOption value="">
  438. {t("settings:codeIndex.selectModel")}
  439. </VSCodeOption>
  440. {getAvailableModels().map((modelId) => {
  441. const model =
  442. codebaseIndexModels?.[
  443. currentSettings.codebaseIndexEmbedderProvider
  444. ]?.[modelId]
  445. return (
  446. <VSCodeOption key={modelId} value={modelId}>
  447. {modelId}{" "}
  448. {model
  449. ? t("settings:codeIndex.modelDimensions", {
  450. dimension: model.dimension,
  451. })
  452. : ""}
  453. </VSCodeOption>
  454. )
  455. })}
  456. </VSCodeDropdown>
  457. </div>
  458. </>
  459. )}
  460. {currentSettings.codebaseIndexEmbedderProvider === "openai-compatible" && (
  461. <>
  462. <div className="space-y-2">
  463. <label className="text-sm font-medium">
  464. {t("settings:codeIndex.openAiCompatibleBaseUrlLabel")}
  465. </label>
  466. <VSCodeTextField
  467. value={currentSettings.codebaseIndexOpenAiCompatibleBaseUrl || ""}
  468. onInput={(e: any) =>
  469. updateSetting(
  470. "codebaseIndexOpenAiCompatibleBaseUrl",
  471. e.target.value,
  472. )
  473. }
  474. placeholder={t("settings:codeIndex.openAiCompatibleBaseUrlPlaceholder")}
  475. className="w-full"
  476. />
  477. </div>
  478. <div className="space-y-2">
  479. <label className="text-sm font-medium">
  480. {t("settings:codeIndex.openAiCompatibleApiKeyLabel")}
  481. </label>
  482. <VSCodeTextField
  483. type="password"
  484. value={currentSettings.codebaseIndexOpenAiCompatibleApiKey || ""}
  485. onInput={(e: any) =>
  486. updateSetting("codebaseIndexOpenAiCompatibleApiKey", e.target.value)
  487. }
  488. placeholder={t("settings:codeIndex.openAiCompatibleApiKeyPlaceholder")}
  489. className="w-full"
  490. />
  491. </div>
  492. <div className="space-y-2">
  493. <label className="text-sm font-medium">
  494. {t("settings:codeIndex.modelLabel")}
  495. </label>
  496. <VSCodeTextField
  497. value={currentSettings.codebaseIndexEmbedderModelId || ""}
  498. onInput={(e: any) =>
  499. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  500. }
  501. placeholder={t("settings:codeIndex.modelPlaceholder")}
  502. className="w-full"
  503. />
  504. </div>
  505. <div className="space-y-2">
  506. <label className="text-sm font-medium">
  507. {t("settings:codeIndex.modelDimensionLabel")}
  508. </label>
  509. <VSCodeTextField
  510. value={
  511. currentSettings.codebaseIndexEmbedderModelDimension?.toString() ||
  512. ""
  513. }
  514. onInput={(e: any) => {
  515. const value = e.target.value ? parseInt(e.target.value) : undefined
  516. updateSetting("codebaseIndexEmbedderModelDimension", value)
  517. }}
  518. placeholder={t("settings:codeIndex.modelDimensionPlaceholder")}
  519. className="w-full"
  520. />
  521. </div>
  522. </>
  523. )}
  524. {currentSettings.codebaseIndexEmbedderProvider === "gemini" && (
  525. <>
  526. <div className="space-y-2">
  527. <label className="text-sm font-medium">
  528. {t("settings:codeIndex.geminiApiKeyLabel")}
  529. </label>
  530. <VSCodeTextField
  531. type="password"
  532. value={currentSettings.codebaseIndexGeminiApiKey || ""}
  533. onInput={(e: any) =>
  534. updateSetting("codebaseIndexGeminiApiKey", e.target.value)
  535. }
  536. placeholder={t("settings:codeIndex.geminiApiKeyPlaceholder")}
  537. className="w-full"
  538. />
  539. </div>
  540. <div className="space-y-2">
  541. <label className="text-sm font-medium">
  542. {t("settings:codeIndex.modelLabel")}
  543. </label>
  544. <VSCodeDropdown
  545. value={currentSettings.codebaseIndexEmbedderModelId}
  546. onChange={(e: any) =>
  547. updateSetting("codebaseIndexEmbedderModelId", e.target.value)
  548. }
  549. className="w-full">
  550. <VSCodeOption value="">
  551. {t("settings:codeIndex.selectModel")}
  552. </VSCodeOption>
  553. {getAvailableModels().map((modelId) => {
  554. const model =
  555. codebaseIndexModels?.[
  556. currentSettings.codebaseIndexEmbedderProvider
  557. ]?.[modelId]
  558. return (
  559. <VSCodeOption key={modelId} value={modelId}>
  560. {modelId}{" "}
  561. {model
  562. ? t("settings:codeIndex.modelDimensions", {
  563. dimension: model.dimension,
  564. })
  565. : ""}
  566. </VSCodeOption>
  567. )
  568. })}
  569. </VSCodeDropdown>
  570. </div>
  571. </>
  572. )}
  573. {/* Qdrant Settings */}
  574. <div className="space-y-2">
  575. <label className="text-sm font-medium">
  576. {t("settings:codeIndex.qdrantUrlLabel")}
  577. </label>
  578. <VSCodeTextField
  579. value={currentSettings.codebaseIndexQdrantUrl || ""}
  580. onInput={(e: any) => updateSetting("codebaseIndexQdrantUrl", e.target.value)}
  581. placeholder={t("settings:codeIndex.qdrantUrlPlaceholder")}
  582. className="w-full"
  583. />
  584. </div>
  585. <div className="space-y-2">
  586. <label className="text-sm font-medium">
  587. {t("settings:codeIndex.qdrantApiKeyLabel")}
  588. </label>
  589. <VSCodeTextField
  590. type="password"
  591. value={currentSettings.codeIndexQdrantApiKey || ""}
  592. onInput={(e: any) => updateSetting("codeIndexQdrantApiKey", e.target.value)}
  593. placeholder={t("settings:codeIndex.qdrantApiKeyPlaceholder")}
  594. className="w-full"
  595. />
  596. </div>
  597. </div>
  598. )}
  599. </div>
  600. {/* Advanced Settings Disclosure */}
  601. <div className="mt-4">
  602. <button
  603. onClick={() => setIsAdvancedSettingsOpen(!isAdvancedSettingsOpen)}
  604. className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
  605. aria-expanded={isAdvancedSettingsOpen}>
  606. <span
  607. className={`codicon codicon-${isAdvancedSettingsOpen ? "chevron-down" : "chevron-right"} mr-1`}></span>
  608. <span className="text-base font-semibold">
  609. {t("settings:codeIndex.advancedConfigLabel")}
  610. </span>
  611. </button>
  612. {isAdvancedSettingsOpen && (
  613. <div className="mt-4 space-y-4">
  614. {/* Search Score Threshold Slider */}
  615. <div className="space-y-2">
  616. <div className="flex items-center gap-2">
  617. <label className="text-sm font-medium">
  618. {t("settings:codeIndex.searchMinScoreLabel")}
  619. </label>
  620. <StandardTooltip content={t("settings:codeIndex.searchMinScoreDescription")}>
  621. <span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
  622. </StandardTooltip>
  623. </div>
  624. <div className="flex items-center gap-2">
  625. <Slider
  626. min={CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_SCORE}
  627. max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_SCORE}
  628. step={CODEBASE_INDEX_DEFAULTS.SEARCH_SCORE_STEP}
  629. value={[
  630. currentSettings.codebaseIndexSearchMinScore ??
  631. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  632. ]}
  633. onValueChange={(values) =>
  634. updateSetting("codebaseIndexSearchMinScore", values[0])
  635. }
  636. className="flex-1"
  637. data-testid="search-min-score-slider"
  638. />
  639. <span className="w-12 text-center">
  640. {(
  641. currentSettings.codebaseIndexSearchMinScore ??
  642. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE
  643. ).toFixed(2)}
  644. </span>
  645. <VSCodeButton
  646. appearance="icon"
  647. title={t("settings:codeIndex.resetToDefault")}
  648. onClick={() =>
  649. updateSetting(
  650. "codebaseIndexSearchMinScore",
  651. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE,
  652. )
  653. }>
  654. <span className="codicon codicon-discard" />
  655. </VSCodeButton>
  656. </div>
  657. </div>
  658. {/* Maximum Search Results Slider */}
  659. <div className="space-y-2">
  660. <div className="flex items-center gap-2">
  661. <label className="text-sm font-medium">
  662. {t("settings:codeIndex.searchMaxResultsLabel")}
  663. </label>
  664. <StandardTooltip content={t("settings:codeIndex.searchMaxResultsDescription")}>
  665. <span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
  666. </StandardTooltip>
  667. </div>
  668. <div className="flex items-center gap-2">
  669. <Slider
  670. min={CODEBASE_INDEX_DEFAULTS.MIN_SEARCH_RESULTS}
  671. max={CODEBASE_INDEX_DEFAULTS.MAX_SEARCH_RESULTS}
  672. step={CODEBASE_INDEX_DEFAULTS.SEARCH_RESULTS_STEP}
  673. value={[
  674. currentSettings.codebaseIndexSearchMaxResults ??
  675. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  676. ]}
  677. onValueChange={(values) =>
  678. updateSetting("codebaseIndexSearchMaxResults", values[0])
  679. }
  680. className="flex-1"
  681. data-testid="search-max-results-slider"
  682. />
  683. <span className="w-12 text-center">
  684. {currentSettings.codebaseIndexSearchMaxResults ??
  685. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS}
  686. </span>
  687. <VSCodeButton
  688. appearance="icon"
  689. title={t("settings:codeIndex.resetToDefault")}
  690. onClick={() =>
  691. updateSetting(
  692. "codebaseIndexSearchMaxResults",
  693. CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS,
  694. )
  695. }>
  696. <span className="codicon codicon-discard" />
  697. </VSCodeButton>
  698. </div>
  699. </div>
  700. </div>
  701. )}
  702. </div>
  703. {/* Action Buttons */}
  704. <div className="flex items-center justify-between gap-2 pt-6">
  705. <div className="flex gap-2">
  706. {(indexingStatus.systemStatus === "Error" || indexingStatus.systemStatus === "Standby") && (
  707. <VSCodeButton
  708. onClick={() => vscode.postMessage({ type: "startIndexing" })}
  709. disabled={saveStatus === "saving" || hasUnsavedChanges}>
  710. {t("settings:codeIndex.startIndexingButton")}
  711. </VSCodeButton>
  712. )}
  713. {(indexingStatus.systemStatus === "Indexed" || indexingStatus.systemStatus === "Error") && (
  714. <AlertDialog>
  715. <AlertDialogTrigger asChild>
  716. <VSCodeButton appearance="secondary">
  717. {t("settings:codeIndex.clearIndexDataButton")}
  718. </VSCodeButton>
  719. </AlertDialogTrigger>
  720. <AlertDialogContent>
  721. <AlertDialogHeader>
  722. <AlertDialogTitle>
  723. {t("settings:codeIndex.clearDataDialog.title")}
  724. </AlertDialogTitle>
  725. <AlertDialogDescription>
  726. {t("settings:codeIndex.clearDataDialog.description")}
  727. </AlertDialogDescription>
  728. </AlertDialogHeader>
  729. <AlertDialogFooter>
  730. <AlertDialogCancel>
  731. {t("settings:codeIndex.clearDataDialog.cancelButton")}
  732. </AlertDialogCancel>
  733. <AlertDialogAction
  734. onClick={() => vscode.postMessage({ type: "clearIndexData" })}>
  735. {t("settings:codeIndex.clearDataDialog.confirmButton")}
  736. </AlertDialogAction>
  737. </AlertDialogFooter>
  738. </AlertDialogContent>
  739. </AlertDialog>
  740. )}
  741. </div>
  742. <VSCodeButton
  743. onClick={handleSaveSettings}
  744. disabled={!hasUnsavedChanges || saveStatus === "saving"}>
  745. {saveStatus === "saving"
  746. ? t("settings:codeIndex.saving")
  747. : t("settings:codeIndex.saveSettings")}
  748. </VSCodeButton>
  749. </div>
  750. {/* Save Status Messages */}
  751. {saveStatus === "error" && (
  752. <div className="mt-2">
  753. <span className="text-sm text-red-600 block">
  754. {saveError || t("settings:codeIndex.saveError")}
  755. </span>
  756. </div>
  757. )}
  758. </div>
  759. </PopoverContent>
  760. </Popover>
  761. )
  762. }