import React, { useCallback, useEffect, useRef, useState, useMemo } from "react" import { useEvent } from "react-use" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { ExtensionMessage } from "@roo/ExtensionMessage" import TranslationProvider from "./i18n/TranslationContext" import { MarketplaceViewStateManager } from "./components/marketplace/MarketplaceViewStateManager" import { vscode } from "./utils/vscode" import { telemetryClient } from "./utils/TelemetryClient" import { TelemetryEventName } from "@roo-code/types" import { initializeSourceMaps, exposeSourceMapsForDebugging } from "./utils/sourceMapInitializer" import { ExtensionStateContextProvider, useExtensionState } from "./context/ExtensionStateContext" import ChatView, { ChatViewRef } from "./components/chat/ChatView" import HistoryView from "./components/history/HistoryView" import SettingsView, { SettingsViewRef } from "./components/settings/SettingsView" import WelcomeView from "./components/welcome/WelcomeView" import McpView from "./components/mcp/McpView" import { MarketplaceView } from "./components/marketplace/MarketplaceView" import ModesView from "./components/modes/ModesView" import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog" import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog" import ErrorBoundary from "./components/ErrorBoundary" import { AccountView } from "./components/account/AccountView" import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick" import { TooltipProvider } from "./components/ui/tooltip" import { STANDARD_TOOLTIP_DELAY } from "./components/ui/standard-tooltip" type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" interface HumanRelayDialogState { isOpen: boolean requestId: string promptText: string } interface DeleteMessageDialogState { isOpen: boolean messageTs: number } interface EditMessageDialogState { isOpen: boolean messageTs: number text: string images?: string[] } // Memoize dialog components to prevent unnecessary re-renders const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog) const MemoizedEditMessageDialog = React.memo(EditMessageDialog) const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog) const tabsByMessageAction: Partial, Tab>> = { chatButtonClicked: "chat", settingsButtonClicked: "settings", promptsButtonClicked: "modes", mcpButtonClicked: "mcp", historyButtonClicked: "history", marketplaceButtonClicked: "marketplace", accountButtonClicked: "account", } const App = () => { const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId, cloudUserInfo, cloudIsAuthenticated, cloudApiUrl, renderContext, mdmCompliant, } = useExtensionState() // Create a persistent state manager const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), []) const [showAnnouncement, setShowAnnouncement] = useState(false) const [tab, setTab] = useState("chat") const [humanRelayDialogState, setHumanRelayDialogState] = useState({ isOpen: false, requestId: "", promptText: "", }) const [deleteMessageDialogState, setDeleteMessageDialogState] = useState({ isOpen: false, messageTs: 0, }) const [editMessageDialogState, setEditMessageDialogState] = useState({ isOpen: false, messageTs: 0, text: "", images: [], }) const settingsRef = useRef(null) const chatViewRef = useRef(null) const switchTab = useCallback( (newTab: Tab) => { // Check MDM compliance before allowing tab switching if (mdmCompliant === false && newTab !== "account") { return } setCurrentSection(undefined) setCurrentMarketplaceTab(undefined) if (settingsRef.current?.checkUnsaveChanges) { settingsRef.current.checkUnsaveChanges(() => setTab(newTab)) } else { setTab(newTab) } }, [mdmCompliant], ) const [currentSection, setCurrentSection] = useState(undefined) const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState(undefined) const onMessage = useCallback( (e: MessageEvent) => { const message: ExtensionMessage = e.data if (message.type === "action" && message.action) { // Handle switchTab action with tab parameter if (message.action === "switchTab" && message.tab) { const targetTab = message.tab as Tab switchTab(targetTab) setCurrentSection(undefined) setCurrentMarketplaceTab(undefined) } else { // Handle other actions using the mapping const newTab = tabsByMessageAction[message.action] const section = message.values?.section as string | undefined const marketplaceTab = message.values?.marketplaceTab as string | undefined if (newTab) { switchTab(newTab) setCurrentSection(section) setCurrentMarketplaceTab(marketplaceTab) } } } if (message.type === "showHumanRelayDialog" && message.requestId && message.promptText) { const { requestId, promptText } = message setHumanRelayDialogState({ isOpen: true, requestId, promptText }) } if (message.type === "showDeleteMessageDialog" && message.messageTs) { setDeleteMessageDialogState({ isOpen: true, messageTs: message.messageTs }) } if (message.type === "showEditMessageDialog" && message.messageTs && message.text) { setEditMessageDialogState({ isOpen: true, messageTs: message.messageTs, text: message.text, images: message.images || [], }) } if (message.type === "acceptInput") { chatViewRef.current?.acceptInput() } }, [switchTab], ) useEvent("message", onMessage) useEffect(() => { if (shouldShowAnnouncement) { setShowAnnouncement(true) vscode.postMessage({ type: "didShowAnnouncement" }) } }, [shouldShowAnnouncement]) useEffect(() => { if (didHydrateState) { telemetryClient.updateTelemetryState(telemetrySetting, telemetryKey, machineId) } }, [telemetrySetting, telemetryKey, machineId, didHydrateState]) // Tell the extension that we are ready to receive messages. useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), []) // Initialize source map support for better error reporting useEffect(() => { // Initialize source maps for better error reporting in production initializeSourceMaps() // Expose source map debugging utilities in production if (process.env.NODE_ENV === "production") { exposeSourceMapsForDebugging() } // Log initialization for debugging console.debug("App initialized with source map support") }, []) // Focus the WebView when non-interactive content is clicked (only in editor/tab mode) useAddNonInteractiveClickListener( useCallback(() => { // Only send focus request if we're in editor (tab) mode, not sidebar if (renderContext === "editor") { vscode.postMessage({ type: "focusPanelRequest" }) } }, [renderContext]), ) // Track marketplace tab views useEffect(() => { if (tab === "marketplace") { telemetryClient.capture(TelemetryEventName.MARKETPLACE_TAB_VIEWED) } }, [tab]) if (!didHydrateState) { return null } // Do not conditionally load ChatView, it's expensive and there's state we // don't want to lose (user input, disableInput, askResponse promise, etc.) return showWelcome ? ( ) : ( <> {tab === "modes" && switchTab("chat")} />} {tab === "mcp" && switchTab("chat")} />} {tab === "history" && switchTab("chat")} />} {tab === "settings" && ( setTab("chat")} targetSection={currentSection} /> )} {tab === "marketplace" && ( switchTab("chat")} targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined} /> )} {tab === "account" && ( switchTab("chat")} /> )} setShowAnnouncement(false)} /> setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))} onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })} onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })} /> setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))} onConfirm={() => { vscode.postMessage({ type: "deleteMessageConfirm", messageTs: deleteMessageDialogState.messageTs, }) setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false })) }} /> setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))} onConfirm={() => { vscode.postMessage({ type: "editMessageConfirm", messageTs: editMessageDialogState.messageTs, text: editMessageDialogState.text, images: editMessageDialogState.images, }) setEditMessageDialogState((prev) => ({ ...prev, isOpen: false })) }} /> ) } const queryClient = new QueryClient() const AppWithProviders = () => ( ) export default AppWithProviders