ExtensionStateContext.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  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. filePaths: string[]
  29. setApiConfiguration: (config: ApiConfiguration) => void
  30. setCustomInstructions: (value?: string) => void
  31. setAlwaysAllowReadOnly: (value: boolean) => void
  32. setAlwaysAllowWrite: (value: boolean) => void
  33. setAlwaysAllowExecute: (value: boolean) => void
  34. setAlwaysAllowBrowser: (value: boolean) => void
  35. setAlwaysAllowMcp: (value: boolean) => void
  36. setAlwaysAllowModeSwitch: (value: boolean) => void
  37. setShowAnnouncement: (value: boolean) => void
  38. setAllowedCommands: (value: string[]) => void
  39. setSoundEnabled: (value: boolean) => void
  40. setSoundVolume: (value: number) => void
  41. setDiffEnabled: (value: boolean) => void
  42. setBrowserViewportSize: (value: string) => void
  43. setFuzzyMatchThreshold: (value: number) => void
  44. preferredLanguage: string
  45. setPreferredLanguage: (value: string) => void
  46. setWriteDelayMs: (value: number) => void
  47. screenshotQuality?: number
  48. setScreenshotQuality: (value: number) => void
  49. terminalOutputLineLimit?: number
  50. setTerminalOutputLineLimit: (value: number) => void
  51. mcpEnabled: boolean
  52. setMcpEnabled: (value: boolean) => void
  53. alwaysApproveResubmit?: boolean
  54. setAlwaysApproveResubmit: (value: boolean) => void
  55. requestDelaySeconds: number
  56. setRequestDelaySeconds: (value: number) => void
  57. setCurrentApiConfigName: (value: string) => void
  58. setListApiConfigMeta: (value: ApiConfigMeta[]) => void
  59. onUpdateApiConfig: (apiConfig: ApiConfiguration) => void
  60. mode: Mode
  61. setMode: (value: Mode) => void
  62. setCustomModePrompts: (value: CustomModePrompts) => void
  63. setCustomSupportPrompts: (value: CustomSupportPrompts) => void
  64. enhancementApiConfigId?: string
  65. setEnhancementApiConfigId: (value: string) => void
  66. setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
  67. setAutoApprovalEnabled: (value: boolean) => void
  68. handleInputChange: (field: keyof ApiConfiguration) => (event: any) => void
  69. customModes: ModeConfig[]
  70. setCustomModes: (value: ModeConfig[]) => void
  71. }
  72. export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
  73. export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  74. const [state, setState] = useState<ExtensionState>({
  75. version: "",
  76. clineMessages: [],
  77. taskHistory: [],
  78. shouldShowAnnouncement: false,
  79. allowedCommands: [],
  80. soundEnabled: false,
  81. soundVolume: 0.5,
  82. diffEnabled: false,
  83. fuzzyMatchThreshold: 1.0,
  84. preferredLanguage: "English",
  85. writeDelayMs: 1000,
  86. browserViewportSize: "900x600",
  87. screenshotQuality: 75,
  88. terminalOutputLineLimit: 500,
  89. mcpEnabled: true,
  90. alwaysApproveResubmit: false,
  91. requestDelaySeconds: 5,
  92. currentApiConfigName: "default",
  93. listApiConfigMeta: [],
  94. mode: defaultModeSlug,
  95. customModePrompts: defaultPrompts,
  96. customSupportPrompts: {},
  97. experiments: experimentDefault,
  98. enhancementApiConfigId: "",
  99. autoApprovalEnabled: false,
  100. customModes: [],
  101. })
  102. const [didHydrateState, setDidHydrateState] = useState(false)
  103. const [showWelcome, setShowWelcome] = useState(false)
  104. const [theme, setTheme] = useState<any>(undefined)
  105. const [filePaths, setFilePaths] = useState<string[]>([])
  106. const [glamaModels, setGlamaModels] = useState<Record<string, ModelInfo>>({
  107. [glamaDefaultModelId]: glamaDefaultModelInfo,
  108. })
  109. const [openRouterModels, setOpenRouterModels] = useState<Record<string, ModelInfo>>({
  110. [openRouterDefaultModelId]: openRouterDefaultModelInfo,
  111. })
  112. const [openAiModels, setOpenAiModels] = useState<string[]>([])
  113. const [mcpServers, setMcpServers] = useState<McpServer[]>([])
  114. const setListApiConfigMeta = useCallback(
  115. (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
  116. [],
  117. )
  118. const onUpdateApiConfig = useCallback((apiConfig: ApiConfiguration) => {
  119. setState((currentState) => {
  120. vscode.postMessage({
  121. type: "upsertApiConfiguration",
  122. text: currentState.currentApiConfigName,
  123. apiConfiguration: apiConfig,
  124. })
  125. return currentState // No state update needed
  126. })
  127. }, [])
  128. const handleInputChange = useCallback(
  129. (field: keyof ApiConfiguration) => (event: any) => {
  130. setState((currentState) => {
  131. vscode.postMessage({
  132. type: "upsertApiConfiguration",
  133. text: currentState.currentApiConfigName,
  134. apiConfiguration: { ...currentState.apiConfiguration, [field]: event.target.value },
  135. })
  136. return currentState // No state update needed
  137. })
  138. },
  139. [],
  140. )
  141. const handleMessage = useCallback(
  142. (event: MessageEvent) => {
  143. const message: ExtensionMessage = event.data
  144. switch (message.type) {
  145. case "state": {
  146. const newState = message.state!
  147. setState((prevState) => ({
  148. ...prevState,
  149. ...newState,
  150. }))
  151. const config = newState.apiConfiguration
  152. const hasKey = checkExistKey(config)
  153. setShowWelcome(!hasKey)
  154. setDidHydrateState(true)
  155. break
  156. }
  157. case "theme": {
  158. if (message.text) {
  159. setTheme(convertTextMateToHljs(JSON.parse(message.text)))
  160. }
  161. break
  162. }
  163. case "workspaceUpdated": {
  164. setFilePaths(message.filePaths ?? [])
  165. break
  166. }
  167. case "partialMessage": {
  168. const partialMessage = message.partialMessage!
  169. setState((prevState) => {
  170. // 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
  171. const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
  172. if (lastIndex !== -1) {
  173. const newClineMessages = [...prevState.clineMessages]
  174. newClineMessages[lastIndex] = partialMessage
  175. return { ...prevState, clineMessages: newClineMessages }
  176. }
  177. return prevState
  178. })
  179. break
  180. }
  181. case "glamaModels": {
  182. const updatedModels = message.glamaModels ?? {}
  183. setGlamaModels({
  184. [glamaDefaultModelId]: glamaDefaultModelInfo, // in case the extension sent a model list without the default model
  185. ...updatedModels,
  186. })
  187. break
  188. }
  189. case "openRouterModels": {
  190. const updatedModels = message.openRouterModels ?? {}
  191. setOpenRouterModels({
  192. [openRouterDefaultModelId]: openRouterDefaultModelInfo, // in case the extension sent a model list without the default model
  193. ...updatedModels,
  194. })
  195. break
  196. }
  197. case "openAiModels": {
  198. const updatedModels = message.openAiModels ?? []
  199. setOpenAiModels(updatedModels)
  200. break
  201. }
  202. case "mcpServers": {
  203. setMcpServers(message.mcpServers ?? [])
  204. break
  205. }
  206. case "listApiConfig": {
  207. setListApiConfigMeta(message.listApiConfig ?? [])
  208. break
  209. }
  210. }
  211. },
  212. [setListApiConfigMeta],
  213. )
  214. useEvent("message", handleMessage)
  215. useEffect(() => {
  216. vscode.postMessage({ type: "webviewDidLaunch" })
  217. }, [])
  218. const contextValue: ExtensionStateContextType = {
  219. ...state,
  220. didHydrateState,
  221. showWelcome,
  222. theme,
  223. glamaModels,
  224. openRouterModels,
  225. openAiModels,
  226. mcpServers,
  227. filePaths,
  228. soundVolume: state.soundVolume,
  229. fuzzyMatchThreshold: state.fuzzyMatchThreshold,
  230. writeDelayMs: state.writeDelayMs,
  231. screenshotQuality: state.screenshotQuality,
  232. setExperimentEnabled: (id, enabled) =>
  233. setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
  234. setApiConfiguration: (value) =>
  235. setState((prevState) => ({
  236. ...prevState,
  237. apiConfiguration: value,
  238. })),
  239. setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
  240. setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
  241. setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
  242. setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
  243. setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
  244. setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
  245. setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
  246. setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
  247. setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
  248. setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
  249. setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
  250. setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
  251. setBrowserViewportSize: (value: string) =>
  252. setState((prevState) => ({ ...prevState, browserViewportSize: value })),
  253. setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
  254. setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
  255. setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
  256. setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
  257. setTerminalOutputLineLimit: (value) =>
  258. setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
  259. setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
  260. setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
  261. setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
  262. setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
  263. setListApiConfigMeta,
  264. onUpdateApiConfig,
  265. setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
  266. setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
  267. setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
  268. setEnhancementApiConfigId: (value) =>
  269. setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
  270. setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
  271. handleInputChange,
  272. setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
  273. }
  274. return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
  275. }
  276. export const useExtensionState = () => {
  277. const context = useContext(ExtensionStateContext)
  278. if (context === undefined) {
  279. throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
  280. }
  281. return context
  282. }