From aef806733bffff7f34c41aab3a6ccdc69080f389 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 28 Jul 2025 23:03:09 +0000 Subject: [PATCH] feat: add export/import buttons to mode selector popover - Add export button for each mode in the popover list - Add import button in the popover header - Implement export/import handlers with loading states - Add inline import dialog for selecting global/project level - Reuse existing backend message handlers for export/import functionality Fixes #6320 --- .../src/components/chat/ModeSelector.tsx | 363 ++++++++++++------ 1 file changed, 254 insertions(+), 109 deletions(-) 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} + <> + + {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 && ( -
- + +
+ {/* 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}

+
+ )} + + {/* 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 && } + + + +
+
+ ))}
)}
- ) : ( -
-

{instructionText}

-
- )} - {/* Mode List */} -
- {filteredModes.length === 0 && searchValue ? ( -
- {t("chat:modeSelector.noResults")} -
- ) : ( -
- {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" - : "", + {/* Bottom bar with buttons on left and title on right */} +
+
+ +
- ))} -
- )} -
- - {/* 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")} -

+ { + 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")} +

+
+
+
+ + + + {/* Import Mode Dialog */} + {showImportDialog && ( +
+
+

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

+

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

+
+ + +
+
+ +
- - + )} + ) }