App.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"
  2. import { useEvent } from "react-use"
  3. import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
  4. import posthog from "posthog-js"
  5. import { ExtensionMessage } from "@roo/ExtensionMessage"
  6. import TranslationProvider from "./i18n/TranslationContext"
  7. import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager"
  8. import { vscode } from "./utils/vscode"
  9. import { telemetryClient } from "./utils/TelemetryClient"
  10. import { TelemetryEventName } from "@roo-code/types"
  11. import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer"
  12. import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext"
  13. import ChatView, { ChatViewRef } from "./components/chat/ChatView"
  14. import HistoryView from "./components/history/HistoryView"
  15. import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView"
  16. import WelcomeView from "./components/welcome/WelcomeView"
  17. import WelcomeViewProvider from "./components/welcome/WelcomeViewProvider"
  18. import McpView from "./components/mcp/McpView"
  19. import { MarketplaceView } from "./components/marketplace/MarketplaceView"
  20. import ModesView from "./components/modes/ModesView"
  21. import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
  22. import { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog"
  23. import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
  24. import ErrorBoundary from "./components/ErrorBoundary"
  25. import { CloudView } from "./components/cloud/CloudView"
  26. import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
  27. import { TooltipProvider } from "./components/ui/tooltip"
  28. import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip"
  29. type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "cloud"
  30. interface HumanRelayDialogState {
  31. isOpen: boolean
  32. requestId: string
  33. promptText: string
  34. }
  35. interface DeleteMessageDialogState {
  36. isOpen: boolean
  37. messageTs: number
  38. hasCheckpoint: boolean
  39. }
  40. interface EditMessageDialogState {
  41. isOpen: boolean
  42. messageTs: number
  43. text: string
  44. hasCheckpoint: boolean
  45. images?: string[]
  46. }
  47. // Memoize dialog components to prevent unnecessary re-renders
  48. const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog)
  49. const MemoizedEditMessageDialog = React.memo(EditMessageDialog)
  50. const MemoizedCheckpointRestoreDialog = React.memo(CheckpointRestoreDialog)
  51. const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog)
  52. const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
  53. chatButtonClicked: "chat",
  54. settingsButtonClicked: "settings",
  55. promptsButtonClicked: "modes",
  56. mcpButtonClicked: "mcp",
  57. historyButtonClicked: "history",
  58. marketplaceButtonClicked: "marketplace",
  59. cloudButtonClicked: "cloud",
  60. }
  61. const App = () => {
  62. const {
  63. didHydrateState,
  64. showWelcome,
  65. shouldShowAnnouncement,
  66. telemetrySetting,
  67. telemetryKey,
  68. machineId,
  69. cloudUserInfo,
  70. cloudIsAuthenticated,
  71. cloudApiUrl,
  72. cloudOrganizations,
  73. renderContext,
  74. mdmCompliant,
  75. } = useExtensionState()
  76. const [useProviderSignupView, setUseProviderSignupView] = useState(false)
  77. // Check PostHog feature flag for provider signup view
  78. useEffect(() => {
  79. posthog.onFeatureFlags(function () {
  80. // Feature flag for new provider-focused welcome view
  81. setUseProviderSignupView(posthog?.getFeatureFlag("welcome-provider-signup") === "test")
  82. })
  83. }, [])
  84. // Create a persistent state manager
  85. const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), [])
  86. const [showAnnouncement, setShowAnnouncement] = useState(false)
  87. const [tab, setTab] = useState<Tab>("chat")
  88. const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
  89. isOpen: false,
  90. requestId: "",
  91. promptText: "",
  92. })
  93. const [deleteMessageDialogState, setDeleteMessageDialogState] = useState<DeleteMessageDialogState>({
  94. isOpen: false,
  95. messageTs: 0,
  96. hasCheckpoint: false,
  97. })
  98. const [editMessageDialogState, setEditMessageDialogState] = useState<EditMessageDialogState>({
  99. isOpen: false,
  100. messageTs: 0,
  101. text: "",
  102. hasCheckpoint: false,
  103. images: [],
  104. })
  105. const settingsRef = useRef<SettingsViewRef>(null)
  106. const chatViewRef = useRef<ChatViewRef>(null)
  107. const switchTab = useCallback(
  108. (newTab: Tab) => {
  109. // Only check MDM compliance if mdmCompliant is explicitly false (meaning there's an MDM policy and user is non-compliant)
  110. // If mdmCompliant is undefined or true, allow tab switching
  111. if (mdmCompliant === false && newTab !== "cloud") {
  112. // Notify the user that authentication is required by their organization
  113. vscode.postMessage({ type: "showMdmAuthRequiredNotification" })
  114. return
  115. }
  116. setCurrentSection(undefined)
  117. setCurrentMarketplaceTab(undefined)
  118. if (settingsRef.current?.checkUnsaveChanges) {
  119. settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
  120. } else {
  121. setTab(newTab)
  122. }
  123. },
  124. [mdmCompliant],
  125. )
  126. const [currentSection, setCurrentSection] = useState<string | undefined>(undefined)
  127. const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState<string | undefined>(undefined)
  128. const onMessage = useCallback(
  129. (e: MessageEvent) => {
  130. const message: ExtensionMessage = e.data
  131. if (message.type === "action" && message.action) {
  132. // Handle switchTab action with tab parameter
  133. if (message.action === "switchTab" && message.tab) {
  134. const targetTab = message.tab as Tab
  135. switchTab(targetTab)
  136. // Extract targetSection from values if provided
  137. const targetSection = message.values?.section as string | undefined
  138. setCurrentSection(targetSection)
  139. setCurrentMarketplaceTab(undefined)
  140. } else {
  141. // Handle other actions using the mapping
  142. const newTab = tabsByMessageAction[message.action]
  143. const section = message.values?.section as string | undefined
  144. const marketplaceTab = message.values?.marketplaceTab as string | undefined
  145. if (newTab) {
  146. switchTab(newTab)
  147. setCurrentSection(section)
  148. setCurrentMarketplaceTab(marketplaceTab)
  149. }
  150. }
  151. }
  152. if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) {
  153. const { requestId, promptText } = message
  154. setHumanRelayDialogState({ isOpen: true, requestId, promptText })
  155. }
  156. if (message.type === "showDeleteMessageDialog" && message.messageTs) {
  157. setDeleteMessageDialogState({
  158. isOpen: true,
  159. messageTs: message.messageTs,
  160. hasCheckpoint: message.hasCheckpoint || false,
  161. })
  162. }
  163. if (message.type === "showEditMessageDialog" && message.messageTs && message.text) {
  164. setEditMessageDialogState({
  165. isOpen: true,
  166. messageTs: message.messageTs,
  167. text: message.text,
  168. hasCheckpoint: message.hasCheckpoint || false,
  169. images: message.images || [],
  170. })
  171. }
  172. if (message.type === "acceptInput") {
  173. chatViewRef.current?.acceptInput()
  174. }
  175. },
  176. [switchTab],
  177. )
  178. useEvent("message", onMessage)
  179. useEffect(() => {
  180. if (shouldShowAnnouncement && tab === "chat") {
  181. setShowAnnouncement(true)
  182. vscode.postMessage({ type: "didShowAnnouncement" })
  183. }
  184. }, [shouldShowAnnouncement, tab])
  185. useEffect(() => {
  186. if (didHydrateState) {
  187. telemetryClient.updateTelemetryState(telemetrySetting, telemetryKey, machineId)
  188. }
  189. }, [telemetrySetting, telemetryKey, machineId, didHydrateState])
  190. // Tell the extension that we are ready to receive messages.
  191. useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
  192. // Initialize source map support for better error reporting
  193. useEffect(() => {
  194. // Initialize source maps for better error reporting in production
  195. initializeSourceMaps()
  196. // Expose source map debugging utilities in production
  197. if (process.env.NODE_ENV === "production") {
  198. exposeSourceMapsForDebugging()
  199. }
  200. // Log initialization for debugging
  201. console.debug("App initialized with source map support")
  202. }, [])
  203. // Focus the WebView when non-interactive content is clicked (only in editor/tab mode)
  204. useAddNonInteractiveClickListener(
  205. useCallback(() => {
  206. // Only send focus request if we're in editor (tab) mode, not sidebar
  207. if (renderContext === "editor") {
  208. vscode.postMessage({ type: "focusPanelRequest" })
  209. }
  210. }, [renderContext]),
  211. )
  212. // Track marketplace tab views
  213. useEffect(() => {
  214. if (tab === "marketplace") {
  215. telemetryClient.capture(TelemetryEventName.MARKETPLACE_TAB_VIEWED)
  216. }
  217. }, [tab])
  218. if (!didHydrateState) {
  219. return null
  220. }
  221. // Do not conditionally load ChatView, it's expensive and there's state we
  222. // don't want to lose (user input, disableInput, askResponse promise, etc.)
  223. return showWelcome ? (
  224. useProviderSignupView ? (
  225. <WelcomeViewProvider />
  226. ) : (
  227. <WelcomeView />
  228. )
  229. ) : (
  230. <>
  231. {tab === "modes" && <ModesView onDone={() => switchTab("chat")} />}
  232. {tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
  233. {tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
  234. {tab === "settings" && (
  235. <SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
  236. )}
  237. {tab === "marketplace" && (
  238. <MarketplaceView
  239. stateManager={marketplaceStateManager}
  240. onDone={() => switchTab("chat")}
  241. targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
  242. />
  243. )}
  244. {tab === "cloud" && (
  245. <CloudView
  246. userInfo={cloudUserInfo}
  247. isAuthenticated={cloudIsAuthenticated}
  248. cloudApiUrl={cloudApiUrl}
  249. organizations={cloudOrganizations}
  250. onDone={() => switchTab("chat")}
  251. />
  252. )}
  253. <ChatView
  254. ref={chatViewRef}
  255. isHidden={tab !== "chat"}
  256. showAnnouncement={showAnnouncement}
  257. hideAnnouncement={() => setShowAnnouncement(false)}
  258. />
  259. <MemoizedHumanRelayDialog
  260. isOpen={humanRelayDialogState.isOpen}
  261. requestId={humanRelayDialogState.requestId}
  262. promptText={humanRelayDialogState.promptText}
  263. onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
  264. onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
  265. onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
  266. />
  267. {deleteMessageDialogState.hasCheckpoint ? (
  268. <MemoizedCheckpointRestoreDialog
  269. open={deleteMessageDialogState.isOpen}
  270. type="delete"
  271. hasCheckpoint={deleteMessageDialogState.hasCheckpoint}
  272. onOpenChange={(open: boolean) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
  273. onConfirm={(restoreCheckpoint: boolean) => {
  274. vscode.postMessage({
  275. type: "deleteMessageConfirm",
  276. messageTs: deleteMessageDialogState.messageTs,
  277. restoreCheckpoint,
  278. })
  279. setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
  280. }}
  281. />
  282. ) : (
  283. <MemoizedDeleteMessageDialog
  284. open={deleteMessageDialogState.isOpen}
  285. onOpenChange={(open: boolean) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
  286. onConfirm={() => {
  287. vscode.postMessage({
  288. type: "deleteMessageConfirm",
  289. messageTs: deleteMessageDialogState.messageTs,
  290. })
  291. setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
  292. }}
  293. />
  294. )}
  295. {editMessageDialogState.hasCheckpoint ? (
  296. <MemoizedCheckpointRestoreDialog
  297. open={editMessageDialogState.isOpen}
  298. type="edit"
  299. hasCheckpoint={editMessageDialogState.hasCheckpoint}
  300. onOpenChange={(open: boolean) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
  301. onConfirm={(restoreCheckpoint: boolean) => {
  302. vscode.postMessage({
  303. type: "editMessageConfirm",
  304. messageTs: editMessageDialogState.messageTs,
  305. text: editMessageDialogState.text,
  306. restoreCheckpoint,
  307. })
  308. setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
  309. }}
  310. />
  311. ) : (
  312. <MemoizedEditMessageDialog
  313. open={editMessageDialogState.isOpen}
  314. onOpenChange={(open: boolean) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
  315. onConfirm={() => {
  316. vscode.postMessage({
  317. type: "editMessageConfirm",
  318. messageTs: editMessageDialogState.messageTs,
  319. text: editMessageDialogState.text,
  320. images: editMessageDialogState.images,
  321. })
  322. setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
  323. }}
  324. />
  325. )}
  326. </>
  327. )
  328. }
  329. const queryClient = new QueryClient()
  330. const AppWithProviders = () => (
  331. <ErrorBoundary>
  332. <ExtensionStateContextProvider>
  333. <TranslationProvider>
  334. <QueryClientProvider client={queryClient}>
  335. <TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
  336. <App />
  337. </TooltipProvider>
  338. </QueryClientProvider>
  339. </TranslationProvider>
  340. </ExtensionStateContextProvider>
  341. </ErrorBoundary>
  342. )
  343. export default AppWithProviders