@@ -38,8 +38,6 @@ namespace OpenClawTray.Chat;
3838/// banner that <c>InputBar</c> used to render are preserved here above the
3939/// composer.
4040/// </summary>
41- public record ChannelGroup ( string AgentLabel , ( string Id , string Title ) [ ] Sessions ) ;
42-
4341public record OpenClawComposerProps (
4442 string ConnectionState ,
4543 bool TurnActive ,
@@ -93,6 +91,9 @@ public sealed class OpenClawComposer : Component<OpenClawComposerProps>
9391 // model id string. Selecting it routes to OnModelCleared (tri-state clear)
9492 // rather than OnModelChanged.
9593 private static readonly object ClearModelTag = new ( ) ;
94+ // Reserved id used to represent the "default / clear" model row in the rich ComboBox
95+ // primitive (which keys selection by string id). Cannot collide with a real SelectionId.
96+ private const string ClearModelId = "\u0000 __default__" ;
9697
9798 // Thinking levels matching the gateway's sessions.patch thinkingLevel values.
9899 // "medium" is the default when the session has no explicit thinkingLevel set.
@@ -246,69 +247,35 @@ public override Element Render()
246247 } ;
247248
248249 // ── Row 1: three compact dropdowns ─────────────────────────────
249- // Build grouped session ComboBox directly (bypassing the FunctionalUI
250- // ComboBox helper which only supports flat string[] items).
250+ // Grouped session picker via the reconciled rich ComboBox primitive. The primitive is
251+ // preserved by render path and only rebuilds its rows when the item set changes, so an
252+ // open dropdown survives unrelated status/thinking re-renders (the #970 regression).
251253 var groups = Props . AvailableChannels ;
252- var channelCombo = Border ( )
253- . Set ( border =>
254+ var multipleGroups = groups . Length > 1 ;
255+ var sessionItems = new List < ComboItem > ( ) ;
256+ foreach ( var group in groups )
257+ {
258+ if ( multipleGroups )
259+ sessionItems . Add ( new ComboItem ( "" , group . AgentLabel , Enabled : false , IsHeader : true ) ) ;
260+ foreach ( var session in group . Sessions )
261+ sessionItems . Add ( new ComboItem ( session . Id , session . Title , Indent : multipleGroups ? 8 : 0 ) ) ;
262+ }
263+
264+ var onChannelChanged = Props . OnChannelChanged ;
265+ var channelCombo = ComboBox ( sessionItems , Props . ChannelId ?? "" , id => onChannelChanged ( id ) )
266+ . Set ( cb =>
254267 {
255- var cb = new ComboBox
256- {
257- MinWidth = 0 ,
258- Width = double . NaN ,
259- Height = 28 ,
260- FontSize = 11 ,
261- Padding = new Thickness ( 8 , 0 , 4 , 0 ) ,
262- CornerRadius = composerCornerRadius ,
263- HorizontalAlignment = HorizontalAlignment . Stretch ,
264- VerticalAlignment = VerticalAlignment . Center ,
265- } ;
268+ cb . MinWidth = 0 ;
269+ cb . Width = double . NaN ;
270+ cb . Height = 28 ;
271+ cb . FontSize = 11 ;
272+ cb . Padding = new Thickness ( 8 , 0 , 4 , 0 ) ;
273+ cb . CornerRadius = composerCornerRadius ;
274+ cb . HorizontalAlignment = HorizontalAlignment . Stretch ;
275+ cb . VerticalAlignment = VerticalAlignment . Center ;
266276 Microsoft . UI . Xaml . Automation . AutomationProperties . SetName (
267277 cb ,
268278 LocalizationHelper . GetString ( "Chat_Composer_Accessibility_Session" ) ) ;
269-
270- ComboBoxItem ? selectedItem = null ;
271- foreach ( var group in groups )
272- {
273- if ( groups . Length > 1 )
274- {
275- cb . Items . Add ( new ComboBoxItem
276- {
277- Content = group . AgentLabel ,
278- IsEnabled = false ,
279- FontWeight = Microsoft . UI . Text . FontWeights . SemiBold ,
280- FontSize = 10 ,
281- Padding = new Thickness ( 4 , 2 , 4 , 2 ) ,
282- IsHitTestVisible = false ,
283- } ) ;
284- }
285- foreach ( var session in group . Sessions )
286- {
287- var item = new ComboBoxItem
288- {
289- Content = session . Title ,
290- Tag = session . Id ,
291- Padding = groups . Length > 1
292- ? new Thickness ( 16 , 4 , 4 , 4 )
293- : new Thickness ( 8 , 4 , 4 , 4 ) ,
294- } ;
295- cb . Items . Add ( item ) ;
296- if ( session . Id == ( Props . ChannelId ?? "" ) )
297- selectedItem = item ;
298- }
299- }
300-
301- if ( selectedItem != null )
302- cb . SelectedItem = selectedItem ;
303-
304- var onChanged = Props . OnChannelChanged ;
305- cb . SelectionChanged += ( _ , _ ) =>
306- {
307- if ( cb . SelectedItem is ComboBoxItem { Tag : string id } )
308- onChanged ( id ) ;
309- } ;
310-
311- border . Child = cb ;
312279 } ) ;
313280
314281 // ── Model picker (provider-rich) ─────────────────────────────────
@@ -357,58 +324,42 @@ public override Element Render()
357324 modelEntries . Add ( ( Props . CurrentModel ?? "model" , Props . CurrentModel ?? "" , false , true ) ) ;
358325 }
359326
360- var modelSelectedIndex = modelEntries . FindIndex ( e => e . IsCurrent ) ;
327+ // Provider-rich model picker via the same reconciled primitive. Unavailable rows stay
328+ // visible but disabled; the default/clear row maps to a reserved id.
329+ var modelItems = new List < ComboItem > ( modelEntries . Count ) ;
330+ string ? modelSelectedId = null ;
331+ foreach ( var entry in modelEntries )
332+ {
333+ var id = ReferenceEquals ( entry . Tag , ClearModelTag )
334+ ? ClearModelId
335+ : entry . Tag as string ?? "" ;
336+ modelItems . Add ( new ComboItem ( id , entry . Label , Enabled : entry . Selectable ) ) ;
337+ if ( entry . IsCurrent ) modelSelectedId ??= id ;
338+ }
361339
362- // Build directly so unavailable rows can be displayed but not selected.
363- var modelCombo = Border ( )
364- . Set ( border =>
340+ var onModelChanged = Props . OnModelChanged ;
341+ var onModelCleared = Props . OnModelCleared ;
342+ var modelCombo = ComboBox ( modelItems , modelSelectedId , id =>
365343 {
366- var cb = new ComboBox
367- {
368- MinWidth = 0 ,
369- Width = double . NaN ,
370- Height = 28 ,
371- FontSize = 11 ,
372- Padding = new Thickness ( 8 , 0 , 4 , 0 ) ,
373- CornerRadius = composerCornerRadius ,
374- HorizontalAlignment = HorizontalAlignment . Stretch ,
375- VerticalAlignment = VerticalAlignment . Center ,
376- IsEnabled = messageOptionControlsEnabled ,
377- } ;
344+ if ( id == ClearModelId )
345+ onModelCleared ? . Invoke ( ) ;
346+ else if ( ! string . IsNullOrEmpty ( id ) )
347+ onModelChanged ( id ) ;
348+ } )
349+ . Set ( cb =>
350+ {
351+ cb . MinWidth = 0 ;
352+ cb . Width = double . NaN ;
353+ cb . Height = 28 ;
354+ cb . FontSize = 11 ;
355+ cb . Padding = new Thickness ( 8 , 0 , 4 , 0 ) ;
356+ cb . CornerRadius = composerCornerRadius ;
357+ cb . HorizontalAlignment = HorizontalAlignment . Stretch ;
358+ cb . VerticalAlignment = VerticalAlignment . Center ;
359+ cb . IsEnabled = messageOptionControlsEnabled ;
378360 Microsoft . UI . Xaml . Automation . AutomationProperties . SetName (
379361 cb ,
380362 LocalizationHelper . GetString ( "Chat_Composer_Accessibility_Model" ) ) ;
381-
382- ComboBoxItem ? selectedItem = null ;
383- for ( int i = 0 ; i < modelEntries . Count ; i ++ )
384- {
385- var entry = modelEntries [ i ] ;
386- var item = new ComboBoxItem
387- {
388- Content = entry . Label ,
389- Tag = entry . Tag ,
390- IsEnabled = entry . Selectable ,
391- Padding = new Thickness ( 8 , 4 , 4 , 4 ) ,
392- } ;
393- cb . Items . Add ( item ) ;
394- if ( i == modelSelectedIndex ) selectedItem = item ;
395- }
396-
397- if ( selectedItem != null )
398- cb . SelectedItem = selectedItem ;
399-
400- var onModelChanged = Props . OnModelChanged ;
401- var onModelCleared = Props . OnModelCleared ;
402- cb . SelectionChanged += ( _ , _ ) =>
403- {
404- if ( cb . SelectedItem is not ComboBoxItem { IsEnabled : true } sel ) return ;
405- if ( ReferenceEquals ( sel . Tag , ClearModelTag ) )
406- onModelCleared ? . Invoke ( ) ;
407- else if ( sel . Tag is string id && ! string . IsNullOrEmpty ( id ) )
408- onModelChanged ( id ) ;
409- } ;
410-
411- border . Child = cb ;
412363 } )
413364 . VAlign ( VerticalAlignment . Center ) ;
414365
0 commit comments