Ver Fonte

App tab layout fixes

cte há 9 meses atrás
pai
commit
9f40c4d57a

+ 1 - 2
.env.sample

@@ -1,2 +1 @@
-# PostHog API Keys for telemetry
-POSTHOG_API_KEY=key-goes-here
+POSTHOG_API_KEY=key-goes-here

+ 18 - 32
webview-ui/src/App.tsx

@@ -17,6 +17,12 @@ import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
 
 type Tab = "settings" | "history" | "mcp" | "prompts" | "chat"
 
+type HumanRelayDialogState = {
+	isOpen: boolean
+	requestId: string
+	promptText: string
+}
+
 const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]>, Tab>> = {
 	chatButtonClicked: "chat",
 	settingsButtonClicked: "settings",
@@ -24,24 +30,21 @@ const tabsByMessageAction: Partial<Record<NonNullable<ExtensionMessage["action"]
 	mcpButtonClicked: "mcp",
 	historyButtonClicked: "history",
 }
+
 const App = () => {
 	const { didHydrateState, showWelcome, shouldShowAnnouncement, telemetrySetting, telemetryKey, machineId } =
 		useExtensionState()
+
 	const [showAnnouncement, setShowAnnouncement] = useState(false)
 	const [tab, setTab] = useState<Tab>("chat")
-	const settingsRef = useRef<SettingsViewRef>(null)
-
-	// Human Relay Dialog Status
-	const [humanRelayDialogState, setHumanRelayDialogState] = useState<{
-		isOpen: boolean
-		requestId: string
-		promptText: string
-	}>({
+	const [humanRelayDialogState, setHumanRelayDialogState] = useState<HumanRelayDialogState>({
 		isOpen: false,
 		requestId: "",
 		promptText: "",
 	})
 
+	const settingsRef = useRef<SettingsViewRef>(null)
+
 	const switchTab = useCallback((newTab: Tab) => {
 		if (settingsRef.current?.checkUnsaveChanges) {
 			settingsRef.current.checkUnsaveChanges(() => setTab(newTab))
@@ -74,23 +77,6 @@ const App = () => {
 		[switchTab],
 	)
 
-	// Processing Human Relay Dialog Submission
-	const handleHumanRelaySubmit = (requestId: string, text: string) => {
-		vscode.postMessage({
-			type: "humanRelayResponse",
-			requestId,
-			text,
-		})
-	}
-
-	// Handle Human Relay dialog box cancel
-	const handleHumanRelayCancel = (requestId: string) => {
-		vscode.postMessage({
-			type: "humanRelayCancel",
-			requestId,
-		})
-	}
-
 	useEvent("message", onMessage)
 
 	useEffect(() => {
@@ -106,7 +92,7 @@ const App = () => {
 		}
 	}, [telemetrySetting, telemetryKey, machineId, didHydrateState])
 
-	// Tell Extension that we are ready to receive messages
+	// Tell the extension that we are ready to receive messages.
 	useEffect(() => {
 		vscode.postMessage({ type: "webviewDidLaunch" })
 	}, [])
@@ -121,24 +107,23 @@ const App = () => {
 		<WelcomeView />
 	) : (
 		<>
-			{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
-			{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
-			{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
 			{tab === "prompts" && <PromptsView onDone={() => switchTab("chat")} />}
+			{tab === "mcp" && <McpView onDone={() => switchTab("chat")} />}
+			{tab === "history" && <HistoryView onDone={() => switchTab("chat")} />}
+			{tab === "settings" && <SettingsView ref={settingsRef} onDone={() => setTab("chat")} />}
 			<ChatView
 				isHidden={tab !== "chat"}
 				showAnnouncement={showAnnouncement}
 				hideAnnouncement={() => setShowAnnouncement(false)}
 				showHistoryView={() => switchTab("history")}
 			/>
-			{/* Human Relay Dialog */}
 			<HumanRelayDialog
 				isOpen={humanRelayDialogState.isOpen}
 				requestId={humanRelayDialogState.requestId}
 				promptText={humanRelayDialogState.promptText}
 				onClose={() => setHumanRelayDialogState((prev) => ({ ...prev, isOpen: false }))}
-				onSubmit={handleHumanRelaySubmit}
-				onCancel={handleHumanRelayCancel}
+				onSubmit={(requestId, text) => vscode.postMessage({ type: "humanRelayResponse", requestId, text })}
+				onCancel={(requestId) => vscode.postMessage({ type: "humanRelayCancel", requestId })}
 			/>
 		</>
 	)
@@ -147,6 +132,7 @@ const App = () => {
 const AppWithProviders = () => (
 	<ExtensionStateContextProvider>
 		<App />
+		<div id="roo-portal" />
 	</ExtensionStateContextProvider>
 )
 

+ 2 - 2
webview-ui/src/components/chat/Announcement.tsx

@@ -33,7 +33,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
 			</p>
 
 			<h3 style={{ margin: "12px 0 8px" }}>What's New</h3>
-			<p style={{ margin: "5px 0px" }}>
+			<div style={{ margin: "5px 0px" }}>
 				<ul style={{ margin: "4px 0 6px 20px", padding: 0 }}>
 					<li>• Faster asynchronous checkpoints</li>
 					<li>• Support for .rooignore files</li>
@@ -44,7 +44,7 @@ const Announcement = ({ version, hideAnnouncement }: AnnouncementProps) => {
 					<li>• Updated DeepSeek provider</li>
 					<li>• New "Human Relay" provider</li>
 				</ul>
-			</p>
+			</div>
 
 			<p style={{ margin: "10px 0px 0px" }}>
 				Get more details and discuss in{" "}

+ 2 - 12
webview-ui/src/components/chat/checkpoints/CheckpointMenu.tsx

@@ -1,4 +1,4 @@
-import { useState, useEffect, useCallback } from "react"
+import { useState, useCallback } from "react"
 import { CheckIcon, Cross2Icon } from "@radix-ui/react-icons"
 
 import { Button, Popover, PopoverContent, PopoverTrigger } from "@/components/ui"
@@ -14,7 +14,6 @@ type CheckpointMenuProps = {
 }
 
 export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: CheckpointMenuProps) => {
-	const [portalContainer, setPortalContainer] = useState<HTMLElement>()
 	const [isOpen, setIsOpen] = useState(false)
 	const [isConfirming, setIsConfirming] = useState(false)
 
@@ -42,15 +41,6 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
 		setIsOpen(false)
 	}, [ts, commitHash])
 
-	useEffect(() => {
-		// The dropdown menu uses a portal from @shadcn/ui which by default renders
-		// at the document root. This causes the menu to remain visible even when
-		// the parent ChatView component is hidden (during settings/history view).
-		// By moving the portal inside ChatView, the menu will properly hide when
-		// its parent is hidden.
-		setPortalContainer(document.getElementById("chat-view-portal") || undefined)
-	}, [])
-
 	return (
 		<div className="flex flex-row gap-1">
 			{isDiffAvailable && (
@@ -70,7 +60,7 @@ export const CheckpointMenu = ({ ts, commitHash, currentHash, checkpoint }: Chec
 							<span className="codicon codicon-history" />
 						</Button>
 					</PopoverTrigger>
-					<PopoverContent align="end" container={portalContainer}>
+					<PopoverContent align="end">
 						<div className="flex flex-col gap-2">
 							{!isCurrent && (
 								<div className="flex flex-col gap-1 group hover:text-foreground">

+ 15 - 0
webview-ui/src/components/common/Alert.tsx

@@ -0,0 +1,15 @@
+import { cn } from "@/lib/utils"
+import { HTMLAttributes } from "react"
+
+type AlertProps = HTMLAttributes<HTMLDivElement>
+
+export const Alert = ({ className, children, ...props }: AlertProps) => (
+	<div
+		className={cn(
+			"text-vscode-inputValidation-infoForeground bg-vscode-inputValidation-infoBackground border border-vscode-inputValidation-infoBorder rounded-xs p-2",
+			className,
+		)}
+		{...props}>
+		{children}
+	</div>
+)

+ 23 - 0
webview-ui/src/components/common/Tab.tsx

@@ -0,0 +1,23 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type TabProps = HTMLAttributes<HTMLDivElement>
+
+export const Tab = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("fixed inset-0 flex flex-col overflow-hidden", className)} {...props}>
+		{children}
+	</div>
+)
+
+export const TabHeader = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("px-5 py-2.5 border-b border-vscode-panel-border", className)} {...props}>
+		{children}
+	</div>
+)
+
+export const TabContent = ({ className, children, ...props }: TabProps) => (
+	<div className={cn("flex-1 overflow-auto p-5", className)} {...props}>
+		{children}
+	</div>
+)

+ 9 - 6
webview-ui/src/components/history/HistoryView.tsx

@@ -9,6 +9,7 @@ import { formatLargeNumber, formatDate } from "@/utils/format"
 import { cn } from "@/lib/utils"
 import { Button } from "@/components/ui"
 
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { useTaskSearch } from "./useTaskSearch"
 import { ExportButton } from "./ExportButton"
 import { CopyButton } from "./CopyButton"
@@ -25,8 +26,8 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 	const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
 
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex flex-col gap-2">
 				<div className="flex justify-between items-center">
 					<h3 className="text-vscode-foreground m-0">History</h3>
 					<VSCodeButton onClick={onDone}>Done</VSCodeButton>
@@ -81,8 +82,9 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						</VSCodeRadio>
 					</VSCodeRadioGroup>
 				</div>
-			</div>
-			<div style={{ flexGrow: 1, overflowY: "auto", margin: 0 }}>
+			</TabHeader>
+
+			<TabContent className="p-0">
 				<Virtuoso
 					style={{
 						flexGrow: 1,
@@ -312,11 +314,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						</div>
 					)}
 				/>
-			</div>
+			</TabContent>
+
 			{deleteTaskId && (
 				<DeleteTaskDialog taskId={deleteTaskId} onOpenChange={(open) => !open && setDeleteTaskId(null)} open />
 			)}
-		</div>
+		</Tab>
 	)
 }
 

+ 15 - 11
webview-ui/src/components/mcp/McpView.tsx

@@ -1,3 +1,4 @@
+import { useState } from "react"
 import {
 	VSCodeButton,
 	VSCodeCheckbox,
@@ -6,14 +7,17 @@ import {
 	VSCodePanelTab,
 	VSCodePanelView,
 } from "@vscode/webview-ui-toolkit/react"
-import { useState } from "react"
-import { vscode } from "../../utils/vscode"
-import { useExtensionState } from "../../context/ExtensionStateContext"
+
 import { McpServer } from "../../../../src/shared/mcp"
+
+import { vscode } from "@/utils/vscode"
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui"
+
+import { useExtensionState } from "../../context/ExtensionStateContext"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 import McpToolRow from "./McpToolRow"
 import McpResourceRow from "./McpResourceRow"
 import McpEnabledToggle from "./McpEnabledToggle"
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "../ui/dialog"
 
 type McpViewProps = {
 	onDone: () => void
@@ -29,12 +33,13 @@ const McpView = ({ onDone }: McpViewProps) => {
 	} = useExtensionState()
 
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex justify-between items-center">
 				<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div className="flex-1 overflow-auto p-5">
+			</TabHeader>
+
+			<TabContent>
 				<div
 					style={{
 						color: "var(--vscode-foreground)",
@@ -103,12 +108,11 @@ const McpView = ({ onDone }: McpViewProps) => {
 						</div>
 					</>
 				)}
-			</div>
-		</div>
+			</TabContent>
+		</Tab>
 	)
 }
 
-// Server Row Component
 const ServerRow = ({ server, alwaysAllowMcp }: { server: McpServer; alwaysAllowMcp?: boolean }) => {
 	const [isExpanded, setIsExpanded] = useState(false)
 	const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)

+ 12 - 6
webview-ui/src/components/prompts/PromptsView.tsx

@@ -42,6 +42,7 @@ import {
 
 import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups"
 import { vscode } from "../../utils/vscode"
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 
 // Get all available groups that should show in prompts view
 const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable)
@@ -406,12 +407,13 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 	}
 
 	return (
-		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+		<Tab>
+			<TabHeader className="flex justify-between items-center">
 				<h3 className="text-vscode-foreground m-0">Prompts</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div className="flex-1 overflow-auto p-5">
+			</TabHeader>
+
+			<TabContent>
 				<div className="pb-5 border-b border-vscode-input-border">
 					<div className="mb-5">
 						<div className="font-bold mb-1">Preferred Language</div>
@@ -934,6 +936,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 						</div>
 					</div>
 				</div>
+
 				<div
 					style={{
 						paddingBottom: "40px",
@@ -1172,7 +1175,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 						)}
 					</div>
 				</div>
-			</div>
+			</TabContent>
+
 			{isCreateModeDialogOpen && (
 				<div
 					style={{
@@ -1396,6 +1400,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 				</div>
 			)}
+
 			{isDialogOpen && (
 				<div
 					style={{
@@ -1463,6 +1468,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 				</div>
 			)}
+
 			{isCustomLanguage && (
 				<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
 					<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
@@ -1497,7 +1503,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 				</div>
 			)}
-		</div>
+		</Tab>
 	)
 }
 

+ 35 - 46
webview-ui/src/components/settings/SettingsView.tsx

@@ -22,6 +22,7 @@ import {
 	Button,
 } from "@/components/ui"
 
+import { Tab, TabContent, TabHeader } from "../common/Tab"
 import { SetCachedStateField, SetExperimentEnabled } from "./types"
 import { SectionHeader } from "./SectionHeader"
 import ApiConfigManager from "./ApiConfigManager"
@@ -263,53 +264,41 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 	const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
 
 	return (
-		<div className="fixed inset-0 flex flex-col overflow-hidden">
-			<div className="px-5 py-2.5 border-b border-vscode-panel-border">
-				<div className="flex flex-col">
-					<div className="flex justify-between items-center">
-						<div className="flex items-center gap-2">
-							<h3 className="text-vscode-foreground m-0">Settings</h3>
-							<div className="hidden [@media(min-width:400px)]:flex items-center">
-								{sections.map(({ id, icon: Icon, ref }) => (
-									<Button
-										key={id}
-										variant="ghost"
-										onClick={() => scrollToSection(ref)}
-										className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
-										<Icon />
-									</Button>
-								))}
-							</div>
-						</div>
-						<div className="flex gap-2">
-							<VSCodeButton
-								appearance={isSettingValid ? "primary" : "secondary"}
-								className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
-								title={
-									!isSettingValid
-										? errorMessage
-										: isChangeDetected
-											? "Save changes"
-											: "Nothing changed"
-								}
-								onClick={handleSubmit}
-								disabled={!isChangeDetected || !isSettingValid}>
-								Save
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="secondary"
-								title="Discard unsaved changes and close settings panel"
-								onClick={() => checkUnsaveChanges(onDone)}>
-								Done
-							</VSCodeButton>
-						</div>
+		<Tab>
+			<TabHeader className="flex justify-between items-center gap-2">
+				<div className="flex items-center gap-2">
+					<h3 className="text-vscode-foreground m-0">Settings</h3>
+					<div className="hidden [@media(min-width:400px)]:flex items-center">
+						{sections.map(({ id, icon: Icon, ref }) => (
+							<Button
+								key={id}
+								variant="ghost"
+								onClick={() => scrollToSection(ref)}
+								className={cn("w-6 h-6", activeSection === id ? "opacity-100" : "opacity-40")}>
+								<Icon />
+							</Button>
+						))}
 					</div>
 				</div>
-			</div>
+				<div className="flex gap-2">
+					<VSCodeButton
+						appearance={isSettingValid ? "primary" : "secondary"}
+						className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
+						title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
+						onClick={handleSubmit}
+						disabled={!isChangeDetected || !isSettingValid}>
+						Save
+					</VSCodeButton>
+					<VSCodeButton
+						appearance="secondary"
+						title="Discard unsaved changes and close settings panel"
+						onClick={() => checkUnsaveChanges(onDone)}>
+						Done
+					</VSCodeButton>
+				</div>
+			</TabHeader>
 
-			<div
-				className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-sideBar-background"
-				onScroll={handleScroll}>
+			<TabContent className="p-0 divide-y divide-vscode-sideBar-background" onScroll={handleScroll}>
 				<div ref={providersRef}>
 					<SectionHeader>
 						<div className="flex items-center gap-2">
@@ -425,7 +414,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					telemetrySetting={telemetrySetting}
 					setTelemetrySetting={setTelemetrySetting}
 				/>
-			</div>
+			</TabContent>
 
 			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
 				<AlertDialogContent>
@@ -442,7 +431,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					</AlertDialogFooter>
 				</AlertDialogContent>
 			</AlertDialog>
-		</div>
+		</Tab>
 	)
 })
 

+ 19 - 17
webview-ui/src/components/ui/dropdown-menu.tsx

@@ -53,23 +53,25 @@ DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayNam
 
 const DropdownMenuContent = React.forwardRef<
 	React.ElementRef<typeof DropdownMenuPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content> & {
-		container?: HTMLElement
-	}
->(({ className, sideOffset = 4, container, ...props }, ref) => (
-	<DropdownMenuPrimitive.Portal container={container}>
-		<DropdownMenuPrimitive.Content
-			ref={ref}
-			sideOffset={sideOffset}
-			className={cn(
-				"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
-				"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
-				className,
-			)}
-			{...props}
-		/>
-	</DropdownMenuPrimitive.Portal>
-))
+	React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(({ className, sideOffset = 4, ...props }, ref) => {
+	const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+	return (
+		<DropdownMenuPrimitive.Portal container={container}>
+			<DropdownMenuPrimitive.Content
+				ref={ref}
+				sideOffset={sideOffset}
+				className={cn(
+					"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
+					"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+					className,
+				)}
+				{...props}
+			/>
+		</DropdownMenuPrimitive.Portal>
+	)
+})
 DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
 
 const DropdownMenuItem = React.forwardRef<

+ 19 - 17
webview-ui/src/components/ui/popover.tsx

@@ -11,23 +11,25 @@ const PopoverAnchor = PopoverPrimitive.Anchor
 
 const PopoverContent = React.forwardRef<
 	React.ElementRef<typeof PopoverPrimitive.Content>,
-	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> & {
-		container?: HTMLElement
-	}
->(({ className, align = "center", sideOffset = 4, container, ...props }, ref) => (
-	<PopoverPrimitive.Portal container={container}>
-		<PopoverPrimitive.Content
-			ref={ref}
-			align={align}
-			sideOffset={sideOffset}
-			className={cn(
-				"z-50 w-72 rounded-xs border border-vscode-dropdown-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
-				className,
-			)}
-			{...props}
-		/>
-	</PopoverPrimitive.Portal>
-))
+	React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = "center", sideOffset = 4, ...props }, ref) => {
+	const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
+	return (
+		<PopoverPrimitive.Portal container={container}>
+			<PopoverPrimitive.Content
+				ref={ref}
+				align={align}
+				sideOffset={sideOffset}
+				className={cn(
+					"z-50 w-72 rounded-xs border border-vscode-dropdown-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
+					className,
+				)}
+				{...props}
+			/>
+		</PopoverPrimitive.Portal>
+	)
+})
 PopoverContent.displayName = PopoverPrimitive.Content.displayName
 
 export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }

+ 2 - 12
webview-ui/src/components/ui/select-dropdown.tsx

@@ -7,7 +7,6 @@ import {
 	DropdownMenuSeparator,
 } from "./dropdown-menu"
 import { cn } from "@/lib/utils"
-import { useEffect, useState } from "react"
 
 // Constants for option types
 export enum DropdownOptionType {
@@ -39,6 +38,8 @@ export interface SelectDropdownProps {
 	shortcutText?: string
 }
 
+// TODO: Get rid of this and use the native @shadcn/ui `Select` component.
+
 export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownMenuTrigger>, SelectDropdownProps>(
 	(
 		{
@@ -60,16 +61,6 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 	) => {
 		// Track open state
 		const [open, setOpen] = React.useState(false)
-		const [portalContainer, setPortalContainer] = useState<HTMLElement>()
-
-		useEffect(() => {
-			// The dropdown menu uses a portal from @shadcn/ui which by default renders
-			// at the document root. This causes the menu to remain visible even when
-			// the parent ChatView component is hidden (during settings/history view).
-			// By moving the portal inside ChatView, the menu will properly hide when
-			// its parent is hidden.
-			setPortalContainer(document.getElementById("chat-view-portal") || undefined)
-		}, [])
 
 		// Find the selected option label
 		const selectedOption = options.find((option) => option.value === value)
@@ -130,7 +121,6 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 					sideOffset={sideOffset}
 					onEscapeKeyDown={() => setOpen(false)}
 					onInteractOutside={() => setOpen(false)}
-					container={portalContainer}
 					className={cn(
 						"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
 						contentClassName,

+ 3 - 1
webview-ui/src/components/ui/select.tsx

@@ -39,8 +39,10 @@ function SelectContent({
 	position = "popper",
 	...props
 }: React.ComponentProps<typeof SelectPrimitive.Content>) {
+	const container = React.useMemo(() => document.getElementById("roo-portal"), [])
+
 	return (
-		<SelectPrimitive.Portal>
+		<SelectPrimitive.Portal container={container}>
 			<SelectPrimitive.Content
 				data-slot="select-content"
 				className={cn(

+ 19 - 19
webview-ui/src/components/welcome/WelcomeView.tsx

@@ -1,9 +1,12 @@
-import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
 import { useCallback, useState } from "react"
+import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
+
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { validateApiConfiguration } from "../../utils/validate"
 import { vscode } from "../../utils/vscode"
 import ApiOptions from "../settings/ApiOptions"
+import { Tab, TabContent } from "../common/Tab"
+import { Alert } from "../common/Alert"
 
 const WelcomeView = () => {
 	const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme } = useExtensionState()
@@ -23,18 +26,16 @@ const WelcomeView = () => {
 	}, [apiConfiguration, currentApiConfigName])
 
 	return (
-		<div className="flex flex-col min-h-screen px-0 pb-5">
-			<h2>Hi, I'm Roo!</h2>
-			<p>
-				I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and access
-				to tools that let me create & edit files, explore complex projects, use the browser, and execute
-				terminal commands (with your permission, of course). I can even use MCP to create new tools and extend
-				my own capabilities.
-			</p>
-
-			<b>To get started, this extension needs an API provider.</b>
-
-			<div className="mt-3">
+		<Tab>
+			<TabContent className="flex flex-col gap-5">
+				<h2 className="m-0 p-0">Hi, I'm Roo!</h2>
+				<div>
+					I can do all kinds of tasks thanks to the latest breakthroughs in agentic coding capabilities and
+					access to tools that let me create & edit files, explore complex projects, use the browser, and
+					execute terminal commands (with your permission, of course). I can even use MCP to create new tools
+					and extend my own capabilities.
+				</div>
+				<Alert className="font-bold text-sm">To get started, this extension needs an API provider.</Alert>
 				<ApiOptions
 					fromWelcomeView
 					apiConfiguration={apiConfiguration || {}}
@@ -43,15 +44,14 @@ const WelcomeView = () => {
 					errorMessage={errorMessage}
 					setErrorMessage={setErrorMessage}
 				/>
-			</div>
-
-			<div className="sticky bottom-0 bg-[var(--vscode-sideBar-background)] py-3">
-				<div className="flex flex-col gap-1.5">
+			</TabContent>
+			<div className="sticky bottom-0 bg-vscode-sideBar-background p-5">
+				<div className="flex flex-col gap-1">
 					<VSCodeButton onClick={handleSubmit}>Let's go!</VSCodeButton>
-					{errorMessage && <span className="text-destructive">{errorMessage}</span>}
+					{errorMessage && <div className="text-vscode-errorForeground">{errorMessage}</div>}
 				</div>
 			</div>
-		</div>
+		</Tab>
 	)
 }
 

+ 4 - 0
webview-ui/src/index.css

@@ -111,6 +111,10 @@
 
 	--color-vscode-charts-green: var(--vscode-charts-green);
 	--color-vscode-charts-yellow: var(--vscode-charts-yellow);
+
+	--color-vscode-inputValidation-infoForeground: var(--vscode-inputValidation-infoForeground);
+	--color-vscode-inputValidation-infoBackground: var(--vscode-inputValidation-infoBackground);
+	--color-vscode-inputValidation-infoBorder: var(--vscode-inputValidation-infoBorder);
 }
 
 @layer base {