Roo-Code/webview-ui/src/App.tsx
roomote[bot] 18117f0723
ux: Updates to CloudView (#9776)
* refactor: remove TabHeader and onDone callback from CloudView

- Removed TabHeader component from CloudView as it is no longer needed
- Removed onDone prop from CloudView component definition and usage
- Updated all test files to reflect the removal of onDone prop
- Kept Button import that was accidentally removed initially

* Updates upsell copy to reflect today's product

* Update webview-ui/src/components/cloud/CloudView.tsx

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update webview-ui/src/i18n/locales/ko/cloud.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Update webview-ui/src/i18n/locales/zh-CN/cloud.json

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>

* Test fixes

---------

Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: Bruno Bergher <bruno@roocode.com>
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-12-03 13:36:33 +00:00

361 lines
12 KiB
TypeScript

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/WelcomeViewProvider"
import { MarketplaceView } from "./components/marketplace/MarketplaceView"
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" | "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",
historyButtonClicked: "history",
marketplaceButtonClicked: "marketplace",
cloudButtonClicked: "cloud",
}
const App = () => {
const {
didHydrateState,
showWelcome,
shouldShowAnnouncement,
telemetrySetting,
telemetryKey,
machineId,
cloudUserInfo,
cloudIsAuthenticated,
cloudApiUrl,
cloudOrganizations,
renderContext,
mdmCompliant,
} = useExtensionState()
// 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 ? (
<WelcomeView />
) : (
<>
{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}
/>
)}
<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