Skip to content

Commit 956891b

Browse files
committed
feat: add export and import buttons to mode selector popover
- Added export button that exports the currently selected mode - Added import button that shows import dialog with project/global options - Added state management for import dialog and importing status - Added message handler for importModeResult - Reused existing export/import functionality from ModesView - Maintained consistent styling with existing UI
1 parent 7b756e3 commit 956891b

File tree

1 file changed

+209
-105
lines changed

1 file changed

+209
-105
lines changed

webview-ui/src/components/chat/ModeSelector.tsx

Lines changed: 209 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react"
22
import { ChevronUp, Check, X } from "lucide-react"
33
import { cn } from "@/lib/utils"
44
import { useRooPortal } from "@/components/ui/hooks/useRooPortal"
5-
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip } from "@/components/ui"
5+
import { Popover, PopoverContent, PopoverTrigger, StandardTooltip, Button } from "@/components/ui"
66
import { IconButton } from "./IconButton"
77
import { vscode } from "@/utils/vscode"
88
import { useExtensionState } from "@/context/ExtensionStateContext"
@@ -45,6 +45,8 @@ export const ModeSelector = ({
4545
const portalContainer = useRooPortal("roo-portal")
4646
const { hasOpenedModeSelector, setHasOpenedModeSelector } = useExtensionState()
4747
const { t } = useAppTranslation()
48+
const [showImportDialog, setShowImportDialog] = React.useState(false)
49+
const [isImporting, setIsImporting] = React.useState(false)
4850

4951
const trackModeSelectorOpened = React.useCallback(() => {
5052
// Track telemetry every time the mode selector is opened
@@ -153,6 +155,23 @@ export const ModeSelector = ({
153155
}
154156
}, [open])
155157

158+
// Handle import/export result messages
159+
React.useEffect(() => {
160+
const handler = (event: MessageEvent) => {
161+
const message = event.data
162+
if (message.type === "importModeResult") {
163+
setIsImporting(false)
164+
setShowImportDialog(false)
165+
if (!message.success && message.error !== "cancelled") {
166+
console.error("Failed to import mode:", message.error)
167+
}
168+
}
169+
}
170+
171+
window.addEventListener("message", handler)
172+
return () => window.removeEventListener("message", handler)
173+
}, [])
174+
156175
// Determine if search should be shown
157176
const showSearch = !disableSearch && modes.length > SEARCH_THRESHOLD
158177

@@ -181,123 +200,208 @@ export const ModeSelector = ({
181200
)
182201

183202
return (
184-
<Popover open={open} onOpenChange={onOpenChange} data-testid="mode-selector-root">
185-
{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
186-
187-
<PopoverContent
188-
align="start"
189-
sideOffset={4}
190-
container={portalContainer}
191-
className="p-0 overflow-hidden min-w-80 max-w-9/10">
192-
<div className="flex flex-col w-full">
193-
{/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */}
194-
{showSearch ? (
195-
<div className="relative p-2 border-b border-vscode-dropdown-border">
196-
<input
197-
aria-label="Search modes"
198-
ref={searchInputRef}
199-
value={searchValue}
200-
onChange={(e) => setSearchValue(e.target.value)}
201-
placeholder={t("chat:modeSelector.searchPlaceholder")}
202-
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"
203-
data-testid="mode-search-input"
204-
/>
205-
{searchValue.length > 0 && (
206-
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
207-
<X
208-
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
209-
onClick={onClearSearch}
210-
/>
211-
</div>
212-
)}
213-
</div>
214-
) : (
215-
<div className="p-3 border-b border-vscode-dropdown-border">
216-
<p className="m-0 text-xs text-vscode-descriptionForeground">{instructionText}</p>
217-
</div>
218-
)}
203+
<>
204+
<Popover open={open} onOpenChange={onOpenChange} data-testid="mode-selector-root">
205+
{title ? <StandardTooltip content={title}>{trigger}</StandardTooltip> : trigger}
219206

220-
{/* Mode List */}
221-
<div className="max-h-[300px] overflow-y-auto">
222-
{filteredModes.length === 0 && searchValue ? (
223-
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
224-
{t("chat:modeSelector.noResults")}
207+
<PopoverContent
208+
align="start"
209+
sideOffset={4}
210+
container={portalContainer}
211+
className="p-0 overflow-hidden min-w-80 max-w-9/10">
212+
<div className="flex flex-col w-full">
213+
{/* Show search bar only when there are more than SEARCH_THRESHOLD items, otherwise show info blurb */}
214+
{showSearch ? (
215+
<div className="relative p-2 border-b border-vscode-dropdown-border">
216+
<input
217+
aria-label="Search modes"
218+
ref={searchInputRef}
219+
value={searchValue}
220+
onChange={(e) => setSearchValue(e.target.value)}
221+
placeholder={t("chat:modeSelector.searchPlaceholder")}
222+
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"
223+
data-testid="mode-search-input"
224+
/>
225+
{searchValue.length > 0 && (
226+
<div className="absolute right-4 top-0 bottom-0 flex items-center justify-center">
227+
<X
228+
className="text-vscode-input-foreground opacity-50 hover:opacity-100 size-4 p-0.5 cursor-pointer"
229+
onClick={onClearSearch}
230+
/>
231+
</div>
232+
)}
225233
</div>
226234
) : (
227-
<div className="py-1">
228-
{filteredModes.map((mode) => (
229-
<div
230-
key={mode.slug}
231-
onClick={() => handleSelect(mode.slug)}
232-
className={cn(
233-
"px-3 py-1.5 text-sm cursor-pointer flex items-center",
234-
"hover:bg-vscode-list-hoverBackground",
235-
mode.slug === value
236-
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
237-
: "",
238-
)}
239-
data-testid="mode-selector-item">
240-
<div className="flex-1 min-w-0">
241-
<div className="font-bold truncate">{mode.name}</div>
242-
{mode.description && (
243-
<div className="text-xs text-vscode-descriptionForeground truncate">
244-
{mode.description}
245-
</div>
235+
<div className="p-3 border-b border-vscode-dropdown-border">
236+
<p className="m-0 text-xs text-vscode-descriptionForeground">{instructionText}</p>
237+
</div>
238+
)}
239+
240+
{/* Mode List */}
241+
<div className="max-h-[300px] overflow-y-auto">
242+
{filteredModes.length === 0 && searchValue ? (
243+
<div className="py-2 px-3 text-sm text-vscode-foreground/70">
244+
{t("chat:modeSelector.noResults")}
245+
</div>
246+
) : (
247+
<div className="py-1">
248+
{filteredModes.map((mode) => (
249+
<div
250+
key={mode.slug}
251+
onClick={() => handleSelect(mode.slug)}
252+
className={cn(
253+
"px-3 py-1.5 text-sm cursor-pointer flex items-center",
254+
"hover:bg-vscode-list-hoverBackground",
255+
mode.slug === value
256+
? "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground"
257+
: "",
246258
)}
259+
data-testid="mode-selector-item">
260+
<div className="flex-1 min-w-0">
261+
<div className="font-bold truncate">{mode.name}</div>
262+
{mode.description && (
263+
<div className="text-xs text-vscode-descriptionForeground truncate">
264+
{mode.description}
265+
</div>
266+
)}
267+
</div>
268+
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
247269
</div>
248-
{mode.slug === value && <Check className="ml-auto size-4 p-0.5" />}
249-
</div>
250-
))}
270+
))}
271+
</div>
272+
)}
273+
</div>
274+
275+
{/* Bottom bar with buttons on left and title on right */}
276+
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
277+
<div className="flex flex-row gap-1">
278+
<IconButton
279+
iconClass="codicon-extensions"
280+
title={t("chat:modeSelector.marketplace")}
281+
onClick={() => {
282+
window.postMessage(
283+
{
284+
type: "action",
285+
action: "marketplaceButtonClicked",
286+
values: { marketplaceTab: "mode" },
287+
},
288+
"*",
289+
)
290+
setOpen(false)
291+
}}
292+
/>
293+
<IconButton
294+
iconClass="codicon-export"
295+
title={t("prompts:exportMode.title")}
296+
onClick={() => {
297+
if (value) {
298+
vscode.postMessage({
299+
type: "exportMode",
300+
slug: value,
301+
})
302+
}
303+
setOpen(false)
304+
}}
305+
/>
306+
<IconButton
307+
iconClass="codicon-import"
308+
title={t("prompts:modes.importMode")}
309+
onClick={() => {
310+
setShowImportDialog(true)
311+
setOpen(false)
312+
}}
313+
/>
314+
<IconButton
315+
iconClass="codicon-settings-gear"
316+
title={t("chat:modeSelector.settings")}
317+
onClick={() => {
318+
vscode.postMessage({
319+
type: "switchTab",
320+
tab: "modes",
321+
})
322+
setOpen(false)
323+
}}
324+
/>
251325
</div>
252-
)}
326+
327+
{/* Info icon and title on the right - only show info icon when search bar is visible */}
328+
<div className="flex items-center gap-1 pr-1">
329+
{showSearch && (
330+
<StandardTooltip content={instructionText}>
331+
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
332+
</StandardTooltip>
333+
)}
334+
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
335+
{t("chat:modeSelector.title")}
336+
</h4>
337+
</div>
338+
</div>
253339
</div>
340+
</PopoverContent>
341+
</Popover>
254342

255-
{/* Bottom bar with buttons on left and title on right */}
256-
<div className="flex flex-row items-center justify-between px-2 py-2 border-t border-vscode-dropdown-border">
257-
<div className="flex flex-row gap-1">
258-
<IconButton
259-
iconClass="codicon-extensions"
260-
title={t("chat:modeSelector.marketplace")}
261-
onClick={() => {
262-
window.postMessage(
263-
{
264-
type: "action",
265-
action: "marketplaceButtonClicked",
266-
values: { marketplaceTab: "mode" },
267-
},
268-
"*",
269-
)
270-
setOpen(false)
271-
}}
272-
/>
273-
<IconButton
274-
iconClass="codicon-settings-gear"
275-
title={t("chat:modeSelector.settings")}
343+
{/* Import Mode Dialog */}
344+
{showImportDialog && (
345+
<div className="fixed inset-0 flex items-center justify-center bg-black/50 z-[1000]">
346+
<div className="bg-vscode-editor-background border border-vscode-editor-lineHighlightBorder rounded-lg shadow-lg p-6 max-w-md w-full">
347+
<h3 className="text-lg font-semibold mb-4">{t("prompts:modes.importMode")}</h3>
348+
<p className="text-sm text-vscode-descriptionForeground mb-4">
349+
{t("prompts:importMode.selectLevel")}
350+
</p>
351+
<div className="space-y-3 mb-6">
352+
<label className="flex items-start gap-2 cursor-pointer">
353+
<input
354+
type="radio"
355+
name="importLevel"
356+
value="project"
357+
className="mt-1"
358+
defaultChecked
359+
/>
360+
<div>
361+
<div className="font-medium">{t("prompts:importMode.project.label")}</div>
362+
<div className="text-xs text-vscode-descriptionForeground">
363+
{t("prompts:importMode.project.description")}
364+
</div>
365+
</div>
366+
</label>
367+
<label className="flex items-start gap-2 cursor-pointer">
368+
<input type="radio" name="importLevel" value="global" className="mt-1" />
369+
<div>
370+
<div className="font-medium">{t("prompts:importMode.global.label")}</div>
371+
<div className="text-xs text-vscode-descriptionForeground">
372+
{t("prompts:importMode.global.description")}
373+
</div>
374+
</div>
375+
</label>
376+
</div>
377+
<div className="flex justify-end gap-2">
378+
<Button variant="secondary" onClick={() => setShowImportDialog(false)}>
379+
{t("prompts:createModeDialog.buttons.cancel")}
380+
</Button>
381+
<Button
382+
variant="default"
276383
onClick={() => {
277-
vscode.postMessage({
278-
type: "switchTab",
279-
tab: "modes",
280-
})
281-
setOpen(false)
384+
if (!isImporting) {
385+
const selectedLevel = (
386+
document.querySelector(
387+
'input[name="importLevel"]:checked',
388+
) as HTMLInputElement
389+
)?.value as "global" | "project"
390+
setIsImporting(true)
391+
vscode.postMessage({
392+
type: "importMode",
393+
source: selectedLevel || "project",
394+
})
395+
}
282396
}}
283-
/>
284-
</div>
285-
286-
{/* Info icon and title on the right - only show info icon when search bar is visible */}
287-
<div className="flex items-center gap-1 pr-1">
288-
{showSearch && (
289-
<StandardTooltip content={instructionText}>
290-
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground opacity-70 hover:opacity-100 cursor-help" />
291-
</StandardTooltip>
292-
)}
293-
<h4 className="m-0 font-medium text-sm text-vscode-descriptionForeground">
294-
{t("chat:modeSelector.title")}
295-
</h4>
397+
disabled={isImporting}>
398+
{isImporting ? t("prompts:importMode.importing") : t("prompts:importMode.import")}
399+
</Button>
296400
</div>
297401
</div>
298402
</div>
299-
</PopoverContent>
300-
</Popover>
403+
)}
404+
</>
301405
)
302406
}
303407

0 commit comments

Comments
 (0)