88 Settings2 ,
99 Sparkles ,
1010 Target ,
11+ X ,
1112} from "lucide-react" ;
1213import { Popover , Tooltip } from "antd" ;
1314import { useMemo , useState } from "react" ;
@@ -20,6 +21,8 @@ import {
2021 type LoopModeInfo ,
2122 useLoopStore ,
2223} from "../../stores/loopStore" ;
24+ import { useIsMobile } from "../../hooks/useIsMobile" ;
25+ import { OsDrawer } from "../../os/OsOverlay" ;
2326import { InlineMarkdown } from "../Markdown/InlineMarkdown" ;
2427import {
2528 resolveLoopModeDescriptionMarkdown ,
@@ -35,10 +38,19 @@ function ModeIcon({ mode, size = 14 }: { mode: LoopModeInfo; size?: number }) {
3538 return < CircleDot size = { size } /> ;
3639}
3740
38- export function LoopModeSelector ( ) {
41+ interface LoopModeSelectorProps {
42+ className ?: string ;
43+ compact ?: boolean ;
44+ }
45+
46+ export function LoopModeSelector ( {
47+ className,
48+ compact = false ,
49+ } : LoopModeSelectorProps = { } ) {
3950 const { t, i18n } = useTranslation ( ) ;
4051 const lang = i18n . language || "en" ;
4152 const navigate = useNavigate ( ) ;
53+ const isMobile = useIsMobile ( ) ;
4254 const [ open , setOpen ] = useState ( false ) ;
4355 const availableModes = useLoopStore ( ( state ) => state . availableModes ) ;
4456 const selectedModeId = useLoopStore ( ( state ) => state . selectedModeId ) ;
@@ -61,14 +73,16 @@ export function LoopModeSelector() {
6173 ) ;
6274
6375 if ( sessionState !== "idle" && activeMode ) {
76+ const modeName = resolveLoopModeName ( activeMode , t , lang ) ;
6477 const tooltip =
6578 activeMode . source === "custom"
6679 ? t ( "loop.activeCustomDescription" )
6780 : t ( "loop.activePersistentDescription" ) ;
6881 return (
6982 < Tooltip title = { tooltip } >
7083 < div
71- className = { styles . activeMode }
84+ className = { [ styles . activeMode , className ] . filter ( Boolean ) . join ( " " ) }
85+ aria-label = { `${ modeName } ${ t ( `loop.${ sessionState } ` ) } ` }
7286 aria-live = "polite"
7387 data-state = { sessionState }
7488 >
@@ -79,10 +93,14 @@ export function LoopModeSelector() {
7993 { sessionState === "awaiting_user" && (
8094 < MessageCircleQuestion size = { 14 } />
8195 ) }
82- < span > { resolveLoopModeName ( activeMode , t , lang ) } </ span >
83- < span className = { styles . activeState } >
84- { t ( `loop.${ sessionState } ` ) }
85- </ span >
96+ { ! compact && (
97+ < >
98+ < span > { modeName } </ span >
99+ < span className = { styles . activeState } >
100+ { t ( `loop.${ sessionState } ` ) }
101+ </ span >
102+ </ >
103+ ) }
86104 </ div >
87105 </ Tooltip >
88106 ) ;
@@ -130,40 +148,115 @@ export function LoopModeSelector() {
130148 ) ;
131149 } ;
132150
151+ const settingsButton = (
152+ < button
153+ aria-label = { t ( "loop.gotoSettings" ) }
154+ className = { styles . settingsButton }
155+ onClick = { ( ) => {
156+ setOpen ( false ) ;
157+ navigate ( "/agent-config?tab=agentLoop" ) ;
158+ } }
159+ type = "button"
160+ >
161+ < Settings2 size = { 16 } />
162+ </ button >
163+ ) ;
164+
133165 const content = (
134- < div className = { styles . modeMenu } role = "listbox" >
166+ < div className = { styles . modeMenu } >
135167 < div className = { styles . menuHeader } >
136168 < div >
137169 < div className = { styles . menuTitle } > { t ( "loop.selectorTitle" ) } </ div >
138170 < div className = { styles . menuHint } > { t ( "loop.selectorHint" ) } </ div >
139171 </ div >
140- < Tooltip title = { t ( "loop.gotoSettings" ) } >
141- < button
142- aria-label = { t ( "loop.gotoSettings" ) }
143- className = { styles . settingsButton }
144- onClick = { ( ) => {
145- setOpen ( false ) ;
146- navigate ( "/agent-config?tab=agentLoop" ) ;
147- } }
148- type = "button"
149- >
150- < Settings2 size = { 16 } />
151- </ button >
152- </ Tooltip >
153- </ div >
154- { renderGroup ( t ( "loop.builtInModes" ) , builtInModes ) }
155- { renderGroup ( t ( "loop.customModes" ) , extendedModes ) }
156- { catalogError ? (
157- < div className = { styles . menuError } >
158- < span > { t ( "loop.loadError" ) } </ span >
159- < button onClick = { ( ) => void fetchAvailableLoopModes ( ) } type = "button" >
160- { t ( "loop.retry" ) }
161- </ button >
172+ < div className = { styles . menuActions } >
173+ { isMobile ? (
174+ settingsButton
175+ ) : (
176+ < Tooltip title = { t ( "loop.gotoSettings" ) } > { settingsButton } </ Tooltip >
177+ ) }
178+ { isMobile && (
179+ < button
180+ aria-label = { t ( "common.close" ) }
181+ className = { styles . settingsButton }
182+ onClick = { ( ) => setOpen ( false ) }
183+ type = "button"
184+ >
185+ < X size = { 18 } />
186+ </ button >
187+ ) }
162188 </ div >
163- ) : null }
189+ </ div >
190+ < div className = { styles . modeList } role = "listbox" >
191+ { renderGroup ( t ( "loop.builtInModes" ) , builtInModes ) }
192+ { renderGroup ( t ( "loop.customModes" ) , extendedModes ) }
193+ { catalogError ? (
194+ < div className = { styles . menuError } >
195+ < span > { t ( "loop.loadError" ) } </ span >
196+ < button
197+ onClick = { ( ) => void fetchAvailableLoopModes ( ) }
198+ type = "button"
199+ >
200+ { t ( "loop.retry" ) }
201+ </ button >
202+ </ div >
203+ ) : null }
204+ </ div >
164205 </ div >
165206 ) ;
166207
208+ const triggerButton = (
209+ < button
210+ aria-expanded = { open }
211+ aria-haspopup = "listbox"
212+ aria-label = { t ( "loop.selectorAria" ) }
213+ className = { [ styles . modeTrigger , className ] . filter ( Boolean ) . join ( " " ) }
214+ disabled = { catalogLoading && availableModes . length === 0 }
215+ onClick = { isMobile ? ( ) => setOpen ( ( current ) => ! current ) : undefined }
216+ type = "button"
217+ >
218+ { catalogLoading ? (
219+ < LoaderCircle className = { styles . spin } size = { 14 } />
220+ ) : (
221+ < ModeIcon mode = { selectedMode } />
222+ ) }
223+ { ! compact && (
224+ < >
225+ < span > { resolveLoopModeName ( selectedMode , t , lang ) } </ span >
226+ < ChevronDown size = { 13 } />
227+ </ >
228+ ) }
229+ </ button >
230+ ) ;
231+
232+ if ( isMobile ) {
233+ return (
234+ < >
235+ { triggerButton }
236+ < OsDrawer
237+ aria-label = { t ( "loop.selectorTitle" ) }
238+ open = { open }
239+ placement = "bottom"
240+ height = "auto"
241+ closable = { false }
242+ destroyOnHidden
243+ rootClassName = { styles . modeDrawer }
244+ onClose = { ( ) => setOpen ( false ) }
245+ styles = { {
246+ body : { padding : 0 , overflow : "hidden" } ,
247+ content : {
248+ borderRadius : "14px 14px 0 0" ,
249+ overflow : "hidden" ,
250+ } ,
251+ wrapper : { maxHeight : "min(48dvh, 400px)" } ,
252+ } }
253+ >
254+ { content }
255+ </ OsDrawer >
256+ </ >
257+ ) ;
258+ }
259+
167260 return (
168261 < Popover
169262 arrow = { false }
@@ -174,22 +267,7 @@ export function LoopModeSelector() {
174267 placement = "topLeft"
175268 trigger = "click"
176269 >
177- < button
178- aria-expanded = { open }
179- aria-haspopup = "listbox"
180- aria-label = { t ( "loop.selectorAria" ) }
181- className = { styles . modeTrigger }
182- disabled = { catalogLoading && availableModes . length === 0 }
183- type = "button"
184- >
185- { catalogLoading ? (
186- < LoaderCircle className = { styles . spin } size = { 14 } />
187- ) : (
188- < ModeIcon mode = { selectedMode } />
189- ) }
190- < span > { resolveLoopModeName ( selectedMode , t , lang ) } </ span >
191- < ChevronDown size = { 13 } />
192- </ button >
270+ { triggerButton }
193271 </ Popover >
194272 ) ;
195273}
0 commit comments