SettingsView.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
  2. import { memo, useEffect, useState } from "react"
  3. import { useExtensionState } from "../../context/ExtensionStateContext"
  4. import { validateApiConfiguration, validateModelId } from "../../utils/validate"
  5. import { vscode } from "../../utils/vscode"
  6. import ApiOptions from "./ApiOptions"
  7. const IS_DEV = false // FIXME: use flags when packaging
  8. type SettingsViewProps = {
  9. onDone: () => void
  10. }
  11. const SettingsView = ({ onDone }: SettingsViewProps) => {
  12. const {
  13. apiConfiguration,
  14. version,
  15. customInstructions,
  16. setCustomInstructions,
  17. alwaysAllowReadOnly,
  18. setAlwaysAllowReadOnly,
  19. alwaysAllowWrite,
  20. setAlwaysAllowWrite,
  21. alwaysAllowExecute,
  22. setAlwaysAllowExecute,
  23. alwaysAllowBrowser,
  24. setAlwaysAllowBrowser,
  25. alwaysAllowMcp,
  26. setAlwaysAllowMcp,
  27. soundEnabled,
  28. setSoundEnabled,
  29. soundVolume,
  30. setSoundVolume,
  31. diffEnabled,
  32. setDiffEnabled,
  33. browserLargeViewport,
  34. setBrowserLargeViewport,
  35. openRouterModels,
  36. setAllowedCommands,
  37. allowedCommands,
  38. fuzzyMatchThreshold,
  39. setFuzzyMatchThreshold,
  40. preferredLanguage,
  41. setPreferredLanguage,
  42. } = useExtensionState()
  43. const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
  44. const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
  45. const [commandInput, setCommandInput] = useState("")
  46. const handleSubmit = () => {
  47. const apiValidationResult = validateApiConfiguration(apiConfiguration)
  48. const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
  49. setApiErrorMessage(apiValidationResult)
  50. setModelIdErrorMessage(modelIdValidationResult)
  51. if (!apiValidationResult && !modelIdValidationResult) {
  52. vscode.postMessage({
  53. type: "apiConfiguration",
  54. apiConfiguration
  55. })
  56. vscode.postMessage({ type: "customInstructions", text: customInstructions })
  57. vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
  58. vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
  59. vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
  60. vscode.postMessage({ type: "alwaysAllowBrowser", bool: alwaysAllowBrowser })
  61. vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
  62. vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
  63. vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
  64. vscode.postMessage({ type: "soundVolume", value: soundVolume })
  65. vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
  66. vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
  67. vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
  68. vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
  69. onDone()
  70. }
  71. }
  72. useEffect(() => {
  73. setApiErrorMessage(undefined)
  74. setModelIdErrorMessage(undefined)
  75. }, [apiConfiguration])
  76. // Initial validation on mount
  77. useEffect(() => {
  78. const apiValidationResult = validateApiConfiguration(apiConfiguration)
  79. const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels)
  80. setApiErrorMessage(apiValidationResult)
  81. setModelIdErrorMessage(modelIdValidationResult)
  82. }, [apiConfiguration, openRouterModels])
  83. const handleResetState = () => {
  84. vscode.postMessage({ type: "resetState" })
  85. }
  86. const handleAddCommand = () => {
  87. const currentCommands = allowedCommands ?? []
  88. if (commandInput && !currentCommands.includes(commandInput)) {
  89. const newCommands = [...currentCommands, commandInput]
  90. setAllowedCommands(newCommands)
  91. setCommandInput("")
  92. vscode.postMessage({
  93. type: "allowedCommands",
  94. commands: newCommands
  95. })
  96. }
  97. }
  98. return (
  99. <div
  100. style={{
  101. position: "fixed",
  102. top: 0,
  103. left: 0,
  104. right: 0,
  105. bottom: 0,
  106. padding: "10px 0px 0px 20px",
  107. display: "flex",
  108. flexDirection: "column",
  109. overflow: "hidden",
  110. }}>
  111. <div
  112. style={{
  113. display: "flex",
  114. justifyContent: "space-between",
  115. alignItems: "center",
  116. marginBottom: "17px",
  117. paddingRight: 17,
  118. }}>
  119. <h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
  120. <VSCodeButton onClick={handleSubmit}>Done</VSCodeButton>
  121. </div>
  122. <div
  123. style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
  124. <div style={{ marginBottom: 5 }}>
  125. <ApiOptions
  126. showModelOptions={true}
  127. apiErrorMessage={apiErrorMessage}
  128. modelIdErrorMessage={modelIdErrorMessage}
  129. />
  130. </div>
  131. <div style={{ marginBottom: 5 }}>
  132. <div style={{ marginBottom: 15 }}>
  133. <label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Preferred Language</label>
  134. <select
  135. value={preferredLanguage}
  136. onChange={(e) => setPreferredLanguage(e.target.value)}
  137. style={{
  138. width: "100%",
  139. padding: "4px 8px",
  140. backgroundColor: "var(--vscode-input-background)",
  141. color: "var(--vscode-input-foreground)",
  142. border: "1px solid var(--vscode-input-border)",
  143. borderRadius: "2px",
  144. height: "28px"
  145. }}>
  146. <option value="English">English</option>
  147. <option value="Arabic">Arabic - العربية</option>
  148. <option value="Brazilian Portuguese">Portuguese - Português (Brasil)</option>
  149. <option value="Czech">Czech - Čeština</option>
  150. <option value="French">French - Français</option>
  151. <option value="German">German - Deutsch</option>
  152. <option value="Hindi">Hindi - हिन्दी</option>
  153. <option value="Hungarian">Hungarian - Magyar</option>
  154. <option value="Italian">Italian - Italiano</option>
  155. <option value="Japanese">Japanese - 日本語</option>
  156. <option value="Korean">Korean - 한국어</option>
  157. <option value="Polish">Polish - Polski</option>
  158. <option value="Portuguese">Portuguese - Português (Portugal)</option>
  159. <option value="Russian">Russian - Русский</option>
  160. <option value="Simplified Chinese">Simplified Chinese - 简体中文</option>
  161. <option value="Spanish">Spanish - Español</option>
  162. <option value="Traditional Chinese">Traditional Chinese - 繁體中文</option>
  163. <option value="Turkish">Turkish - Türkçe</option>
  164. </select>
  165. <p style={{
  166. fontSize: "12px",
  167. marginTop: "5px",
  168. color: "var(--vscode-descriptionForeground)",
  169. }}>
  170. Select the language that Cline should use for communication.
  171. </p>
  172. </div>
  173. <VSCodeTextArea
  174. value={customInstructions ?? ""}
  175. style={{ width: "100%" }}
  176. rows={4}
  177. placeholder={
  178. 'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
  179. }
  180. onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}>
  181. <span style={{ fontWeight: "500" }}>Custom Instructions</span>
  182. </VSCodeTextArea>
  183. <p
  184. style={{
  185. fontSize: "12px",
  186. marginTop: "5px",
  187. color: "var(--vscode-descriptionForeground)",
  188. }}>
  189. These instructions are added to the end of the system prompt sent with every request. Custom instructions set in .clinerules and .cursorrules in the working directory are also included.
  190. </p>
  191. </div>
  192. <div style={{ marginBottom: 5 }}>
  193. <VSCodeCheckbox checked={diffEnabled} onChange={(e: any) => setDiffEnabled(e.target.checked)}>
  194. <span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
  195. </VSCodeCheckbox>
  196. <p
  197. style={{
  198. fontSize: "12px",
  199. marginTop: "5px",
  200. color: "var(--vscode-descriptionForeground)",
  201. }}>
  202. When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
  203. </p>
  204. {diffEnabled && (
  205. <div style={{ marginTop: 10 }}>
  206. <div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
  207. <span style={{ fontWeight: "500", minWidth: '100px' }}>Match precision</span>
  208. <input
  209. type="range"
  210. min="0.9"
  211. max="1"
  212. step="0.005"
  213. value={fuzzyMatchThreshold ?? 1.0}
  214. onChange={(e) => {
  215. setFuzzyMatchThreshold(parseFloat(e.target.value));
  216. }}
  217. style={{
  218. flexGrow: 1,
  219. accentColor: 'var(--vscode-button-background)',
  220. height: '2px'
  221. }}
  222. />
  223. <span style={{ minWidth: '35px', textAlign: 'left' }}>
  224. {Math.round((fuzzyMatchThreshold || 1) * 100)}%
  225. </span>
  226. </div>
  227. <p style={{ fontSize: "12px", marginBottom: 10, color: "var(--vscode-descriptionForeground)" }}>
  228. This slider controls how precisely code sections must match when applying diffs. Lower values allow more flexible matching but increase the risk of incorrect replacements. Use values below 100% with extreme caution.
  229. </p>
  230. </div>
  231. )}
  232. </div>
  233. <div style={{ marginBottom: 5 }}>
  234. <VSCodeCheckbox
  235. checked={alwaysAllowReadOnly}
  236. onChange={(e: any) => setAlwaysAllowReadOnly(e.target.checked)}>
  237. <span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
  238. </VSCodeCheckbox>
  239. <p
  240. style={{
  241. fontSize: "12px",
  242. marginTop: "5px",
  243. color: "var(--vscode-descriptionForeground)",
  244. }}>
  245. When enabled, Cline will automatically view directory contents and read files without requiring
  246. you to click the Approve button.
  247. </p>
  248. </div>
  249. <div style={{ marginBottom: 5, border: "2px solid var(--vscode-errorForeground)", borderRadius: "4px", padding: "10px" }}>
  250. <h4 style={{ fontWeight: 500, margin: "0 0 10px 0", color: "var(--vscode-errorForeground)" }}>⚠️ High-Risk Auto-Approve Settings</h4>
  251. <p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
  252. The following settings allow Cline to automatically perform potentially dangerous operations without requiring approval.
  253. Enable these settings only if you fully trust the AI and understand the associated security risks.
  254. </p>
  255. <div style={{ marginBottom: 5 }}>
  256. <VSCodeCheckbox
  257. checked={alwaysAllowWrite}
  258. onChange={(e: any) => setAlwaysAllowWrite(e.target.checked)}>
  259. <span style={{ fontWeight: "500" }}>Always approve write operations</span>
  260. </VSCodeCheckbox>
  261. <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
  262. Automatically create and edit files without requiring approval
  263. </p>
  264. </div>
  265. <div style={{ marginBottom: 5 }}>
  266. <VSCodeCheckbox
  267. checked={alwaysAllowBrowser}
  268. onChange={(e: any) => setAlwaysAllowBrowser(e.target.checked)}>
  269. <span style={{ fontWeight: "500" }}>Always approve browser actions</span>
  270. </VSCodeCheckbox>
  271. <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
  272. Automatically perform browser actions without requiring approval<br/>
  273. Note: Only applies when the model supports computer use
  274. </p>
  275. </div>
  276. <div style={{ marginBottom: 5 }}>
  277. <VSCodeCheckbox
  278. checked={alwaysAllowMcp}
  279. onChange={(e: any) => {
  280. setAlwaysAllowMcp(e.target.checked)
  281. vscode.postMessage({ type: "alwaysAllowMcp", bool: e.target.checked })
  282. }}>
  283. <span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
  284. </VSCodeCheckbox>
  285. <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
  286. Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this setting and the tool's individual "Always allow" checkbox)
  287. </p>
  288. </div>
  289. <div style={{ marginBottom: 5 }}>
  290. <VSCodeCheckbox
  291. checked={alwaysAllowExecute}
  292. onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
  293. <span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
  294. </VSCodeCheckbox>
  295. <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
  296. Automatically execute allowed terminal commands without requiring approval
  297. </p>
  298. {alwaysAllowExecute && (
  299. <div style={{ marginTop: 10 }}>
  300. <span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
  301. <p style={{
  302. fontSize: "12px",
  303. marginTop: "5px",
  304. color: "var(--vscode-descriptionForeground)",
  305. }}>
  306. Command prefixes that can be auto-executed when "Always approve execute operations" is enabled.
  307. </p>
  308. <div style={{ display: 'flex', gap: '5px', marginTop: '10px' }}>
  309. <VSCodeTextField
  310. value={commandInput}
  311. onInput={(e: any) => setCommandInput(e.target.value)}
  312. onKeyDown={(e: any) => {
  313. if (e.key === 'Enter') {
  314. e.preventDefault()
  315. handleAddCommand()
  316. }
  317. }}
  318. placeholder="Enter command prefix (e.g., 'git ')"
  319. style={{ flexGrow: 1 }}
  320. />
  321. <VSCodeButton onClick={handleAddCommand}>
  322. Add
  323. </VSCodeButton>
  324. </div>
  325. <div style={{
  326. marginTop: '10px',
  327. display: 'flex',
  328. flexWrap: 'wrap',
  329. gap: '5px'
  330. }}>
  331. {(allowedCommands ?? []).map((cmd, index) => (
  332. <div key={index} style={{
  333. display: 'flex',
  334. alignItems: 'center',
  335. gap: '5px',
  336. backgroundColor: 'var(--vscode-button-secondaryBackground)',
  337. padding: '2px 6px',
  338. borderRadius: '4px',
  339. border: '1px solid var(--vscode-button-secondaryBorder)',
  340. height: '24px'
  341. }}>
  342. <span>{cmd}</span>
  343. <VSCodeButton
  344. appearance="icon"
  345. style={{
  346. padding: 0,
  347. margin: 0,
  348. height: '20px',
  349. width: '20px',
  350. minWidth: '20px',
  351. display: 'flex',
  352. alignItems: 'center',
  353. justifyContent: 'center'
  354. }}
  355. onClick={() => {
  356. const newCommands = (allowedCommands ?? []).filter((_, i) => i !== index)
  357. setAllowedCommands(newCommands)
  358. vscode.postMessage({
  359. type: "allowedCommands",
  360. commands: newCommands
  361. })
  362. }}
  363. >
  364. <span className="codicon codicon-close" />
  365. </VSCodeButton>
  366. </div>
  367. ))}
  368. </div>
  369. </div>
  370. )}
  371. </div>
  372. </div>
  373. <div style={{ marginBottom: 5 }}>
  374. <h4 style={{ fontWeight: 500, marginBottom: 10 }}>Experimental Features</h4>
  375. <div style={{ marginBottom: 10 }}>
  376. <VSCodeCheckbox checked={browserLargeViewport} onChange={(e: any) => setBrowserLargeViewport(e.target.checked)}>
  377. <span style={{ fontWeight: "500" }}>Use larger browser viewport (1280x800)</span>
  378. </VSCodeCheckbox>
  379. <p
  380. style={{
  381. fontSize: "12px",
  382. marginTop: "5px",
  383. color: "var(--vscode-descriptionForeground)",
  384. }}>
  385. When enabled, Cline will use a larger viewport size for browser interactions.
  386. </p>
  387. </div>
  388. <div style={{ marginBottom: 5 }}>
  389. <div style={{ marginBottom: 10 }}>
  390. <VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
  391. <span style={{ fontWeight: "500" }}>Enable sound effects</span>
  392. </VSCodeCheckbox>
  393. <p
  394. style={{
  395. fontSize: "12px",
  396. marginTop: "5px",
  397. color: "var(--vscode-descriptionForeground)",
  398. }}>
  399. When enabled, Cline will play sound effects for notifications and events.
  400. </p>
  401. </div>
  402. {soundEnabled && (
  403. <div style={{ marginLeft: 0 }}>
  404. <div style={{ display: 'flex', alignItems: 'center', gap: '5px' }}>
  405. <span style={{ fontWeight: "500", minWidth: '100px' }}>Volume</span>
  406. <input
  407. type="range"
  408. min="0"
  409. max="1"
  410. step="0.01"
  411. value={soundVolume ?? 0.5}
  412. onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
  413. style={{
  414. flexGrow: 1,
  415. accentColor: 'var(--vscode-button-background)',
  416. height: '2px'
  417. }}
  418. />
  419. <span style={{ minWidth: '35px', textAlign: 'left' }}>
  420. {Math.round((soundVolume ?? 0.5) * 100)}%
  421. </span>
  422. </div>
  423. </div>
  424. )}
  425. </div>
  426. </div>
  427. {IS_DEV && (
  428. <>
  429. <div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
  430. <VSCodeButton onClick={handleResetState} style={{ marginTop: "5px", width: "auto" }}>
  431. Reset State
  432. </VSCodeButton>
  433. <p
  434. style={{
  435. fontSize: "12px",
  436. marginTop: "5px",
  437. color: "var(--vscode-descriptionForeground)",
  438. }}>
  439. This will reset all global state and secret storage in the extension.
  440. </p>
  441. </>
  442. )}
  443. <div
  444. style={{
  445. textAlign: "center",
  446. color: "var(--vscode-descriptionForeground)",
  447. fontSize: "12px",
  448. lineHeight: "1.2",
  449. marginTop: "auto",
  450. padding: "10px 8px 15px 0px",
  451. }}>
  452. <p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
  453. If you have any questions or feedback, feel free to open an issue at{" "}
  454. <VSCodeLink href="https://github.com/RooVetGit/Roo-Cline" style={{ display: "inline" }}>
  455. https://github.com/RooVetGit/Roo-Cline
  456. </VSCodeLink>
  457. </p>
  458. <p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
  459. </div>
  460. </div>
  461. </div>
  462. )
  463. }
  464. export default memo(SettingsView)