diff --git a/webview-ui/src/components/chat/ModeSelector.tsx b/webview-ui/src/components/chat/ModeSelector.tsx index 93dd2f1f4f..468c90c3b5 100644 --- a/webview-ui/src/components/chat/ModeSelector.tsx +++ b/webview-ui/src/components/chat/ModeSelector.tsx @@ -1,8 +1,8 @@ import React from "react" -import { ChevronUp, Check, X } from "lucide-react" +import { ChevronUp, Check, X, Upload, Download } from "lucide-react" import { cn } from "@/lib/utils" import { useRooPortal } from "@/components/ui/hooks/useRooPortal" -import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui" +import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, Button } from "@/components/ui" import { IconButton } from "./IconButton" import { vscode } from "@/utils/vscode" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -46,6 +46,11 @@ export const ModeSelector = ({ const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState() const { t } = useAppTranslation() + // Export/Import state + const [isExporting, setIsExporting] = React.useState(null) + const [isImporting, setIsImporting] = React.useState(false) + const [showImportDialog, setShowImportDialog] = React.useState(false) + const trackModeSelectorOpened = React.useCallback(() => { // Track telemetry every time the mode selector is opened telemetryClient.capture(TelemetryEventName.MODE_SELECTOR_OPENED) @@ -146,6 +151,56 @@ export const ModeSelector = ({ [trackModeSelectorOpened], ) + // Handle export mode + const handleExportMode = React.useCallback( + (slug: string) => { + if (!isExporting) { + setIsExporting(slug) + vscode.postMessage({ + type: "exportMode", + slug: slug, + }) + } + }, + [isExporting], + ) + + // Handle import mode + const handleImportMode = React.useCallback( + (source: "global" | "project") => { + if (!isImporting) { + setIsImporting(true) + vscode.postMessage({ + type: "importMode", + source: source, + }) + } + }, + [isImporting], + ) + + // Listen for export/import results + React.useEffect(() => { + const handler = (event: MessageEvent) => { + const message = event.data + if (message.type === "exportModeResult") { + setIsExporting(null) + if (!message.success) { + console.error("Failed to export mode:", message.error) + } + } else if (message.type === "importModeResult") { + setIsImporting(false) + setShowImportDialog(false) + if (!message.success && message.error !== "cancelled") { + console.error("Failed to import mode:", message.error) + } + } + } + + window.addEventListener("message", handler) + return () => window.removeEventListener("message", handler) + }, []) + // Auto-focus search input when popover opens React.useEffect(() => { if (open && searchInputRef.current) { @@ -181,123 +236,213 @@ export const ModeSelector = ({ ) return ( - - {title ? {trigger} : trigger} - - -
- {/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */} - {showSearch ? ( -
- setSearchValue(e.target.value)} - placeholder={t("chat:modeSelector.searchPlaceholder")} - className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" - data-testid="mode-search-input" - /> - {searchValue.length > 0 && ( -
- -
- )} -
- ) : ( -
-

{instructionText}

-
- )} + <> + + {title ? {trigger} : trigger} - {/* Mode List */} -
- {filteredModes.length === 0 && searchValue ? ( -
- {t("chat:modeSelector.noResults")} + +
+ {/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */} + {showSearch ? ( +
+ setSearchValue(e.target.value)} + placeholder={t("chat:modeSelector.searchPlaceholder")} + className="w-full h-8 px-2 py-1 text-xs bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded focus:outline-0" + data-testid="mode-search-input" + /> + {searchValue.length > 0 && ( +
+ +
+ )}
) : ( -
- {filteredModes.map((mode) => ( -
handleSelect(mode.slug)} - className={cn( - "px-3 py-1.5 text-sm cursor-pointer flex items-center", - "hover:bg-vscode-list-hoverBackground", - mode.slug === value - ? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground" - : "", - )} - data-testid="mode-selector-item"> -
-
{mode.name}
- {mode.description && ( -
- {mode.description} -
+
+

{instructionText}

+
+ )} + + {/* Mode List */} +
+ {filteredModes.length === 0 && searchValue ? ( +
+ {t("chat:modeSelector.noResults")} +
+ ) : ( +
+ {filteredModes.map((mode) => ( +
+
handleSelect(mode.slug)}> +
{mode.name}
+ {mode.description && ( +
+ {mode.description} +
+ )} +
+
+ {mode.slug === value && } + + + +
- {mode.slug === value && } -
- ))} + ))} +
+ )} +
+ + {/* Bottom bar with buttons on left and title on right */} +
+
+ + + + { + window.postMessage( + { + type: "action", + action: "marketplaceButtonClicked", + values: { marketplaceTab: "mode" }, + }, + "*", + ) + setOpen(false) + }} + /> + { + vscode.postMessage({ + type: "switchTab", + tab: "modes", + }) + setOpen(false) + }} + />
- )} + + {/* Info icon and title on the right - only show info icon when search bar is visible */} +
+ {showSearch && ( + + + + )} +

+ {t("chat:modeSelector.title")} +

+
+
+ + - {/* Bottom bar with buttons on left and title on right */} -
-
- { - window.postMessage( - { - type: "action", - action: "marketplaceButtonClicked", - values: { marketplaceTab: "mode" }, - }, - "*", - ) - setOpen(false) - }} - /> - +
+

{t("prompts:modes.importMode")}

+

+ {t("prompts:importMode.selectLevel")} +

+
+ + +
+
+ +
- - {/* Info icon and title on the right - only show info icon when search bar is visible */} -
- {showSearch && ( - - - - )} -

- {t("chat:modeSelector.title")} -

+ disabled={isImporting}> + {isImporting ? t("prompts:importMode.importing") : t("prompts:importMode.import")} +
- - + )} + ) }