@@ -80,6 +80,9 @@ export struct ComposerBar {
8080 @Local showModelSelectorSheet: boolean = false;
8181 @Local showModelSelectorPopover: boolean = false;
8282 @Local isPreparingModelSelectorPopover: boolean = false;
83+ @Local keepExpandedForModelSelection: boolean = false;
84+ private restoreInputFocusAfterModelSelection: boolean = false;
85+ private restoreInputFocusTimerId: number = -1;
8386 @Event onPickImages: () => void = () => {};
8487 @Event onRemoveImage: (imageId: string) => void = (_imageId: string) => {};
8588 @Event onSend: () => void = () => {};
@@ -97,6 +100,10 @@ export struct ComposerBar {
97100 clearTimeout(this.sendTransitionTimerId);
98101 this.sendTransitionTimerId = -1;
99102 }
103+ if (this.restoreInputFocusTimerId >= 0) {
104+ clearTimeout(this.restoreInputFocusTimerId);
105+ this.restoreInputFocusTimerId = -1;
106+ }
100107 }
101108
102109 @Monitor('chatInput')
@@ -111,6 +118,13 @@ export struct ComposerBar {
111118 this.closeModelSelector();
112119 }
113120
121+ @Monitor('showModelSelectorSheet')
122+ onModelSelectorSheetChanged(): void {
123+ if (!this.showModelSelectorSheet) {
124+ this.restoreComposerFocusAfterModelSelection();
125+ }
126+ }
127+
114128 build() {
115129 Column() {
116130 this.AdaptiveComposer()
@@ -239,9 +253,17 @@ export struct ComposerBar {
239253 onStateChange: (event) => {
240254 if (!event.isVisible) {
241255 this.showModelSelectorPopover = false;
256+ this.restoreComposerFocusAfterModelSelection();
242257 }
243258 }
244259 })
260+ .onTouch((event: TouchEvent) => {
261+ if (event.type === TouchType.Down) {
262+ // Capture focus before the sheet/popover moves it away. Choosing a
263+ // model is part of the current edit, not the end of that edit.
264+ this.beginModelSelectionFromComposer();
265+ }
266+ })
245267 .onClick(() => {
246268 if (this.presentation === ComposerPresentation.Floating) {
247269 this.toggleModelSelectorPopover();
@@ -270,7 +292,7 @@ export struct ComposerBar {
270292 .height(32)
271293 .accessibilityText(RemoteI18n.t('common.close'))
272294 .onClick(() => {
273- this.closeModelSelector();
295+ this.closeModelSelector(true );
274296 })
275297 }
276298 .width('100%')
@@ -337,7 +359,7 @@ export struct ComposerBar {
337359 .backgroundColor(this.isSelectedModel(model) ? SOFT : TRANSPARENT)
338360 .borderRadius(MobileDesignGeometry.composerModelSelectorRowRadius)
339361 .onClick(() => {
340- this.closeModelSelector();
362+ this.closeModelSelector(true );
341363 this.onSelectModel(model.id);
342364 })
343365 }
@@ -387,6 +409,7 @@ export struct ComposerBar {
387409 .defaultFocus(false)
388410 .onFocus(() => {
389411 this.inputFocused = true;
412+ this.cancelModelSelectionEditingContinuation();
390413 })
391414 .onBlur(() => {
392415 this.inputFocused = false;
@@ -668,7 +691,8 @@ export struct ComposerBar {
668691 return ChatComposerPolicy.isExpanded(
669692 this.inputText,
670693 this.inputFocused,
671- this.isModelSelectorExpanded()
694+ this.isModelSelectorExpanded(),
695+ this.keepExpandedForModelSelection
672696 );
673697 }
674698
@@ -714,15 +738,21 @@ export struct ComposerBar {
714738 return [selected, ...models.filter((model: ConversationUiModel) => model.id !== selected.id)];
715739 }
716740
717- private closeModelSelector(): void {
741+ private closeModelSelector(restoreInputFocus: boolean = false): void {
742+ const shouldRestore = restoreInputFocus && this.restoreInputFocusAfterModelSelection;
718743 this.showModelSelectorSheet = false;
719744 this.showModelSelectorPopover = false;
720745 this.isPreparingModelSelectorPopover = false;
746+ if (shouldRestore) {
747+ this.restoreComposerFocusAfterModelSelection();
748+ } else if (!restoreInputFocus) {
749+ this.cancelModelSelectionEditingContinuation();
750+ }
721751 }
722752
723753 private async toggleModelSelectorPopover(): Promise<void> {
724754 if (this.showModelSelectorPopover) {
725- this.closeModelSelector();
755+ this.closeModelSelector(true );
726756 return;
727757 }
728758 if (this.isPreparingModelSelectorPopover) {
@@ -758,6 +788,44 @@ export struct ComposerBar {
758788 this.getUIContext().getFocusController().clearFocus();
759789 }
760790
791+ private beginModelSelectionFromComposer(): void {
792+ if (!this.inputFocused) {
793+ return;
794+ }
795+ this.keepExpandedForModelSelection = true;
796+ this.restoreInputFocusAfterModelSelection = true;
797+ }
798+
799+ private restoreComposerFocusAfterModelSelection(): void {
800+ if (!this.restoreInputFocusAfterModelSelection || this.showModelSelectorSheet ||
801+ this.showModelSelectorPopover || this.isPreparingModelSelectorPopover) {
802+ return;
803+ }
804+ if (this.restoreInputFocusTimerId >= 0) {
805+ clearTimeout(this.restoreInputFocusTimerId);
806+ }
807+ // Let the selector release its focus scope first. Retaining the expanded
808+ // layout during this handoff removes the collapse/re-expand jump.
809+ this.restoreInputFocusTimerId = setTimeout(() => {
810+ this.restoreInputFocusTimerId = -1;
811+ try {
812+ this.getUIContext().getFocusController().requestFocus(this.inputId);
813+ } catch (_error) {
814+ // If the route disappeared during dismissal, component teardown owns
815+ // the continuation and clears its timer.
816+ }
817+ }, MobileDesignMotion.structure);
818+ }
819+
820+ private cancelModelSelectionEditingContinuation(): void {
821+ if (this.restoreInputFocusTimerId >= 0) {
822+ clearTimeout(this.restoreInputFocusTimerId);
823+ this.restoreInputFocusTimerId = -1;
824+ }
825+ this.restoreInputFocusAfterModelSelection = false;
826+ this.keepExpandedForModelSelection = false;
827+ }
828+
761829 private async startVoiceInput(): Promise<void> {
762830 await this.releaseComposerKeyboard();
763831 this.onVoiceInput();
0 commit comments