updateSettingsCli.ts 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { buildApiHandler } from "@core/api"
  2. import { Empty } from "@shared/proto/cline/common"
  3. import {
  4. PlanActMode,
  5. OpenaiReasoningEffort as ProtoOpenaiReasoningEffort,
  6. UpdateSettingsRequestCli,
  7. } from "@shared/proto/cline/state"
  8. import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
  9. import { Settings } from "@shared/storage/state-keys"
  10. import { TelemetrySetting } from "@shared/TelemetrySetting"
  11. import { ClineEnv } from "@/config"
  12. import { HostProvider } from "@/hosts/host-provider"
  13. import { ShowMessageType } from "@/shared/proto/host/window"
  14. import { Mode, OpenaiReasoningEffort } from "@/shared/storage/types"
  15. import { telemetryService } from "../../../services/telemetry"
  16. import { Controller } from ".."
  17. import { accountLogoutClicked } from "../account/accountLogoutClicked"
  18. /**
  19. * Updates multiple extension settings in a single request
  20. * @param controller The controller instance
  21. * @param request The request containing the settings to update
  22. * @returns An empty response
  23. */
  24. export async function updateSettingsCli(controller: Controller, request: UpdateSettingsRequestCli): Promise<Empty> {
  25. const convertOpenaiReasoningEffort = (effort: ProtoOpenaiReasoningEffort): OpenaiReasoningEffort => {
  26. switch (effort) {
  27. case ProtoOpenaiReasoningEffort.LOW:
  28. return "low"
  29. case ProtoOpenaiReasoningEffort.MEDIUM:
  30. return "medium"
  31. case ProtoOpenaiReasoningEffort.HIGH:
  32. return "high"
  33. case ProtoOpenaiReasoningEffort.MINIMAL:
  34. return "minimal"
  35. default:
  36. return "medium"
  37. }
  38. }
  39. const convertPlanActMode = (mode: PlanActMode): Mode => {
  40. return mode === PlanActMode.PLAN ? "plan" : "act"
  41. }
  42. try {
  43. if (request.environment !== undefined) {
  44. ClineEnv.setEnvironment(request.environment)
  45. await accountLogoutClicked(controller, Empty.create())
  46. }
  47. if (request.settings) {
  48. // Extract all special case fields that need dedicated handlers
  49. // These should NOT be included in the batch update
  50. const {
  51. // Fields requiring conversion
  52. autoApprovalSettings,
  53. openaiReasoningEffort,
  54. mode,
  55. customPrompt,
  56. planModeApiProvider,
  57. actModeApiProvider,
  58. // Fields requiring special logic (telemetry, merging, etc.)
  59. telemetrySetting,
  60. yoloModeToggled,
  61. useAutoCondense,
  62. focusChainSettings,
  63. browserSettings,
  64. defaultTerminalProfile,
  65. ...simpleSettings
  66. } = request.settings
  67. // Batch update for simple pass-through fields
  68. const filteredSettings: Partial<Settings> = Object.fromEntries(
  69. Object.entries(simpleSettings).filter(([_, value]) => value !== undefined),
  70. )
  71. controller.stateManager.setGlobalStateBatch(filteredSettings)
  72. console.log("autoApprovalSettings", controller.stateManager.getGlobalSettingsKey("autoApprovalSettings"))
  73. // Handle fields requiring type conversion from generated protobuf types to application types
  74. if (autoApprovalSettings) {
  75. // Merge with current settings to preserve unspecified fields
  76. const currentAutoApprovalSettings = controller.stateManager.getGlobalSettingsKey("autoApprovalSettings")
  77. const mergedSettings = {
  78. ...currentAutoApprovalSettings,
  79. ...(autoApprovalSettings.version !== undefined && { version: autoApprovalSettings.version }),
  80. ...(autoApprovalSettings.enableNotifications !== undefined && {
  81. enableNotifications: autoApprovalSettings.enableNotifications,
  82. }),
  83. actions: {
  84. ...currentAutoApprovalSettings.actions,
  85. ...(autoApprovalSettings.actions
  86. ? Object.fromEntries(Object.entries(autoApprovalSettings.actions).filter(([_, v]) => v !== undefined))
  87. : {}),
  88. },
  89. }
  90. controller.stateManager.setGlobalState("autoApprovalSettings", mergedSettings)
  91. }
  92. if (openaiReasoningEffort !== undefined) {
  93. const converted = convertOpenaiReasoningEffort(openaiReasoningEffort)
  94. controller.stateManager.setGlobalState("openaiReasoningEffort", converted)
  95. }
  96. if (mode !== undefined) {
  97. const converted = convertPlanActMode(mode)
  98. controller.stateManager.setGlobalState("mode", converted)
  99. }
  100. if (customPrompt === "compact") {
  101. controller.stateManager.setGlobalState("customPrompt", "compact")
  102. }
  103. if (planModeApiProvider !== undefined) {
  104. const converted = convertProtoToApiProvider(planModeApiProvider)
  105. controller.stateManager.setGlobalState("planModeApiProvider", converted)
  106. }
  107. if (actModeApiProvider !== undefined) {
  108. const converted = convertProtoToApiProvider(actModeApiProvider)
  109. controller.stateManager.setGlobalState("actModeApiProvider", converted)
  110. }
  111. if (controller.task) {
  112. const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
  113. const apiConfigForHandler = {
  114. ...controller.stateManager.getApiConfiguration(),
  115. ulid: controller.task.ulid,
  116. }
  117. controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
  118. }
  119. // Update telemetry setting
  120. if (telemetrySetting) {
  121. await controller.updateTelemetrySetting(telemetrySetting as TelemetrySetting)
  122. }
  123. // Update yolo mode setting (requires telemetry)
  124. if (yoloModeToggled !== undefined) {
  125. if (controller.task) {
  126. telemetryService.captureYoloModeToggle(controller.task.ulid, yoloModeToggled)
  127. }
  128. controller.stateManager.setGlobalState("yoloModeToggled", yoloModeToggled)
  129. }
  130. // Update auto-condense setting (requires telemetry)
  131. if (useAutoCondense !== undefined) {
  132. if (controller.task) {
  133. telemetryService.captureAutoCondenseToggle(
  134. controller.task.ulid,
  135. useAutoCondense,
  136. controller.task.api.getModel().id,
  137. )
  138. }
  139. controller.stateManager.setGlobalState("useAutoCondense", useAutoCondense)
  140. }
  141. // Update focus chain settings (requires telemetry on state change)
  142. if (focusChainSettings !== undefined) {
  143. const currentSettings = controller.stateManager.getGlobalSettingsKey("focusChainSettings")
  144. const wasEnabled = currentSettings?.enabled ?? false
  145. const isEnabled = focusChainSettings.enabled
  146. const newFocusChainSettings = {
  147. enabled: isEnabled,
  148. remindClineInterval: focusChainSettings.remindClineInterval,
  149. }
  150. controller.stateManager.setGlobalState("focusChainSettings", newFocusChainSettings)
  151. // Capture telemetry when setting changes
  152. if (wasEnabled !== isEnabled) {
  153. telemetryService.captureFocusChainToggle(isEnabled)
  154. }
  155. }
  156. // Update browser settings (requires careful merging to avoid protobuf defaults)
  157. if (browserSettings !== undefined) {
  158. const currentSettings = controller.stateManager.getGlobalSettingsKey("browserSettings")
  159. const newBrowserSettings = {
  160. ...currentSettings,
  161. viewport: {
  162. width: browserSettings.viewport?.width || currentSettings.viewport.width,
  163. height: browserSettings.viewport?.height || currentSettings.viewport.height,
  164. },
  165. ...(browserSettings.remoteBrowserEnabled !== undefined && {
  166. remoteBrowserEnabled: browserSettings.remoteBrowserEnabled,
  167. }),
  168. ...(browserSettings.remoteBrowserHost !== undefined && {
  169. remoteBrowserHost: browserSettings.remoteBrowserHost,
  170. }),
  171. ...(browserSettings.chromeExecutablePath !== undefined && {
  172. chromeExecutablePath: browserSettings.chromeExecutablePath,
  173. }),
  174. ...(browserSettings.disableToolUse !== undefined && {
  175. disableToolUse: browserSettings.disableToolUse,
  176. }),
  177. ...(browserSettings.customArgs !== undefined && {
  178. customArgs: browserSettings.customArgs,
  179. }),
  180. }
  181. controller.stateManager.setGlobalState("browserSettings", newBrowserSettings)
  182. }
  183. // Update default terminal profile (requires terminal manager updates and notifications)
  184. if (defaultTerminalProfile !== undefined && defaultTerminalProfile !== "") {
  185. const profileId = defaultTerminalProfile
  186. // Update the terminal profile in the state
  187. controller.stateManager.setGlobalState("defaultTerminalProfile", profileId)
  188. let closedCount = 0
  189. let busyTerminalsCount = 0
  190. // Update the terminal manager of the current task if it exists
  191. if (controller.task) {
  192. // Terminal manager must exist when task is active
  193. if (!controller.task.terminalManager) {
  194. throw new Error("Cannot update terminal profile: Terminal manager missing from active task")
  195. }
  196. // Call the updated setDefaultTerminalProfile method that returns closed terminal info
  197. // Use `as any` to handle type incompatibility between VSCode's TerminalInfo and standalone TerminalInfo
  198. const result = controller.task.terminalManager.setDefaultTerminalProfile(profileId) as any
  199. closedCount = result.closedCount
  200. busyTerminalsCount = result.busyTerminals?.length ?? 0
  201. // Show information message if terminals were closed
  202. if (closedCount > 0) {
  203. const message = `Closed ${closedCount} ${closedCount === 1 ? "terminal" : "terminals"} with different profile.`
  204. HostProvider.window.showMessage({
  205. type: ShowMessageType.INFORMATION,
  206. message,
  207. })
  208. }
  209. // Show warning if there are busy terminals that couldn't be closed
  210. if (busyTerminalsCount > 0) {
  211. const message =
  212. `${busyTerminalsCount} busy ${busyTerminalsCount === 1 ? "terminal has" : "terminals have"} a different profile. ` +
  213. `Close ${busyTerminalsCount === 1 ? "it" : "them"} to use the new profile for all commands.`
  214. HostProvider.window.showMessage({
  215. type: ShowMessageType.WARNING,
  216. message,
  217. })
  218. }
  219. }
  220. }
  221. }
  222. // Handle secrets updates
  223. if (request.secrets) {
  224. const filteredSecrets = Object.fromEntries(
  225. Object.entries(request.secrets).filter(([_, value]) => value !== undefined),
  226. )
  227. controller.stateManager.setSecretsBatch(filteredSecrets)
  228. }
  229. // Post updated state to webview
  230. await controller.postStateToWebview()
  231. return Empty.create()
  232. } catch (error) {
  233. console.error("Failed to update settings:", error)
  234. throw error
  235. }
  236. }