| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- import React, { useCallback, useEffect, useRef, useState, useMemo } from "react"
- import { useEvent } from "react-use"
- import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
- import posthog from "posthog-js"
- 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 WelcomeViewProvider from "./components/welcome/WelcomeViewProvider"
- 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 { CheckpointRestoreDialog } from "./components/chat/CheckpointRestoreDialog"
- import { DeleteMessageDialog, EditMessageDialog } from "./components/chat/MessageModificationConfirmationDialog"
- import ErrorBoundary from "./components/ErrorBoundary"
- import { CloudView } from "./components/cloud/CloudView"
- 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" | "cloud"
- interface HumanRelayDialogState {
- isOpen: boolean
- requestId: string
- promptText: string
- }
- interface DeleteMessageDialogState {
- isOpen: boolean
- messageTs: number
- hasCheckpoint: boolean
- }
- interface EditMessageDialogState {
- isOpen: boolean
- messageTs: number
- text: string
- hasCheckpoint: boolean
- images?: string[]
- }
- // Memoize dialog components to prevent unnecessary re-renders
- const MemoizedDeleteMessageDialog = React.memo(DeleteMessageDialog)
- const MemoizedEditMessageDialog = React.memo(EditMessageDialog)
- const MemoizedCheckpointRestoreDialog = React.memo(CheckpointRestoreDialog)
- const MemoizedHumanRelayDialog = React.memo(HumanRelayDialog)
- const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
- chatButtonClicked: "chat",
- settingsButtonClicked: "settings",
- promptsButtonClicked: "modes",
- mcpButtonClicked: "mcp",
- historyButtonClicked: "history",
- marketplaceButtonClicked: "marketplace",
- cloudButtonClicked: "cloud",
- }
- const App = () => {
- const {
- didHydrateState,
- showWelcome,
- shouldShowAnnouncement,
- telemetrySetting,
- telemetryKey,
- machineId,
- cloudUserInfo,
- cloudIsAuthenticated,
- cloudApiUrl,
- cloudOrganizations,
- renderContext,
- mdmCompliant,
- } = useExtensionState()
- const [useProviderSignupView, setUseProviderSignupView] = useState(false)
- // Check PostHog feature flag for provider signup view
- useEffect(() => {
- posthog.onFeatureFlags(function () {
- // Feature flag for new provider-focused welcome view
- setUseProviderSignupView(posthog?.getFeatureFlag("welcome-provider-signup") === "test")
- })
- }, [])
- // Create a persistent state manager
- const marketplaceStateManager = useMemo(() => new MarketplaceViewStateManager(), [])
- const [showAnnouncement, setShowAnnouncement] = useState(false)
- const [tab, setTab] = useState<Tab>("chat")
- const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
- isOpen: false,
- requestId: "",
- promptText: "",
- })
- const [deleteMessageDialogState, setDeleteMessageDialogState] = useState<DeleteMessageDialogState>({
- isOpen: false,
- messageTs: 0,
- hasCheckpoint: false,
- })
- const [editMessageDialogState, setEditMessageDialogState] = useState<EditMessageDialogState>({
- isOpen: false,
- messageTs: 0,
- text: "",
- hasCheckpoint: false,
- images: [],
- })
- const settingsRef = useRef<SettingsViewRef>(null)
- const chatViewRef = useRef<ChatViewRef>(null)
- const switchTab = useCallback(
- (newTab: Tab) => {
- // Only check MDM compliance if mdmCompliant is explicitly false (meaning there's an MDM policy and user is non-compliant)
- // If mdmCompliant is undefined or true, allow tab switching
- if (mdmCompliant === false && newTab !== "cloud") {
- // Notify the user that authentication is required by their organization
- vscode.postMessage({ type: "showMdmAuthRequiredNotification" })
- return
- }
- setCurrentSection(undefined)
- setCurrentMarketplaceTab(undefined)
- if (settingsRef.current?.checkUnsaveChanges) {
- settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
- } else {
- setTab(newTab)
- }
- },
- [mdmCompliant],
- )
- const [currentSection, setCurrentSection] = useState<string | undefined>(undefined)
- const [currentMarketplaceTab, setCurrentMarketplaceTab] = useState<string | undefined>(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)
- // Extract targetSection from values if provided
- const targetSection = message.values?.section as string | undefined
- setCurrentSection(targetSection)
- 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,
- hasCheckpoint: message.hasCheckpoint || false,
- })
- }
- if (message.type === "showEditMessageDialog" && message.messageTs && message.text) {
- setEditMessageDialogState({
- isOpen: true,
- messageTs: message.messageTs,
- text: message.text,
- hasCheckpoint: message.hasCheckpoint || false,
- images: message.images || [],
- })
- }
- if (message.type === "acceptInput") {
- chatViewRef.current?.acceptInput()
- }
- },
- [switchTab],
- )
- useEvent("message", onMessage)
- useEffect(() => {
- if (shouldShowAnnouncement && tab === "chat") {
- setShowAnnouncement(true)
- vscode.postMessage({ type: "didShowAnnouncement" })
- }
- }, [shouldShowAnnouncement, tab])
- 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 ? (
- useProviderSignupView ? (
- <WelcomeViewProvider />
- ) : (
- <WelcomeView />
- )
- ) : (
- <>
- {tab === "modes" && <ModesView onDone={() => switchTab("chat")} />}
- {tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
- {tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
- {tab === "settings" && (
- <SettingsView ref={settingsRef} onDone={() => setTab("chat")} targetSection={currentSection} />
- )}
- {tab === "marketplace" && (
- <MarketplaceView
- stateManager={marketplaceStateManager}
- onDone={() => switchTab("chat")}
- targetTab={currentMarketplaceTab as "mcp" | "mode" | undefined}
- />
- )}
- {tab === "cloud" && (
- <CloudView
- userInfo={cloudUserInfo}
- isAuthenticated={cloudIsAuthenticated}
- cloudApiUrl={cloudApiUrl}
- organizations={cloudOrganizations}
- onDone={() => switchTab("chat")}
- />
- )}
- <ChatView
- ref={chatViewRef}
- isHidden={tab !== "chat"}
- showAnnouncement={showAnnouncement}
- hideAnnouncement={() => setShowAnnouncement(false)}
- />
- <MemoizedHumanRelayDialog
- isOpen={humanRelayDialogState.isOpen}
- requestId={humanRelayDialogState.requestId}
- promptText={humanRelayDialogState.promptText}
- onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
- onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
- onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
- />
- {deleteMessageDialogState.hasCheckpoint ? (
- <MemoizedCheckpointRestoreDialog
- open={deleteMessageDialogState.isOpen}
- type="delete"
- hasCheckpoint={deleteMessageDialogState.hasCheckpoint}
- onOpenChange={(open: boolean) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
- onConfirm={(restoreCheckpoint: boolean) => {
- vscode.postMessage({
- type: "deleteMessageConfirm",
- messageTs: deleteMessageDialogState.messageTs,
- restoreCheckpoint,
- })
- setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
- }}
- />
- ) : (
- <MemoizedDeleteMessageDialog
- open={deleteMessageDialogState.isOpen}
- onOpenChange={(open: boolean) => setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
- onConfirm={() => {
- vscode.postMessage({
- type: "deleteMessageConfirm",
- messageTs: deleteMessageDialogState.messageTs,
- })
- setDeleteMessageDialogState((prev) => ({ ...prev, isOpen: false }))
- }}
- />
- )}
- {editMessageDialogState.hasCheckpoint ? (
- <MemoizedCheckpointRestoreDialog
- open={editMessageDialogState.isOpen}
- type="edit"
- hasCheckpoint={editMessageDialogState.hasCheckpoint}
- onOpenChange={(open: boolean) => setEditMessageDialogState((prev) => ({ ...prev, isOpen: open }))}
- onConfirm={(restoreCheckpoint: boolean) => {
- vscode.postMessage({
- type: "editMessageConfirm",
- messageTs: editMessageDialogState.messageTs,
- text: editMessageDialogState.text,
- restoreCheckpoint,
- })
- setEditMessageDialogState((prev) => ({ ...prev, isOpen: false }))
- }}
- />
- ) : (
- <MemoizedEditMessageDialog
- open={editMessageDialogState.isOpen}
- onOpenChange={(open: boolean) => 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 = () => (
- <ErrorBoundary>
- <ExtensionStateContextProvider>
- <TranslationProvider>
- <QueryClientProvider client={queryClient}>
- <TooltipProvider delayDuration={STANDARD_TOOLTIP_DELAY}>
- <App />
- </TooltipProvider>
- </QueryClientProvider>
- </TranslationProvider>
- </ExtensionStateContextProvider>
- </ErrorBoundary>
- )
- export default AppWithProviders
|