ExtensionStateContext.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
  2. import { useEvent } from "react-use"
  3. import { ProviderSettingsEntry, ExtensionMessage, ExtensionState } from "@roo/shared/ExtensionMessage"
  4. import { ProviderSettings } from "@roo/shared/api"
  5. import { findLastIndex } from "@roo/shared/array"
  6. import { McpServer } from "@roo/shared/mcp"
  7. import { checkExistKey } from "@roo/shared/checkExistApiConfig"
  8. import { Mode, CustomModePrompts, defaultModeSlug, defaultPrompts, ModeConfig } from "@roo/shared/modes"
  9. import { CustomSupportPrompts } from "@roo/shared/support-prompt"
  10. import { experimentDefault, ExperimentId } from "@roo/shared/experiments"
  11. import { TelemetrySetting } from "@roo/shared/TelemetrySetting"
  12. import { vscode } from "@src/utils/vscode"
  13. import { convertTextMateToHljs } from "@src/utils/textMateToHljs"
  14. export interface ExtensionStateContextType extends ExtensionState {
  15. historyPreviewCollapsed?: boolean // Add the new state property
  16. didHydrateState: boolean
  17. showWelcome: boolean
  18. theme: any
  19. mcpServers: McpServer[]
  20. hasSystemPromptOverride?: boolean
  21. currentCheckpoint?: string
  22. filePaths: string[]
  23. openedTabs: Array<{ label: string; isActive: boolean; path?: string }>
  24. setApiConfiguration: (config: ProviderSettings) => void
  25. setCustomInstructions: (value?: string) => void
  26. setAlwaysAllowReadOnly: (value: boolean) => void
  27. setAlwaysAllowReadOnlyOutsideWorkspace: (value: boolean) => void
  28. setAlwaysAllowWrite: (value: boolean) => void
  29. setAlwaysAllowWriteOutsideWorkspace: (value: boolean) => void
  30. setAlwaysAllowExecute: (value: boolean) => void
  31. setAlwaysAllowBrowser: (value: boolean) => void
  32. setAlwaysAllowMcp: (value: boolean) => void
  33. setAlwaysAllowModeSwitch: (value: boolean) => void
  34. setAlwaysAllowSubtasks: (value: boolean) => void
  35. setBrowserToolEnabled: (value: boolean) => void
  36. setShowRooIgnoredFiles: (value: boolean) => void
  37. setShowAnnouncement: (value: boolean) => void
  38. setAllowedCommands: (value: string[]) => void
  39. setAllowedMaxRequests: (value: number | undefined) => void
  40. setSoundEnabled: (value: boolean) => void
  41. setSoundVolume: (value: number) => void
  42. terminalShellIntegrationTimeout?: number
  43. setTerminalShellIntegrationTimeout: (value: number) => void
  44. terminalShellIntegrationDisabled?: boolean
  45. setTerminalShellIntegrationDisabled: (value: boolean) => void
  46. terminalZdotdir?: boolean
  47. setTerminalZdotdir: (value: boolean) => void
  48. setTtsEnabled: (value: boolean) => void
  49. setTtsSpeed: (value: number) => void
  50. setDiffEnabled: (value: boolean) => void
  51. setEnableCheckpoints: (value: boolean) => void
  52. setBrowserViewportSize: (value: string) => void
  53. setFuzzyMatchThreshold: (value: number) => void
  54. setWriteDelayMs: (value: number) => void
  55. screenshotQuality?: number
  56. setScreenshotQuality: (value: number) => void
  57. terminalOutputLineLimit?: number
  58. setTerminalOutputLineLimit: (value: number) => void
  59. mcpEnabled: boolean
  60. setMcpEnabled: (value: boolean) => void
  61. enableMcpServerCreation: boolean
  62. setEnableMcpServerCreation: (value: boolean) => void
  63. alwaysApproveResubmit?: boolean
  64. setAlwaysApproveResubmit: (value: boolean) => void
  65. requestDelaySeconds: number
  66. setRequestDelaySeconds: (value: number) => void
  67. setCurrentApiConfigName: (value: string) => void
  68. setListApiConfigMeta: (value: ProviderSettingsEntry[]) => void
  69. mode: Mode
  70. setMode: (value: Mode) => void
  71. setCustomModePrompts: (value: CustomModePrompts) => void
  72. setCustomSupportPrompts: (value: CustomSupportPrompts) => void
  73. enhancementApiConfigId?: string
  74. setEnhancementApiConfigId: (value: string) => void
  75. setExperimentEnabled: (id: ExperimentId, enabled: boolean) => void
  76. setAutoApprovalEnabled: (value: boolean) => void
  77. customModes: ModeConfig[]
  78. setCustomModes: (value: ModeConfig[]) => void
  79. setMaxOpenTabsContext: (value: number) => void
  80. maxWorkspaceFiles: number
  81. setMaxWorkspaceFiles: (value: number) => void
  82. setTelemetrySetting: (value: TelemetrySetting) => void
  83. remoteBrowserEnabled?: boolean
  84. setRemoteBrowserEnabled: (value: boolean) => void
  85. awsUsePromptCache?: boolean
  86. setAwsUsePromptCache: (value: boolean) => void
  87. maxReadFileLine: number
  88. setMaxReadFileLine: (value: number) => void
  89. machineId?: string
  90. pinnedApiConfigs?: Record<string, boolean>
  91. setPinnedApiConfigs: (value: Record<string, boolean>) => void
  92. togglePinnedApiConfig: (configName: string) => void
  93. terminalCompressProgressBar?: boolean
  94. setTerminalCompressProgressBar: (value: boolean) => void
  95. setHistoryPreviewCollapsed: (value: boolean) => void
  96. }
  97. export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
  98. export const mergeExtensionState = (prevState: ExtensionState, newState: ExtensionState) => {
  99. const {
  100. customModePrompts: prevCustomModePrompts,
  101. customSupportPrompts: prevCustomSupportPrompts,
  102. experiments: prevExperiments,
  103. ...prevRest
  104. } = prevState
  105. const {
  106. apiConfiguration,
  107. customModePrompts: newCustomModePrompts,
  108. customSupportPrompts: newCustomSupportPrompts,
  109. experiments: newExperiments,
  110. ...newRest
  111. } = newState
  112. const customModePrompts = { ...prevCustomModePrompts, ...newCustomModePrompts }
  113. const customSupportPrompts = { ...prevCustomSupportPrompts, ...newCustomSupportPrompts }
  114. const experiments = { ...prevExperiments, ...newExperiments }
  115. const rest = { ...prevRest, ...newRest }
  116. // Note that we completely replace the previous apiConfiguration object with
  117. // a new one since the state that is broadcast is the entire apiConfiguration
  118. // and therefore merging is not necessary.
  119. return { ...rest, apiConfiguration, customModePrompts, customSupportPrompts, experiments }
  120. }
  121. export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  122. const [state, setState] = useState<ExtensionState>({
  123. version: "",
  124. clineMessages: [],
  125. taskHistory: [],
  126. shouldShowAnnouncement: false,
  127. allowedCommands: [],
  128. allowedMaxRequests: Infinity,
  129. soundEnabled: false,
  130. soundVolume: 0.5,
  131. ttsEnabled: false,
  132. ttsSpeed: 1.0,
  133. diffEnabled: false,
  134. enableCheckpoints: true,
  135. fuzzyMatchThreshold: 1.0,
  136. language: "en", // Default language code
  137. writeDelayMs: 1000,
  138. browserViewportSize: "900x600",
  139. screenshotQuality: 75,
  140. terminalOutputLineLimit: 500,
  141. terminalShellIntegrationTimeout: 4000,
  142. mcpEnabled: true,
  143. enableMcpServerCreation: true,
  144. alwaysApproveResubmit: false,
  145. requestDelaySeconds: 5,
  146. currentApiConfigName: "default",
  147. listApiConfigMeta: [],
  148. mode: defaultModeSlug,
  149. customModePrompts: defaultPrompts,
  150. customSupportPrompts: {},
  151. experiments: experimentDefault,
  152. enhancementApiConfigId: "",
  153. autoApprovalEnabled: false,
  154. customModes: [],
  155. maxOpenTabsContext: 20,
  156. maxWorkspaceFiles: 200,
  157. cwd: "",
  158. browserToolEnabled: true,
  159. telemetrySetting: "unset",
  160. showRooIgnoredFiles: true, // Default to showing .rooignore'd files with lock symbol (current behavior).
  161. renderContext: "sidebar",
  162. maxReadFileLine: 500, // Default max read file line limit
  163. pinnedApiConfigs: {}, // Empty object for pinned API configs
  164. terminalZshOhMy: false, // Default Oh My Zsh integration setting
  165. terminalZshP10k: false, // Default Powerlevel10k integration setting
  166. terminalZdotdir: false, // Default ZDOTDIR handling setting
  167. terminalCompressProgressBar: true, // Default to compress progress bar output
  168. historyPreviewCollapsed: false, // Initialize the new state (default to expanded)
  169. })
  170. const [didHydrateState, setDidHydrateState] = useState(false)
  171. const [showWelcome, setShowWelcome] = useState(false)
  172. const [theme, setTheme] = useState<any>(undefined)
  173. const [filePaths, setFilePaths] = useState<string[]>([])
  174. const [openedTabs, setOpenedTabs] = useState<Array<{ label: string; isActive: boolean; path?: string }>>([])
  175. const [mcpServers, setMcpServers] = useState<McpServer[]>([])
  176. const [currentCheckpoint, setCurrentCheckpoint] = useState<string>()
  177. const setListApiConfigMeta = useCallback(
  178. (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
  179. [],
  180. )
  181. const handleMessage = useCallback(
  182. (event: MessageEvent) => {
  183. const message: ExtensionMessage = event.data
  184. switch (message.type) {
  185. case "state": {
  186. const newState = message.state!
  187. setState((prevState) => mergeExtensionState(prevState, newState))
  188. setShowWelcome(!checkExistKey(newState.apiConfiguration))
  189. setDidHydrateState(true)
  190. break
  191. }
  192. case "theme": {
  193. if (message.text) {
  194. setTheme(convertTextMateToHljs(JSON.parse(message.text)))
  195. }
  196. break
  197. }
  198. case "workspaceUpdated": {
  199. const paths = message.filePaths ?? []
  200. const tabs = message.openedTabs ?? []
  201. setFilePaths(paths)
  202. setOpenedTabs(tabs)
  203. break
  204. }
  205. case "partialMessage": {
  206. const partialMessage = message.partialMessage!
  207. setState((prevState) => {
  208. // 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
  209. const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === partialMessage.ts)
  210. if (lastIndex !== -1) {
  211. const newClineMessages = [...prevState.clineMessages]
  212. newClineMessages[lastIndex] = partialMessage
  213. return { ...prevState, clineMessages: newClineMessages }
  214. }
  215. return prevState
  216. })
  217. break
  218. }
  219. case "mcpServers": {
  220. setMcpServers(message.mcpServers ?? [])
  221. break
  222. }
  223. case "currentCheckpointUpdated": {
  224. setCurrentCheckpoint(message.text)
  225. break
  226. }
  227. case "listApiConfig": {
  228. setListApiConfigMeta(message.listApiConfig ?? [])
  229. break
  230. }
  231. }
  232. },
  233. [setListApiConfigMeta],
  234. )
  235. useEvent("message", handleMessage)
  236. useEffect(() => {
  237. vscode.postMessage({ type: "webviewDidLaunch" })
  238. }, [])
  239. const contextValue: ExtensionStateContextType = {
  240. ...state,
  241. didHydrateState,
  242. showWelcome,
  243. theme,
  244. mcpServers,
  245. currentCheckpoint,
  246. filePaths,
  247. openedTabs,
  248. soundVolume: state.soundVolume,
  249. ttsSpeed: state.ttsSpeed,
  250. fuzzyMatchThreshold: state.fuzzyMatchThreshold,
  251. writeDelayMs: state.writeDelayMs,
  252. screenshotQuality: state.screenshotQuality,
  253. setExperimentEnabled: (id, enabled) =>
  254. setState((prevState) => ({ ...prevState, experiments: { ...prevState.experiments, [id]: enabled } })),
  255. setApiConfiguration: (value) =>
  256. setState((prevState) => ({
  257. ...prevState,
  258. apiConfiguration: {
  259. ...prevState.apiConfiguration,
  260. ...value,
  261. },
  262. })),
  263. setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
  264. setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
  265. setAlwaysAllowReadOnlyOutsideWorkspace: (value) =>
  266. setState((prevState) => ({ ...prevState, alwaysAllowReadOnlyOutsideWorkspace: value })),
  267. setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
  268. setAlwaysAllowWriteOutsideWorkspace: (value) =>
  269. setState((prevState) => ({ ...prevState, alwaysAllowWriteOutsideWorkspace: value })),
  270. setAlwaysAllowExecute: (value) => setState((prevState) => ({ ...prevState, alwaysAllowExecute: value })),
  271. setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })),
  272. setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })),
  273. setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })),
  274. setAlwaysAllowSubtasks: (value) => setState((prevState) => ({ ...prevState, alwaysAllowSubtasks: value })),
  275. setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
  276. setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
  277. setAllowedMaxRequests: (value) => setState((prevState) => ({ ...prevState, allowedMaxRequests: value })),
  278. setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
  279. setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
  280. setTtsEnabled: (value) => setState((prevState) => ({ ...prevState, ttsEnabled: value })),
  281. setTtsSpeed: (value) => setState((prevState) => ({ ...prevState, ttsSpeed: value })),
  282. setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
  283. setEnableCheckpoints: (value) => setState((prevState) => ({ ...prevState, enableCheckpoints: value })),
  284. setBrowserViewportSize: (value: string) =>
  285. setState((prevState) => ({ ...prevState, browserViewportSize: value })),
  286. setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
  287. setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
  288. setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })),
  289. setTerminalOutputLineLimit: (value) =>
  290. setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })),
  291. setTerminalShellIntegrationTimeout: (value) =>
  292. setState((prevState) => ({ ...prevState, terminalShellIntegrationTimeout: value })),
  293. setTerminalShellIntegrationDisabled: (value) =>
  294. setState((prevState) => ({ ...prevState, terminalShellIntegrationDisabled: value })),
  295. setTerminalZdotdir: (value) => setState((prevState) => ({ ...prevState, terminalZdotdir: 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. setCurrentApiConfigName: (value) => setState((prevState) => ({ ...prevState, currentApiConfigName: value })),
  302. setListApiConfigMeta,
  303. setMode: (value: Mode) => setState((prevState) => ({ ...prevState, mode: value })),
  304. setCustomModePrompts: (value) => setState((prevState) => ({ ...prevState, customModePrompts: value })),
  305. setCustomSupportPrompts: (value) => setState((prevState) => ({ ...prevState, customSupportPrompts: value })),
  306. setEnhancementApiConfigId: (value) =>
  307. setState((prevState) => ({ ...prevState, enhancementApiConfigId: value })),
  308. setAutoApprovalEnabled: (value) => setState((prevState) => ({ ...prevState, autoApprovalEnabled: value })),
  309. setCustomModes: (value) => setState((prevState) => ({ ...prevState, customModes: value })),
  310. setMaxOpenTabsContext: (value) => setState((prevState) => ({ ...prevState, maxOpenTabsContext: value })),
  311. setMaxWorkspaceFiles: (value) => setState((prevState) => ({ ...prevState, maxWorkspaceFiles: value })),
  312. setBrowserToolEnabled: (value) => setState((prevState) => ({ ...prevState, browserToolEnabled: value })),
  313. setTelemetrySetting: (value) => setState((prevState) => ({ ...prevState, telemetrySetting: value })),
  314. setShowRooIgnoredFiles: (value) => setState((prevState) => ({ ...prevState, showRooIgnoredFiles: value })),
  315. setRemoteBrowserEnabled: (value) => setState((prevState) => ({ ...prevState, remoteBrowserEnabled: value })),
  316. setAwsUsePromptCache: (value) => setState((prevState) => ({ ...prevState, awsUsePromptCache: value })),
  317. setMaxReadFileLine: (value) => setState((prevState) => ({ ...prevState, maxReadFileLine: value })),
  318. setPinnedApiConfigs: (value) => setState((prevState) => ({ ...prevState, pinnedApiConfigs: value })),
  319. setTerminalCompressProgressBar: (value) =>
  320. setState((prevState) => ({ ...prevState, terminalCompressProgressBar: value })),
  321. togglePinnedApiConfig: (configId) =>
  322. setState((prevState) => {
  323. const currentPinned = prevState.pinnedApiConfigs || {}
  324. const newPinned = {
  325. ...currentPinned,
  326. [configId]: !currentPinned[configId],
  327. }
  328. // If the config is now unpinned, remove it from the object
  329. if (!newPinned[configId]) {
  330. delete newPinned[configId]
  331. }
  332. return { ...prevState, pinnedApiConfigs: newPinned }
  333. }),
  334. setHistoryPreviewCollapsed: (value) =>
  335. setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })), // Implement the setter
  336. }
  337. return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
  338. }
  339. export const useExtensionState = () => {
  340. const context = useContext(ExtensionStateContext)
  341. if (context === undefined) {
  342. throw new Error("useExtensionState must be used within an ExtensionStateContextProvider")
  343. }
  344. return context
  345. }