ExtensionStateContext.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
  1. import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
  2. import { useEvent } from "react-use"
  3. import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
  4. import {
  5. ApiConfiguration,
  6. ModelInfo,
  7. glamaDefaultModelId,
  8. glamaDefaultModelInfo,
  9. openRouterDefaultModelId,
  10. openRouterDefaultModelInfo,
  11. } from "../../../src/shared/api"
  12. import { vscode } from "../utils/vscode"
  13. import { convertTextMateToHljs } from "../utils/textMateToHljs"
  14. import { findLastIndex } from "../../../src/shared/array"
  15. import { McpServer } from "../../../src/shared/mcp"
  16. import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
  17. import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes"
  18. import { CustomSupportPrompts } from "../../../src/shared/support-prompt"
  19. import { experimentDefault, ExperimentId } from "../../../src/shared/experiments"
  20. export interface ExtensionStateContextType extends ExtensionState {
  21. didHydrateState: boolean
  22. showWelcome: boolean
  23. theme: any
  24. glamaModels: Record<string, ModelInfo>
  25. openRouterModels: Record<string, ModelInfo>
  26. openAiModels: string[]
  27. mcpServers: McpServer[]
  28. currentCheckpoint?: string
  29. filePaths: string[]
  30. openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
  31. setApiConfiguration: (config: ApiConfiguration) => void
  32. setCustomInstructions: (value?: string) => void
  33. setAlwaysAllowReadOnly: (value: boolean) => void
  34. setAlwaysAllowWrite: (value: boolean) => void
  35. setAlwaysAllowExecute: (value: boolean) => void
  36. setAlwaysAllowBrowser: (value: boolean) => void
  37. setAlwaysAllowMcp: (value: boolean) => void
  38. setAlwaysAllowModeSwitch: (value: boolean) => void
  39. setShowAnnouncement: (value: boolean) => void
  40. setAllowedCommands: (value: string[]) => void
  41. setSoundEnabled: (value: boolean) => void
  42. setSoundVolume: (value: number) => void
  43. setDiffEnabled: (value: boolean) => void
  44. setCheckpointsEnabled: (value: boolean) => void
  45. setBrowserViewportSize: (value: string) => void
  46. setFuzzyMatchThreshold: (value: number) => void
  47. preferredLanguage: string
  48. setPreferredLanguage: (value: string) => void
  49. setWriteDelayMs: (value: number) => void
  50. screenshotQuality?: number
  51. setScreenshotQuality: (value: number) => void
  52. terminalOutputLineLimit?: number
  53. setTerminalOutputLineLimit: (value: number) => void
  54. mcpEnabled: boolean
  55. setMcpEnabled: (value: boolean) => void
  56. enableMcpServerCreation: boolean
  57. setEnableMcpServerCreation: (value: boolean) => void
  58. alwaysApproveResubmit?: boolean
  59. setAlwaysApproveResubmit: (value: boolean) => void
  60. requestDelaySeconds: number
  61. setRequestDelaySeconds: (value: number) => void
  62. rateLimitSeconds: number
  63. setRateLimitSeconds: (value: number) => void
  64. setCurrentApiConfigName: (value: string) => void
  65. setListApiConfigMeta: (value: ApiConfigMeta[]) => void
  66. onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
  67. mode: Mode
  68. setMode: (value: Mode) => void
  69. setCustomModePrompts: (value: CustomModePrompts) => void
  70. setCustomSupportPrompts: (value: CustomSupportPrompts) => void
  71. enhancementApiConfigId?: string
  72. setEnhancementApiConfigId: (value: string) => void
  73. setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
  74. setAutoApprovalEnabled: (value: boolean) => void
  75. handleInputChange: (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => void
  76. customModes: ModeConfig[]
  77. setCustomModes: (value: ModeConfig[]) => void
  78. }
  79. export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
  80. export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  81. const [state, setState] = useState<ExtensionState>({
  82. version: "",
  83. clineMessages: [],
  84. taskHistory: [],
  85. shouldShowAnnouncement: false,
  86. allowedCommands: [],
  87. soundEnabled: false,
  88. soundVolume: 0.5,
  89. diffEnabled: false,
  90. checkpointsEnabled: false,
  91. fuzzyMatchThreshold: 1.0,
  92. preferredLanguage: "English",
  93. writeDelayMs: 1000,
  94. browserViewportSize: "900x600",
  95. screenshotQuality: 75,
  96. terminalOutputLineLimit: 500,
  97. mcpEnabled: true,
  98. enableMcpServerCreation: true,
  99. alwaysApproveResubmit: false,
  100. requestDelaySeconds: 5,
  101. rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
  102. currentApiConfigName: "default",
  103. listApiConfigMeta: [],
  104. mode: defaultModeSlug,
  105. customModePrompts: defaultPrompts,
  106. customSupportPrompts: {},
  107. experiments: experimentDefault,
  108. enhancementApiConfigId: "",
  109. autoApprovalEnabled: false,
  110. customModes: [],
  111. })
  112. const [didHydrateState, setDidHydrateState] = useState(false)
  113. const [showWelcome, setShowWelcome] = useState(false)
  114. const [theme, setTheme] = useState<any>(undefined)
  115. const [filePaths, setFilePaths] = useState<string[]>([])
  116. const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
  117. [glamaDefaultModelId]: glamaDefaultModelInfo,
  118. })
  119. const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
  120. const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
  121. [openRouterDefaultModelId]: openRouterDefaultModelInfo,
  122. })
  123. const [openAiModels, setOpenAiModels] = useState<string[]>([])
  124. const [mcpServers, setMcpServers] = useState<McpServer[]>([])
  125. const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
  126. const setListApiConfigMeta = useCallback(
  127. (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
  128. [],
  129. )
  130. const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
  131. setState((currentState) => {
  132. vscode.postMessage({
  133. type: "upsertApiConfiguration",
  134. text: currentState.currentApiConfigName,
  135. apiConfiguration: apiConfig,
  136. })
  137. return currentState // No state update needed
  138. })
  139. }, [])
  140. const handleInputChange = useCallback(
  141. // Returns a function that handles an input change event for a specific API configuration field.
  142. // The optional "softUpdate" flag determines whether to immediately update local state or send an external update.
  143. (field: keyof ApiConfiguration, softUpdate?: boolean) => (event: any) => {
  144. // Use the functional form of setState to ensure the latest state is used in the update logic.
  145. setState((currentState) => {
  146. if (softUpdate) {
  147. // Return a new state object with the updated apiConfiguration.
  148. // This will trigger a re-render with the new configuration value.
  149. return {
  150. ...currentState,
  151. apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
  152. }
  153. } else {
  154. // For non-soft updates, send a message to the VS Code extension with the updated config.
  155. // This side effect communicates the change without updating local React state.
  156. vscode.postMessage({
  157. type: "upsertApiConfiguration",
  158. text: currentState.currentApiConfigName,
  159. apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
  160. })
  161. // Return the unchanged state as no local state update is intended in this branch.
  162. return currentState
  163. }
  164. })
  165. },
  166. [],
  167. )
  168. const handleMessage = useCallback(
  169. (event: MessageEvent) => {
  170. const message: ExtensionMessage = event.data
  171. switch (message.type) {
  172. case "state": {
  173. const newState = message.state!
  174. setState((prevState) => ({
  175. ...prevState,
  176. ...newState,
  177. }))
  178. const config = newState.apiConfiguration
  179. const hasKey = checkExistKey(config)
  180. setShowWelcome(!hasKey)
  181. setDidHydrateState(true)
  182. break
  183. }
  184. case "theme": {
  185. if (message.text) {
  186. setTheme(convertTextMateToHljs(JSON.parse(message.text)))
  187. }
  188. break
  189. }
  190. case "workspaceUpdated": {
  191. const paths = message.filePaths ?? []
  192. const tabs = message.openedTabs ?? []
  193. setFilePaths(paths)
  194. setOpenedTabs(tabs)
  195. break
  196. }
  197. case "partialMessage": {
  198. const partialMessage = message.partialMessage!
  199. setState((prevState) => {
  200. // worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock
  201. const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
  202. if (lastIndex !== -1) {
  203. const newClineMessages = [...prevState.clineMessages]
  204. newClineMessages[lastIndex] = partialMessage
  205. return { ...prevState, clineMessages: newClineMessages }
  206. }
  207. return prevState
  208. })
  209. break
  210. }
  211. case "glamaModels": {
  212. const updatedModels = message.glamaModels ?? {}
  213. setGlamaModels({
  214. [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
  215. ...updatedModels,
  216. })
  217. break
  218. }
  219. case "openRouterModels": {
  220. const updatedModels = message.openRouterModels ?? {}
  221. setOpenRouterModels({
  222. [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
  223. ...updatedModels,
  224. })
  225. break
  226. }
  227. case "openAiModels": {
  228. const updatedModels = message.openAiModels ?? []
  229. setOpenAiModels(updatedModels)
  230. break
  231. }
  232. case "mcpServers": {
  233. setMcpServers(message.mcpServers ?? [])
  234. break
  235. }
  236. case "currentCheckpointUpdated": {
  237. setCurrentCheckpoint(message.text)
  238. break
  239. }
  240. case "listApiConfig": {
  241. setListApiConfigMeta(message.listApiConfig ?? [])
  242. break
  243. }
  244. }
  245. },
  246. [setListApiConfigMeta],
  247. )
  248. useEvent("message", handleMessage)
  249. useEffect(() => {
  250. vscode.postMessage({ type: "webviewDidLaunch" })
  251. }, [])
  252. const contextValue: ExtensionStateContextType = {
  253. ...state,
  254. didHydrateState,
  255. showWelcome,
  256. theme,
  257. glamaModels,
  258. openRouterModels,
  259. openAiModels,
  260. mcpServers,
  261. currentCheckpoint,
  262. filePaths,
  263. openedTabs,
  264. soundVolume: state.soundVolume,
  265. fuzzyMatchThreshold: state.fuzzyMatchThreshold,
  266. writeDelayMs: state.writeDelayMs,
  267. screenshotQuality: state.screenshotQuality,
  268. setExperimentEnabled: (id, enabled) =>
  269. setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
  270. setApiConfiguration: (value) =>
  271. setState((prevState) => ({
  272. ...prevState,
  273. apiConfiguration: value,
  274. })),
  275. setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
  276. setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
  277. setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
  278. setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
  279. setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
  280. setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
  281. setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
  282. setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
  283. setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
  284. setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
  285. setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
  286. setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
  287. setCheckpointsEnabled: (value) => setState((prevState) => ({ ...prevState, checkpointsEnabled: value })),
  288. setBrowserViewportSize: (value: string) =>
  289. setState((prevState) => ({ ...prevState, browserViewportSize: value })),
  290. setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
  291. setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
  292. setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
  293. setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
  294. setTerminalOutputLineLimit: (value) =>
  295. setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
  296. setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
  297. setEnableMcpServerCreation: (value) =>
  298. setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
  299. setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
  300. setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
  301. setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
  302. setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
  303. setListApiConfigMeta,
  304. onUpdateApiConfig,
  305. setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
  306. setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
  307. setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
  308. setEnhancementApiConfigId: (value) =>
  309. setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
  310. setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
  311. handleInputChange,
  312. setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
  313. }
  314. return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
  315. }
  316. export const useExtensionState = () => {
  317. const context = useContext(ExtensionStateContext)
  318. if (context === undefined) {
  319. throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
  320. }
  321. return context
  322. }