@@ -145,22 +145,19 @@ func (mv *messageModel) Render(width int) string {
145145 prefix = mv .senderPrefix (msg .Sender )
146146 }
147147
148- // Show copy icon in the top-right corner when hovered or selected.
149- // AssistantMessageStyle has PaddingTop=0 (unlike UserMessageStyle which has
150- // PaddingTop=1) , so we cannot unconditionally prepend topRow+"\n" — doing so
151- // would add a spurious blank line to every message in the default state .
152- // Accept the 1-line layout shift on hover; it is less disruptive than the
153- // blank-line artifact that affects all messages at all times.
148+ // Always reserve a top row to avoid layout shifts when the copy icon
149+ // appears on hover. When not hovered, the row is filled with spaces
150+ // (invisible). AssistantMessageStyle has PaddingTop=0 , so this extra
151+ // row acts as a stable spacer .
152+ innerWidth := width - messageStyle . GetHorizontalFrameSize ()
153+ topRow := strings . Repeat ( " " , innerWidth )
154154 if mv .hovered || mv .selected {
155- innerWidth := width - messageStyle .GetHorizontalFrameSize ()
156155 copyIcon := styles .MutedStyle .Render (types .AssistantMessageCopyLabel )
157156 iconWidth := ansi .StringWidth (types .AssistantMessageCopyLabel )
158157 padding := max (innerWidth - iconWidth , 0 )
159- topRow := strings .Repeat (" " , padding ) + copyIcon
160- noTopPaddingStyle := messageStyle .PaddingTop (0 )
161- return prefix + noTopPaddingStyle .Width (width ).Render (topRow + "\n " + rendered )
158+ topRow = strings .Repeat (" " , padding ) + copyIcon
162159 }
163- return prefix + messageStyle .Render (rendered )
160+ return prefix + messageStyle .Width ( width ). Render (topRow + " \n " + rendered )
164161 case types .MessageTypeShellOutput :
165162 if rendered , err := markdown .NewRenderer (width ).Render (fmt .Sprintf ("```console\n %s\n ```" , msg .Content )); err == nil {
166163 return rendered
0 commit comments