ExtensionStateContext.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
  2. import { useEvent } from "react-use"
  3. import { merge } from "lodash"
  4. import { ApiConfigMeta, ExtensionMessage, ExtensionState } from "../../../src/shared/ExtensionMessage"
  5. import { ApiConfiguration } from "../../../src/shared/api"
  6. import { vscode } from "../utils/vscode"
  7. import { convertTextMateToHljs } from "../utils/textMateToHljs"
  8. import { findLastIndex } from "../../../src/shared/array"
  9. import { McpServer } from "../../../src/shared/mcp"
  10. import { checkExistKey } from "../../../src/shared/checkExistApiConfig"
  11. import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "../../../src/shared/modes"
  12. import { CustomSupportPrompts } from "../../../src/shared/support-prompt"
  13. import { experimentDefault, ExperimentId } from "../../../src/shared/experiments"
  14. export interface ExtensionStateContextType extends ExtensionState {
  15. didHydrateState: boolean
  16. showWelcome: boolean
  17. theme: any
  18. mcpServers: McpServer[]
  19. currentCheckpoint?: string
  20. filePaths: string[]
  21. openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
  22. setApiConfiguration: (config: ApiConfiguration) => void
  23. setCustomInstructions: (value?: string) => void
  24. setAlwaysAllowReadOnly: (value: boolean) => void
  25. setAlwaysAllowWrite: (value: boolean) => void
  26. setAlwaysAllowExecute: (value: boolean) => void
  27. setAlwaysAllowBrowser: (value: boolean) => void
  28. setAlwaysAllowMcp: (value: boolean) => void
  29. setAlwaysAllowModeSwitch: (value: boolean) => void
  30. setShowAnnouncement: (value: boolean) => void
  31. setAllowedCommands: (value: string[]) => void
  32. setSoundEnabled: (value: boolean) => void
  33. setSoundVolume: (value: number) => void
  34. setDiffEnabled: (value: boolean) => void
  35. setEnableCheckpoints: (value: boolean) => void
  36. setBrowserViewportSize: (value: string) => void
  37. setFuzzyMatchThreshold: (value: number) => void
  38. preferredLanguage: string
  39. setPreferredLanguage: (value: string) => void
  40. setWriteDelayMs: (value: number) => void
  41. screenshotQuality?: number
  42. setScreenshotQuality: (value: number) => void
  43. terminalOutputLineLimit?: number
  44. setTerminalOutputLineLimit: (value: number) => void
  45. mcpEnabled: boolean
  46. setMcpEnabled: (value: boolean) => void
  47. enableMcpServerCreation: boolean
  48. setEnableMcpServerCreation: (value: boolean) => void
  49. alwaysApproveResubmit?: boolean
  50. setAlwaysApproveResubmit: (value: boolean) => void
  51. requestDelaySeconds: number
  52. setRequestDelaySeconds: (value: number) => void
  53. rateLimitSeconds: number
  54. setRateLimitSeconds: (value: number) => void
  55. setCurrentApiConfigName: (value: string) => void
  56. setListApiConfigMeta: (value: ApiConfigMeta[]) => void
  57. mode: Mode
  58. setMode: (value: Mode) => void
  59. setCustomModePrompts: (value: CustomModePrompts) => void
  60. setCustomSupportPrompts: (value: CustomSupportPrompts) => void
  61. enhancementApiConfigId?: string
  62. setEnhancementApiConfigId: (value: string) => void
  63. setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
  64. setAutoApprovalEnabled: (value: boolean) => void
  65. customModes: ModeConfig[]
  66. setCustomModes: (value: ModeConfig[]) => void
  67. setMaxOpenTabsContext: (value: number) => void
  68. }
  69. export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
  70. export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  71. const [state, setState] = useState<ExtensionState>({
  72. version: "",
  73. clineMessages: [],
  74. taskHistory: [],
  75. shouldShowAnnouncement: false,
  76. allowedCommands: [],
  77. soundEnabled: false,
  78. soundVolume: 0.5,
  79. diffEnabled: false,
  80. enableCheckpoints: true,
  81. fuzzyMatchThreshold: 1.0,
  82. preferredLanguage: "English",
  83. writeDelayMs: 1000,
  84. browserViewportSize: "900x600",
  85. screenshotQuality: 75,
  86. terminalOutputLineLimit: 500,
  87. mcpEnabled: true,
  88. enableMcpServerCreation: true,
  89. alwaysApproveResubmit: false,
  90. requestDelaySeconds: 5,
  91. rateLimitSeconds: 0, // Minimum time between successive requests (0 = disabled)
  92. currentApiConfigName: "default",
  93. listApiConfigMeta: [],
  94. mode: defaultModeSlug,
  95. customModePrompts: defaultPrompts,
  96. customSupportPrompts: {},
  97. experiments: experimentDefault,
  98. enhancementApiConfigId: "",
  99. autoApprovalEnabled: false,
  100. customModes: [],
  101. maxOpenTabsContext: 20,
  102. cwd: "",
  103. })
  104. const [didHydrateState, setDidHydrateState] = useState(false)
  105. const [showWelcome, setShowWelcome] = useState(false)
  106. const [theme, setTheme] = useState<any>(undefined)
  107. const [filePaths, setFilePaths] = useState<string[]>([])
  108. const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
  109. const [mcpServers, setMcpServers] = useState<McpServer[]>([])
  110. const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
  111. const setListApiConfigMeta = useCallback(
  112. (value: ApiConfigMeta[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
  113. [],
  114. )
  115. const handleMessage = useCallback(
  116. (event: MessageEvent) => {
  117. const message: ExtensionMessage = event.data
  118. switch (message.type) {
  119. case "state": {
  120. const newState = message.state!
  121. setState((prevState) => mergeExtensionState(prevState, newState))
  122. setShowWelcome(!checkExistKey(newState.apiConfiguration))
  123. setDidHydrateState(true)
  124. break
  125. }
  126. case "theme": {
  127. if (message.text) {
  128. setTheme(convertTextMateToHljs(JSON.parse(message.text)))
  129. }
  130. break
  131. }
  132. case "workspaceUpdated": {
  133. const paths = message.filePaths ?? []
  134. const tabs = message.openedTabs ?? []
  135. setFilePaths(paths)
  136. setOpenedTabs(tabs)
  137. break
  138. }
  139. case "partialMessage": {
  140. const partialMessage = message.partialMessage!
  141. setState((prevState) => {
  142. // 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
  143. const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
  144. if (lastIndex !== -1) {
  145. const newClineMessages = [...prevState.clineMessages]
  146. newClineMessages[lastIndex] = partialMessage
  147. return { ...prevState, clineMessages: newClineMessages }
  148. }
  149. return prevState
  150. })
  151. break
  152. }
  153. case "mcpServers": {
  154. setMcpServers(message.mcpServers ?? [])
  155. break
  156. }
  157. case "currentCheckpointUpdated": {
  158. setCurrentCheckpoint(message.text)
  159. break
  160. }
  161. case "listApiConfig": {
  162. setListApiConfigMeta(message.listApiConfig ?? [])
  163. break
  164. }
  165. }
  166. },
  167. [setListApiConfigMeta],
  168. )
  169. useEvent("message", handleMessage)
  170. useEffect(() => {
  171. vscode.postMessage({ type: "webviewDidLaunch" })
  172. }, [])
  173. const contextValue: ExtensionStateContextType = {
  174. ...state,
  175. didHydrateState,
  176. showWelcome,
  177. theme,
  178. mcpServers,
  179. currentCheckpoint,
  180. filePaths,
  181. openedTabs,
  182. soundVolume: state.soundVolume,
  183. fuzzyMatchThreshold: state.fuzzyMatchThreshold,
  184. writeDelayMs: state.writeDelayMs,
  185. screenshotQuality: state.screenshotQuality,
  186. setExperimentEnabled: (id, enabled) =>
  187. setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
  188. setApiConfiguration: (value) =>
  189. setState((prevState) => ({
  190. ...prevState,
  191. apiConfiguration: {
  192. ...prevState.apiConfiguration,
  193. ...value,
  194. },
  195. })),
  196. setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
  197. setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
  198. setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
  199. setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
  200. setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
  201. setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
  202. setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
  203. setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
  204. setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
  205. setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
  206. setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
  207. setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
  208. setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
  209. setBrowserViewportSize: (value: string) =>
  210. setState((prevState) => ({ ...prevState, browserViewportSize: value })),
  211. setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
  212. setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
  213. setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
  214. setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
  215. setTerminalOutputLineLimit: (value) =>
  216. setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
  217. setMcpEnabled: (value) => setState((prevState) => ({ ...prevState, mcpEnabled: value })),
  218. setEnableMcpServerCreation: (value) =>
  219. setState((prevState) => ({ ...prevState, enableMcpServerCreation: value })),
  220. setAlwaysApproveResubmit: (value) => setState((prevState) => ({ ...prevState, alwaysApproveResubmit: value })),
  221. setRequestDelaySeconds: (value) => setState((prevState) => ({ ...prevState, requestDelaySeconds: value })),
  222. setRateLimitSeconds: (value) => setState((prevState) => ({ ...prevState, rateLimitSeconds: value })),
  223. setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
  224. setListApiConfigMeta,
  225. setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
  226. setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
  227. setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
  228. setEnhancementApiConfigId: (value) =>
  229. setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
  230. setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
  231. setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
  232. setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),
  233. }
  234. return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
  235. }
  236. export const useExtensionState = () => {
  237. const context = useContext(ExtensionStateContext)
  238. if (context === undefined) {
  239. throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
  240. }
  241. return context
  242. }
  243. export const mergeExtensionState = (prevState: ExtensionState, newState: ExtensionState): ExtensionState =>
  244. merge(prevState, newState)