@@ -2,7 +2,7 @@ import React from "react"
22import { ChevronUp , Check , X } from "lucide-react"
33import { cn } from "@/lib/utils"
44import { 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"
66import { IconButton } from "./IconButton"
77import { vscode } from "@/utils/vscode"
88import { 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