@@ -717,17 +717,30 @@ function createSnapshotWithPendingUserInput(): OrchestrationReadModel {
717717 } ;
718718}
719719
720- function createSnapshotWithPlanFollowUpPrompt ( ) : OrchestrationReadModel {
720+ function createSnapshotWithPlanFollowUpPrompt ( options ?: {
721+ modelSelection ?: { provider : "codex" ; model : string } ;
722+ planMarkdown ?: string ;
723+ } ) : OrchestrationReadModel {
721724 const snapshot = createSnapshotForTargetUser ( {
722725 targetMessageId : "msg-user-plan-follow-up-target" as MessageId ,
723726 targetText : "plan follow-up thread" ,
724727 } ) ;
728+ const modelSelection = options ?. modelSelection ?? {
729+ provider : "codex" as const ,
730+ model : "gpt-5" ,
731+ } ;
732+ const planMarkdown =
733+ options ?. planMarkdown ?? "# Follow-up plan\n\n- Keep the composer footer stable on resize." ;
725734
726735 return {
727736 ...snapshot ,
737+ projects : snapshot . projects . map ( ( project ) =>
738+ project . id === PROJECT_ID ? { ...project , defaultModelSelection : modelSelection } : project ,
739+ ) ,
728740 threads : snapshot . threads . map ( ( thread ) =>
729741 thread . id === THREAD_ID
730742 ? Object . assign ( { } , thread , {
743+ modelSelection,
731744 interactionMode : "plan" ,
732745 latestTurn : {
733746 turnId : "turn-plan-follow-up" as TurnId ,
@@ -741,7 +754,7 @@ function createSnapshotWithPlanFollowUpPrompt(): OrchestrationReadModel {
741754 {
742755 id : "plan-follow-up-browser-test" ,
743756 turnId : "turn-plan-follow-up" as TurnId ,
744- planMarkdown : "# Follow-up plan\n\n- Keep the composer footer stable on resize." ,
757+ planMarkdown,
745758 implementedAt : null ,
746759 implementationThreadId : null ,
747760 createdAt : isoAt ( 1_002 ) ,
@@ -3720,8 +3733,9 @@ describe("ChatView timeline estimator parity (full app)", () => {
37203733 ) ;
37213734 const initialModelPickerOffset =
37223735 initialModelPicker . getBoundingClientRect ( ) . left - footer . getBoundingClientRect ( ) . left ;
3736+ const initialImplementButton = await waitForButtonByText ( "Implement" ) ;
3737+ const initialImplementWidth = initialImplementButton . getBoundingClientRect ( ) . width ;
37233738
3724- await waitForButtonByText ( "Implement" ) ;
37253739 await waitForElement (
37263740 ( ) =>
37273741 document . querySelector < HTMLButtonElement > ( 'button[aria-label="Implementation actions"]' ) ,
@@ -3753,6 +3767,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
37533767
37543768 expect ( Math . abs ( implementRect . right - implementActionsRect . left ) ) . toBeLessThanOrEqual ( 1 ) ;
37553769 expect ( Math . abs ( implementRect . top - implementActionsRect . top ) ) . toBeLessThanOrEqual ( 1 ) ;
3770+ expect ( Math . abs ( implementRect . width - initialImplementWidth ) ) . toBeLessThanOrEqual ( 1 ) ;
37563771 expect ( Math . abs ( compactModelPickerOffset - initialModelPickerOffset ) ) . toBeLessThanOrEqual (
37573772 1 ,
37583773 ) ;
@@ -3764,6 +3779,73 @@ describe("ChatView timeline estimator parity (full app)", () => {
37643779 }
37653780 } ) ;
37663781
3782+ it ( "keeps the wide desktop follow-up layout expanded when the footer still fits" , async ( ) => {
3783+ const mounted = await mountChatView ( {
3784+ viewport : WIDE_FOOTER_VIEWPORT ,
3785+ snapshot : createSnapshotWithPlanFollowUpPrompt ( {
3786+ modelSelection : { provider : "codex" , model : "gpt-5.3-codex-spark" } ,
3787+ planMarkdown :
3788+ "# Imaginary Long-Range Plan: T3 Code Adaptive Orchestration and Safe-Delay Execution Initiative" ,
3789+ } ) ,
3790+ } ) ;
3791+
3792+ try {
3793+ await waitForButtonByText ( "Implement" ) ;
3794+
3795+ await vi . waitFor (
3796+ ( ) => {
3797+ const footer = document . querySelector < HTMLElement > ( '[data-chat-composer-footer="true"]' ) ;
3798+ const actions = document . querySelector < HTMLElement > (
3799+ '[data-chat-composer-actions="right"]' ,
3800+ ) ;
3801+
3802+ expect ( footer ?. dataset . chatComposerFooterCompact ) . toBe ( "false" ) ;
3803+ expect ( actions ?. dataset . chatComposerPrimaryActionsCompact ) . toBe ( "false" ) ;
3804+ } ,
3805+ { timeout : 8_000 , interval : 16 } ,
3806+ ) ;
3807+ } finally {
3808+ await mounted . cleanup ( ) ;
3809+ }
3810+ } ) ;
3811+
3812+ it ( "compacts the footer when a wide desktop follow-up layout starts overflowing" , async ( ) => {
3813+ const mounted = await mountChatView ( {
3814+ viewport : WIDE_FOOTER_VIEWPORT ,
3815+ snapshot : createSnapshotWithPlanFollowUpPrompt ( {
3816+ modelSelection : { provider : "codex" , model : "gpt-5.3-codex-spark" } ,
3817+ planMarkdown :
3818+ "# Imaginary Long-Range Plan: T3 Code Adaptive Orchestration and Safe-Delay Execution Initiative" ,
3819+ } ) ,
3820+ } ) ;
3821+
3822+ try {
3823+ await waitForButtonByText ( "Implement" ) ;
3824+
3825+ await mounted . setContainerSize ( {
3826+ width : 804 ,
3827+ height : WIDE_FOOTER_VIEWPORT . height ,
3828+ } ) ;
3829+
3830+ await expectComposerActionsContained ( ) ;
3831+
3832+ await vi . waitFor (
3833+ ( ) => {
3834+ const footer = document . querySelector < HTMLElement > ( '[data-chat-composer-footer="true"]' ) ;
3835+ const actions = document . querySelector < HTMLElement > (
3836+ '[data-chat-composer-actions="right"]' ,
3837+ ) ;
3838+
3839+ expect ( footer ?. dataset . chatComposerFooterCompact ) . toBe ( "true" ) ;
3840+ expect ( actions ?. dataset . chatComposerPrimaryActionsCompact ) . toBe ( "true" ) ;
3841+ } ,
3842+ { timeout : 8_000 , interval : 16 } ,
3843+ ) ;
3844+ } finally {
3845+ await mounted . cleanup ( ) ;
3846+ }
3847+ } ) ;
3848+
37673849 it ( "keeps the slash-command menu visible above the composer" , async ( ) => {
37683850 const mounted = await mountChatView ( {
37693851 viewport : DEFAULT_VIEWPORT ,
0 commit comments